Browse Source

fix: remove scene from getElementAbsoluteCoords and dependent functions and use elementsMap (#7663)

* fix: remove scene from getElementAbsoluteCoords and dependent functions and use elementsMap

* lint

* fix

* use non deleted elements where possible

* use non deleted elements map in actions

* pass elementsMap instead of array to elementOverlapsWithFrame

* lint

* fix

* pass elementsMap to getElementsCorners

* pass elementsMap to getEligibleElementsForBinding

* pass elementsMap in bindOrUnbindSelectedElements and unbindLinearElements

* pass elementsMap in elementsAreInFrameBounds,elementOverlapsWithFrame,isCursorInFrame,getElementsInResizingFrame

* pass elementsMap in getElementsWithinSelection, getElementsCompletelyInFrame, isElementContainingFrame, getElementsInNewFrame

* pass elementsMap to getElementWithTransformHandleType

* pass elementsMap to getVisibleGaps, getMaximumGroups,getReferenceSnapPoints,snapDraggedElements

* lint

* pass elementsMap to bindTextToShapeAfterDuplication,bindLinearElementToElement,getTextBindableContainerAtPosition

* revert changes for bindTextToShapeAfterDuplication
Aakansha Doshi 1 year ago
parent
commit
47f87f4ecb
36 changed files with 779 additions and 270 deletions
  1. 15 3
      packages/excalidraw/actions/actionBoundText.tsx
  2. 6 1
      packages/excalidraw/actions/actionFinalize.tsx
  3. 1 1
      packages/excalidraw/actions/actionFlip.ts
  4. 8 1
      packages/excalidraw/actions/actionGroup.tsx
  5. 4 0
      packages/excalidraw/actions/actionProperties.tsx
  6. 5 1
      packages/excalidraw/actions/actionStyles.ts
  7. 91 37
      packages/excalidraw/components/App.tsx
  8. 1 0
      packages/excalidraw/data/restore.ts
  9. 8 2
      packages/excalidraw/data/transform.ts
  10. 6 3
      packages/excalidraw/element/ElementCanvasButtons.tsx
  11. 37 12
      packages/excalidraw/element/Hyperlink.tsx
  12. 96 22
      packages/excalidraw/element/binding.ts
  13. 9 8
      packages/excalidraw/element/bounds.test.ts
  14. 13 5
      packages/excalidraw/element/bounds.ts
  15. 70 20
      packages/excalidraw/element/collision.ts
  16. 1 1
      packages/excalidraw/element/dragElements.ts
  17. 68 15
      packages/excalidraw/element/linearElementEditor.ts
  18. 7 3
      packages/excalidraw/element/newElement.ts
  19. 19 10
      packages/excalidraw/element/resizeElements.ts
  20. 5 1
      packages/excalidraw/element/resizeTest.ts
  21. 27 8
      packages/excalidraw/element/textElement.ts
  22. 11 5
      packages/excalidraw/element/textWysiwyg.tsx
  23. 4 1
      packages/excalidraw/element/transformHandles.ts
  24. 55 31
      packages/excalidraw/frame.ts
  25. 25 12
      packages/excalidraw/renderer/renderElement.ts
  26. 40 13
      packages/excalidraw/renderer/renderScene.ts
  27. 2 4
      packages/excalidraw/scene/Fonts.ts
  28. 2 1
      packages/excalidraw/scene/export.ts
  29. 4 2
      packages/excalidraw/scene/selection.ts
  30. 24 16
      packages/excalidraw/snapping.ts
  31. 7 2
      packages/excalidraw/tests/binding.test.tsx
  32. 9 4
      packages/excalidraw/tests/flip.test.tsx
  33. 7 3
      packages/excalidraw/tests/helpers/ui.ts
  34. 76 18
      packages/excalidraw/tests/linearElementEditor.test.tsx
  35. 3 1
      packages/excalidraw/tests/move.test.tsx
  36. 13 3
      packages/excalidraw/tests/resize.test.tsx

+ 15 - 3
packages/excalidraw/actions/actionBoundText.tsx

@@ -58,7 +58,11 @@ export const actionUnbindText = register({
           element.id,
         );
         resetOriginalContainerCache(element.id);
-        const { x, y } = computeBoundTextPosition(element, boundTextElement);
+        const { x, y } = computeBoundTextPosition(
+          element,
+          boundTextElement,
+          elementsMap,
+        );
         mutateElement(boundTextElement as ExcalidrawTextElement, {
           containerId: null,
           width,
@@ -145,7 +149,11 @@ export const actionBindText = register({
       }),
     });
     const originalContainerHeight = container.height;
-    redrawTextBoundingBox(textElement, container);
+    redrawTextBoundingBox(
+      textElement,
+      container,
+      app.scene.getNonDeletedElementsMap(),
+    );
     // overwritting the cache with original container height so
     // it can be restored when unbind
     updateOriginalContainerCache(container.id, originalContainerHeight);
@@ -286,7 +294,11 @@ export const actionWrapTextInContainer = register({
           },
           false,
         );
-        redrawTextBoundingBox(textElement, container);
+        redrawTextBoundingBox(
+          textElement,
+          container,
+          app.scene.getNonDeletedElementsMap(),
+        );
 
         updatedElements = pushContainerBelowText(
           [...updatedElements, container],

+ 6 - 1
packages/excalidraw/actions/actionFinalize.tsx

@@ -1,6 +1,6 @@
 import { KEYS } from "../keys";
 import { isInvisiblySmallElement } from "../element";
-import { updateActiveTool } from "../utils";
+import { arrayToMap, updateActiveTool } from "../utils";
 import { ToolButton } from "../components/ToolButton";
 import { done } from "../components/icons";
 import { t } from "../i18n";
@@ -26,6 +26,8 @@ export const actionFinalize = register({
     _,
     { interactiveCanvas, focusContainer, scene },
   ) => {
+    const elementsMap = arrayToMap(elements);
+
     if (appState.editingLinearElement) {
       const { elementId, startBindingElement, endBindingElement } =
         appState.editingLinearElement;
@@ -37,6 +39,7 @@ export const actionFinalize = register({
             element,
             startBindingElement,
             endBindingElement,
+            elementsMap,
           );
         }
         return {
@@ -125,12 +128,14 @@ export const actionFinalize = register({
         const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
           multiPointElement,
           -1,
+          arrayToMap(elements),
         );
         maybeBindLinearElement(
           multiPointElement,
           appState,
           Scene.getScene(multiPointElement)!,
           { x, y },
+          elementsMap,
         );
       }
     }

+ 1 - 1
packages/excalidraw/actions/actionFlip.ts

@@ -115,7 +115,7 @@ const flipElements = (
 
   (isBindingEnabled(appState)
     ? bindOrUnbindSelectedElements
-    : unbindLinearElements)(selectedElements);
+    : unbindLinearElements)(selectedElements, elementsMap);
 
   return selectedElements;
 };

+ 8 - 1
packages/excalidraw/actions/actionGroup.tsx

@@ -180,6 +180,8 @@ export const actionUngroup = register({
   trackEvent: { category: "element" },
   perform: (elements, appState, _, app) => {
     const groupIds = getSelectedGroupIds(appState);
+    const elementsMap = arrayToMap(elements);
+
     if (groupIds.length === 0) {
       return { appState, elements, commitToHistory: false };
     }
@@ -226,7 +228,12 @@ export const actionUngroup = register({
       if (frame) {
         nextElements = replaceAllElementsInFrame(
           nextElements,
-          getElementsInResizingFrame(nextElements, frame, appState),
+          getElementsInResizingFrame(
+            nextElements,
+            frame,
+            appState,
+            elementsMap,
+          ),
           frame,
           app,
         );

+ 4 - 0
packages/excalidraw/actions/actionProperties.tsx

@@ -209,6 +209,7 @@ const changeFontSize = (
           redrawTextBoundingBox(
             newElement,
             app.scene.getContainerElement(oldElement),
+            app.scene.getNonDeletedElementsMap(),
           );
 
           newElement = offsetElementAfterFontResize(oldElement, newElement);
@@ -730,6 +731,7 @@ export const actionChangeFontFamily = register({
             redrawTextBoundingBox(
               newElement,
               app.scene.getContainerElement(oldElement),
+              app.scene.getNonDeletedElementsMap(),
             );
             return newElement;
           }
@@ -829,6 +831,7 @@ export const actionChangeTextAlign = register({
             redrawTextBoundingBox(
               newElement,
               app.scene.getContainerElement(oldElement),
+              app.scene.getNonDeletedElementsMap(),
             );
             return newElement;
           }
@@ -918,6 +921,7 @@ export const actionChangeVerticalAlign = register({
             redrawTextBoundingBox(
               newElement,
               app.scene.getContainerElement(oldElement),
+              app.scene.getNonDeletedElementsMap(),
             );
             return newElement;
           }

+ 5 - 1
packages/excalidraw/actions/actionStyles.ts

@@ -128,7 +128,11 @@ export const actionPasteStyles = register({
                     element.id === newElement.containerId,
                 ) || null;
             }
-            redrawTextBoundingBox(newElement, container);
+            redrawTextBoundingBox(
+              newElement,
+              container,
+              app.scene.getNonDeletedElementsMap(),
+            );
           }
 
           if (

+ 91 - 37
packages/excalidraw/components/App.tsx

@@ -1536,6 +1536,7 @@ class App extends React.Component<AppProps, AppState> {
                             <Hyperlink
                               key={firstSelectedElement.id}
                               element={firstSelectedElement}
+                              elementsMap={allElementsMap}
                               setAppState={this.setAppState}
                               onLinkOpen={this.props.onLinkOpen}
                               setToast={this.setToast}
@@ -1549,6 +1550,7 @@ class App extends React.Component<AppProps, AppState> {
                           isMagicFrameElement(firstSelectedElement) && (
                             <ElementCanvasButtons
                               element={firstSelectedElement}
+                              elementsMap={elementsMap}
                             >
                               <ElementCanvasButton
                                 title={t("labels.convertToCode")}
@@ -1569,6 +1571,7 @@ class App extends React.Component<AppProps, AppState> {
                             ?.status === "done" && (
                             <ElementCanvasButtons
                               element={firstSelectedElement}
+                              elementsMap={elementsMap}
                             >
                               <ElementCanvasButton
                                 title={t("labels.copySource")}
@@ -2599,10 +2602,10 @@ class App extends React.Component<AppProps, AppState> {
 
   componentDidUpdate(prevProps: AppProps, prevState: AppState) {
     this.updateEmbeddables();
-    if (
-      !this.state.showWelcomeScreen &&
-      !this.scene.getElementsIncludingDeleted().length
-    ) {
+    const elements = this.scene.getElementsIncludingDeleted();
+    const elementsMap = this.scene.getElementsMapIncludingDeleted();
+
+    if (!this.state.showWelcomeScreen && !elements.length) {
       this.setState({ showWelcomeScreen: true });
     }
 
@@ -2756,27 +2759,21 @@ class App extends React.Component<AppProps, AppState> {
           LinearElementEditor.getPointAtIndexGlobalCoordinates(
             multiElement,
             -1,
+            elementsMap,
           ),
         ),
+        elementsMap,
       );
     }
-    this.history.record(this.state, this.scene.getElementsIncludingDeleted());
+    this.history.record(this.state, elements);
 
     // Do not notify consumers if we're still loading the scene. Among other
     // potential issues, this fixes a case where the tab isn't focused during
     // init, which would trigger onChange with empty elements, which would then
     // override whatever is in localStorage currently.
     if (!this.state.isLoading) {
-      this.props.onChange?.(
-        this.scene.getElementsIncludingDeleted(),
-        this.state,
-        this.files,
-      );
-      this.onChangeEmitter.trigger(
-        this.scene.getElementsIncludingDeleted(),
-        this.state,
-        this.files,
-      );
+      this.props.onChange?.(elements, this.state, this.files);
+      this.onChangeEmitter.trigger(elements, this.state, this.files);
     }
   }
 
@@ -3126,7 +3123,11 @@ class App extends React.Component<AppProps, AppState> {
           newElement,
           this.scene.getElementsMapIncludingDeleted(),
         );
-        redrawTextBoundingBox(newElement, container);
+        redrawTextBoundingBox(
+          newElement,
+          container,
+          this.scene.getElementsMapIncludingDeleted(),
+        );
       }
     });
 
@@ -3836,7 +3837,7 @@ class App extends React.Component<AppProps, AppState> {
             y: element.y + offsetY,
           });
 
-          updateBoundElements(element, {
+          updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
             simultaneouslyUpdated: selectedElements,
           });
         });
@@ -4010,9 +4011,10 @@ class App extends React.Component<AppProps, AppState> {
     }
     if (isArrowKey(event.key)) {
       const selectedElements = this.scene.getSelectedElements(this.state);
+      const elementsMap = this.scene.getNonDeletedElementsMap();
       isBindingEnabled(this.state)
-        ? bindOrUnbindSelectedElements(selectedElements)
-        : unbindLinearElements(selectedElements);
+        ? bindOrUnbindSelectedElements(selectedElements, elementsMap)
+        : unbindLinearElements(selectedElements, elementsMap);
       this.setState({ suggestedBindings: [] });
     }
   });
@@ -4193,20 +4195,21 @@ class App extends React.Component<AppProps, AppState> {
       isExistingElement?: boolean;
     },
   ) {
+    const elementsMap = this.scene.getElementsMapIncludingDeleted();
+
     const updateElement = (
       text: string,
       originalText: string,
       isDeleted: boolean,
     ) => {
       this.scene.replaceAllElements([
+        // Not sure why we include deleted elements as well hence using deleted elements map
         ...this.scene.getElementsIncludingDeleted().map((_element) => {
           if (_element.id === element.id && isTextElement(_element)) {
             return updateTextElement(
               _element,
-              getContainerElement(
-                _element,
-                this.scene.getElementsMapIncludingDeleted(),
-              ),
+              getContainerElement(_element, elementsMap),
+              elementsMap,
               {
                 text,
                 isDeleted,
@@ -4238,7 +4241,7 @@ class App extends React.Component<AppProps, AppState> {
       onChange: withBatchedUpdates((text) => {
         updateElement(text, text, false);
         if (isNonDeletedElement(element)) {
-          updateBoundElements(element);
+          updateBoundElements(element, elementsMap);
         }
       }),
       onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
@@ -4377,6 +4380,7 @@ class App extends React.Component<AppProps, AppState> {
                   !(isTextElement(element) && element.containerId)),
             );
 
+    const elementsMap = this.scene.getNonDeletedElementsMap();
     return getElementsAtPosition(elements, (element) =>
       hitTest(
         element,
@@ -4384,7 +4388,7 @@ class App extends React.Component<AppProps, AppState> {
         this.frameNameBoundsCache,
         x,
         y,
-        this.scene.getNonDeletedElementsMap(),
+        elementsMap,
       ),
     ).filter((element) => {
       // hitting a frame's element from outside the frame is not considered a hit
@@ -4392,7 +4396,7 @@ class App extends React.Component<AppProps, AppState> {
       return containingFrame &&
         this.state.frameRendering.enabled &&
         this.state.frameRendering.clip
-        ? isCursorInFrame({ x, y }, containingFrame)
+        ? isCursorInFrame({ x, y }, containingFrame, elementsMap)
         : true;
     });
   }
@@ -4637,6 +4641,7 @@ class App extends React.Component<AppProps, AppState> {
         this.state,
         sceneX,
         sceneY,
+        this.scene.getNonDeletedElementsMap(),
       );
 
       if (container) {
@@ -4648,6 +4653,7 @@ class App extends React.Component<AppProps, AppState> {
             this.state,
             this.frameNameBoundsCache,
             [sceneX, sceneY],
+            this.scene.getNonDeletedElementsMap(),
           )
         ) {
           const midPoint = getContainerCenter(
@@ -4688,6 +4694,7 @@ class App extends React.Component<AppProps, AppState> {
         index <= hitElementIndex &&
         isPointHittingLink(
           element,
+          this.scene.getNonDeletedElementsMap(),
           this.state,
           [scenePointer.x, scenePointer.y],
           this.device.editor.isMobile,
@@ -4718,8 +4725,10 @@ class App extends React.Component<AppProps, AppState> {
       this.lastPointerDownEvent!,
       this.state,
     );
+    const elementsMap = this.scene.getNonDeletedElementsMap();
     const lastPointerDownHittingLinkIcon = isPointHittingLink(
       this.hitLinkElement,
+      elementsMap,
       this.state,
       [lastPointerDownCoords.x, lastPointerDownCoords.y],
       this.device.editor.isMobile,
@@ -4730,6 +4739,7 @@ class App extends React.Component<AppProps, AppState> {
     );
     const lastPointerUpHittingLinkIcon = isPointHittingLink(
       this.hitLinkElement,
+      elementsMap,
       this.state,
       [lastPointerUpCoords.x, lastPointerUpCoords.y],
       this.device.editor.isMobile,
@@ -4766,10 +4776,11 @@ class App extends React.Component<AppProps, AppState> {
     x: number;
     y: number;
   }) => {
+    const elementsMap = this.scene.getNonDeletedElementsMap();
     const frames = this.scene
       .getNonDeletedFramesLikes()
       .filter((frame): frame is ExcalidrawFrameLikeElement =>
-        isCursorInFrame(sceneCoords, frame),
+        isCursorInFrame(sceneCoords, frame, elementsMap),
       );
 
     return frames.length ? frames[frames.length - 1] : null;
@@ -4873,6 +4884,7 @@ class App extends React.Component<AppProps, AppState> {
           y: scenePointerY,
         },
         event,
+        this.scene.getNonDeletedElementsMap(),
       );
 
       this.setState((prevState) => {
@@ -4912,6 +4924,7 @@ class App extends React.Component<AppProps, AppState> {
         scenePointerX,
         scenePointerY,
         this.state,
+        this.scene.getNonDeletedElementsMap(),
       );
 
       if (
@@ -5062,6 +5075,7 @@ class App extends React.Component<AppProps, AppState> {
         scenePointerY,
         this.state.zoom,
         event.pointerType,
+        this.scene.getNonDeletedElementsMap(),
       );
       if (
         elementWithTransformHandleType &&
@@ -5109,7 +5123,11 @@ class App extends React.Component<AppProps, AppState> {
       !this.state.selectedElementIds[this.hitLinkElement.id]
     ) {
       setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
-      showHyperlinkTooltip(this.hitLinkElement, this.state);
+      showHyperlinkTooltip(
+        this.hitLinkElement,
+        this.state,
+        this.scene.getNonDeletedElementsMap(),
+      );
     } else {
       hideHyperlinkToolip();
       if (
@@ -5305,10 +5323,12 @@ class App extends React.Component<AppProps, AppState> {
           this.state,
           this.frameNameBoundsCache,
           [scenePointerX, scenePointerY],
+          elementsMap,
         )
       ) {
         hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
           element,
+          elementsMap,
           this.state.zoom,
           scenePointerX,
           scenePointerY,
@@ -5738,10 +5758,12 @@ class App extends React.Component<AppProps, AppState> {
       if (
         clicklength < 300 &&
         isIframeLikeElement(this.hitLinkElement) &&
-        !isPointHittingLinkIcon(this.hitLinkElement, this.state, [
-          scenePointer.x,
-          scenePointer.y,
-        ])
+        !isPointHittingLinkIcon(
+          this.hitLinkElement,
+          this.scene.getNonDeletedElementsMap(),
+          this.state,
+          [scenePointer.x, scenePointer.y],
+        )
       ) {
         this.handleEmbeddableCenterClick(this.hitLinkElement);
       } else {
@@ -6039,7 +6061,9 @@ class App extends React.Component<AppProps, AppState> {
   ): boolean => {
     if (this.state.activeTool.type === "selection") {
       const elements = this.scene.getNonDeletedElements();
+      const elementsMap = this.scene.getNonDeletedElementsMap();
       const selectedElements = this.scene.getSelectedElements(this.state);
+
       if (selectedElements.length === 1 && !this.state.editingLinearElement) {
         const elementWithTransformHandleType =
           getElementWithTransformHandleType(
@@ -6049,6 +6073,7 @@ class App extends React.Component<AppProps, AppState> {
             pointerDownState.origin.y,
             this.state.zoom,
             event.pointerType,
+            this.scene.getNonDeletedElementsMap(),
           );
         if (elementWithTransformHandleType != null) {
           this.setState({
@@ -6072,6 +6097,7 @@ class App extends React.Component<AppProps, AppState> {
           getResizeOffsetXY(
             pointerDownState.resize.handleType,
             selectedElements,
+            elementsMap,
             pointerDownState.origin.x,
             pointerDownState.origin.y,
           ),
@@ -6352,6 +6378,7 @@ class App extends React.Component<AppProps, AppState> {
       this.state,
       sceneX,
       sceneY,
+      this.scene.getNonDeletedElementsMap(),
     );
 
     if (hasBoundTextElement(element)) {
@@ -6846,6 +6873,7 @@ class App extends React.Component<AppProps, AppState> {
           this.scene.getNonDeletedElements(),
           selectedElements,
           this.state,
+          this.scene.getNonDeletedElementsMap(),
         ),
       );
     }
@@ -6869,6 +6897,7 @@ class App extends React.Component<AppProps, AppState> {
           this.scene.getNonDeletedElements(),
           selectedElements,
           this.state,
+          this.scene.getNonDeletedElementsMap(),
         ),
       );
     }
@@ -6985,6 +7014,7 @@ class App extends React.Component<AppProps, AppState> {
             pointerCoords,
             this.state,
             !event[KEYS.CTRL_OR_CMD],
+            this.scene.getNonDeletedElementsMap(),
           );
           if (!ret) {
             return;
@@ -7143,10 +7173,11 @@ class App extends React.Component<AppProps, AppState> {
           this.maybeCacheReferenceSnapPoints(event, selectedElements);
 
           const { snapOffset, snapLines } = snapDraggedElements(
-            getSelectedElements(originalElements, this.state),
+            originalElements,
             dragOffset,
             this.state,
             event,
+            this.scene.getNonDeletedElementsMap(),
           );
 
           this.setState({ snapLines });
@@ -7330,6 +7361,7 @@ class App extends React.Component<AppProps, AppState> {
             event,
             this.state,
             this.setState.bind(this),
+            this.scene.getNonDeletedElementsMap(),
           );
           // regular box-select
         } else {
@@ -7360,6 +7392,7 @@ class App extends React.Component<AppProps, AppState> {
           const elementsWithinSelection = getElementsWithinSelection(
             elements,
             draggingElement,
+            this.scene.getNonDeletedElementsMap(),
           );
 
           this.setState((prevState) => {
@@ -7491,7 +7524,7 @@ class App extends React.Component<AppProps, AppState> {
       this.setState({
         selectedElementsAreBeingDragged: false,
       });
-
+      const elementsMap = this.scene.getNonDeletedElementsMap();
       // Handle end of dragging a point of a linear element, might close a loop
       // and sets binding element
       if (this.state.editingLinearElement) {
@@ -7506,6 +7539,7 @@ class App extends React.Component<AppProps, AppState> {
             childEvent,
             this.state.editingLinearElement,
             this.state,
+            elementsMap,
           );
           if (editingLinearElement !== this.state.editingLinearElement) {
             this.setState({
@@ -7529,6 +7563,7 @@ class App extends React.Component<AppProps, AppState> {
             childEvent,
             this.state.selectedLinearElement,
             this.state,
+            elementsMap,
           );
 
           const { startBindingElement, endBindingElement } =
@@ -7539,6 +7574,7 @@ class App extends React.Component<AppProps, AppState> {
               element,
               startBindingElement,
               endBindingElement,
+              elementsMap,
             );
           }
 
@@ -7678,6 +7714,7 @@ class App extends React.Component<AppProps, AppState> {
               this.state,
               this.scene,
               pointerCoords,
+              elementsMap,
             );
           }
           this.setState({ suggestedBindings: [], startBoundElement: null });
@@ -7748,7 +7785,13 @@ class App extends React.Component<AppProps, AppState> {
               const frame = getContainingFrame(linearElement);
 
               if (frame && linearElement) {
-                if (!elementOverlapsWithFrame(linearElement, frame)) {
+                if (
+                  !elementOverlapsWithFrame(
+                    linearElement,
+                    frame,
+                    this.scene.getNonDeletedElementsMap(),
+                  )
+                ) {
                   // remove the linear element from all groups
                   // before removing it from the frame as well
                   mutateElement(linearElement, {
@@ -7859,6 +7902,7 @@ class App extends React.Component<AppProps, AppState> {
           const elementsInsideFrame = getElementsInNewFrame(
             this.scene.getElementsIncludingDeleted(),
             draggingElement,
+            this.scene.getNonDeletedElementsMap(),
           );
 
           this.scene.replaceAllElements(
@@ -7909,6 +7953,7 @@ class App extends React.Component<AppProps, AppState> {
               this.scene.getElementsIncludingDeleted(),
               frame,
               this.state,
+              elementsMap,
             ),
             frame,
             this,
@@ -8189,7 +8234,10 @@ class App extends React.Component<AppProps, AppState> {
       if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
         (isBindingEnabled(this.state)
           ? bindOrUnbindSelectedElements
-          : unbindLinearElements)(this.scene.getSelectedElements(this.state));
+          : unbindLinearElements)(
+          this.scene.getSelectedElements(this.state),
+          elementsMap,
+        );
       }
 
       if (activeTool.type === "laser") {
@@ -8719,7 +8767,10 @@ class App extends React.Component<AppProps, AppState> {
     if (selectedElements.length > 50) {
       return;
     }
-    const suggestedBindings = getEligibleElementsForBinding(selectedElements);
+    const suggestedBindings = getEligibleElementsForBinding(
+      selectedElements,
+      this.scene.getNonDeletedElementsMap(),
+    );
     this.setState({ suggestedBindings });
   }
 
@@ -9058,6 +9109,7 @@ class App extends React.Component<AppProps, AppState> {
           x: gridX - pointerDownState.originInGrid.x,
           y: gridY - pointerDownState.originInGrid.y,
         },
+        this.scene.getNonDeletedElementsMap(),
       );
 
       gridX += snapOffset.x;
@@ -9096,6 +9148,7 @@ class App extends React.Component<AppProps, AppState> {
             this.scene.getNonDeletedElements(),
             draggingElement as ExcalidrawFrameLikeElement,
             this.state,
+            this.scene.getNonDeletedElementsMap(),
           ),
         });
       }
@@ -9215,6 +9268,7 @@ class App extends React.Component<AppProps, AppState> {
           this.scene.getNonDeletedElements(),
           frame,
           this.state,
+          this.scene.getNonDeletedElementsMap(),
         ).forEach((element) => elementsToHighlight.add(element));
       });
 

+ 1 - 0
packages/excalidraw/data/restore.ts

@@ -462,6 +462,7 @@ export const restoreElements = (
         refreshTextDimensions(
           element,
           getContainerElement(element, restoredElementsMap),
+          restoredElementsMap,
         ),
       );
     }

+ 8 - 2
packages/excalidraw/data/transform.ts

@@ -222,7 +222,7 @@ const bindTextToContainer = (
     }),
   });
 
-  redrawTextBoundingBox(textElement, container);
+  redrawTextBoundingBox(textElement, container, elementsMap);
   return [container, textElement] as const;
 };
 
@@ -231,6 +231,7 @@ const bindLinearElementToElement = (
   start: ValidLinearElement["start"],
   end: ValidLinearElement["end"],
   elementStore: ElementStore,
+  elementsMap: ElementsMap,
 ): {
   linearElement: ExcalidrawLinearElement;
   startBoundElement?: ExcalidrawElement;
@@ -316,6 +317,7 @@ const bindLinearElementToElement = (
         linearElement,
         startBoundElement as ExcalidrawBindableElement,
         "start",
+        elementsMap,
       );
     }
   }
@@ -390,6 +392,7 @@ const bindLinearElementToElement = (
         linearElement,
         endBoundElement as ExcalidrawBindableElement,
         "end",
+        elementsMap,
       );
     }
   }
@@ -612,6 +615,7 @@ export const convertToExcalidrawElements = (
     }
   }
 
+  const elementsMap = arrayToMap(elementStore.getElements());
   // Add labels and arrow bindings
   for (const [id, element] of elementsWithIds) {
     const excalidrawElement = elementStore.getElement(id)!;
@@ -625,7 +629,7 @@ export const convertToExcalidrawElements = (
           let [container, text] = bindTextToContainer(
             excalidrawElement,
             element?.label,
-            arrayToMap(elementStore.getElements()),
+            elementsMap,
           );
           elementStore.add(container);
           elementStore.add(text);
@@ -653,6 +657,7 @@ export const convertToExcalidrawElements = (
                 originalStart,
                 originalEnd,
                 elementStore,
+                elementsMap,
               );
             container = linearElement;
             elementStore.add(linearElement);
@@ -677,6 +682,7 @@ export const convertToExcalidrawElements = (
                   start,
                   end,
                   elementStore,
+                  elementsMap,
                 );
 
               elementStore.add(linearElement);

+ 6 - 3
packages/excalidraw/element/ElementCanvasButtons.tsx

@@ -1,6 +1,6 @@
 import { AppState } from "../types";
 import { sceneCoordsToViewportCoords } from "../utils";
-import { NonDeletedExcalidrawElement } from "./types";
+import { ElementsMap, NonDeletedExcalidrawElement } from "./types";
 import { getElementAbsoluteCoords } from ".";
 import { useExcalidrawAppState } from "../components/App";
 
@@ -11,8 +11,9 @@ const CONTAINER_PADDING = 5;
 const getContainerCoords = (
   element: NonDeletedExcalidrawElement,
   appState: AppState,
+  elementsMap: ElementsMap,
 ) => {
-  const [x1, y1] = getElementAbsoluteCoords(element);
+  const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
   const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
     { sceneX: x1 + element.width, sceneY: y1 },
     appState,
@@ -25,9 +26,11 @@ const getContainerCoords = (
 export const ElementCanvasButtons = ({
   children,
   element,
+  elementsMap,
 }: {
   children: React.ReactNode;
   element: NonDeletedExcalidrawElement;
+  elementsMap: ElementsMap;
 }) => {
   const appState = useExcalidrawAppState();
 
@@ -42,7 +45,7 @@ export const ElementCanvasButtons = ({
     return null;
   }
 
-  const { x, y } = getContainerCoords(element, appState);
+  const { x, y } = getContainerCoords(element, appState, elementsMap);
 
   return (
     <div

+ 37 - 12
packages/excalidraw/element/Hyperlink.tsx

@@ -8,6 +8,7 @@ import {
 import { getEmbedLink, embeddableURLValidator } from "./embeddable";
 import { mutateElement } from "./mutateElement";
 import {
+  ElementsMap,
   ExcalidrawEmbeddableElement,
   NonDeletedExcalidrawElement,
 } from "./types";
@@ -60,12 +61,14 @@ const embeddableLinkCache = new Map<
 
 export const Hyperlink = ({
   element,
+  elementsMap,
   setAppState,
   onLinkOpen,
   setToast,
   updateEmbedValidationStatus,
 }: {
   element: NonDeletedExcalidrawElement;
+  elementsMap: ElementsMap;
   setAppState: React.Component<any, AppState>["setState"];
   onLinkOpen: ExcalidrawProps["onLinkOpen"];
   setToast: (
@@ -182,7 +185,7 @@ export const Hyperlink = ({
       if (timeoutId) {
         clearTimeout(timeoutId);
       }
-      const shouldHide = shouldHideLinkPopup(element, appState, [
+      const shouldHide = shouldHideLinkPopup(element, elementsMap, appState, [
         event.clientX,
         event.clientY,
       ]) as boolean;
@@ -199,7 +202,7 @@ export const Hyperlink = ({
         clearTimeout(timeoutId);
       }
     };
-  }, [appState, element, isEditing, setAppState]);
+  }, [appState, element, isEditing, setAppState, elementsMap]);
 
   const handleRemove = useCallback(() => {
     trackEvent("hyperlink", "delete");
@@ -214,7 +217,7 @@ export const Hyperlink = ({
     trackEvent("hyperlink", "edit", "popup-ui");
     setAppState({ showHyperlinkPopup: "editor" });
   };
-  const { x, y } = getCoordsForPopover(element, appState);
+  const { x, y } = getCoordsForPopover(element, appState, elementsMap);
   if (
     appState.contextMenu ||
     appState.draggingElement ||
@@ -324,8 +327,9 @@ export const Hyperlink = ({
 const getCoordsForPopover = (
   element: NonDeletedExcalidrawElement,
   appState: AppState,
+  elementsMap: ElementsMap,
 ) => {
-  const [x1, y1] = getElementAbsoluteCoords(element);
+  const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
   const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
     { sceneX: x1 + element.width / 2, sceneY: y1 },
     appState,
@@ -430,11 +434,12 @@ export const getLinkHandleFromCoords = (
 
 export const isPointHittingLinkIcon = (
   element: NonDeletedExcalidrawElement,
+  elementsMap: ElementsMap,
   appState: AppState,
   [x, y]: Point,
 ) => {
   const threshold = 4 / appState.zoom.value;
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
   const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
     [x1, y1, x2, y2],
     element.angle,
@@ -450,6 +455,7 @@ export const isPointHittingLinkIcon = (
 
 export const isPointHittingLink = (
   element: NonDeletedExcalidrawElement,
+  elementsMap: ElementsMap,
   appState: AppState,
   [x, y]: Point,
   isMobile: boolean,
@@ -461,23 +467,30 @@ export const isPointHittingLink = (
   if (
     !isMobile &&
     appState.viewModeEnabled &&
-    isPointHittingElementBoundingBox(element, [x, y], threshold, null)
+    isPointHittingElementBoundingBox(
+      element,
+      elementsMap,
+      [x, y],
+      threshold,
+      null,
+    )
   ) {
     return true;
   }
-  return isPointHittingLinkIcon(element, appState, [x, y]);
+  return isPointHittingLinkIcon(element, elementsMap, appState, [x, y]);
 };
 
 let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null;
 export const showHyperlinkTooltip = (
   element: NonDeletedExcalidrawElement,
   appState: AppState,
+  elementsMap: ElementsMap,
 ) => {
   if (HYPERLINK_TOOLTIP_TIMEOUT_ID) {
     clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID);
   }
   HYPERLINK_TOOLTIP_TIMEOUT_ID = window.setTimeout(
-    () => renderTooltip(element, appState),
+    () => renderTooltip(element, appState, elementsMap),
     HYPERLINK_TOOLTIP_DELAY,
   );
 };
@@ -485,6 +498,7 @@ export const showHyperlinkTooltip = (
 const renderTooltip = (
   element: NonDeletedExcalidrawElement,
   appState: AppState,
+  elementsMap: ElementsMap,
 ) => {
   if (!element.link) {
     return;
@@ -496,7 +510,7 @@ const renderTooltip = (
   tooltipDiv.style.maxWidth = "20rem";
   tooltipDiv.textContent = element.link;
 
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
 
   const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
     [x1, y1, x2, y2],
@@ -535,6 +549,7 @@ export const hideHyperlinkToolip = () => {
 
 export const shouldHideLinkPopup = (
   element: NonDeletedExcalidrawElement,
+  elementsMap: ElementsMap,
   appState: AppState,
   [clientX, clientY]: Point,
 ): Boolean => {
@@ -546,11 +561,17 @@ export const shouldHideLinkPopup = (
   const threshold = 15 / appState.zoom.value;
   // hitbox to prevent hiding when hovered in element bounding box
   if (
-    isPointHittingElementBoundingBox(element, [sceneX, sceneY], threshold, null)
+    isPointHittingElementBoundingBox(
+      element,
+      elementsMap,
+      [sceneX, sceneY],
+      threshold,
+      null,
+    )
   ) {
     return false;
   }
-  const [x1, y1, x2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
   // hit box to prevent hiding when hovered in the vertical area between element and popover
   if (
     sceneX >= x1 &&
@@ -561,7 +582,11 @@ export const shouldHideLinkPopup = (
     return false;
   }
   // hit box to prevent hiding when hovered around popover within threshold
-  const { x: popoverX, y: popoverY } = getCoordsForPopover(element, appState);
+  const { x: popoverX, y: popoverY } = getCoordsForPopover(
+    element,
+    appState,
+    elementsMap,
+  );
 
   if (
     clientX >= popoverX - threshold &&

+ 96 - 22
packages/excalidraw/element/binding.ts

@@ -5,6 +5,7 @@ import {
   NonDeletedExcalidrawElement,
   PointBinding,
   ExcalidrawElement,
+  ElementsMap,
 } from "./types";
 import { getElementAtPosition } from "../scene";
 import { AppState } from "../types";
@@ -66,6 +67,7 @@ export const bindOrUnbindLinearElement = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   startBindingElement: ExcalidrawBindableElement | null | "keep",
   endBindingElement: ExcalidrawBindableElement | null | "keep",
+  elementsMap: ElementsMap,
 ): void => {
   const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
   const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
@@ -76,6 +78,7 @@ export const bindOrUnbindLinearElement = (
     "start",
     boundToElementIds,
     unboundFromElementIds,
+    elementsMap,
   );
   bindOrUnbindLinearElementEdge(
     linearElement,
@@ -84,6 +87,7 @@ export const bindOrUnbindLinearElement = (
     "end",
     boundToElementIds,
     unboundFromElementIds,
+    elementsMap,
   );
 
   const onlyUnbound = Array.from(unboundFromElementIds).filter(
@@ -111,6 +115,7 @@ const bindOrUnbindLinearElementEdge = (
   boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
   // Is mutated
   unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
+  elementsMap: ElementsMap,
 ): void => {
   if (bindableElement !== "keep") {
     if (bindableElement != null) {
@@ -127,7 +132,12 @@ const bindOrUnbindLinearElementEdge = (
           : startOrEnd === "start" ||
             otherEdgeBindableElement.id !== bindableElement.id)
       ) {
-        bindLinearElement(linearElement, bindableElement, startOrEnd);
+        bindLinearElement(
+          linearElement,
+          bindableElement,
+          startOrEnd,
+          elementsMap,
+        );
         boundToElementIds.add(bindableElement.id);
       }
     } else {
@@ -140,23 +150,34 @@ const bindOrUnbindLinearElementEdge = (
 };
 
 export const bindOrUnbindSelectedElements = (
-  elements: NonDeleted<ExcalidrawElement>[],
+  selectedElements: NonDeleted<ExcalidrawElement>[],
+  elementsMap: ElementsMap,
 ): void => {
-  elements.forEach((element) => {
-    if (isBindingElement(element)) {
+  selectedElements.forEach((selectedElement) => {
+    if (isBindingElement(selectedElement)) {
       bindOrUnbindLinearElement(
-        element,
-        getElligibleElementForBindingElement(element, "start"),
-        getElligibleElementForBindingElement(element, "end"),
+        selectedElement,
+        getElligibleElementForBindingElement(
+          selectedElement,
+          "start",
+          elementsMap,
+        ),
+        getElligibleElementForBindingElement(
+          selectedElement,
+          "end",
+          elementsMap,
+        ),
+        elementsMap,
       );
-    } else if (isBindableElement(element)) {
-      maybeBindBindableElement(element);
+    } else if (isBindableElement(selectedElement)) {
+      maybeBindBindableElement(selectedElement, elementsMap);
     }
   });
 };
 
 const maybeBindBindableElement = (
   bindableElement: NonDeleted<ExcalidrawBindableElement>,
+  elementsMap: ElementsMap,
 ): void => {
   getElligibleElementsForBindableElementAndWhere(bindableElement).forEach(
     ([linearElement, where]) =>
@@ -164,6 +185,7 @@ const maybeBindBindableElement = (
         linearElement,
         where === "end" ? "keep" : bindableElement,
         where === "start" ? "keep" : bindableElement,
+        elementsMap,
       ),
   );
 };
@@ -173,9 +195,15 @@ export const maybeBindLinearElement = (
   appState: AppState,
   scene: Scene,
   pointerCoords: { x: number; y: number },
+  elementsMap: ElementsMap,
 ): void => {
   if (appState.startBoundElement != null) {
-    bindLinearElement(linearElement, appState.startBoundElement, "start");
+    bindLinearElement(
+      linearElement,
+      appState.startBoundElement,
+      "start",
+      elementsMap,
+    );
   }
   const hoveredElement = getHoveredElementForBinding(pointerCoords, scene);
   if (
@@ -186,7 +214,7 @@ export const maybeBindLinearElement = (
       "end",
     )
   ) {
-    bindLinearElement(linearElement, hoveredElement, "end");
+    bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
   }
 };
 
@@ -194,11 +222,17 @@ export const bindLinearElement = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   hoveredElement: ExcalidrawBindableElement,
   startOrEnd: "start" | "end",
+  elementsMap: ElementsMap,
 ): void => {
   mutateElement(linearElement, {
     [startOrEnd === "start" ? "startBinding" : "endBinding"]: {
       elementId: hoveredElement.id,
-      ...calculateFocusAndGap(linearElement, hoveredElement, startOrEnd),
+      ...calculateFocusAndGap(
+        linearElement,
+        hoveredElement,
+        startOrEnd,
+        elementsMap,
+      ),
     } as PointBinding,
   });
 
@@ -240,10 +274,11 @@ export const isLinearElementSimpleAndAlreadyBound = (
 
 export const unbindLinearElements = (
   elements: NonDeleted<ExcalidrawElement>[],
+  elementsMap: ElementsMap,
 ): void => {
   elements.forEach((element) => {
     if (isBindingElement(element)) {
-      bindOrUnbindLinearElement(element, null, null);
+      bindOrUnbindLinearElement(element, null, null, elementsMap);
     }
   });
 };
@@ -272,7 +307,11 @@ export const getHoveredElementForBinding = (
     scene.getNonDeletedElements(),
     (element) =>
       isBindableElement(element, false) &&
-      bindingBorderTest(element, pointerCoords),
+      bindingBorderTest(
+        element,
+        pointerCoords,
+        scene.getNonDeletedElementsMap(),
+      ),
   );
   return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
 };
@@ -281,21 +320,33 @@ const calculateFocusAndGap = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   hoveredElement: ExcalidrawBindableElement,
   startOrEnd: "start" | "end",
+  elementsMap: ElementsMap,
 ): { focus: number; gap: number } => {
   const direction = startOrEnd === "start" ? -1 : 1;
   const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
   const adjacentPointIndex = edgePointIndex - direction;
+
   const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
     linearElement,
     edgePointIndex,
+    elementsMap,
   );
   const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
     linearElement,
     adjacentPointIndex,
+    elementsMap,
   );
   return {
-    focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
-    gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
+    focus: determineFocusDistance(
+      hoveredElement,
+      adjacentPoint,
+      edgePoint,
+      elementsMap,
+    ),
+    gap: Math.max(
+      1,
+      distanceToBindableElement(hoveredElement, edgePoint, elementsMap),
+    ),
   };
 };
 
@@ -306,6 +357,8 @@ const calculateFocusAndGap = (
 // in explicitly.
 export const updateBoundElements = (
   changedElement: NonDeletedExcalidrawElement,
+  elementsMap: ElementsMap,
+
   options?: {
     simultaneouslyUpdated?: readonly ExcalidrawElement[];
     newSize?: { width: number; height: number };
@@ -355,12 +408,14 @@ export const updateBoundElements = (
       "start",
       startBinding,
       changedElement as ExcalidrawBindableElement,
+      elementsMap,
     );
     updateBoundPoint(
       element,
       "end",
       endBinding,
       changedElement as ExcalidrawBindableElement,
+      elementsMap,
     );
     const boundText = getBoundTextElement(
       element,
@@ -393,6 +448,7 @@ const updateBoundPoint = (
   startOrEnd: "start" | "end",
   binding: PointBinding | null | undefined,
   changedElement: ExcalidrawBindableElement,
+  elementsMap: ElementsMap,
 ): void => {
   if (
     binding == null ||
@@ -414,11 +470,13 @@ const updateBoundPoint = (
   const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
     linearElement,
     adjacentPointIndex,
+    elementsMap,
   );
   const focusPointAbsolute = determineFocusPoint(
     bindingElement,
     binding.focus,
     adjacentPoint,
+    elementsMap,
   );
   let newEdgePoint;
   // The linear element was not originally pointing inside the bound shape,
@@ -431,6 +489,7 @@ const updateBoundPoint = (
       adjacentPoint,
       focusPointAbsolute,
       binding.gap,
+      elementsMap,
     );
     if (intersections.length === 0) {
       // This should never happen, since focusPoint should always be
@@ -449,6 +508,7 @@ const updateBoundPoint = (
         point: LinearElementEditor.pointFromAbsoluteCoords(
           linearElement,
           newEdgePoint,
+          elementsMap,
         ),
       },
     ],
@@ -480,12 +540,14 @@ const maybeCalculateNewGapWhenScaling = (
 // TODO: this is a bottleneck, optimise
 export const getEligibleElementsForBinding = (
   elements: NonDeleted<ExcalidrawElement>[],
+  elementsMap: ElementsMap,
 ): SuggestedBinding[] => {
   const includedElementIds = new Set(elements.map(({ id }) => id));
   return elements.flatMap((element) =>
     isBindingElement(element, false)
       ? (getElligibleElementsForBindingElement(
           element as NonDeleted<ExcalidrawLinearElement>,
+          elementsMap,
         ).filter(
           (element) => !includedElementIds.has(element.id),
         ) as SuggestedBinding[])
@@ -499,10 +561,11 @@ export const getEligibleElementsForBinding = (
 
 const getElligibleElementsForBindingElement = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
+  elementsMap: ElementsMap,
 ): NonDeleted<ExcalidrawBindableElement>[] => {
   return [
-    getElligibleElementForBindingElement(linearElement, "start"),
-    getElligibleElementForBindingElement(linearElement, "end"),
+    getElligibleElementForBindingElement(linearElement, "start", elementsMap),
+    getElligibleElementForBindingElement(linearElement, "end", elementsMap),
   ].filter(
     (element): element is NonDeleted<ExcalidrawBindableElement> =>
       element != null,
@@ -512,9 +575,10 @@ const getElligibleElementsForBindingElement = (
 const getElligibleElementForBindingElement = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   startOrEnd: "start" | "end",
+  elementsMap: ElementsMap,
 ): NonDeleted<ExcalidrawBindableElement> | null => {
   return getHoveredElementForBinding(
-    getLinearElementEdgeCoors(linearElement, startOrEnd),
+    getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
     Scene.getScene(linearElement)!,
   );
 };
@@ -522,17 +586,23 @@ const getElligibleElementForBindingElement = (
 const getLinearElementEdgeCoors = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   startOrEnd: "start" | "end",
+  elementsMap: ElementsMap,
 ): { x: number; y: number } => {
   const index = startOrEnd === "start" ? 0 : -1;
   return tupleToCoors(
-    LinearElementEditor.getPointAtIndexGlobalCoordinates(linearElement, index),
+    LinearElementEditor.getPointAtIndexGlobalCoordinates(
+      linearElement,
+      index,
+      elementsMap,
+    ),
   );
 };
 
 const getElligibleElementsForBindableElementAndWhere = (
   bindableElement: NonDeleted<ExcalidrawBindableElement>,
 ): SuggestedPointBinding[] => {
-  return Scene.getScene(bindableElement)!
+  const scene = Scene.getScene(bindableElement)!;
+  return scene
     .getNonDeletedElements()
     .map((element) => {
       if (!isBindingElement(element, false)) {
@@ -542,11 +612,13 @@ const getElligibleElementsForBindableElementAndWhere = (
         element,
         "start",
         bindableElement,
+        scene.getNonDeletedElementsMap(),
       );
       const canBindEnd = isLinearElementEligibleForNewBindingByBindable(
         element,
         "end",
         bindableElement,
+        scene.getNonDeletedElementsMap(),
       );
       if (!canBindStart && !canBindEnd) {
         return null;
@@ -564,6 +636,7 @@ const isLinearElementEligibleForNewBindingByBindable = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   startOrEnd: "start" | "end",
   bindableElement: NonDeleted<ExcalidrawBindableElement>,
+  elementsMap: ElementsMap,
 ): boolean => {
   const existingBinding =
     linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"];
@@ -576,7 +649,8 @@ const isLinearElementEligibleForNewBindingByBindable = (
     ) &&
     bindingBorderTest(
       bindableElement,
-      getLinearElementEdgeCoors(linearElement, startOrEnd),
+      getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
+      elementsMap,
     )
   );
 };

+ 9 - 8
packages/excalidraw/element/bounds.test.ts

@@ -1,4 +1,5 @@
 import { ROUNDNESS } from "../constants";
+import { arrayToMap } from "../utils";
 import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
 import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
 
@@ -35,26 +36,26 @@ const _ce = ({
 
 describe("getElementAbsoluteCoords", () => {
   it("test x1 coordinate", () => {
-    const [x1] = getElementAbsoluteCoords(_ce({ x: 10, y: 0, w: 10, h: 0 }));
+    const element = _ce({ x: 10, y: 20, w: 10, h: 0 });
+    const [x1] = getElementAbsoluteCoords(element, arrayToMap([element]));
     expect(x1).toEqual(10);
   });
 
   it("test x2 coordinate", () => {
-    const [, , x2] = getElementAbsoluteCoords(
-      _ce({ x: 10, y: 0, w: 10, h: 0 }),
-    );
+    const element = _ce({ x: 10, y: 20, w: 10, h: 0 });
+    const [, , x2] = getElementAbsoluteCoords(element, arrayToMap([element]));
     expect(x2).toEqual(20);
   });
 
   it("test y1 coordinate", () => {
-    const [, y1] = getElementAbsoluteCoords(_ce({ x: 0, y: 10, w: 0, h: 10 }));
+    const element = _ce({ x: 0, y: 10, w: 0, h: 10 });
+    const [, y1] = getElementAbsoluteCoords(element, arrayToMap([element]));
     expect(y1).toEqual(10);
   });
 
   it("test y2 coordinate", () => {
-    const [, , , y2] = getElementAbsoluteCoords(
-      _ce({ x: 0, y: 10, w: 0, h: 10 }),
-    );
+    const element = _ce({ x: 0, y: 10, w: 0, h: 10 });
+    const [, , , y2] = getElementAbsoluteCoords(element, arrayToMap([element]));
     expect(y2).toEqual(20);
   });
 });

+ 13 - 5
packages/excalidraw/element/bounds.ts

@@ -102,8 +102,10 @@ export class ElementBounds {
   ): Bounds {
     let bounds: Bounds;
 
-    const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
-
+    const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
+      element,
+      elementsMap,
+    );
     if (isFreeDrawElement(element)) {
       const [minX, minY, maxX, maxY] = getBoundsFromPoints(
         element.points.map(([x, y]) =>
@@ -159,10 +161,9 @@ export class ElementBounds {
 // This set of functions retrieves the absolute position of the 4 points.
 export const getElementAbsoluteCoords = (
   element: ExcalidrawElement,
+  elementsMap: ElementsMap,
   includeBoundText: boolean = false,
 ): [number, number, number, number, number, number] => {
-  const elementsMap =
-    Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map();
   if (isFreeDrawElement(element)) {
     return getFreeDrawElementAbsoluteCoords(element);
   } else if (isLinearElement(element)) {
@@ -179,6 +180,7 @@ export const getElementAbsoluteCoords = (
       const coords = LinearElementEditor.getBoundTextElementPosition(
         container,
         element as ExcalidrawTextElementWithContainer,
+        elementsMap,
       );
       return [
         coords.x,
@@ -207,8 +209,12 @@ export const getElementAbsoluteCoords = (
  */
 export const getElementLineSegments = (
   element: ExcalidrawElement,
+  elementsMap: ElementsMap,
 ): [Point, Point][] => {
-  const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
+    element,
+    elementsMap,
+  );
 
   const center: Point = [cx, cy];
 
@@ -703,6 +709,7 @@ const getLinearElementRotatedBounds = (
     if (boundTextElement) {
       const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
         element,
+        elementsMap,
         [x, y, x, y],
         boundTextElement,
       );
@@ -727,6 +734,7 @@ const getLinearElementRotatedBounds = (
   if (boundTextElement) {
     const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
       element,
+      elementsMap,
       coords,
       boundTextElement,
     );

+ 70 - 20
packages/excalidraw/element/collision.ts

@@ -91,6 +91,7 @@ export const hitTest = (
   ) {
     return isPointHittingElementBoundingBox(
       element,
+      elementsMap,
       point,
       threshold,
       frameNameBoundsCache,
@@ -116,6 +117,7 @@ export const hitTest = (
     appState,
     frameNameBoundsCache,
     point,
+    elementsMap,
   );
 };
 
@@ -145,9 +147,11 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
       appState,
       frameNameBoundsCache,
       [x, y],
+      elementsMap,
     ) &&
     isPointHittingElementBoundingBox(
       element,
+      elementsMap,
       [x, y],
       threshold,
       frameNameBoundsCache,
@@ -160,6 +164,7 @@ export const isHittingElementNotConsideringBoundingBox = (
   appState: AppState,
   frameNameBoundsCache: FrameNameBoundsCache | null,
   point: Point,
+  elementsMap: ElementsMap,
 ): boolean => {
   const threshold = 10 / appState.zoom.value;
   const check = isTextElement(element)
@@ -169,6 +174,7 @@ export const isHittingElementNotConsideringBoundingBox = (
     : isNearCheck;
   return hitTestPointAgainstElement({
     element,
+    elementsMap,
     point,
     threshold,
     check,
@@ -183,6 +189,7 @@ const isElementSelected = (
 
 export const isPointHittingElementBoundingBox = (
   element: NonDeleted<ExcalidrawElement>,
+  elementsMap: ElementsMap,
   [x, y]: Point,
   threshold: number,
   frameNameBoundsCache: FrameNameBoundsCache | null,
@@ -194,6 +201,7 @@ export const isPointHittingElementBoundingBox = (
   if (isFrameLikeElement(element)) {
     return hitTestPointAgainstElement({
       element,
+      elementsMap,
       point: [x, y],
       threshold,
       check: isInsideCheck,
@@ -201,7 +209,7 @@ export const isPointHittingElementBoundingBox = (
     });
   }
 
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
   const elementCenterX = (x1 + x2) / 2;
   const elementCenterY = (y1 + y2) / 2;
   // reverse rotate to take element's angle into account.
@@ -224,12 +232,14 @@ export const isPointHittingElementBoundingBox = (
 export const bindingBorderTest = (
   element: NonDeleted<ExcalidrawBindableElement>,
   { x, y }: { x: number; y: number },
+  elementsMap: ElementsMap,
 ): boolean => {
   const threshold = maxBindingGap(element, element.width, element.height);
   const check = isOutsideCheck;
   const point: Point = [x, y];
   return hitTestPointAgainstElement({
     element,
+    elementsMap,
     point,
     threshold,
     check,
@@ -251,6 +261,7 @@ export const maxBindingGap = (
 
 type HitTestArgs = {
   element: NonDeletedExcalidrawElement;
+  elementsMap: ElementsMap;
   point: Point;
   threshold: number;
   check: (distance: number, threshold: number) => boolean;
@@ -266,19 +277,28 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
     case "text":
     case "diamond":
     case "ellipse":
-      const distance = distanceToBindableElement(args.element, args.point);
+      const distance = distanceToBindableElement(
+        args.element,
+        args.point,
+        args.elementsMap,
+      );
       return args.check(distance, args.threshold);
     case "freedraw": {
       if (
         !args.check(
-          distanceToRectangle(args.element, args.point),
+          distanceToRectangle(args.element, args.point, args.elementsMap),
           args.threshold,
         )
       ) {
         return false;
       }
 
-      return hitTestFreeDrawElement(args.element, args.point, args.threshold);
+      return hitTestFreeDrawElement(
+        args.element,
+        args.point,
+        args.threshold,
+        args.elementsMap,
+      );
     }
     case "arrow":
     case "line":
@@ -293,7 +313,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
       // check distance to frame element first
       if (
         args.check(
-          distanceToBindableElement(args.element, args.point),
+          distanceToBindableElement(args.element, args.point, args.elementsMap),
           args.threshold,
         )
       ) {
@@ -316,6 +336,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
 export const distanceToBindableElement = (
   element: ExcalidrawBindableElement,
   point: Point,
+  elementsMap: ElementsMap,
 ): number => {
   switch (element.type) {
     case "rectangle":
@@ -325,11 +346,11 @@ export const distanceToBindableElement = (
     case "embeddable":
     case "frame":
     case "magicframe":
-      return distanceToRectangle(element, point);
+      return distanceToRectangle(element, point, elementsMap);
     case "diamond":
-      return distanceToDiamond(element, point);
+      return distanceToDiamond(element, point, elementsMap);
     case "ellipse":
-      return distanceToEllipse(element, point);
+      return distanceToEllipse(element, point, elementsMap);
   }
 };
 
@@ -358,8 +379,13 @@ const distanceToRectangle = (
     | ExcalidrawIframeLikeElement
     | ExcalidrawFrameLikeElement,
   point: Point,
+  elementsMap: ElementsMap,
 ): number => {
-  const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
+  const [, pointRel, hwidth, hheight] = pointRelativeToElement(
+    element,
+    point,
+    elementsMap,
+  );
   return Math.max(
     GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)),
     GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)),
@@ -377,8 +403,13 @@ const distanceToRectangleBox = (box: RectangleBox, point: Point): number => {
 const distanceToDiamond = (
   element: ExcalidrawDiamondElement,
   point: Point,
+  elementsMap: ElementsMap,
 ): number => {
-  const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
+  const [, pointRel, hwidth, hheight] = pointRelativeToElement(
+    element,
+    point,
+    elementsMap,
+  );
   const side = GALine.equation(hheight, hwidth, -hheight * hwidth);
   return GAPoint.distanceToLine(pointRel, side);
 };
@@ -386,16 +417,22 @@ const distanceToDiamond = (
 const distanceToEllipse = (
   element: ExcalidrawEllipseElement,
   point: Point,
+  elementsMap: ElementsMap,
 ): number => {
-  const [pointRel, tangent] = ellipseParamsForTest(element, point);
+  const [pointRel, tangent] = ellipseParamsForTest(element, point, elementsMap);
   return -GALine.sign(tangent) * GAPoint.distanceToLine(pointRel, tangent);
 };
 
 const ellipseParamsForTest = (
   element: ExcalidrawEllipseElement,
   point: Point,
+  elementsMap: ElementsMap,
 ): [GA.Point, GA.Line] => {
-  const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
+  const [, pointRel, hwidth, hheight] = pointRelativeToElement(
+    element,
+    point,
+    elementsMap,
+  );
   const [px, py] = GAPoint.toTuple(pointRel);
 
   // We're working in positive quadrant, so start with `t = 45deg`, `tx=cos(t)`
@@ -440,6 +477,7 @@ const hitTestFreeDrawElement = (
   element: ExcalidrawFreeDrawElement,
   point: Point,
   threshold: number,
+  elementsMap: ElementsMap,
 ): boolean => {
   // Check point-distance-to-line-segment for every segment in the
   // element's points (its input points, not its outline points).
@@ -454,7 +492,10 @@ const hitTestFreeDrawElement = (
     y = point[1] - element.y;
   } else {
     // Counter-rotate the point around center before testing
-    const [minX, minY, maxX, maxY] = getElementAbsoluteCoords(element);
+    const [minX, minY, maxX, maxY] = getElementAbsoluteCoords(
+      element,
+      elementsMap,
+    );
     const rotatedPoint = rotatePoint(
       point,
       [minX + (maxX - minX) / 2, minY + (maxY - minY) / 2],
@@ -520,6 +561,7 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
   const [point, pointAbs, hwidth, hheight] = pointRelativeToElement(
     args.element,
     args.point,
+    args.elementsMap,
   );
   const side1 = GALine.equation(0, 1, -hheight);
   const side2 = GALine.equation(1, 0, -hwidth);
@@ -572,9 +614,10 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
 const pointRelativeToElement = (
   element: ExcalidrawElement,
   pointTuple: Point,
+  elementsMap: ElementsMap,
 ): [GA.Point, GA.Point, number, number] => {
   const point = GAPoint.from(pointTuple);
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
   const center = coordsCenter(x1, y1, x2, y2);
   // GA has angle orientation opposite to `rotate`
   const rotate = GATransform.rotation(center, element.angle);
@@ -609,11 +652,12 @@ const pointRelativeToDivElement = (
 // Returns point in absolute coordinates
 export const pointInAbsoluteCoords = (
   element: ExcalidrawElement,
+  elementsMap: ElementsMap,
   // Point relative to the element position
   point: Point,
 ): Point => {
   const [x, y] = point;
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
   const cx = (x2 - x1) / 2;
   const cy = (y2 - y1) / 2;
   const [rotatedX, rotatedY] = rotate(x, y, cx, cy, element.angle);
@@ -622,8 +666,9 @@ export const pointInAbsoluteCoords = (
 
 const relativizationToElementCenter = (
   element: ExcalidrawElement,
+  elementsMap: ElementsMap,
 ): GA.Transform => {
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
   const center = coordsCenter(x1, y1, x2, y2);
   // GA has angle orientation opposite to `rotate`
   const rotate = GATransform.rotation(center, element.angle);
@@ -649,12 +694,14 @@ const coordsCenter = (
 // of the element.
 export const determineFocusDistance = (
   element: ExcalidrawBindableElement,
+
   // Point on the line, in absolute coordinates
   a: Point,
   // Another point on the line, in absolute coordinates (closer to element)
   b: Point,
+  elementsMap: ElementsMap,
 ): number => {
-  const relateToCenter = relativizationToElementCenter(element);
+  const relateToCenter = relativizationToElementCenter(element, elementsMap);
   const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
   const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
   const line = GALine.through(aRel, bRel);
@@ -693,13 +740,14 @@ export const determineFocusPoint = (
   // returned focusPoint
   focus: number,
   adjecentPoint: Point,
+  elementsMap: ElementsMap,
 ): Point => {
   if (focus === 0) {
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
     const center = coordsCenter(x1, y1, x2, y2);
     return GAPoint.toTuple(center);
   }
-  const relateToCenter = relativizationToElementCenter(element);
+  const relateToCenter = relativizationToElementCenter(element, elementsMap);
   const adjecentPointRel = GATransform.apply(
     relateToCenter,
     GAPoint.from(adjecentPoint),
@@ -728,14 +776,16 @@ export const determineFocusPoint = (
 // and the `element`, in ascending order of distance from `a`.
 export const intersectElementWithLine = (
   element: ExcalidrawBindableElement,
+
   // Point on the line, in absolute coordinates
   a: Point,
   // Another point on the line, in absolute coordinates
   b: Point,
   // If given, the element is inflated by this value
   gap: number = 0,
+  elementsMap: ElementsMap,
 ): Point[] => {
-  const relateToCenter = relativizationToElementCenter(element);
+  const relateToCenter = relativizationToElementCenter(element, elementsMap);
   const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
   const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
   const line = GALine.through(aRel, bRel);

+ 1 - 1
packages/excalidraw/element/dragElements.ts

@@ -65,7 +65,7 @@ export const dragSelectedElements = (
         updateElementCoords(pointerDownState, textElement, adjustedOffset);
       }
     }
-    updateBoundElements(element, {
+    updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
       simultaneouslyUpdated: Array.from(elementsToUpdate),
     });
   });

+ 68 - 15
packages/excalidraw/element/linearElementEditor.ts

@@ -135,6 +135,7 @@ export class LinearElementEditor {
     event: PointerEvent,
     appState: AppState,
     setState: React.Component<any, AppState>["setState"],
+    elementsMap: ElementsMap,
   ) {
     if (
       !appState.editingLinearElement ||
@@ -151,10 +152,12 @@ export class LinearElementEditor {
     }
 
     const [selectionX1, selectionY1, selectionX2, selectionY2] =
-      getElementAbsoluteCoords(appState.draggingElement);
+      getElementAbsoluteCoords(appState.draggingElement, elementsMap);
 
-    const pointsSceneCoords =
-      LinearElementEditor.getPointsGlobalCoordinates(element);
+    const pointsSceneCoords = LinearElementEditor.getPointsGlobalCoordinates(
+      element,
+      elementsMap,
+    );
 
     const nextSelectedPoints = pointsSceneCoords.reduce(
       (acc: number[], point, index) => {
@@ -222,6 +225,7 @@ export class LinearElementEditor {
 
         const [width, height] = LinearElementEditor._getShiftLockedDelta(
           element,
+          elementsMap,
           referencePoint,
           [scenePointerX, scenePointerY],
           event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -239,6 +243,7 @@ export class LinearElementEditor {
       } else {
         const newDraggingPointPosition = LinearElementEditor.createPointAt(
           element,
+          elementsMap,
           scenePointerX - linearElementEditor.pointerOffset.x,
           scenePointerY - linearElementEditor.pointerOffset.y,
           event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -255,6 +260,7 @@ export class LinearElementEditor {
               linearElementEditor.pointerDownState.lastClickedPoint
                 ? LinearElementEditor.createPointAt(
                     element,
+                    elementsMap,
                     scenePointerX - linearElementEditor.pointerOffset.x,
                     scenePointerY - linearElementEditor.pointerOffset.y,
                     event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -290,6 +296,7 @@ export class LinearElementEditor {
               LinearElementEditor.getPointGlobalCoordinates(
                 element,
                 element.points[0],
+                elementsMap,
               ),
             ),
           );
@@ -303,6 +310,7 @@ export class LinearElementEditor {
               LinearElementEditor.getPointGlobalCoordinates(
                 element,
                 element.points[lastSelectedIndex],
+                elementsMap,
               ),
             ),
           );
@@ -323,6 +331,7 @@ export class LinearElementEditor {
     event: PointerEvent,
     editingLinearElement: LinearElementEditor,
     appState: AppState,
+    elementsMap: ElementsMap,
   ): LinearElementEditor {
     const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
       editingLinearElement;
@@ -364,6 +373,7 @@ export class LinearElementEditor {
                   LinearElementEditor.getPointAtIndexGlobalCoordinates(
                     element,
                     selectedPoint!,
+                    elementsMap,
                   ),
                 ),
                 Scene.getScene(element)!,
@@ -425,15 +435,23 @@ export class LinearElementEditor {
     ) {
       return editorMidPointsCache.points;
     }
-    LinearElementEditor.updateEditorMidPointsCache(element, appState);
+    LinearElementEditor.updateEditorMidPointsCache(
+      element,
+      elementsMap,
+      appState,
+    );
     return editorMidPointsCache.points!;
   };
 
   static updateEditorMidPointsCache = (
     element: NonDeleted<ExcalidrawLinearElement>,
+    elementsMap: ElementsMap,
     appState: InteractiveCanvasAppState,
   ) => {
-    const points = LinearElementEditor.getPointsGlobalCoordinates(element);
+    const points = LinearElementEditor.getPointsGlobalCoordinates(
+      element,
+      elementsMap,
+    );
 
     let index = 0;
     const midpoints: (Point | null)[] = [];
@@ -455,6 +473,7 @@ export class LinearElementEditor {
         points[index],
         points[index + 1],
         index + 1,
+        elementsMap,
       );
       midpoints.push(segmentMidPoint);
       index++;
@@ -477,6 +496,7 @@ export class LinearElementEditor {
     }
     const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
       element,
+      elementsMap,
       appState.zoom,
       scenePointer.x,
       scenePointer.y,
@@ -484,7 +504,10 @@ export class LinearElementEditor {
     if (clickedPointIndex >= 0) {
       return null;
     }
-    const points = LinearElementEditor.getPointsGlobalCoordinates(element);
+    const points = LinearElementEditor.getPointsGlobalCoordinates(
+      element,
+      elementsMap,
+    );
     if (points.length >= 3 && !appState.editingLinearElement) {
       return null;
     }
@@ -550,6 +573,7 @@ export class LinearElementEditor {
     startPoint: Point,
     endPoint: Point,
     endPointIndex: number,
+    elementsMap: ElementsMap,
   ) {
     let segmentMidPoint = centerPoint(startPoint, endPoint);
     if (element.points.length > 2 && element.roundness) {
@@ -574,6 +598,7 @@ export class LinearElementEditor {
         segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates(
           element,
           [tx, ty],
+          elementsMap,
         );
       }
     }
@@ -658,6 +683,7 @@ export class LinearElementEditor {
             ...element.points,
             LinearElementEditor.createPointAt(
               element,
+              elementsMap,
               scenePointer.x,
               scenePointer.y,
               event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -693,6 +719,7 @@ export class LinearElementEditor {
 
     const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
       element,
+      elementsMap,
       appState.zoom,
       scenePointer.x,
       scenePointer.y,
@@ -713,11 +740,12 @@ export class LinearElementEditor {
           element,
           startBindingElement,
           endBindingElement,
+          elementsMap,
         );
       }
     }
 
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
     const cx = (x1 + x2) / 2;
     const cy = (y1 + y2) / 2;
     const targetPoint =
@@ -779,6 +807,7 @@ export class LinearElementEditor {
     scenePointerX: number,
     scenePointerY: number,
     appState: AppState,
+    elementsMap: ElementsMap,
   ): LinearElementEditor | null {
     if (!appState.editingLinearElement) {
       return null;
@@ -809,6 +838,7 @@ export class LinearElementEditor {
 
       const [width, height] = LinearElementEditor._getShiftLockedDelta(
         element,
+        elementsMap,
         lastCommittedPoint,
         [scenePointerX, scenePointerY],
         event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -821,6 +851,7 @@ export class LinearElementEditor {
     } else {
       newPoint = LinearElementEditor.createPointAt(
         element,
+        elementsMap,
         scenePointerX - appState.editingLinearElement.pointerOffset.x,
         scenePointerY - appState.editingLinearElement.pointerOffset.y,
         event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -847,8 +878,9 @@ export class LinearElementEditor {
   static getPointGlobalCoordinates(
     element: NonDeleted<ExcalidrawLinearElement>,
     point: Point,
+    elementsMap: ElementsMap,
   ) {
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
     const cx = (x1 + x2) / 2;
     const cy = (y1 + y2) / 2;
 
@@ -860,8 +892,9 @@ export class LinearElementEditor {
   /** scene coords */
   static getPointsGlobalCoordinates(
     element: NonDeleted<ExcalidrawLinearElement>,
+    elementsMap: ElementsMap,
   ): Point[] {
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
     const cx = (x1 + x2) / 2;
     const cy = (y1 + y2) / 2;
     return element.points.map((point) => {
@@ -873,13 +906,15 @@ export class LinearElementEditor {
 
   static getPointAtIndexGlobalCoordinates(
     element: NonDeleted<ExcalidrawLinearElement>,
+
     indexMaybeFromEnd: number, // -1 for last element
+    elementsMap: ElementsMap,
   ): Point {
     const index =
       indexMaybeFromEnd < 0
         ? element.points.length + indexMaybeFromEnd
         : indexMaybeFromEnd;
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
     const cx = (x1 + x2) / 2;
     const cy = (y1 + y2) / 2;
 
@@ -893,8 +928,9 @@ export class LinearElementEditor {
   static pointFromAbsoluteCoords(
     element: NonDeleted<ExcalidrawLinearElement>,
     absoluteCoords: Point,
+    elementsMap: ElementsMap,
   ): Point {
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
     const cx = (x1 + x2) / 2;
     const cy = (y1 + y2) / 2;
     const [x, y] = rotate(
@@ -909,12 +945,15 @@ export class LinearElementEditor {
 
   static getPointIndexUnderCursor(
     element: NonDeleted<ExcalidrawLinearElement>,
+    elementsMap: ElementsMap,
     zoom: AppState["zoom"],
     x: number,
     y: number,
   ) {
-    const pointHandles =
-      LinearElementEditor.getPointsGlobalCoordinates(element);
+    const pointHandles = LinearElementEditor.getPointsGlobalCoordinates(
+      element,
+      elementsMap,
+    );
     let idx = pointHandles.length;
     // loop from right to left because points on the right are rendered over
     // points on the left, thus should take precedence when clicking, if they
@@ -934,12 +973,13 @@ export class LinearElementEditor {
 
   static createPointAt(
     element: NonDeleted<ExcalidrawLinearElement>,
+    elementsMap: ElementsMap,
     scenePointerX: number,
     scenePointerY: number,
     gridSize: number | null,
   ): Point {
     const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize);
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
     const cx = (x1 + x2) / 2;
     const cy = (y1 + y2) / 2;
     const [rotatedX, rotatedY] = rotate(
@@ -1190,6 +1230,7 @@ export class LinearElementEditor {
     pointerCoords: PointerCoords,
     appState: AppState,
     snapToGrid: boolean,
+    elementsMap: ElementsMap,
   ) {
     const element = LinearElementEditor.getElement(
       linearElementEditor.elementId,
@@ -1208,6 +1249,7 @@ export class LinearElementEditor {
 
     const midpoint = LinearElementEditor.createPointAt(
       element,
+      elementsMap,
       pointerCoords.x,
       pointerCoords.y,
       snapToGrid ? appState.gridSize : null,
@@ -1260,6 +1302,7 @@ export class LinearElementEditor {
 
   private static _getShiftLockedDelta(
     element: NonDeleted<ExcalidrawLinearElement>,
+    elementsMap: ElementsMap,
     referencePoint: Point,
     scenePointer: Point,
     gridSize: number | null,
@@ -1267,6 +1310,7 @@ export class LinearElementEditor {
     const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
       element,
       referencePoint,
+      elementsMap,
     );
 
     const [gridX, gridY] = getGridPoint(
@@ -1288,8 +1332,12 @@ export class LinearElementEditor {
   static getBoundTextElementPosition = (
     element: ExcalidrawLinearElement,
     boundTextElement: ExcalidrawTextElementWithContainer,
+    elementsMap: ElementsMap,
   ): { x: number; y: number } => {
-    const points = LinearElementEditor.getPointsGlobalCoordinates(element);
+    const points = LinearElementEditor.getPointsGlobalCoordinates(
+      element,
+      elementsMap,
+    );
     if (points.length < 2) {
       mutateElement(boundTextElement, { isDeleted: true });
     }
@@ -1300,6 +1348,7 @@ export class LinearElementEditor {
       const midPoint = LinearElementEditor.getPointGlobalCoordinates(
         element,
         element.points[index],
+        elementsMap,
       );
       x = midPoint[0] - boundTextElement.width / 2;
       y = midPoint[1] - boundTextElement.height / 2;
@@ -1319,6 +1368,7 @@ export class LinearElementEditor {
           points[index],
           points[index + 1],
           index + 1,
+          elementsMap,
         );
       }
       x = midSegmentMidpoint[0] - boundTextElement.width / 2;
@@ -1329,6 +1379,7 @@ export class LinearElementEditor {
 
   static getMinMaxXYWithBoundText = (
     element: ExcalidrawLinearElement,
+    elementsMap: ElementsMap,
     elementBounds: Bounds,
     boundTextElement: ExcalidrawTextElementWithContainer,
   ): [number, number, number, number, number, number] => {
@@ -1339,6 +1390,7 @@ export class LinearElementEditor {
       LinearElementEditor.getBoundTextElementPosition(
         element,
         boundTextElement,
+        elementsMap,
       );
     const boundTextX2 = boundTextX1 + boundTextElement.width;
     const boundTextY2 = boundTextY1 + boundTextElement.height;
@@ -1479,6 +1531,7 @@ export class LinearElementEditor {
     if (boundTextElement) {
       coords = LinearElementEditor.getMinMaxXYWithBoundText(
         element,
+        elementsMap,
         [x1, y1, x2, y2],
         boundTextElement,
       );

+ 7 - 3
packages/excalidraw/element/newElement.ts

@@ -16,6 +16,7 @@ import {
   ExcalidrawEmbeddableElement,
   ExcalidrawMagicFrameElement,
   ExcalidrawIframeElement,
+  ElementsMap,
 } from "./types";
 import {
   arrayToMap,
@@ -260,6 +261,7 @@ export const newTextElement = (
 
 const getAdjustedDimensions = (
   element: ExcalidrawTextElement,
+  elementsMap: ElementsMap,
   nextText: string,
 ): {
   x: number;
@@ -294,7 +296,7 @@ const getAdjustedDimensions = (
     x = element.x - offsets.x;
     y = element.y - offsets.y;
   } else {
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
 
     const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
       element,
@@ -335,6 +337,7 @@ const getAdjustedDimensions = (
 export const refreshTextDimensions = (
   textElement: ExcalidrawTextElement,
   container: ExcalidrawTextContainer | null,
+  elementsMap: ElementsMap,
   text = textElement.text,
 ) => {
   if (textElement.isDeleted) {
@@ -347,13 +350,14 @@ export const refreshTextDimensions = (
       getBoundTextMaxWidth(container, textElement),
     );
   }
-  const dimensions = getAdjustedDimensions(textElement, text);
+  const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
   return { text, ...dimensions };
 };
 
 export const updateTextElement = (
   textElement: ExcalidrawTextElement,
   container: ExcalidrawTextContainer | null,
+  elementsMap: ElementsMap,
   {
     text,
     isDeleted,
@@ -367,7 +371,7 @@ export const updateTextElement = (
   return newElementWith(textElement, {
     originalText,
     isDeleted: isDeleted ?? textElement.isDeleted,
-    ...refreshTextDimensions(textElement, container, originalText),
+    ...refreshTextDimensions(textElement, container, elementsMap, originalText),
   });
 };
 

+ 19 - 10
packages/excalidraw/element/resizeElements.ts

@@ -86,11 +86,12 @@ export const transformElements = (
     if (transformHandleType === "rotation") {
       rotateSingleElement(
         element,
+        elementsMap,
         pointerX,
         pointerY,
         shouldRotateWithDiscreteAngle,
       );
-      updateBoundElements(element);
+      updateBoundElements(element, elementsMap);
     } else if (
       isTextElement(element) &&
       (transformHandleType === "nw" ||
@@ -106,7 +107,7 @@ export const transformElements = (
         pointerX,
         pointerY,
       );
-      updateBoundElements(element);
+      updateBoundElements(element, elementsMap);
     } else if (transformHandleType) {
       resizeSingleElement(
         originalElements,
@@ -157,11 +158,12 @@ export const transformElements = (
 
 const rotateSingleElement = (
   element: NonDeletedExcalidrawElement,
+  elementsMap: ElementsMap,
   pointerX: number,
   pointerY: number,
   shouldRotateWithDiscreteAngle: boolean,
 ) => {
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
   const cx = (x1 + x2) / 2;
   const cy = (y1 + y2) / 2;
   let angle: number;
@@ -266,7 +268,7 @@ const resizeSingleTextElement = (
   pointerX: number,
   pointerY: number,
 ) => {
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
   const cx = (x1 + x2) / 2;
   const cy = (y1 + y2) / 2;
   // rotation pointer with reverse angle
@@ -629,7 +631,7 @@ export const resizeSingleElement = (
   ) {
     mutateElement(element, resizedElement);
 
-    updateBoundElements(element, {
+    updateBoundElements(element, elementsMap, {
       newSize: { width: resizedElement.width, height: resizedElement.height },
     });
 
@@ -696,7 +698,11 @@ export const resizeMultipleElements = (
     if (!isBoundToContainer(text)) {
       return acc;
     }
-    const xy = LinearElementEditor.getBoundTextElementPosition(orig, text);
+    const xy = LinearElementEditor.getBoundTextElementPosition(
+      orig,
+      text,
+      elementsMap,
+    );
     return [...acc, { ...text, ...xy }];
   }, [] as ExcalidrawTextElementWithContainer[]);
 
@@ -879,7 +885,7 @@ export const resizeMultipleElements = (
 
     mutateElement(element, update, false);
 
-    updateBoundElements(element, {
+    updateBoundElements(element, elementsMap, {
       simultaneouslyUpdated: elementsToUpdate,
       newSize: { width, height },
     });
@@ -921,7 +927,7 @@ const rotateMultipleElements = (
   elements
     .filter((element) => !isFrameLikeElement(element))
     .forEach((element) => {
-      const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+      const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
       const cx = (x1 + x2) / 2;
       const cy = (y1 + y2) / 2;
       const origAngle =
@@ -942,7 +948,9 @@ const rotateMultipleElements = (
         },
         false,
       );
-      updateBoundElements(element, { simultaneouslyUpdated: elements });
+      updateBoundElements(element, elementsMap, {
+        simultaneouslyUpdated: elements,
+      });
 
       const boundText = getBoundTextElement(element, elementsMap);
       if (boundText && !isArrowElement(element)) {
@@ -964,12 +972,13 @@ const rotateMultipleElements = (
 export const getResizeOffsetXY = (
   transformHandleType: MaybeTransformHandleType,
   selectedElements: NonDeletedExcalidrawElement[],
+  elementsMap: ElementsMap,
   x: number,
   y: number,
 ): [number, number] => {
   const [x1, y1, x2, y2] =
     selectedElements.length === 1
-      ? getElementAbsoluteCoords(selectedElements[0])
+      ? getElementAbsoluteCoords(selectedElements[0], elementsMap)
       : getCommonBounds(selectedElements);
   const cx = (x1 + x2) / 2;
   const cy = (y1 + y2) / 2;

+ 5 - 1
packages/excalidraw/element/resizeTest.ts

@@ -2,6 +2,7 @@ import {
   ExcalidrawElement,
   PointerType,
   NonDeletedExcalidrawElement,
+  ElementsMap,
 } from "./types";
 
 import {
@@ -27,6 +28,7 @@ const isInsideTransformHandle = (
 
 export const resizeTest = (
   element: NonDeletedExcalidrawElement,
+  elementsMap: ElementsMap,
   appState: AppState,
   x: number,
   y: number,
@@ -38,7 +40,7 @@ export const resizeTest = (
   }
 
   const { rotation: rotationTransformHandle, ...transformHandles } =
-    getTransformHandles(element, zoom, pointerType);
+    getTransformHandles(element, zoom, elementsMap, pointerType);
 
   if (
     rotationTransformHandle &&
@@ -70,6 +72,7 @@ export const getElementWithTransformHandleType = (
   scenePointerY: number,
   zoom: Zoom,
   pointerType: PointerType,
+  elementsMap: ElementsMap,
 ) => {
   return elements.reduce((result, element) => {
     if (result) {
@@ -77,6 +80,7 @@ export const getElementWithTransformHandleType = (
     }
     const transformHandleType = resizeTest(
       element,
+      elementsMap,
       appState,
       scenePointerX,
       scenePointerY,

+ 27 - 8
packages/excalidraw/element/textElement.ts

@@ -53,6 +53,7 @@ const splitIntoLines = (text: string) => {
 export const redrawTextBoundingBox = (
   textElement: ExcalidrawTextElement,
   container: ExcalidrawElement | null,
+  elementsMap: ElementsMap,
 ) => {
   let maxWidth = undefined;
   const boundTextUpdates = {
@@ -110,7 +111,11 @@ export const redrawTextBoundingBox = (
       ...textElement,
       ...boundTextUpdates,
     } as ExcalidrawTextElementWithContainer;
-    const { x, y } = computeBoundTextPosition(container, updatedTextElement);
+    const { x, y } = computeBoundTextPosition(
+      container,
+      updatedTextElement,
+      elementsMap,
+    );
     boundTextUpdates.x = x;
     boundTextUpdates.y = y;
   }
@@ -119,11 +124,11 @@ export const redrawTextBoundingBox = (
 };
 
 export const bindTextToShapeAfterDuplication = (
-  sceneElements: ExcalidrawElement[],
+  newElements: ExcalidrawElement[],
   oldElements: ExcalidrawElement[],
   oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
 ): void => {
-  const sceneElementMap = arrayToMap(sceneElements) as Map<
+  const newElementsMap = arrayToMap(newElements) as Map<
     ExcalidrawElement["id"],
     ExcalidrawElement
   >;
@@ -134,7 +139,7 @@ export const bindTextToShapeAfterDuplication = (
     if (boundTextElementId) {
       const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId);
       if (newTextElementId) {
-        const newContainer = sceneElementMap.get(newElementId);
+        const newContainer = newElementsMap.get(newElementId);
         if (newContainer) {
           mutateElement(newContainer, {
             boundElements: (element.boundElements || [])
@@ -149,7 +154,7 @@ export const bindTextToShapeAfterDuplication = (
               }),
           });
         }
-        const newTextElement = sceneElementMap.get(newTextElementId);
+        const newTextElement = newElementsMap.get(newTextElementId);
         if (newTextElement && isTextElement(newTextElement)) {
           mutateElement(newTextElement, {
             containerId: newContainer ? newElementId : null,
@@ -236,7 +241,7 @@ export const handleBindTextResize = (
     if (!isArrowElement(container)) {
       mutateElement(
         textElement,
-        computeBoundTextPosition(container, textElement),
+        computeBoundTextPosition(container, textElement, elementsMap),
       );
     }
   }
@@ -245,11 +250,13 @@ export const handleBindTextResize = (
 export const computeBoundTextPosition = (
   container: ExcalidrawElement,
   boundTextElement: ExcalidrawTextElementWithContainer,
+  elementsMap: ElementsMap,
 ) => {
   if (isArrowElement(container)) {
     return LinearElementEditor.getBoundTextElementPosition(
       container,
       boundTextElement,
+      elementsMap,
     );
   }
   const containerCoords = getContainerCoords(container);
@@ -698,12 +705,16 @@ export const getContainerCenter = (
       y: container.y + container.height / 2,
     };
   }
-  const points = LinearElementEditor.getPointsGlobalCoordinates(container);
+  const points = LinearElementEditor.getPointsGlobalCoordinates(
+    container,
+    elementsMap,
+  );
   if (points.length % 2 === 1) {
     const index = Math.floor(container.points.length / 2);
     const midPoint = LinearElementEditor.getPointGlobalCoordinates(
       container,
       container.points[index],
+      elementsMap,
     );
     return { x: midPoint[0], y: midPoint[1] };
   }
@@ -719,6 +730,7 @@ export const getContainerCenter = (
       points[index],
       points[index + 1],
       index + 1,
+      elementsMap,
     );
   }
   return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
@@ -757,11 +769,13 @@ export const getTextElementAngle = (
 export const getBoundTextElementPosition = (
   container: ExcalidrawElement,
   boundTextElement: ExcalidrawTextElementWithContainer,
+  elementsMap: ElementsMap,
 ) => {
   if (isArrowElement(container)) {
     return LinearElementEditor.getBoundTextElementPosition(
       container,
       boundTextElement,
+      elementsMap,
     );
   }
 };
@@ -804,6 +818,7 @@ export const getTextBindableContainerAtPosition = (
   appState: AppState,
   x: number,
   y: number,
+  elementsMap: ElementsMap,
 ): ExcalidrawTextContainer | null => {
   const selectedElements = getSelectedElements(elements, appState);
   if (selectedElements.length === 1) {
@@ -817,7 +832,10 @@ export const getTextBindableContainerAtPosition = (
     if (elements[index].isDeleted) {
       continue;
     }
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(
+      elements[index],
+      elementsMap,
+    );
     if (
       isArrowElement(elements[index]) &&
       isHittingElementNotConsideringBoundingBox(
@@ -825,6 +843,7 @@ export const getTextBindableContainerAtPosition = (
         appState,
         null,
         [x, y],
+        elementsMap,
       )
     ) {
       hitElement = elements[index];

+ 11 - 5
packages/excalidraw/element/textWysiwyg.tsx

@@ -121,13 +121,13 @@ export const textWysiwyg = ({
       return;
     }
     const { textAlign, verticalAlign } = updatedTextElement;
-
+    const elementsMap = app.scene.getNonDeletedElementsMap();
     if (updatedTextElement && isTextElement(updatedTextElement)) {
       let coordX = updatedTextElement.x;
       let coordY = updatedTextElement.y;
       const container = getContainerElement(
         updatedTextElement,
-        app.scene.getElementsMapIncludingDeleted(),
+        app.scene.getNonDeletedElementsMap(),
       );
       let maxWidth = updatedTextElement.width;
 
@@ -143,6 +143,7 @@ export const textWysiwyg = ({
             LinearElementEditor.getBoundTextElementPosition(
               container,
               updatedTextElement as ExcalidrawTextElementWithContainer,
+              elementsMap,
             );
           coordX = boundTextCoords.x;
           coordY = boundTextCoords.y;
@@ -200,6 +201,7 @@ export const textWysiwyg = ({
           const { y } = computeBoundTextPosition(
             container,
             updatedTextElement as ExcalidrawTextElementWithContainer,
+            elementsMap,
           );
           coordY = y;
         }
@@ -326,7 +328,7 @@ export const textWysiwyg = ({
       }
       const container = getContainerElement(
         element,
-        app.scene.getElementsMapIncludingDeleted(),
+        app.scene.getNonDeletedElementsMap(),
       );
 
       const font = getFontString({
@@ -513,7 +515,7 @@ export const textWysiwyg = ({
     let text = editable.value;
     const container = getContainerElement(
       updateElement,
-      app.scene.getElementsMapIncludingDeleted(),
+      app.scene.getNonDeletedElementsMap(),
     );
 
     if (container) {
@@ -541,7 +543,11 @@ export const textWysiwyg = ({
           ),
         });
       }
-      redrawTextBoundingBox(updateElement, container);
+      redrawTextBoundingBox(
+        updateElement,
+        container,
+        app.scene.getNonDeletedElementsMap(),
+      );
     }
 
     onSubmit({

+ 4 - 1
packages/excalidraw/element/transformHandles.ts

@@ -1,4 +1,5 @@
 import {
+  ElementsMap,
   ExcalidrawElement,
   NonDeletedExcalidrawElement,
   PointerType,
@@ -230,6 +231,8 @@ export const getTransformHandlesFromCoords = (
 export const getTransformHandles = (
   element: ExcalidrawElement,
   zoom: Zoom,
+  elementsMap: ElementsMap,
+
   pointerType: PointerType = "mouse",
 ): TransformHandles => {
   // so that when locked element is selected (especially when you toggle lock
@@ -267,7 +270,7 @@ export const getTransformHandles = (
     ? DEFAULT_TRANSFORM_HANDLE_SPACING + 8
     : DEFAULT_TRANSFORM_HANDLE_SPACING;
   return getTransformHandlesFromCoords(
-    getElementAbsoluteCoords(element, true),
+    getElementAbsoluteCoords(element, elementsMap, true),
     element.angle,
     zoom,
     pointerType,

+ 55 - 31
packages/excalidraw/frame.ts

@@ -65,10 +65,11 @@ export const bindElementsToFramesAfterDuplication = (
 export function isElementIntersectingFrame(
   element: ExcalidrawElement,
   frame: ExcalidrawFrameLikeElement,
+  elementsMap: ElementsMap,
 ) {
-  const frameLineSegments = getElementLineSegments(frame);
+  const frameLineSegments = getElementLineSegments(frame, elementsMap);
 
-  const elementLineSegments = getElementLineSegments(element);
+  const elementLineSegments = getElementLineSegments(element, elementsMap);
 
   const intersecting = frameLineSegments.some((frameLineSegment) =>
     elementLineSegments.some((elementLineSegment) =>
@@ -82,9 +83,10 @@ export function isElementIntersectingFrame(
 export const getElementsCompletelyInFrame = (
   elements: readonly ExcalidrawElement[],
   frame: ExcalidrawFrameLikeElement,
+  elementsMap: ElementsMap,
 ) =>
   omitGroupsContainingFrameLikes(
-    getElementsWithinSelection(elements, frame, false),
+    getElementsWithinSelection(elements, frame, elementsMap, false),
   ).filter(
     (element) =>
       (!isFrameLikeElement(element) && !element.frameId) ||
@@ -95,8 +97,9 @@ export const isElementContainingFrame = (
   elements: readonly ExcalidrawElement[],
   element: ExcalidrawElement,
   frame: ExcalidrawFrameLikeElement,
+  elementsMap: ElementsMap,
 ) => {
-  return getElementsWithinSelection(elements, element).some(
+  return getElementsWithinSelection(elements, element, elementsMap).some(
     (e) => e.id === frame.id,
   );
 };
@@ -104,13 +107,22 @@ export const isElementContainingFrame = (
 export const getElementsIntersectingFrame = (
   elements: readonly ExcalidrawElement[],
   frame: ExcalidrawFrameLikeElement,
-) => elements.filter((element) => isElementIntersectingFrame(element, frame));
+) => {
+  const elementsMap = arrayToMap(elements);
+  return elements.filter((element) =>
+    isElementIntersectingFrame(element, frame, elementsMap),
+  );
+};
 
 export const elementsAreInFrameBounds = (
   elements: readonly ExcalidrawElement[],
   frame: ExcalidrawFrameLikeElement,
+  elementsMap: ElementsMap,
 ) => {
-  const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame);
+  const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(
+    frame,
+    elementsMap,
+  );
 
   const [elementX1, elementY1, elementX2, elementY2] =
     getCommonBounds(elements);
@@ -126,11 +138,12 @@ export const elementsAreInFrameBounds = (
 export const elementOverlapsWithFrame = (
   element: ExcalidrawElement,
   frame: ExcalidrawFrameLikeElement,
+  elementsMap: ElementsMap,
 ) => {
   return (
-    elementsAreInFrameBounds([element], frame) ||
-    isElementIntersectingFrame(element, frame) ||
-    isElementContainingFrame([frame], element, frame)
+    elementsAreInFrameBounds([element], frame, elementsMap) ||
+    isElementIntersectingFrame(element, frame, elementsMap) ||
+    isElementContainingFrame([frame], element, frame, elementsMap)
   );
 };
 
@@ -140,8 +153,9 @@ export const isCursorInFrame = (
     y: number;
   },
   frame: NonDeleted<ExcalidrawFrameLikeElement>,
+  elementsMap: ElementsMap,
 ) => {
-  const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame);
+  const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
 
   return isPointWithinBounds(
     [fx1, fy1],
@@ -155,6 +169,7 @@ export const groupsAreAtLeastIntersectingTheFrame = (
   groupIds: readonly string[],
   frame: ExcalidrawFrameLikeElement,
 ) => {
+  const elementsMap = arrayToMap(elements);
   const elementsInGroup = groupIds.flatMap((groupId) =>
     getElementsInGroup(elements, groupId),
   );
@@ -165,8 +180,8 @@ export const groupsAreAtLeastIntersectingTheFrame = (
 
   return !!elementsInGroup.find(
     (element) =>
-      elementsAreInFrameBounds([element], frame) ||
-      isElementIntersectingFrame(element, frame),
+      elementsAreInFrameBounds([element], frame, elementsMap) ||
+      isElementIntersectingFrame(element, frame, elementsMap),
   );
 };
 
@@ -175,6 +190,7 @@ export const groupsAreCompletelyOutOfFrame = (
   groupIds: readonly string[],
   frame: ExcalidrawFrameLikeElement,
 ) => {
+  const elementsMap = arrayToMap(elements);
   const elementsInGroup = groupIds.flatMap((groupId) =>
     getElementsInGroup(elements, groupId),
   );
@@ -186,8 +202,8 @@ export const groupsAreCompletelyOutOfFrame = (
   return (
     elementsInGroup.find(
       (element) =>
-        elementsAreInFrameBounds([element], frame) ||
-        isElementIntersectingFrame(element, frame),
+        elementsAreInFrameBounds([element], frame, elementsMap) ||
+        isElementIntersectingFrame(element, frame, elementsMap),
     ) === undefined
   );
 };
@@ -258,14 +274,15 @@ export const getElementsInResizingFrame = (
   allElements: ExcalidrawElementsIncludingDeleted,
   frame: ExcalidrawFrameLikeElement,
   appState: AppState,
+  elementsMap: ElementsMap,
 ): ExcalidrawElement[] => {
   const prevElementsInFrame = getFrameChildren(allElements, frame.id);
   const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame);
 
   const elementsCompletelyInFrame = new Set([
-    ...getElementsCompletelyInFrame(allElements, frame),
+    ...getElementsCompletelyInFrame(allElements, frame, elementsMap),
     ...prevElementsInFrame.filter((element) =>
-      isElementContainingFrame(allElements, element, frame),
+      isElementContainingFrame(allElements, element, frame, elementsMap),
     ),
   ]);
 
@@ -283,7 +300,7 @@ export const getElementsInResizingFrame = (
   );
 
   for (const element of elementsNotCompletelyInFrame) {
-    if (!isElementIntersectingFrame(element, frame)) {
+    if (!isElementIntersectingFrame(element, frame, elementsMap)) {
       if (element.groupIds.length === 0) {
         nextElementsInFrame.delete(element);
       }
@@ -334,7 +351,7 @@ export const getElementsInResizingFrame = (
     if (isSelected) {
       const elementsInGroup = getElementsInGroup(allElements, id);
 
-      if (elementsAreInFrameBounds(elementsInGroup, frame)) {
+      if (elementsAreInFrameBounds(elementsInGroup, frame, elementsMap)) {
         for (const element of elementsInGroup) {
           nextElementsInFrame.add(element);
         }
@@ -348,12 +365,13 @@ export const getElementsInResizingFrame = (
 };
 
 export const getElementsInNewFrame = (
-  allElements: ExcalidrawElementsIncludingDeleted,
+  elements: ExcalidrawElementsIncludingDeleted,
   frame: ExcalidrawFrameLikeElement,
+  elementsMap: ElementsMap,
 ) => {
   return omitGroupsContainingFrameLikes(
-    allElements,
-    getElementsCompletelyInFrame(allElements, frame),
+    elements,
+    getElementsCompletelyInFrame(elements, frame, elementsMap),
   );
 };
 
@@ -388,7 +406,7 @@ export const filterElementsEligibleAsFrameChildren = (
   frame: ExcalidrawFrameLikeElement,
 ) => {
   const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
-
+  const elementsMap = arrayToMap(elements);
   elements = omitGroupsContainingFrameLikes(elements);
 
   for (const element of elements) {
@@ -415,14 +433,18 @@ export const filterElementsEligibleAsFrameChildren = (
       if (!processedGroups.has(shallowestGroupId)) {
         processedGroups.add(shallowestGroupId);
         const groupElements = getElementsInGroup(elements, shallowestGroupId);
-        if (groupElements.some((el) => elementOverlapsWithFrame(el, frame))) {
+        if (
+          groupElements.some((el) =>
+            elementOverlapsWithFrame(el, frame, elementsMap),
+          )
+        ) {
           for (const child of groupElements) {
             eligibleElements.push(child);
           }
         }
       }
     } else {
-      const overlaps = elementOverlapsWithFrame(element, frame);
+      const overlaps = elementOverlapsWithFrame(element, frame, elementsMap);
       if (overlaps) {
         eligibleElements.push(element);
       }
@@ -682,12 +704,12 @@ export const getTargetFrame = (
 // given an element, return if the element is in some frame
 export const isElementInFrame = (
   element: ExcalidrawElement,
-  allElements: ElementsMap,
+  allElementsMap: ElementsMap,
   appState: StaticCanvasAppState,
 ) => {
-  const frame = getTargetFrame(element, allElements, appState);
+  const frame = getTargetFrame(element, allElementsMap, appState);
   const _element = isTextElement(element)
-    ? getContainerElement(element, allElements) || element
+    ? getContainerElement(element, allElementsMap) || element
     : element;
 
   if (frame) {
@@ -703,16 +725,18 @@ export const isElementInFrame = (
     }
 
     if (_element.groupIds.length === 0) {
-      return elementOverlapsWithFrame(_element, frame);
+      return elementOverlapsWithFrame(_element, frame, allElementsMap);
     }
 
     const allElementsInGroup = new Set(
-      _element.groupIds.flatMap((gid) => getElementsInGroup(allElements, gid)),
+      _element.groupIds.flatMap((gid) =>
+        getElementsInGroup(allElementsMap, gid),
+      ),
     );
 
     if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
       const selectedElements = new Set(
-        getSelectedElements(allElements, appState),
+        getSelectedElements(allElementsMap, appState),
       );
 
       const editingGroupOverlapsFrame = appState.frameToHighlight !== null;
@@ -733,7 +757,7 @@ export const isElementInFrame = (
     }
 
     for (const elementInGroup of allElementsInGroup) {
-      if (elementOverlapsWithFrame(elementInGroup, frame)) {
+      if (elementOverlapsWithFrame(elementInGroup, frame, allElementsMap)) {
         return true;
       }
     }

+ 25 - 12
packages/excalidraw/renderer/renderElement.ts

@@ -7,6 +7,7 @@ import {
   ExcalidrawTextElementWithContainer,
   ExcalidrawFrameLikeElement,
   NonDeletedSceneElementsMap,
+  ElementsMap,
 } from "../element/types";
 import {
   isTextElement,
@@ -137,6 +138,7 @@ export interface ExcalidrawElementWithCanvas {
 
 const cappedElementCanvasSize = (
   element: NonDeletedExcalidrawElement,
+  elementsMap: ElementsMap,
   zoom: Zoom,
 ): {
   width: number;
@@ -155,7 +157,7 @@ const cappedElementCanvasSize = (
 
   const padding = getCanvasPadding(element);
 
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
   const elementWidth =
     isLinearElement(element) || isFreeDrawElement(element)
       ? distance(x1, x2)
@@ -200,7 +202,11 @@ const generateElementCanvas = (
   const context = canvas.getContext("2d")!;
   const padding = getCanvasPadding(element);
 
-  const { width, height, scale } = cappedElementCanvasSize(element, zoom);
+  const { width, height, scale } = cappedElementCanvasSize(
+    element,
+    elementsMap,
+    zoom,
+  );
 
   canvas.width = width;
   canvas.height = height;
@@ -209,7 +215,7 @@ const generateElementCanvas = (
   let canvasOffsetY = 0;
 
   if (isLinearElement(element) || isFreeDrawElement(element)) {
-    const [x1, y1] = getElementAbsoluteCoords(element);
+    const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
 
     canvasOffsetX =
       element.x > x1
@@ -468,7 +474,7 @@ const drawElementFromCanvas = (
   const element = elementWithCanvas.element;
   const padding = getCanvasPadding(element);
   const zoom = elementWithCanvas.scale;
-  let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  let [x1, y1, x2, y2] = getElementAbsoluteCoords(element, allElementsMap);
 
   // Free draw elements will otherwise "shuffle" as the min x and y change
   if (isFreeDrawElement(element)) {
@@ -513,8 +519,10 @@ const drawElementFromCanvas = (
       elementWithCanvas.canvas.height,
     );
 
-    const [, , , , boundTextCx, boundTextCy] =
-      getElementAbsoluteCoords(boundTextElement);
+    const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords(
+      boundTextElement,
+      allElementsMap,
+    );
 
     tempCanvasContext.rotate(-element.angle);
 
@@ -694,7 +702,7 @@ export const renderElement = (
       ShapeCache.generateElementShape(element, null);
 
       if (renderConfig.isExporting) {
-        const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+        const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
         const cx = (x1 + x2) / 2 + appState.scrollX;
         const cy = (y1 + y2) / 2 + appState.scrollY;
         const shiftX = (x2 - x1) / 2 - (element.x - x1);
@@ -737,7 +745,7 @@ export const renderElement = (
       // rely on existing shapes
       ShapeCache.generateElementShape(element, renderConfig);
       if (renderConfig.isExporting) {
-        const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+        const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
         const cx = (x1 + x2) / 2 + appState.scrollX;
         const cy = (y1 + y2) / 2 + appState.scrollY;
         let shiftX = (x2 - x1) / 2 - (element.x - x1);
@@ -749,6 +757,7 @@ export const renderElement = (
               LinearElementEditor.getBoundTextElementPosition(
                 container,
                 element as ExcalidrawTextElementWithContainer,
+                elementsMap,
               );
             shiftX = (x2 - x1) / 2 - (boundTextCoords.x - x1);
             shiftY = (y2 - y1) / 2 - (boundTextCoords.y - y1);
@@ -804,8 +813,10 @@ export const renderElement = (
           tempCanvasContext.rotate(-element.angle);
 
           // Shift the canvas to center of bound text
-          const [, , , , boundTextCx, boundTextCy] =
-            getElementAbsoluteCoords(boundTextElement);
+          const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords(
+            boundTextElement,
+            elementsMap,
+          );
           const boundTextShiftX = (x1 + x2) / 2 - boundTextCx;
           const boundTextShiftY = (y1 + y2) / 2 - boundTextCy;
           tempCanvasContext.translate(-boundTextShiftX, -boundTextShiftY);
@@ -939,17 +950,18 @@ export const renderElementToSvg = (
   renderConfig: SVGRenderConfig,
 ) => {
   const offset = { x: offsetX, y: offsetY };
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
   let cx = (x2 - x1) / 2 - (element.x - x1);
   let cy = (y2 - y1) / 2 - (element.y - y1);
   if (isTextElement(element)) {
     const container = getContainerElement(element, elementsMap);
     if (isArrowElement(container)) {
-      const [x1, y1, x2, y2] = getElementAbsoluteCoords(container);
+      const [x1, y1, x2, y2] = getElementAbsoluteCoords(container, elementsMap);
 
       const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(
         container,
         element as ExcalidrawTextElementWithContainer,
+        elementsMap,
       );
       cx = (x2 - x1) / 2 - (boundTextCoords.x - x1);
       cy = (y2 - y1) / 2 - (boundTextCoords.y - y1);
@@ -1151,6 +1163,7 @@ export const renderElementToSvg = (
         const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(
           element,
           boundText,
+          elementsMap,
         );
 
         const maskX = offsetX + boundTextCoords.x - element.x;

+ 40 - 13
packages/excalidraw/renderer/renderScene.ts

@@ -17,6 +17,7 @@ import {
   GroupId,
   ExcalidrawBindableElement,
   ExcalidrawFrameLikeElement,
+  ElementsMap,
 } from "../element/types";
 import {
   getElementAbsoluteCoords,
@@ -256,7 +257,10 @@ const renderLinearPointHandles = (
   context.save();
   context.translate(appState.scrollX, appState.scrollY);
   context.lineWidth = 1 / appState.zoom.value;
-  const points = LinearElementEditor.getPointsGlobalCoordinates(element);
+  const points = LinearElementEditor.getPointsGlobalCoordinates(
+    element,
+    elementsMap,
+  );
 
   const { POINT_HANDLE_SIZE } = LinearElementEditor;
   const radius = appState.editingLinearElement
@@ -340,6 +344,7 @@ const highlightPoint = (
 const renderLinearElementPointHighlight = (
   context: CanvasRenderingContext2D,
   appState: InteractiveCanvasAppState,
+  elementsMap: ElementsMap,
 ) => {
   const { elementId, hoverPointIndex } = appState.selectedLinearElement!;
   if (
@@ -356,6 +361,7 @@ const renderLinearElementPointHighlight = (
   const point = LinearElementEditor.getPointAtIndexGlobalCoordinates(
     element,
     hoverPointIndex,
+    elementsMap,
   );
   context.save();
   context.translate(appState.scrollX, appState.scrollY);
@@ -510,12 +516,22 @@ const _renderInteractiveScene = ({
     appState.suggestedBindings
       .filter((binding) => binding != null)
       .forEach((suggestedBinding) => {
-        renderBindingHighlight(context, appState, suggestedBinding!);
+        renderBindingHighlight(
+          context,
+          appState,
+          suggestedBinding!,
+          elementsMap,
+        );
       });
   }
 
   if (appState.frameToHighlight) {
-    renderFrameHighlight(context, appState, appState.frameToHighlight);
+    renderFrameHighlight(
+      context,
+      appState,
+      appState.frameToHighlight,
+      elementsMap,
+    );
   }
 
   if (appState.elementsToHighlight) {
@@ -545,7 +561,7 @@ const _renderInteractiveScene = ({
     appState.selectedLinearElement &&
     appState.selectedLinearElement.hoverPointIndex >= 0
   ) {
-    renderLinearElementPointHighlight(context, appState);
+    renderLinearElementPointHighlight(context, appState, elementsMap);
   }
   // Paint selected elements
   if (!appState.multiElement && !appState.editingLinearElement) {
@@ -608,7 +624,7 @@ const _renderInteractiveScene = ({
 
         if (selectionColors.length) {
           const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
-            getElementAbsoluteCoords(element, true);
+            getElementAbsoluteCoords(element, elementsMap, true);
           selections.push({
             angle: element.angle,
             elementX1,
@@ -666,7 +682,8 @@ const _renderInteractiveScene = ({
       const transformHandles = getTransformHandles(
         selectedElements[0],
         appState.zoom,
-        "mouse", // when we render we don't know which pointer type so use mouse
+        elementsMap,
+        "mouse", // when we render we don't know which pointer type so use mouse,
       );
       if (!appState.viewModeEnabled && showBoundingBox) {
         renderTransformHandles(
@@ -953,7 +970,11 @@ const _renderStaticScene = ({
       element.groupIds.length > 0 &&
       appState.frameToHighlight &&
       appState.selectedElementIds[element.id] &&
-      (elementOverlapsWithFrame(element, appState.frameToHighlight) ||
+      (elementOverlapsWithFrame(
+        element,
+        appState.frameToHighlight,
+        elementsMap,
+      ) ||
         element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId)))
     ) {
       element.groupIds.forEach((groupId) =>
@@ -1004,7 +1025,7 @@ const _renderStaticScene = ({
           );
         }
         if (!isExporting) {
-          renderLinkIcon(element, context, appState);
+          renderLinkIcon(element, context, appState, elementsMap);
         }
       } catch (error: any) {
         console.error(error);
@@ -1048,7 +1069,7 @@ const _renderStaticScene = ({
             );
           }
           if (!isExporting) {
-            renderLinkIcon(element, context, appState);
+            renderLinkIcon(element, context, appState, elementsMap);
           }
         };
         // - when exporting the whole canvas, we DO NOT apply clipping
@@ -1247,6 +1268,7 @@ const renderBindingHighlight = (
   context: CanvasRenderingContext2D,
   appState: InteractiveCanvasAppState,
   suggestedBinding: SuggestedBinding,
+  elementsMap: ElementsMap,
 ) => {
   const renderHighlight = Array.isArray(suggestedBinding)
     ? renderBindingHighlightForSuggestedPointBinding
@@ -1254,7 +1276,7 @@ const renderBindingHighlight = (
 
   context.save();
   context.translate(appState.scrollX, appState.scrollY);
-  renderHighlight(context, suggestedBinding as any);
+  renderHighlight(context, suggestedBinding as any, elementsMap);
 
   context.restore();
 };
@@ -1262,8 +1284,9 @@ const renderBindingHighlight = (
 const renderBindingHighlightForBindableElement = (
   context: CanvasRenderingContext2D,
   element: ExcalidrawBindableElement,
+  elementsMap: ElementsMap,
 ) => {
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
   const width = x2 - x1;
   const height = y2 - y1;
   const threshold = maxBindingGap(element, width, height);
@@ -1323,8 +1346,9 @@ const renderFrameHighlight = (
   context: CanvasRenderingContext2D,
   appState: InteractiveCanvasAppState,
   frame: NonDeleted<ExcalidrawFrameLikeElement>,
+  elementsMap: ElementsMap,
 ) => {
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
   const width = x2 - x1;
   const height = y2 - y1;
 
@@ -1398,6 +1422,7 @@ const renderElementsBoxHighlight = (
 const renderBindingHighlightForSuggestedPointBinding = (
   context: CanvasRenderingContext2D,
   suggestedBinding: SuggestedPointBinding,
+  elementsMap: ElementsMap,
 ) => {
   const [element, startOrEnd, bindableElement] = suggestedBinding;
 
@@ -1416,6 +1441,7 @@ const renderBindingHighlightForSuggestedPointBinding = (
     const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
       element,
       index,
+      elementsMap,
     );
     fillCircle(context, x, y, threshold);
   });
@@ -1426,9 +1452,10 @@ const renderLinkIcon = (
   element: NonDeletedExcalidrawElement,
   context: CanvasRenderingContext2D,
   appState: StaticCanvasAppState,
+  elementsMap: ElementsMap,
 ) => {
   if (element.link && !appState.selectedElementIds[element.id]) {
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
     const [x, y, width, height] = getLinkHandleFromCoords(
       [x1, y1, x2, y2],
       element.angle,

+ 2 - 4
packages/excalidraw/scene/Fonts.ts

@@ -60,10 +60,8 @@ export class Fonts {
         return newElementWith(element, {
           ...refreshTextDimensions(
             element,
-            getContainerElement(
-              element,
-              this.scene.getElementsMapIncludingDeleted(),
-            ),
+            getContainerElement(element, this.scene.getNonDeletedElementsMap()),
+            this.scene.getNonDeletedElementsMap(),
           ),
         });
       }

+ 2 - 1
packages/excalidraw/scene/export.ts

@@ -392,8 +392,9 @@ export const exportToSvg = async (
   const frameElements = getFrameLikeElements(elements);
 
   let exportingFrameClipPath = "";
+  const elementsMap = arrayToMap(elements);
   for (const frame of frameElements) {
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame);
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
     const cx = (x2 - x1) / 2 - (frame.x - x1);
     const cy = (y2 - y1) / 2 - (frame.y - y1);
 

+ 4 - 2
packages/excalidraw/scene/selection.ts

@@ -1,4 +1,5 @@
 import {
+  ElementsMap,
   ElementsMapOrArray,
   ExcalidrawElement,
   NonDeletedExcalidrawElement,
@@ -44,10 +45,11 @@ export const excludeElementsInFramesFromSelection = <
 export const getElementsWithinSelection = (
   elements: readonly NonDeletedExcalidrawElement[],
   selection: NonDeletedExcalidrawElement,
+  elementsMap: ElementsMap,
   excludeElementsInFrames: boolean = true,
 ) => {
   const [selectionX1, selectionY1, selectionX2, selectionY2] =
-    getElementAbsoluteCoords(selection);
+    getElementAbsoluteCoords(selection, elementsMap);
 
   let elementsInSelection = elements.filter((element) => {
     let [elementX1, elementY1, elementX2, elementY2] =
@@ -82,7 +84,7 @@ export const getElementsWithinSelection = (
     const containingFrame = getContainingFrame(element);
 
     if (containingFrame) {
-      return elementOverlapsWithFrame(element, containingFrame);
+      return elementOverlapsWithFrame(element, containingFrame, elementsMap);
     }
 
     return true;

+ 24 - 16
packages/excalidraw/snapping.ts

@@ -8,15 +8,18 @@ import {
 import { MaybeTransformHandleType } from "./element/transformHandles";
 import { isBoundToContainer, isFrameLikeElement } from "./element/typeChecks";
 import {
+  ElementsMap,
   ExcalidrawElement,
   NonDeletedExcalidrawElement,
 } from "./element/types";
 import { getMaximumGroups } from "./groups";
 import { KEYS } from "./keys";
 import { rangeIntersection, rangesOverlap, rotatePoint } from "./math";
-import { getVisibleAndNonSelectedElements } from "./scene/selection";
+import {
+  getSelectedElements,
+  getVisibleAndNonSelectedElements,
+} from "./scene/selection";
 import { AppState, KeyboardModifiersObject, Point } from "./types";
-import { arrayToMap } from "./utils";
 
 const SNAP_DISTANCE = 8;
 
@@ -167,6 +170,7 @@ export const areRoughlyEqual = (a: number, b: number, precision = 0.01) => {
 
 export const getElementsCorners = (
   elements: ExcalidrawElement[],
+  elementsMap: ElementsMap,
   {
     omitCenter,
     boundingBoxCorners,
@@ -185,7 +189,10 @@ export const getElementsCorners = (
   if (elements.length === 1) {
     const element = elements[0];
 
-    let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
+    let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
+      element,
+      elementsMap,
+    );
 
     if (dragOffset) {
       x1 += dragOffset.x;
@@ -280,6 +287,7 @@ export const getVisibleGaps = (
   elements: readonly NonDeletedExcalidrawElement[],
   selectedElements: ExcalidrawElement[],
   appState: AppState,
+  elementsMap: ElementsMap,
 ) => {
   const referenceElements: ExcalidrawElement[] = getReferenceElements(
     elements,
@@ -287,10 +295,7 @@ export const getVisibleGaps = (
     appState,
   );
 
-  const referenceBounds = getMaximumGroups(
-    referenceElements,
-    arrayToMap(elements),
-  )
+  const referenceBounds = getMaximumGroups(referenceElements, elementsMap)
     .filter(
       (elementsGroup) =>
         !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
@@ -569,19 +574,19 @@ export const getReferenceSnapPoints = (
   elements: readonly NonDeletedExcalidrawElement[],
   selectedElements: ExcalidrawElement[],
   appState: AppState,
+  elementsMap: ElementsMap,
 ) => {
   const referenceElements = getReferenceElements(
     elements,
     selectedElements,
     appState,
   );
-
-  return getMaximumGroups(referenceElements, arrayToMap(elements))
+  return getMaximumGroups(referenceElements, elementsMap)
     .filter(
       (elementsGroup) =>
         !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
     )
-    .flatMap((elementGroup) => getElementsCorners(elementGroup));
+    .flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap));
 };
 
 const getPointSnaps = (
@@ -641,11 +646,13 @@ const getPointSnaps = (
 };
 
 export const snapDraggedElements = (
-  selectedElements: ExcalidrawElement[],
+  elements: ExcalidrawElement[],
   dragOffset: Vector2D,
   appState: AppState,
   event: KeyboardModifiersObject,
+  elementsMap: ElementsMap,
 ) => {
+  const selectedElements = getSelectedElements(elements, appState);
   if (
     !isSnappingEnabled({ appState, event, selectedElements }) ||
     selectedElements.length === 0
@@ -658,7 +665,6 @@ export const snapDraggedElements = (
       snapLines: [],
     };
   }
-
   dragOffset.x = round(dragOffset.x);
   dragOffset.y = round(dragOffset.y);
   const nearestSnapsX: Snaps = [];
@@ -669,7 +675,7 @@ export const snapDraggedElements = (
     y: snapDistance,
   };
 
-  const selectionPoints = getElementsCorners(selectedElements, {
+  const selectionPoints = getElementsCorners(selectedElements, elementsMap, {
     dragOffset,
   });
 
@@ -719,7 +725,7 @@ export const snapDraggedElements = (
 
   getPointSnaps(
     selectedElements,
-    getElementsCorners(selectedElements, {
+    getElementsCorners(selectedElements, elementsMap, {
       dragOffset: newDragOffset,
     }),
     appState,
@@ -1204,6 +1210,7 @@ export const snapNewElement = (
   event: KeyboardModifiersObject,
   origin: Vector2D,
   dragOffset: Vector2D,
+  elementsMap: ElementsMap,
 ) => {
   if (
     !isSnappingEnabled({ event, selectedElements: [draggingElement], appState })
@@ -1248,7 +1255,7 @@ export const snapNewElement = (
   nearestSnapsX.length = 0;
   nearestSnapsY.length = 0;
 
-  const corners = getElementsCorners([draggingElement], {
+  const corners = getElementsCorners([draggingElement], elementsMap, {
     boundingBoxCorners: true,
     omitCenter: true,
   });
@@ -1276,6 +1283,7 @@ export const getSnapLinesAtPointer = (
   appState: AppState,
   pointer: Vector2D,
   event: KeyboardModifiersObject,
+  elementsMap: ElementsMap,
 ) => {
   if (!isSnappingEnabled({ event, selectedElements: [], appState })) {
     return {
@@ -1301,7 +1309,7 @@ export const getSnapLinesAtPointer = (
   const verticalSnapLines: PointerSnapLine[] = [];
 
   for (const referenceElement of referenceElements) {
-    const corners = getElementsCorners([referenceElement]);
+    const corners = getElementsCorners([referenceElement], elementsMap);
 
     for (const corner of corners) {
       const offsetX = corner[0] - pointer.x;

+ 7 - 2
packages/excalidraw/tests/binding.test.tsx

@@ -5,6 +5,7 @@ import { getTransformHandles } from "../element/transformHandles";
 import { API } from "./helpers/api";
 import { KEYS } from "../keys";
 import { actionWrapTextInContainer } from "../actions/actionBoundText";
+import { arrayToMap } from "../utils";
 
 const { h } = window;
 
@@ -91,8 +92,12 @@ describe("element binding", () => {
     expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
     expect(arrow.endBinding?.elementId).toBe(rectRight.id);
 
-    const rotation = getTransformHandles(arrow, h.state.zoom, "mouse")
-      .rotation!;
+    const rotation = getTransformHandles(
+      arrow,
+      h.state.zoom,
+      arrayToMap(h.elements),
+      "mouse",
+    ).rotation!;
     const rotationHandleX = rotation[0] + rotation[2] / 2;
     const rotationHandleY = rotation[1] + rotation[3] / 2;
     mouse.down(rotationHandleX, rotationHandleY);

+ 9 - 4
packages/excalidraw/tests/flip.test.tsx

@@ -27,7 +27,7 @@ import * as blob from "../data/blob";
 import { KEYS } from "../keys";
 import { getBoundTextElementPosition } from "../element/textElement";
 import { createPasteEvent } from "../clipboard";
-import { cloneJSON } from "../utils";
+import { arrayToMap, cloneJSON } from "../utils";
 
 const { h } = window;
 const mouse = new Pointer("mouse");
@@ -194,9 +194,10 @@ const checkElementsBoundingBox = async (
   element2: ExcalidrawElement,
   toleranceInPx: number = 0,
 ) => {
-  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1);
+  const elementsMap = arrayToMap([element1, element2]);
+  const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1, elementsMap);
 
-  const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2);
+  const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2, elementsMap);
 
   await waitFor(() => {
     // Check if width and height did not change
@@ -853,7 +854,11 @@ describe("mutliple elements", () => {
     h.app.actionManager.executeAction(actionFlipVertical);
 
     const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer;
-    const arrowTextPos = getBoundTextElementPosition(arrow.get(), arrowText)!;
+    const arrowTextPos = getBoundTextElementPosition(
+      arrow.get(),
+      arrowText,
+      arrayToMap(h.elements),
+    )!;
     const rectText = h.elements[3] as ExcalidrawTextElementWithContainer;
 
     expect(arrow.x).toBeCloseTo(180);

+ 7 - 3
packages/excalidraw/tests/helpers/ui.ts

@@ -32,6 +32,7 @@ import {
 import { getCommonBounds, getElementPointsCoords } from "../../element/bounds";
 import { rotatePoint } from "../../math";
 import { getTextEditor } from "../queries/dom";
+import { arrayToMap } from "../../utils";
 
 const { h } = window;
 
@@ -286,9 +287,12 @@ const transform = (
   let handleCoords: TransformHandle | undefined;
 
   if (elements.length === 1) {
-    handleCoords = getTransformHandles(elements[0], h.state.zoom, "mouse")[
-      handle
-    ];
+    handleCoords = getTransformHandles(
+      elements[0],
+      h.state.zoom,
+      arrayToMap(h.elements),
+      "mouse",
+    )[handle];
   } else {
     const [x1, y1, x2, y2] = getCommonBounds(elements);
     const isFrameSelected = elements.some(isFrameLikeElement);

+ 76 - 18
packages/excalidraw/tests/linearElementEditor.test.tsx

@@ -343,6 +343,8 @@ describe("Test Linear Elements", () => {
     });
 
     it("should update all the midpoints when element position changed", async () => {
+      const elementsMap = arrayToMap(h.elements);
+
       createThreePointerLinearElement("line", {
         type: ROUNDNESS.PROPORTIONAL_RADIUS,
       });
@@ -351,7 +353,10 @@ describe("Test Linear Elements", () => {
       expect(line.points.length).toEqual(3);
       enterLineEditingMode(line);
 
-      const points = LinearElementEditor.getPointsGlobalCoordinates(line);
+      const points = LinearElementEditor.getPointsGlobalCoordinates(
+        line,
+        elementsMap,
+      );
       expect([line.x, line.y]).toEqual(points[0]);
 
       const midPoints = LinearElementEditor.getEditorMidPoints(
@@ -465,7 +470,11 @@ describe("Test Linear Elements", () => {
       });
 
       it("should update only the first segment midpoint when its point is dragged", async () => {
-        const points = LinearElementEditor.getPointsGlobalCoordinates(line);
+        const elementsMap = arrayToMap(h.elements);
+        const points = LinearElementEditor.getPointsGlobalCoordinates(
+          line,
+          elementsMap,
+        );
         const midPoints = LinearElementEditor.getEditorMidPoints(
           line,
           h.app.scene.getNonDeletedElementsMap(),
@@ -482,7 +491,10 @@ describe("Test Linear Elements", () => {
         );
         expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
 
-        const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
+        const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
+          line,
+          elementsMap,
+        );
         expect([newPoints[0][0], newPoints[0][1]]).toEqual([
           points[0][0] - delta,
           points[0][1] - delta,
@@ -499,7 +511,11 @@ describe("Test Linear Elements", () => {
       });
 
       it("should hide midpoints in the segment when points moved close", async () => {
-        const points = LinearElementEditor.getPointsGlobalCoordinates(line);
+        const elementsMap = arrayToMap(h.elements);
+        const points = LinearElementEditor.getPointsGlobalCoordinates(
+          line,
+          elementsMap,
+        );
         const midPoints = LinearElementEditor.getEditorMidPoints(
           line,
           h.app.scene.getNonDeletedElementsMap(),
@@ -516,7 +532,10 @@ describe("Test Linear Elements", () => {
         );
         expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
 
-        const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
+        const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
+          line,
+          elementsMap,
+        );
         expect([newPoints[0][0], newPoints[0][1]]).toEqual([
           points[0][0] + delta,
           points[0][1] + delta,
@@ -535,7 +554,10 @@ describe("Test Linear Elements", () => {
       it("should remove the midpoint when one of the points in the segment is deleted", async () => {
         const line = h.elements[0] as ExcalidrawLinearElement;
         enterLineEditingMode(line);
-        const points = LinearElementEditor.getPointsGlobalCoordinates(line);
+        const points = LinearElementEditor.getPointsGlobalCoordinates(
+          line,
+          arrayToMap(h.elements),
+        );
 
         // dragging line from last segment midpoint
         drag(lastSegmentMidpoint, [
@@ -637,7 +659,11 @@ describe("Test Linear Elements", () => {
       });
 
       it("should update all the midpoints when its point is dragged", async () => {
-        const points = LinearElementEditor.getPointsGlobalCoordinates(line);
+        const elementsMap = arrayToMap(h.elements);
+        const points = LinearElementEditor.getPointsGlobalCoordinates(
+          line,
+          elementsMap,
+        );
         const midPoints = LinearElementEditor.getEditorMidPoints(
           line,
           h.app.scene.getNonDeletedElementsMap(),
@@ -649,7 +675,10 @@ describe("Test Linear Elements", () => {
         // Drag from first point
         drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
 
-        const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
+        const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
+          line,
+          elementsMap,
+        );
         expect([newPoints[0][0], newPoints[0][1]]).toEqual([
           points[0][0] - delta,
           points[0][1] - delta,
@@ -678,7 +707,11 @@ describe("Test Linear Elements", () => {
       });
 
       it("should hide midpoints in the segment when points moved close", async () => {
-        const points = LinearElementEditor.getPointsGlobalCoordinates(line);
+        const elementsMap = arrayToMap(h.elements);
+        const points = LinearElementEditor.getPointsGlobalCoordinates(
+          line,
+          elementsMap,
+        );
         const midPoints = LinearElementEditor.getEditorMidPoints(
           line,
           h.app.scene.getNonDeletedElementsMap(),
@@ -695,7 +728,10 @@ describe("Test Linear Elements", () => {
         );
         expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
 
-        const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
+        const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
+          line,
+          elementsMap,
+        );
         expect([newPoints[0][0], newPoints[0][1]]).toEqual([
           points[0][0] + delta,
           points[0][1] + delta,
@@ -712,6 +748,8 @@ describe("Test Linear Elements", () => {
       });
 
       it("should update all the midpoints when a point is deleted", async () => {
+        const elementsMap = arrayToMap(h.elements);
+
         drag(lastSegmentMidpoint, [
           lastSegmentMidpoint[0] + delta,
           lastSegmentMidpoint[1] + delta,
@@ -723,7 +761,10 @@ describe("Test Linear Elements", () => {
           h.app.scene.getNonDeletedElementsMap(),
           h.state,
         );
-        const points = LinearElementEditor.getPointsGlobalCoordinates(line);
+        const points = LinearElementEditor.getPointsGlobalCoordinates(
+          line,
+          elementsMap,
+        );
 
         // delete 3rd point
         deletePoint(points[2]);
@@ -837,6 +878,7 @@ describe("Test Linear Elements", () => {
         const position = LinearElementEditor.getBoundTextElementPosition(
           container,
           textElement,
+          arrayToMap(h.elements),
         );
         expect(position).toMatchInlineSnapshot(`
           {
@@ -859,6 +901,7 @@ describe("Test Linear Elements", () => {
         const position = LinearElementEditor.getBoundTextElementPosition(
           container,
           textElement,
+          arrayToMap(h.elements),
         );
         expect(position).toMatchInlineSnapshot(`
           {
@@ -893,6 +936,7 @@ describe("Test Linear Elements", () => {
         const position = LinearElementEditor.getBoundTextElementPosition(
           container,
           textElement,
+          arrayToMap(h.elements),
         );
         expect(position).toMatchInlineSnapshot(`
           {
@@ -1012,8 +1056,13 @@ describe("Test Linear Elements", () => {
       );
       expect(container.width).toBe(70);
       expect(container.height).toBe(50);
-      expect(getBoundTextElementPosition(container, textElement))
-        .toMatchInlineSnapshot(`
+      expect(
+        getBoundTextElementPosition(
+          container,
+          textElement,
+          arrayToMap(h.elements),
+        ),
+      ).toMatchInlineSnapshot(`
           {
             "x": 75,
             "y": 60,
@@ -1051,8 +1100,13 @@ describe("Test Linear Elements", () => {
           }
         `);
 
-      expect(getBoundTextElementPosition(container, textElement))
-        .toMatchInlineSnapshot(`
+      expect(
+        getBoundTextElementPosition(
+          container,
+          textElement,
+          arrayToMap(h.elements),
+        ),
+      ).toMatchInlineSnapshot(`
           {
             "x": 271.11716195150507,
             "y": 45,
@@ -1090,7 +1144,8 @@ describe("Test Linear Elements", () => {
         arrow,
       );
       expect(container.width).toBe(40);
-      expect(getBoundTextElementPosition(container, textElement))
+      const elementsMap = arrayToMap(h.elements);
+      expect(getBoundTextElementPosition(container, textElement, elementsMap))
         .toMatchInlineSnapshot(`
           {
             "x": 25,
@@ -1102,7 +1157,10 @@ describe("Test Linear Elements", () => {
         collaboration made 
         easy"
       `);
-      const points = LinearElementEditor.getPointsGlobalCoordinates(container);
+      const points = LinearElementEditor.getPointsGlobalCoordinates(
+        container,
+        elementsMap,
+      );
 
       // Drag from last point
       drag(points[1], [points[1][0] + 300, points[1][1]]);
@@ -1115,7 +1173,7 @@ describe("Test Linear Elements", () => {
           }
         `);
 
-      expect(getBoundTextElementPosition(container, textElement))
+      expect(getBoundTextElementPosition(container, textElement, elementsMap))
         .toMatchInlineSnapshot(`
           {
             "x": 75,

+ 3 - 1
packages/excalidraw/tests/move.test.tsx

@@ -13,6 +13,7 @@ import {
 import { UI, Pointer, Keyboard } from "./helpers/ui";
 import { KEYS } from "../keys";
 import { vi } from "vitest";
+import { arrayToMap } from "../utils";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -75,12 +76,13 @@ describe("move element", () => {
     const rectA = UI.createElement("rectangle", { size: 100 });
     const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
     const line = UI.createElement("line", { x: 110, y: 50, size: 80 });
-
+    const elementsMap = arrayToMap(h.elements);
     // bind line to two rectangles
     bindOrUnbindLinearElement(
       line.get() as NonDeleted<ExcalidrawLinearElement>,
       rectA.get() as ExcalidrawRectangleElement,
       rectB.get() as ExcalidrawRectangleElement,
+      elementsMap,
     );
 
     // select the second rectangles

+ 13 - 3
packages/excalidraw/tests/resize.test.tsx

@@ -13,6 +13,7 @@ import { API } from "./helpers/api";
 import { KEYS } from "../keys";
 import { isLinearElement } from "../element/typeChecks";
 import { LinearElementEditor } from "../element/linearElementEditor";
+import { arrayToMap } from "../utils";
 
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
@@ -301,10 +302,12 @@ describe("arrow element", () => {
       ],
     });
     const label = await UI.editText(arrow, "Hello");
+    const elementsMap = arrayToMap(h.elements);
     UI.resize(arrow, "se", [50, 30]);
     let labelPos = LinearElementEditor.getBoundTextElementPosition(
       arrow,
       label,
+      elementsMap,
     );
 
     expect(labelPos.x + label.width / 2).toBeCloseTo(
@@ -317,7 +320,11 @@ describe("arrow element", () => {
     expect(label.fontSize).toEqual(20);
 
     UI.resize(arrow, "w", [20, 0]);
-    labelPos = LinearElementEditor.getBoundTextElementPosition(arrow, label);
+    labelPos = LinearElementEditor.getBoundTextElementPosition(
+      arrow,
+      label,
+      elementsMap,
+    );
 
     expect(labelPos.x + label.width / 2).toBeCloseTo(
       arrow.x + arrow.points[2][0],
@@ -743,15 +750,17 @@ describe("multiple selection", () => {
     const selectionTop = 20 - topArrowLabel.height / 2;
     const move = [80, 0] as [number, number];
     const scale = move[0] / selectionWidth + 1;
-
+    const elementsMap = arrayToMap(h.elements);
     UI.resize([topArrow.get(), bottomArrow.get()], "se", move);
     const topArrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
       topArrow,
       topArrowLabel,
+      elementsMap,
     );
     const bottomArrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
       bottomArrow,
       bottomArrowLabel,
+      elementsMap,
     );
 
     expect(topArrow.x).toBeCloseTo(0);
@@ -944,12 +953,13 @@ describe("multiple selection", () => {
     const scaleX = move[0] / selectionWidth + 1;
     const scaleY = -scaleX;
     const lineOrigBounds = getBoundsFromPoints(line);
-
+    const elementsMap = arrayToMap(h.elements);
     UI.resize([line, image, rectangle, boundArrow], "se", move);
     const lineNewBounds = getBoundsFromPoints(line);
     const arrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
       boundArrow,
       arrowLabel,
+      elementsMap,
     );
 
     expect(line.x).toBeCloseTo(60 * scaleX);