@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
@@ -20,7 +20,7 @@ import { bindOrUnbindLinearElement, bindOrUnbindLinearElements, fixBindingsAfter
20
20
  import { LinearElementEditor } from "../element/linearElementEditor";
21
21
  import { mutateElement, newElementWith } from "../element/mutateElement";
22
22
  import { deepCopyElement, duplicateElements, newFrameElement, newFreeDrawElement, newEmbeddableElement, newMagicFrameElement, newIframeElement, newArrowElement, } from "../element/newElement";
23
- import { hasBoundTextElement, isArrowElement, isBindingElement, isBindingElementType, isBoundToContainer, isFrameLikeElement, isImageElement, isEmbeddableElement, isInitializedImageElement, isLinearElement, isLinearElementType, isUsingAdaptiveRadius, isIframeElement, isIframeLikeElement, isMagicFrameElement, isTextBindableContainer, isElbowArrow, isFlowchartNodeElement, } from "../element/typeChecks";
23
+ import { hasBoundTextElement, isArrowElement, isBindingElement, isBindingElementType, isBoundToContainer, isFrameLikeElement, isImageElement, isEmbeddableElement, isInitializedImageElement, isLinearElement, isLinearElementType, isUsingAdaptiveRadius, isIframeElement, isIframeLikeElement, isMagicFrameElement, isTextBindableContainer, isElbowArrow, isFlowchartNodeElement, isBindableElement, } from "../element/typeChecks";
24
24
  import { getCenter, getDistance } from "../gesture";
25
25
  import { editGroupForSelectedElement, getElementsInGroup, getSelectedGroupIdForElement, getSelectedGroupIds, isElementInGroup, isSelectedViaGroup, selectGroupsForSelectedElements, } from "../groups";
26
26
  import { History } from "../history";
@@ -33,7 +33,7 @@ import { getStateForZoom } from "../scene/zoom";
33
33
  import { findShapeByKey, getBoundTextShape, getCornerRadius, getElementShape, isPathALoop, } from "../shapes";
34
34
  import { getSelectionBoxShape } from "../../utils/geometry/shape";
35
35
  import { isPointInShape } from "../../utils/collision";
36
- import { debounce, distance, getFontString, getNearestScrollableContainer, isInputLike, isToolIcon, isWritableElement, sceneCoordsToViewportCoords, tupleToCoors, viewportCoordsToSceneCoords, wrapEvent, updateObject, updateActiveTool, getShortcutKey, isTransparent, easeToValuesRAF, muteFSAbortError, isTestEnv, easeOut, updateStable, addEventListener, normalizeEOL, getDateTime, isShallowEqual, arrayToMap, toBrandedType, } from "../utils";
36
+ import { debounce, distance, getFontString, getNearestScrollableContainer, isInputLike, isToolIcon, isWritableElement, sceneCoordsToViewportCoords, tupleToCoors, viewportCoordsToSceneCoords, wrapEvent, updateObject, updateActiveTool, getShortcutKey, isTransparent, easeToValuesRAF, muteFSAbortError, isTestEnv, easeOut, updateStable, addEventListener, normalizeEOL, getDateTime, isShallowEqual, arrayToMap, } from "../utils";
37
37
  import { createSrcDoc, embeddableURLValidator, maybeParseEmbedSrc, getEmbedLink, } from "../element/embeddable";
38
38
  import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
39
39
  import LayerUI from "./LayerUI";
@@ -52,9 +52,9 @@ import { Fonts, getLineHeight } from "../fonts";
52
52
  import { getFrameChildren, isCursorInFrame, bindElementsToFramesAfterDuplication, addElementsToFrame, replaceAllElementsInFrame, removeElementsFromFrame, getElementsInResizingFrame, getElementsInNewFrame, getContainingFrame, elementOverlapsWithFrame, updateFrameMembershipOfSelectedElements, isElementInFrame, getFrameLikeTitle, getElementsOverlappingFrame, filterElementsEligibleAsFrameChildren, } from "../frame";
53
53
  import { excludeElementsInFramesFromSelection, makeNextSelectedElementIds, } from "../scene/selection";
54
54
  import { actionPaste } from "../actions/actionClipboard";
55
- import { actionRemoveAllElementsFromFrame, actionSelectAllElementsInFrame, } from "../actions/actionFrame";
55
+ import { actionRemoveAllElementsFromFrame, actionSelectAllElementsInFrame, actionWrapSelectionInFrame, } from "../actions/actionFrame";
56
56
  import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
57
- import { jotaiStore } from "../jotai";
57
+ import { editorJotaiStore } from "../editor-jotai";
58
58
  import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
59
59
  import { ImageSceneDataError } from "../errors";
60
60
  import { getSnapLinesAtPointer, snapDraggedElements, isActiveToolNonLinearSnappable, snapNewElement, snapResizingElements, isSnappingEnabled, getVisibleGaps, getReferenceSnapPoints, SnapCache, isGridModeEnabled, getGridPoint, } from "../snapping";
@@ -90,7 +90,6 @@ import { actionTextAutoResize } from "../actions/actionTextAutoResize";
90
90
  import { getVisibleSceneBounds } from "../element/bounds";
91
91
  import { isMaybeMermaidDefinition } from "../mermaid";
92
92
  import NewElementCanvas from "./canvases/NewElementCanvas";
93
- import { mutateElbowArrow, updateElbowArrow } from "../element/routing";
94
93
  import { FlowChartCreator, FlowChartNavigator, getLinkDirectionFromKey, } from "../element/flowchart";
95
94
  import { searchItemInFocusAtom } from "./SearchMenu";
96
95
  import { clamp, pointFrom, pointDistance, vector, pointRotateRads, vectorScale, vectorFromPoint, vectorSubtract, vectorDot, vectorNormalize, } from "../../math";
@@ -714,8 +713,17 @@ class App extends React.Component {
714
713
  */
715
714
  _cache: new Map(),
716
715
  };
716
+ resetEditingFrame = (frame) => {
717
+ if (frame) {
718
+ mutateElement(frame, { name: frame.name?.trim() || null });
719
+ }
720
+ this.setState({ editingFrame: null });
721
+ };
717
722
  renderFrameNames = () => {
718
723
  if (!this.state.frameRendering.enabled || !this.state.frameRendering.name) {
724
+ if (this.state.editingFrame) {
725
+ this.resetEditingFrame(null);
726
+ }
719
727
  return null;
720
728
  }
721
729
  const isDarkTheme = this.state.theme === THEME.DARK;
@@ -727,15 +735,14 @@ class App extends React.Component {
727
735
  scrollY: this.state.scrollY,
728
736
  zoom: this.state.zoom,
729
737
  }, this.scene.getNonDeletedElementsMap())) {
738
+ if (this.state.editingFrame === f.id) {
739
+ this.resetEditingFrame(f);
740
+ }
730
741
  // if frame not visible, don't render its name
731
742
  return null;
732
743
  }
733
744
  const { x: x1, y: y1 } = sceneCoordsToViewportCoords({ sceneX: f.x, sceneY: f.y }, this.state);
734
745
  const FRAME_NAME_EDIT_PADDING = 6;
735
- const reset = () => {
736
- mutateElement(f, { name: f.name?.trim() || null });
737
- this.setState({ editingFrame: null });
738
- };
739
746
  let frameNameJSX;
740
747
  const frameName = getFrameLikeTitle(f);
741
748
  if (f.id === this.state.editingFrame) {
@@ -744,12 +751,12 @@ class App extends React.Component {
744
751
  mutateElement(f, {
745
752
  name: e.target.value,
746
753
  });
747
- }, onFocus: (e) => e.target.select(), onBlur: () => reset(), onKeyDown: (event) => {
754
+ }, onFocus: (e) => e.target.select(), onBlur: () => this.resetEditingFrame(f), onKeyDown: (event) => {
748
755
  // for some inexplicable reason, `onBlur` triggered on ESC
749
756
  // does not reset `state.editingFrame` despite being called,
750
757
  // and we need to reset it here as well
751
758
  if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
752
- reset();
759
+ this.resetEditingFrame(f);
753
760
  }
754
761
  }, style: {
755
762
  background: this.state.viewBackgroundColor,
@@ -830,13 +837,18 @@ class App extends React.Component {
830
837
  });
831
838
  this.visibleElements = visibleElements;
832
839
  const allElementsMap = this.scene.getNonDeletedElementsMap();
833
- const shouldBlockPointerEvents = this.state.selectionElement ||
834
- this.state.newElement ||
835
- this.state.selectedElementsAreBeingDragged ||
836
- this.state.resizingElement ||
837
- (this.state.activeTool.type === "laser" &&
838
- // technically we can just test on this once we make it more safe
839
- this.state.cursorButton === "down");
840
+ const shouldBlockPointerEvents =
841
+ // default back to `--ui-pointerEvents` flow if setPointerCapture
842
+ // not supported
843
+ "setPointerCapture" in HTMLElement.prototype
844
+ ? false
845
+ : this.state.selectionElement ||
846
+ this.state.newElement ||
847
+ this.state.selectedElementsAreBeingDragged ||
848
+ this.state.resizingElement ||
849
+ (this.state.activeTool.type === "laser" &&
850
+ // technically we can just test on this once we make it more safe
851
+ this.state.cursorButton === "down");
840
852
  const firstSelectedElement = selectedElements[0];
841
853
  return (_jsx("div", { className: clsx("excalidraw excalidraw-container", {
842
854
  "excalidraw--view-mode": this.state.viewModeEnabled ||
@@ -1094,7 +1106,7 @@ class App extends React.Component {
1094
1106
  }
1095
1107
  };
1096
1108
  openEyeDropper = ({ type }) => {
1097
- jotaiStore.set(activeEyeDropperAtom, {
1109
+ editorJotaiStore.set(activeEyeDropperAtom, {
1098
1110
  swapPreviewOnAlt: true,
1099
1111
  colorPickerType: type === "stroke" ? "elementStroke" : "elementBackground",
1100
1112
  onSelect: (color, event) => {
@@ -1902,30 +1914,7 @@ class App extends React.Component {
1902
1914
  event?.preventDefault();
1903
1915
  });
1904
1916
  addElementsFromPasteOrLibrary = (opts) => {
1905
- let elements = opts.elements.map((el, _, elements) => {
1906
- if (isElbowArrow(el)) {
1907
- const startEndElements = [
1908
- el.startBinding &&
1909
- elements.find((l) => l.id === el.startBinding?.elementId),
1910
- el.endBinding &&
1911
- elements.find((l) => l.id === el.endBinding?.elementId),
1912
- ];
1913
- const startBinding = startEndElements[0] ? el.startBinding : null;
1914
- const endBinding = startEndElements[1] ? el.endBinding : null;
1915
- return {
1916
- ...el,
1917
- ...updateElbowArrow({
1918
- ...el,
1919
- startBinding,
1920
- endBinding,
1921
- }, toBrandedType(new Map(startEndElements
1922
- .filter((x) => x != null)
1923
- .map((el) => [el.id, el]))), [el.points[0], el.points[el.points.length - 1]]),
1924
- };
1925
- }
1926
- return el;
1927
- });
1928
- elements = restoreElements(elements, null, undefined);
1917
+ const elements = restoreElements(opts.elements, null, undefined);
1929
1918
  const [minX, minY, maxX, maxY] = getCommonBounds(elements);
1930
1919
  const elementsCenterX = distance(minX, maxX) / 2;
1931
1920
  const elementsCenterY = distance(minY, maxY) / 2;
@@ -1952,12 +1941,14 @@ class App extends React.Component {
1952
1941
  randomizeSeed: !opts.retainSeed,
1953
1942
  });
1954
1943
  const prevElements = this.scene.getElementsIncludingDeleted();
1955
- const nextElements = [...prevElements, ...newElements];
1944
+ let nextElements = [...prevElements, ...newElements];
1945
+ const mappedNewSceneElements = this.props.onDuplicate?.(nextElements, prevElements);
1946
+ nextElements = mappedNewSceneElements || nextElements;
1956
1947
  syncMovedIndices(nextElements, arrayToMap(newElements));
1957
1948
  const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
1958
1949
  if (topLayerFrame) {
1959
1950
  const eligibleElements = filterElementsEligibleAsFrameChildren(newElements, topLayerFrame);
1960
- addElementsToFrame(nextElements, eligibleElements, topLayerFrame);
1951
+ addElementsToFrame(nextElements, eligibleElements, topLayerFrame, this.state);
1961
1952
  }
1962
1953
  this.scene.replaceAllElements(nextElements);
1963
1954
  newElements.forEach((newElement) => {
@@ -1986,7 +1977,7 @@ class App extends React.Component {
1986
1977
  // from library, not when pasting from clipboard. Alas.
1987
1978
  openSidebar: this.state.openSidebar &&
1988
1979
  this.device.editor.canFitSidebar &&
1989
- jotaiStore.get(isSidebarDockedAtom)
1980
+ editorJotaiStore.get(isSidebarDockedAtom)
1990
1981
  ? this.state.openSidebar
1991
1982
  : null,
1992
1983
  ...selectGroupsForSelectedElements({
@@ -2399,11 +2390,16 @@ class App extends React.Component {
2399
2390
  addedFiles[fileData.id] = fileData;
2400
2391
  nextFiles[fileData.id] = fileData;
2401
2392
  if (fileData.mimeType === MIME_TYPES.svg) {
2402
- const restoredDataURL = getDataURL_sync(normalizeSVG(dataURLToString(fileData.dataURL)), MIME_TYPES.svg);
2403
- if (fileData.dataURL !== restoredDataURL) {
2404
- // bump version so persistence layer can update the store
2405
- fileData.version = (fileData.version ?? 1) + 1;
2406
- fileData.dataURL = restoredDataURL;
2393
+ try {
2394
+ const restoredDataURL = getDataURL_sync(normalizeSVG(dataURLToString(fileData.dataURL)), MIME_TYPES.svg);
2395
+ if (fileData.dataURL !== restoredDataURL) {
2396
+ // bump version so persistence layer can update the store
2397
+ fileData.version = (fileData.version ?? 1) + 1;
2398
+ fileData.dataURL = restoredDataURL;
2399
+ }
2400
+ }
2401
+ catch (error) {
2402
+ console.error(error);
2407
2403
  }
2408
2404
  }
2409
2405
  }
@@ -2732,14 +2728,15 @@ class App extends React.Component {
2732
2728
  mutateElement(element, {
2733
2729
  x: element.x + offsetX,
2734
2730
  y: element.y + offsetY,
2735
- });
2731
+ }, false);
2736
2732
  updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
2737
2733
  simultaneouslyUpdated: selectedElements,
2738
2734
  });
2739
2735
  });
2740
2736
  this.setState({
2741
- suggestedBindings: getSuggestedBindingsForArrows(selectedElements.filter((element) => element.id !== elbowArrow?.id || step !== 0), this.scene.getNonDeletedElementsMap()),
2737
+ suggestedBindings: getSuggestedBindingsForArrows(selectedElements.filter((element) => element.id !== elbowArrow?.id || step !== 0), this.scene.getNonDeletedElementsMap(), this.state.zoom),
2742
2738
  });
2739
+ this.scene.triggerUpdate();
2743
2740
  event.preventDefault();
2744
2741
  }
2745
2742
  else if (event.key === KEYS.ENTER) {
@@ -2862,7 +2859,7 @@ class App extends React.Component {
2862
2859
  }
2863
2860
  if (event[KEYS.CTRL_OR_CMD] &&
2864
2861
  (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)) {
2865
- jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
2862
+ editorJotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
2866
2863
  }
2867
2864
  // eye dropper
2868
2865
  // -----------------------------------------------------------------------
@@ -2900,7 +2897,7 @@ class App extends React.Component {
2900
2897
  this.setState({ isBindingEnabled: true });
2901
2898
  }
2902
2899
  if (isArrowKey(event.key)) {
2903
- bindOrUnbindLinearElements(this.scene.getSelectedElements(this.state).filter(isLinearElement), this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElements(), this.scene, isBindingEnabled(this.state), this.state.selectedLinearElement?.selectedPointsIndices ?? []);
2900
+ bindOrUnbindLinearElements(this.scene.getSelectedElements(this.state).filter(isLinearElement), this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElements(), this.scene, isBindingEnabled(this.state), this.state.selectedLinearElement?.selectedPointsIndices ?? [], this.state.zoom);
2904
2901
  this.setState({ suggestedBindings: [] });
2905
2902
  }
2906
2903
  if (!event.altKey) {
@@ -2955,7 +2952,10 @@ class App extends React.Component {
2955
2952
  setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
2956
2953
  }
2957
2954
  else if (!isHoldingSpace) {
2958
- setCursorForShape(this.interactiveCanvas, this.state);
2955
+ setCursorForShape(this.interactiveCanvas, {
2956
+ ...this.state,
2957
+ activeTool: nextActiveTool,
2958
+ });
2959
2959
  }
2960
2960
  if (isToolIcon(document.activeElement)) {
2961
2961
  this.focusContainer();
@@ -3258,7 +3258,11 @@ class App extends React.Component {
3258
3258
  this.state.selectedElementIds[element.id] &&
3259
3259
  shouldShowBoundingBox([element], this.state)) {
3260
3260
  const selectionShape = getSelectionBoxShape(element, this.scene.getNonDeletedElementsMap(), isImageElement(element) ? 0 : this.getElementHitThreshold());
3261
- return isPointInShape(pointFrom(x, y), selectionShape);
3261
+ // if hitting the bounding box, return early
3262
+ // but if not, we should check for other cases as well (e.g. frame name)
3263
+ if (isPointInShape(pointFrom(x, y), selectionShape)) {
3264
+ return true;
3265
+ }
3262
3266
  }
3263
3267
  // take bound text element into consideration for hit collision as well
3264
3268
  const hitBoundTextOfElement = hitElementBoundText(x, y, getBoundTextShape(element, this.scene.getNonDeletedElementsMap()));
@@ -3447,6 +3451,7 @@ class App extends React.Component {
3447
3451
  return;
3448
3452
  }
3449
3453
  const selectedElements = this.scene.getSelectedElements(this.state);
3454
+ let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(event, this.state);
3450
3455
  if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
3451
3456
  if (event[KEYS.CTRL_OR_CMD] &&
3452
3457
  (!this.state.editingLinearElement ||
@@ -3459,13 +3464,45 @@ class App extends React.Component {
3459
3464
  });
3460
3465
  return;
3461
3466
  }
3467
+ else if (this.state.selectedLinearElement &&
3468
+ isElbowArrow(selectedElements[0])) {
3469
+ const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords(this.state.selectedLinearElement, { x: sceneX, y: sceneY }, this.state, this.scene.getNonDeletedElementsMap());
3470
+ const midPoint = hitCoords
3471
+ ? LinearElementEditor.getSegmentMidPointIndex(this.state.selectedLinearElement, this.state, hitCoords, this.scene.getNonDeletedElementsMap())
3472
+ : -1;
3473
+ if (midPoint && midPoint > -1) {
3474
+ this.store.shouldCaptureIncrement();
3475
+ LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
3476
+ const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords({
3477
+ ...this.state.selectedLinearElement,
3478
+ segmentMidPointHoveredCoords: null,
3479
+ }, { x: sceneX, y: sceneY }, this.state, this.scene.getNonDeletedElementsMap());
3480
+ const nextIndex = nextCoords
3481
+ ? LinearElementEditor.getSegmentMidPointIndex(this.state.selectedLinearElement, this.state, nextCoords, this.scene.getNonDeletedElementsMap())
3482
+ : null;
3483
+ this.setState({
3484
+ selectedLinearElement: {
3485
+ ...this.state.selectedLinearElement,
3486
+ pointerDownState: {
3487
+ ...this.state.selectedLinearElement.pointerDownState,
3488
+ segmentMidpoint: {
3489
+ index: nextIndex,
3490
+ value: hitCoords,
3491
+ added: false,
3492
+ },
3493
+ },
3494
+ segmentMidPointHoveredCoords: nextCoords,
3495
+ },
3496
+ });
3497
+ return;
3498
+ }
3499
+ }
3462
3500
  }
3463
3501
  if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
3464
3502
  this.startImageCropping(selectedElements[0]);
3465
3503
  return;
3466
3504
  }
3467
3505
  resetCursor(this.interactiveCanvas);
3468
- let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(event, this.state);
3469
3506
  const selectedGroupIds = getSelectedGroupIds(this.state);
3470
3507
  if (selectedGroupIds.length > 0) {
3471
3508
  const hitElement = this.getElementAtPosition(sceneX, sceneY);
@@ -3691,7 +3728,7 @@ class App extends React.Component {
3691
3728
  });
3692
3729
  }
3693
3730
  if (editingLinearElement?.lastUncommittedPoint != null) {
3694
- this.maybeSuggestBindingAtCursor(scenePointer);
3731
+ this.maybeSuggestBindingAtCursor(scenePointer, editingLinearElement.elbowed);
3695
3732
  }
3696
3733
  else {
3697
3734
  // causes stack overflow if not sync
@@ -3708,7 +3745,7 @@ class App extends React.Component {
3708
3745
  this.maybeSuggestBindingsForLinearElementAtCoords(newElement, [scenePointer], this.state.startBoundElement);
3709
3746
  }
3710
3747
  else {
3711
- this.maybeSuggestBindingAtCursor(scenePointer);
3748
+ this.maybeSuggestBindingAtCursor(scenePointer, false);
3712
3749
  }
3713
3750
  }
3714
3751
  if (this.state.multiElement) {
@@ -3760,24 +3797,15 @@ class App extends React.Component {
3760
3797
  if (isPathALoop(points, this.state.zoom.value)) {
3761
3798
  setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
3762
3799
  }
3763
- if (isElbowArrow(multiElement)) {
3764
- mutateElbowArrow(multiElement, this.scene.getNonDeletedElementsMap(), [
3800
+ // update last uncommitted point
3801
+ mutateElement(multiElement, {
3802
+ points: [
3765
3803
  ...points.slice(0, -1),
3766
3804
  pointFrom(lastCommittedX + dxFromLastCommitted, lastCommittedY + dyFromLastCommitted),
3767
- ], undefined, undefined, {
3768
- isDragging: true,
3769
- informMutation: false,
3770
- });
3771
- }
3772
- else {
3773
- // update last uncommitted point
3774
- mutateElement(multiElement, {
3775
- points: [
3776
- ...points.slice(0, -1),
3777
- pointFrom(lastCommittedX + dxFromLastCommitted, lastCommittedY + dyFromLastCommitted),
3778
- ],
3779
- }, false);
3780
- }
3805
+ ],
3806
+ }, false, {
3807
+ isDragging: true,
3808
+ });
3781
3809
  // in this path, we're mutating multiElement to reflect
3782
3810
  // how it will be after adding pointer position as the next point
3783
3811
  // trigger update here so that new element canvas renders again to reflect this
@@ -3873,7 +3901,7 @@ class App extends React.Component {
3873
3901
  activeEmbeddable: { element: hitElement, state: "hover" },
3874
3902
  });
3875
3903
  }
3876
- else {
3904
+ else if (!hitElement || !isElbowArrow(hitElement)) {
3877
3905
  setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
3878
3906
  if (this.state.activeEmbeddable?.state === "hover") {
3879
3907
  this.setState({ activeEmbeddable: null });
@@ -4001,7 +4029,11 @@ class App extends React.Component {
4001
4029
  hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(element, elementsMap, this.state.zoom, scenePointerX, scenePointerY);
4002
4030
  segmentMidPointHoveredCoords =
4003
4031
  LinearElementEditor.getSegmentMidpointHitCoords(linearElementEditor, { x: scenePointerX, y: scenePointerY }, this.state, this.scene.getNonDeletedElementsMap());
4004
- if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
4032
+ const isHoveringAPointHandle = isElbowArrow(element)
4033
+ ? hoverPointIndex === 0 ||
4034
+ hoverPointIndex === element.points.length - 1
4035
+ : hoverPointIndex >= 0;
4036
+ if (isHoveringAPointHandle || segmentMidPointHoveredCoords) {
4005
4037
  setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
4006
4038
  }
4007
4039
  else if (this.hitElement(scenePointerX, scenePointerY, element)) {
@@ -4009,7 +4041,9 @@ class App extends React.Component {
4009
4041
  }
4010
4042
  }
4011
4043
  else if (this.hitElement(scenePointerX, scenePointerY, element)) {
4012
- if (!isElbowArrow(element) ||
4044
+ if (
4045
+ // Ebow arrows can only be moved when unconnected
4046
+ !isElbowArrow(element) ||
4013
4047
  !(element.startBinding || element.endBinding)) {
4014
4048
  setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
4015
4049
  }
@@ -4036,6 +4070,12 @@ class App extends React.Component {
4036
4070
  }
4037
4071
  }
4038
4072
  handleCanvasPointerDown = (event) => {
4073
+ const target = event.target;
4074
+ // capture subsequent pointer events to the canvas
4075
+ // this makes other elements non-interactive until pointer up
4076
+ if (target.setPointerCapture) {
4077
+ target.setPointerCapture(event.pointerId);
4078
+ }
4039
4079
  this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
4040
4080
  this.maybeUnfollowRemoteUser();
4041
4081
  if (this.state.searchMatches) {
@@ -4045,7 +4085,7 @@ class App extends React.Component {
4045
4085
  focus: false,
4046
4086
  })),
4047
4087
  }));
4048
- jotaiStore.set(searchItemInFocusAtom, null);
4088
+ editorJotaiStore.set(searchItemInFocusAtom, null);
4049
4089
  }
4050
4090
  // since contextMenu options are potentially evaluated on each render,
4051
4091
  // and an contextMenu action may depend on selection state, we must
@@ -4541,6 +4581,7 @@ class App extends React.Component {
4541
4581
  const selectedElements = this.scene.getSelectedElements(this.state);
4542
4582
  if (selectedElements.length === 1 &&
4543
4583
  !this.state.editingLinearElement &&
4584
+ !isElbowArrow(selectedElements[0]) &&
4544
4585
  !(this.state.selectedLinearElement &&
4545
4586
  this.state.selectedLinearElement.hoverPointIndex !== -1)) {
4546
4587
  const elementWithTransformHandleType = getElementWithTransformHandleType(elements, this.state, pointerDownState.origin.x, pointerDownState.origin.y, this.state.zoom, event.pointerType, this.scene.getNonDeletedElementsMap(), this.device);
@@ -4837,7 +4878,7 @@ class App extends React.Component {
4837
4878
  selectedElementIds: makeNextSelectedElementIds(nextSelectedElementIds, prevState),
4838
4879
  };
4839
4880
  });
4840
- const boundElement = getHoveredElementForBinding(pointerDownState.origin, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap());
4881
+ const boundElement = getHoveredElementForBinding(pointerDownState.origin, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), this.state.zoom);
4841
4882
  this.setState({
4842
4883
  newElement: element,
4843
4884
  startBoundElement: boundElement,
@@ -5007,6 +5048,9 @@ class App extends React.Component {
5007
5048
  locked: false,
5008
5049
  frameId: topLayerFrame ? topLayerFrame.id : null,
5009
5050
  elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
5051
+ fixedSegments: this.state.currentItemArrowType === ARROW_TYPE.elbow
5052
+ ? []
5053
+ : null,
5010
5054
  })
5011
5055
  : newLinearElement({
5012
5056
  type: elementType,
@@ -5037,7 +5081,7 @@ class App extends React.Component {
5037
5081
  mutateElement(element, {
5038
5082
  points: [...element.points, pointFrom(0, 0)],
5039
5083
  });
5040
- const boundElement = getHoveredElementForBinding(pointerDownState.origin, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), isElbowArrow(element));
5084
+ const boundElement = getHoveredElementForBinding(pointerDownState.origin, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), this.state.zoom, isElbowArrow(element), isElbowArrow(element));
5041
5085
  this.scene.insertElement(element);
5042
5086
  this.setState({
5043
5087
  newElement: element,
@@ -5167,6 +5211,35 @@ class App extends React.Component {
5167
5211
  return;
5168
5212
  }
5169
5213
  const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
5214
+ if (this.state.selectedLinearElement &&
5215
+ this.state.selectedLinearElement.elbowed &&
5216
+ this.state.selectedLinearElement.pointerDownState.segmentMidpoint.index) {
5217
+ const [gridX, gridY] = getGridPoint(pointerCoords.x, pointerCoords.y, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize());
5218
+ let index = this.state.selectedLinearElement.pointerDownState.segmentMidpoint
5219
+ .index;
5220
+ if (index < 0) {
5221
+ const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords({
5222
+ ...this.state.selectedLinearElement,
5223
+ segmentMidPointHoveredCoords: null,
5224
+ }, { x: gridX, y: gridY }, this.state, this.scene.getNonDeletedElementsMap());
5225
+ index = nextCoords
5226
+ ? LinearElementEditor.getSegmentMidPointIndex(this.state.selectedLinearElement, this.state, nextCoords, this.scene.getNonDeletedElementsMap())
5227
+ : -1;
5228
+ }
5229
+ const ret = LinearElementEditor.moveFixedSegment(this.state.selectedLinearElement, index, gridX, gridY, this.scene.getNonDeletedElementsMap());
5230
+ flushSync(() => {
5231
+ if (this.state.selectedLinearElement) {
5232
+ this.setState({
5233
+ selectedLinearElement: {
5234
+ ...this.state.selectedLinearElement,
5235
+ segmentMidPointHoveredCoords: ret.segmentMidPointHoveredCoords,
5236
+ pointerDownState: ret.pointerDownState,
5237
+ },
5238
+ });
5239
+ }
5240
+ });
5241
+ return;
5242
+ }
5170
5243
  const lastPointerCoords = this.lastPointerMoveCoords ?? pointerDownState.origin;
5171
5244
  this.lastPointerMoveCoords = pointerCoords;
5172
5245
  // We need to initialize dragOffsetXY only after we've updated
@@ -5370,13 +5443,12 @@ class App extends React.Component {
5370
5443
  this.maybeCacheVisibleGaps(event, selectedElements);
5371
5444
  this.maybeCacheReferenceSnapPoints(event, selectedElements);
5372
5445
  const { snapOffset, snapLines } = snapDraggedElements(originalElements, dragOffset, this, event, this.scene.getNonDeletedElementsMap());
5373
- flushSync(() => {
5374
- this.setState({ snapLines });
5375
- });
5446
+ this.setState({ snapLines });
5376
5447
  // when we're editing the name of a frame, we want the user to be
5377
5448
  // able to select and interact with the text input
5378
- !this.state.editingFrame &&
5449
+ if (!this.state.editingFrame) {
5379
5450
  dragSelectedElements(pointerDownState, selectedElements, dragOffset, this.scene, snapOffset, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize());
5451
+ }
5380
5452
  this.setState({
5381
5453
  selectedElementsAreBeingDragged: true,
5382
5454
  // element is being dragged and selectionElement that was created on pointer down
@@ -5386,7 +5458,7 @@ class App extends React.Component {
5386
5458
  if (selectedElements.length !== 1 ||
5387
5459
  !isElbowArrow(selectedElements[0])) {
5388
5460
  this.setState({
5389
- suggestedBindings: getSuggestedBindingsForArrows(selectedElements, this.scene.getNonDeletedElementsMap()),
5461
+ suggestedBindings: getSuggestedBindingsForArrows(selectedElements, this.scene.getNonDeletedElementsMap(), this.state.zoom),
5390
5462
  });
5391
5463
  }
5392
5464
  // We duplicate the selected element if alt is pressed on pointer move
@@ -5431,7 +5503,12 @@ class App extends React.Component {
5431
5503
  nextElements.push(element);
5432
5504
  }
5433
5505
  }
5434
- const nextSceneElements = [...nextElements, ...elementsToAppend];
5506
+ let nextSceneElements = [
5507
+ ...nextElements,
5508
+ ...elementsToAppend,
5509
+ ];
5510
+ const mappedNewSceneElements = this.props.onDuplicate?.(nextSceneElements, elements);
5511
+ nextSceneElements = mappedNewSceneElements || nextSceneElements;
5435
5512
  syncMovedIndices(nextSceneElements, arrayToMap(elementsToAppend));
5436
5513
  bindTextToShapeAfterDuplication(nextElements, elementsToAppend, oldIdToDuplicatedId);
5437
5514
  fixBindingsAfterDuplication(nextSceneElements, elementsToAppend, oldIdToDuplicatedId, "duplicatesServeAsOld");
@@ -5487,16 +5564,11 @@ class App extends React.Component {
5487
5564
  points: [...points, pointFrom(dx, dy)],
5488
5565
  }, false);
5489
5566
  }
5490
- else if (points.length > 1 && isElbowArrow(newElement)) {
5491
- mutateElbowArrow(newElement, elementsMap, [...points.slice(0, -1), pointFrom(dx, dy)], vector(0, 0), undefined, {
5492
- isDragging: true,
5493
- informMutation: false,
5494
- });
5495
- }
5496
- else if (points.length === 2) {
5567
+ else if (points.length === 2 ||
5568
+ (points.length > 1 && isElbowArrow(newElement))) {
5497
5569
  mutateElement(newElement, {
5498
5570
  points: [...points.slice(0, -1), pointFrom(dx, dy)],
5499
- }, false);
5571
+ }, false, { isDragging: true });
5500
5572
  }
5501
5573
  this.setState({
5502
5574
  newElement,
@@ -5537,7 +5609,7 @@ class App extends React.Component {
5537
5609
  }
5538
5610
  }
5539
5611
  const elementsWithinSelection = this.state.selectionElement
5540
- ? getElementsWithinSelection(elements, this.state.selectionElement, this.scene.getNonDeletedElementsMap())
5612
+ ? getElementsWithinSelection(elements, this.state.selectionElement, this.scene.getNonDeletedElementsMap(), false)
5541
5613
  : [];
5542
5614
  this.setState((prevState) => {
5543
5615
  const nextSelectedElementIds = {
@@ -5630,6 +5702,20 @@ class App extends React.Component {
5630
5702
  selectedElementsAreBeingDragged: false,
5631
5703
  });
5632
5704
  const elementsMap = this.scene.getNonDeletedElementsMap();
5705
+ if (pointerDownState.drag.hasOccurred &&
5706
+ pointerDownState.hit?.element?.id) {
5707
+ const element = elementsMap.get(pointerDownState.hit.element.id);
5708
+ if (isBindableElement(element)) {
5709
+ // Renormalize elbow arrows when they are changed via indirect move
5710
+ element.boundElements
5711
+ ?.filter((e) => e.type === "arrow")
5712
+ .map((e) => elementsMap.get(e.id))
5713
+ .filter((e) => isElbowArrow(e))
5714
+ .forEach((e) => {
5715
+ !!e && mutateElement(e, {}, true);
5716
+ });
5717
+ }
5718
+ }
5633
5719
  // Handle end of dragging a point of a linear element, might close a loop
5634
5720
  // and sets binding element
5635
5721
  if (this.state.editingLinearElement) {
@@ -5649,6 +5735,13 @@ class App extends React.Component {
5649
5735
  }
5650
5736
  }
5651
5737
  else if (this.state.selectedLinearElement) {
5738
+ // Normalize elbow arrow points, remove close parallel segments
5739
+ if (this.state.selectedLinearElement.elbowed) {
5740
+ const element = LinearElementEditor.getElement(this.state.selectedLinearElement.elementId, this.scene.getNonDeletedElementsMap());
5741
+ if (element) {
5742
+ mutateElement(element, {}, true);
5743
+ }
5744
+ }
5652
5745
  if (pointerDownState.hit?.element?.id !==
5653
5746
  this.state.selectedLinearElement.elementId) {
5654
5747
  const selectedELements = this.scene.getSelectedElements(this.state);
@@ -5805,7 +5898,7 @@ class App extends React.Component {
5805
5898
  }
5806
5899
  if (isFrameLikeElement(newElement)) {
5807
5900
  const elementsInsideFrame = getElementsInNewFrame(this.scene.getElementsIncludingDeleted(), newElement, this.scene.getNonDeletedElementsMap());
5808
- this.scene.replaceAllElements(addElementsToFrame(this.scene.getElementsMapIncludingDeleted(), elementsInsideFrame, newElement));
5901
+ this.scene.replaceAllElements(addElementsToFrame(this.scene.getElementsMapIncludingDeleted(), elementsInsideFrame, newElement, this.state));
5809
5902
  }
5810
5903
  if (newElement) {
5811
5904
  mutateElement(newElement, getNormalizedDimensions(newElement));
@@ -5868,7 +5961,7 @@ class App extends React.Component {
5868
5961
  if (this.state.editingGroupId) {
5869
5962
  updateGroupIdsAfterEditingGroup(elementsToAdd);
5870
5963
  }
5871
- nextElements = addElementsToFrame(nextElements, elementsToAdd, topLayerFrame);
5964
+ nextElements = addElementsToFrame(nextElements, elementsToAdd, topLayerFrame, this.state);
5872
5965
  }
5873
5966
  else if (!topLayerFrame) {
5874
5967
  if (this.state.editingGroupId) {
@@ -5909,10 +6002,10 @@ class App extends React.Component {
5909
6002
  const hitElement = pointerDownState.hit.element;
5910
6003
  if (this.state.selectedLinearElement?.elementId !== hitElement?.id &&
5911
6004
  isLinearElement(hitElement)) {
5912
- const selectedELements = this.scene.getSelectedElements(this.state);
6005
+ const selectedElements = this.scene.getSelectedElements(this.state);
5913
6006
  // set selectedLinearElement when no other element selected except
5914
6007
  // the one we've hit
5915
- if (selectedELements.length === 1) {
6008
+ if (selectedElements.length === 1) {
5916
6009
  this.setState({
5917
6010
  selectedLinearElement: new LinearElementEditor(hitElement),
5918
6011
  });
@@ -6059,8 +6152,10 @@ class App extends React.Component {
6059
6152
  }
6060
6153
  }
6061
6154
  if (
6062
- // not dragged
6063
- !pointerDownState.drag.hasOccurred &&
6155
+ // not elbow midpoint dragged
6156
+ !(hitElement && isElbowArrow(hitElement)) &&
6157
+ // not dragged
6158
+ !pointerDownState.drag.hasOccurred &&
6064
6159
  // not resized
6065
6160
  !this.state.isResizing &&
6066
6161
  // only hitting the bounding box of the previous hit element
@@ -6118,7 +6213,7 @@ class App extends React.Component {
6118
6213
  const linearElements = this.scene
6119
6214
  .getSelectedElements(this.state)
6120
6215
  .filter(isLinearElement);
6121
- bindOrUnbindLinearElements(linearElements, this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElements(), this.scene, isBindingEnabled(this.state), this.state.selectedLinearElement?.selectedPointsIndices ?? []);
6216
+ bindOrUnbindLinearElements(linearElements, this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElements(), this.scene, isBindingEnabled(this.state), this.state.selectedLinearElement?.selectedPointsIndices ?? [], this.state.zoom);
6122
6217
  }
6123
6218
  if (activeTool.type === "laser") {
6124
6219
  this.laserTrails.endPath();
@@ -6465,8 +6560,8 @@ class App extends React.Component {
6465
6560
  this.setState({ isBindingEnabled: shouldEnableBinding });
6466
6561
  }
6467
6562
  };
6468
- maybeSuggestBindingAtCursor = (pointerCoords) => {
6469
- const hoveredBindableElement = getHoveredElementForBinding(pointerCoords, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap());
6563
+ maybeSuggestBindingAtCursor = (pointerCoords, considerAll) => {
6564
+ const hoveredBindableElement = getHoveredElementForBinding(pointerCoords, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), this.state.zoom, false, considerAll);
6470
6565
  this.setState({
6471
6566
  suggestedBindings: hoveredBindableElement != null ? [hoveredBindableElement] : [],
6472
6567
  });
@@ -6481,7 +6576,7 @@ class App extends React.Component {
6481
6576
  return;
6482
6577
  }
6483
6578
  const suggestedBindings = pointerCoords.reduce((acc, coords) => {
6484
- const hoveredBindableElement = getHoveredElementForBinding(coords, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), isArrowElement(linearElement) && isElbowArrow(linearElement));
6579
+ const hoveredBindableElement = getHoveredElementForBinding(coords, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), this.state.zoom, isElbowArrow(linearElement), isElbowArrow(linearElement));
6485
6580
  if (hoveredBindableElement != null &&
6486
6581
  !isLinearElementSimpleAndAlreadyBound(linearElement, oppositeBindingBoundElement?.id, hoveredBindableElement)) {
6487
6582
  acc.push(hoveredBindableElement);
@@ -6798,7 +6893,7 @@ class App extends React.Component {
6798
6893
  }
6799
6894
  const transformHandleType = pointerDownState.resize.handleType;
6800
6895
  const pointerCoords = pointerDownState.lastCoords;
6801
- const [x, y] = getGridPoint(pointerCoords.x - pointerDownState.resize.offset.x, pointerCoords.y - pointerDownState.resize.offset.y, this.getEffectiveGridSize());
6896
+ const [x, y] = getGridPoint(pointerCoords.x - pointerDownState.resize.offset.x, pointerCoords.y - pointerDownState.resize.offset.y, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize());
6802
6897
  const croppingElement = this.scene
6803
6898
  .getNonDeletedElementsMap()
6804
6899
  .get(this.state.croppingElementId);
@@ -6812,7 +6907,14 @@ class App extends React.Component {
6812
6907
  isImageElement(croppingAtStateStart) &&
6813
6908
  image &&
6814
6909
  !(image instanceof Promise)) {
6815
- mutateElement(croppingElement, cropElement(croppingElement, transformHandleType, image.naturalWidth, image.naturalHeight, x, y, event.shiftKey
6910
+ const [gridX, gridY] = getGridPoint(pointerCoords.x, pointerCoords.y, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize());
6911
+ const dragOffset = {
6912
+ x: gridX - pointerDownState.originInGrid.x,
6913
+ y: gridY - pointerDownState.originInGrid.y,
6914
+ };
6915
+ this.maybeCacheReferenceSnapPoints(event, [croppingElement]);
6916
+ const { snapOffset, snapLines } = snapResizingElements([croppingElement], [croppingAtStateStart], this, event, dragOffset, transformHandleType);
6917
+ mutateElement(croppingElement, cropElement(croppingElement, transformHandleType, image.naturalWidth, image.naturalHeight, x + snapOffset.x, y + snapOffset.y, event.shiftKey
6816
6918
  ? croppingAtStateStart.width / croppingAtStateStart.height
6817
6919
  : undefined));
6818
6920
  updateBoundElements(croppingElement, this.scene.getNonDeletedElementsMap(), {
@@ -6823,6 +6925,7 @@ class App extends React.Component {
6823
6925
  });
6824
6926
  this.setState({
6825
6927
  isCropping: transformHandleType && transformHandleType !== "rotation",
6928
+ snapLines,
6826
6929
  });
6827
6930
  }
6828
6931
  return true;
@@ -6879,10 +6982,10 @@ class App extends React.Component {
6879
6982
  snapLines,
6880
6983
  });
6881
6984
  }
6882
- if (transformElements(pointerDownState.originalElements, transformHandleType, selectedElements, this.scene.getElementsMapIncludingDeleted(), shouldRotateWithDiscreteAngle(event), shouldResizeFromCenter(event), selectedElements.some((element) => isImageElement(element))
6985
+ if (transformElements(pointerDownState.originalElements, transformHandleType, selectedElements, this.scene.getElementsMapIncludingDeleted(), this.scene, shouldRotateWithDiscreteAngle(event), shouldResizeFromCenter(event), selectedElements.some((element) => isImageElement(element))
6883
6986
  ? !shouldMaintainAspectRatio(event)
6884
6987
  : shouldMaintainAspectRatio(event), resizeX, resizeY, pointerDownState.resize.center.x, pointerDownState.resize.center.y)) {
6885
- const suggestedBindings = getSuggestedBindingsForArrows(selectedElements, this.scene.getNonDeletedElementsMap());
6988
+ const suggestedBindings = getSuggestedBindingsForArrows(selectedElements, this.scene.getNonDeletedElementsMap(), this.state.zoom);
6886
6989
  const elementsToHighlight = new Set();
6887
6990
  selectedFrames.forEach((frame) => {
6888
6991
  getElementsInResizingFrame(this.scene.getNonDeletedElements(), frame, this.state, this.scene.getNonDeletedElementsMap()).forEach((element) => elementsToHighlight.add(element));
@@ -6938,8 +7041,10 @@ class App extends React.Component {
6938
7041
  actionCut,
6939
7042
  actionCopy,
6940
7043
  actionPaste,
7044
+ CONTEXT_MENU_SEPARATOR,
6941
7045
  actionSelectAllElementsInFrame,
6942
7046
  actionRemoveAllElementsFromFrame,
7047
+ actionWrapSelectionInFrame,
6943
7048
  CONTEXT_MENU_SEPARATOR,
6944
7049
  actionToggleCropEditor,
6945
7050
  CONTEXT_MENU_SEPARATOR,