Browse Source

fix: make getBoundTextElement and related helpers pure (#7601)

* fix: make getBoundTextElement pure

* updating args

* fix

* pass boundTextElement to getBoundTextMaxWidth

* fix labelled arrows

* lint

* pass elementsMap to removeElementsFromFrame

* pass elementsMap to getMaximumGroups, alignElements and distributeElements

* lint

* pass allElementsMap to renderElement

* lint

* feat: make more typesafe

* fix: remove unnecessary assertion

* fix: remove unused params

---------

Co-authored-by: dwelle <[email protected]>
Aakansha Doshi 1 year ago
parent
commit
10bd08ef19
34 changed files with 385 additions and 143 deletions
  1. 6 1
      packages/excalidraw/actions/actionAlign.tsx
  2. 6 2
      packages/excalidraw/actions/actionBoundText.tsx
  3. 5 1
      packages/excalidraw/actions/actionDistribute.tsx
  4. 1 1
      packages/excalidraw/actions/actionDuplicateSelection.tsx
  5. 3 2
      packages/excalidraw/actions/actionFlip.ts
  6. 5 1
      packages/excalidraw/actions/actionGroup.tsx
  7. 38 12
      packages/excalidraw/actions/actionProperties.tsx
  8. 6 3
      packages/excalidraw/actions/actionStyles.ts
  9. 6 3
      packages/excalidraw/align.ts
  10. 6 2
      packages/excalidraw/components/Actions.tsx
  11. 48 9
      packages/excalidraw/components/App.tsx
  12. 6 1
      packages/excalidraw/components/canvases/StaticCanvas.tsx
  13. 4 1
      packages/excalidraw/data/transform.ts
  14. 3 2
      packages/excalidraw/distribute.ts
  15. 7 4
      packages/excalidraw/element/binding.ts
  16. 18 9
      packages/excalidraw/element/bounds.ts
  17. 7 3
      packages/excalidraw/element/collision.ts
  18. 4 1
      packages/excalidraw/element/dragElements.ts
  19. 19 6
      packages/excalidraw/element/linearElementEditor.ts
  20. 1 1
      packages/excalidraw/element/newElement.ts
  21. 9 6
      packages/excalidraw/element/resizeElements.ts
  22. 3 3
      packages/excalidraw/element/textElement.test.ts
  23. 17 27
      packages/excalidraw/element/textElement.ts
  24. 8 2
      packages/excalidraw/element/textWysiwyg.tsx
  25. 10 0
      packages/excalidraw/element/types.ts
  26. 7 4
      packages/excalidraw/frame.ts
  27. 3 2
      packages/excalidraw/groups.ts
  28. 21 7
      packages/excalidraw/renderer/renderElement.ts
  29. 15 1
      packages/excalidraw/renderer/renderScene.ts
  30. 5 4
      packages/excalidraw/scene/Scene.ts
  31. 7 5
      packages/excalidraw/scene/export.ts
  32. 2 0
      packages/excalidraw/scene/types.ts
  33. 6 2
      packages/excalidraw/snapping.ts
  34. 73 15
      packages/excalidraw/tests/linearElementEditor.test.tsx

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

@@ -40,8 +40,13 @@ const alignSelectedElements = (
   alignment: Alignment,
 ) => {
   const selectedElements = app.scene.getSelectedElements(appState);
+  const elementsMap = arrayToMap(elements);
 
-  const updatedElements = alignElements(selectedElements, alignment);
+  const updatedElements = alignElements(
+    selectedElements,
+    elementsMap,
+    alignment,
+  );
 
   const updatedElementsMap = arrayToMap(updatedElements);
 

+ 6 - 2
packages/excalidraw/actions/actionBoundText.tsx

@@ -45,8 +45,9 @@ export const actionUnbindText = register({
   },
   perform: (elements, appState, _, app) => {
     const selectedElements = app.scene.getSelectedElements(appState);
+    const elementsMap = app.scene.getNonDeletedElementsMap();
     selectedElements.forEach((element) => {
-      const boundTextElement = getBoundTextElement(element);
+      const boundTextElement = getBoundTextElement(element, elementsMap);
       if (boundTextElement) {
         const { width, height, baseline } = measureText(
           boundTextElement.originalText,
@@ -106,7 +107,10 @@ export const actionBindText = register({
       if (
         textElement &&
         bindingContainer &&
-        getBoundTextElement(bindingContainer) === null
+        getBoundTextElement(
+          bindingContainer,
+          app.scene.getNonDeletedElementsMap(),
+        ) === null
       ) {
         return true;
       }

+ 5 - 1
packages/excalidraw/actions/actionDistribute.tsx

@@ -32,7 +32,11 @@ const distributeSelectedElements = (
 ) => {
   const selectedElements = app.scene.getSelectedElements(appState);
 
-  const updatedElements = distributeElements(selectedElements, distribution);
+  const updatedElements = distributeElements(
+    selectedElements,
+    app.scene.getNonDeletedElementsMap(),
+    distribution,
+  );
 
   const updatedElementsMap = arrayToMap(updatedElements);
 

+ 1 - 1
packages/excalidraw/actions/actionDuplicateSelection.tsx

@@ -139,7 +139,7 @@ const duplicateElements = (
       continue;
     }
 
-    const boundTextElement = getBoundTextElement(element);
+    const boundTextElement = getBoundTextElement(element, arrayToMap(elements));
     const isElementAFrameLike = isFrameLikeElement(element);
 
     if (idsOfElementsToDuplicate.get(element.id)) {

+ 3 - 2
packages/excalidraw/actions/actionFlip.ts

@@ -5,6 +5,7 @@ import {
   ExcalidrawElement,
   NonDeleted,
   NonDeletedElementsMap,
+  NonDeletedSceneElementsMap,
 } from "../element/types";
 import { resizeMultipleElements } from "../element/resizeElements";
 import { AppState } from "../types";
@@ -67,7 +68,7 @@ export const actionFlipVertical = register({
 
 const flipSelectedElements = (
   elements: readonly ExcalidrawElement[],
-  elementsMap: NonDeletedElementsMap,
+  elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
   appState: Readonly<AppState>,
   flipDirection: "horizontal" | "vertical",
 ) => {
@@ -96,7 +97,7 @@ const flipSelectedElements = (
 
 const flipElements = (
   selectedElements: NonDeleted<ExcalidrawElement>[],
-  elementsMap: NonDeletedElementsMap,
+  elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
   appState: AppState,
   flipDirection: "horizontal" | "vertical",
 ): ExcalidrawElement[] => {

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

@@ -105,7 +105,10 @@ export const actionGroup = register({
       const frameElementsMap = groupByFrameLikes(selectedElements);
 
       frameElementsMap.forEach((elementsInFrame, frameId) => {
-        removeElementsFromFrame(elementsInFrame);
+        removeElementsFromFrame(
+          elementsInFrame,
+          app.scene.getNonDeletedElementsMap(),
+        );
       });
     }
 
@@ -225,6 +228,7 @@ export const actionUngroup = register({
           nextElements,
           getElementsInResizingFrame(nextElements, frame, appState),
           frame,
+          app,
         );
       }
     });

+ 38 - 12
packages/excalidraw/actions/actionProperties.tsx

@@ -606,7 +606,7 @@ export const actionChangeFontSize = register({
   perform: (elements, appState, value, app) => {
     return changeFontSize(elements, appState, app, () => value, value);
   },
-  PanelComponent: ({ elements, appState, updateData }) => (
+  PanelComponent: ({ elements, appState, updateData, app }) => (
     <fieldset>
       <legend>{t("labels.fontSize")}</legend>
       <ButtonIconSelect
@@ -644,14 +644,21 @@ export const actionChangeFontSize = register({
             if (isTextElement(element)) {
               return element.fontSize;
             }
-            const boundTextElement = getBoundTextElement(element);
+            const boundTextElement = getBoundTextElement(
+              element,
+              app.scene.getNonDeletedElementsMap(),
+            );
             if (boundTextElement) {
               return boundTextElement.fontSize;
             }
             return null;
           },
           (element) =>
-            isTextElement(element) || getBoundTextElement(element) !== null,
+            isTextElement(element) ||
+            getBoundTextElement(
+              element,
+              app.scene.getNonDeletedElementsMap(),
+            ) !== null,
           (hasSelection) =>
             hasSelection
               ? null
@@ -738,7 +745,7 @@ export const actionChangeFontFamily = register({
       commitToHistory: true,
     };
   },
-  PanelComponent: ({ elements, appState, updateData }) => {
+  PanelComponent: ({ elements, appState, updateData, app }) => {
     const options: {
       value: FontFamilyValues;
       text: string;
@@ -778,14 +785,21 @@ export const actionChangeFontFamily = register({
               if (isTextElement(element)) {
                 return element.fontFamily;
               }
-              const boundTextElement = getBoundTextElement(element);
+              const boundTextElement = getBoundTextElement(
+                element,
+                app.scene.getNonDeletedElementsMap(),
+              );
               if (boundTextElement) {
                 return boundTextElement.fontFamily;
               }
               return null;
             },
             (element) =>
-              isTextElement(element) || getBoundTextElement(element) !== null,
+              isTextElement(element) ||
+              getBoundTextElement(
+                element,
+                app.scene.getNonDeletedElementsMap(),
+              ) !== null,
             (hasSelection) =>
               hasSelection
                 ? null
@@ -830,7 +844,8 @@ export const actionChangeTextAlign = register({
       commitToHistory: true,
     };
   },
-  PanelComponent: ({ elements, appState, updateData }) => {
+  PanelComponent: ({ elements, appState, updateData, app }) => {
+    const elementsMap = app.scene.getNonDeletedElementsMap();
     return (
       <fieldset>
         <legend>{t("labels.textAlign")}</legend>
@@ -863,14 +878,18 @@ export const actionChangeTextAlign = register({
               if (isTextElement(element)) {
                 return element.textAlign;
               }
-              const boundTextElement = getBoundTextElement(element);
+              const boundTextElement = getBoundTextElement(
+                element,
+                elementsMap,
+              );
               if (boundTextElement) {
                 return boundTextElement.textAlign;
               }
               return null;
             },
             (element) =>
-              isTextElement(element) || getBoundTextElement(element) !== null,
+              isTextElement(element) ||
+              getBoundTextElement(element, elementsMap) !== null,
             (hasSelection) =>
               hasSelection ? null : appState.currentItemTextAlign,
           )}
@@ -913,7 +932,7 @@ export const actionChangeVerticalAlign = register({
       commitToHistory: true,
     };
   },
-  PanelComponent: ({ elements, appState, updateData }) => {
+  PanelComponent: ({ elements, appState, updateData, app }) => {
     return (
       <fieldset>
         <ButtonIconSelect<VerticalAlign | false>
@@ -945,14 +964,21 @@ export const actionChangeVerticalAlign = register({
               if (isTextElement(element) && element.containerId) {
                 return element.verticalAlign;
               }
-              const boundTextElement = getBoundTextElement(element);
+              const boundTextElement = getBoundTextElement(
+                element,
+                app.scene.getNonDeletedElementsMap(),
+              );
               if (boundTextElement) {
                 return boundTextElement.verticalAlign;
               }
               return null;
             },
             (element) =>
-              isTextElement(element) || getBoundTextElement(element) !== null,
+              isTextElement(element) ||
+              getBoundTextElement(
+                element,
+                app.scene.getNonDeletedElementsMap(),
+              ) !== null,
             (hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
           )}
           onChange={(value) => updateData(value)}

+ 6 - 3
packages/excalidraw/actions/actionStyles.ts

@@ -32,12 +32,15 @@ export let copiedStyles: string = "{}";
 export const actionCopyStyles = register({
   name: "copyStyles",
   trackEvent: { category: "element" },
-  perform: (elements, appState) => {
+  perform: (elements, appState, formData, app) => {
     const elementsCopied = [];
     const element = elements.find((el) => appState.selectedElementIds[el.id]);
     elementsCopied.push(element);
     if (element && hasBoundTextElement(element)) {
-      const boundTextElement = getBoundTextElement(element);
+      const boundTextElement = getBoundTextElement(
+        element,
+        app.scene.getNonDeletedElementsMap(),
+      );
       elementsCopied.push(boundTextElement);
     }
     if (element) {
@@ -59,7 +62,7 @@ export const actionCopyStyles = register({
 export const actionPasteStyles = register({
   name: "pasteStyles",
   trackEvent: { category: "element" },
-  perform: (elements, appState) => {
+  perform: (elements, appState, formData, app) => {
     const elementsCopied = JSON.parse(copiedStyles);
     const pastedElement = elementsCopied[0];
     const boundTextElement = elementsCopied[1];

+ 6 - 3
packages/excalidraw/align.ts

@@ -1,4 +1,4 @@
-import { ExcalidrawElement } from "./element/types";
+import { ElementsMap, ExcalidrawElement } from "./element/types";
 import { newElementWith } from "./element/mutateElement";
 import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
 import { getMaximumGroups } from "./groups";
@@ -10,10 +10,13 @@ export interface Alignment {
 
 export const alignElements = (
   selectedElements: ExcalidrawElement[],
+  elementsMap: ElementsMap,
   alignment: Alignment,
 ): ExcalidrawElement[] => {
-  const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements);
-
+  const groups: ExcalidrawElement[][] = getMaximumGroups(
+    selectedElements,
+    elementsMap,
+  );
   const selectionBoundingBox = getCommonBoundingBox(selectedElements);
 
   return groups.flatMap((group) => {

+ 6 - 2
packages/excalidraw/components/Actions.tsx

@@ -1,6 +1,10 @@
 import { useState } from "react";
 import { ActionManager } from "../actions/manager";
-import { ExcalidrawElementType, NonDeletedElementsMap } from "../element/types";
+import {
+  ExcalidrawElementType,
+  NonDeletedElementsMap,
+  NonDeletedSceneElementsMap,
+} from "../element/types";
 import { t } from "../i18n";
 import { useDevice } from "./App";
 import {
@@ -47,7 +51,7 @@ export const SelectedShapeActions = ({
   renderAction,
 }: {
   appState: UIAppState;
-  elementsMap: NonDeletedElementsMap;
+  elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
   renderAction: ActionManager["renderAction"];
 }) => {
   const targetElements = getTargetElements(elementsMap, appState);

+ 48 - 9
packages/excalidraw/components/App.tsx

@@ -1431,6 +1431,8 @@ class App extends React.Component<AppProps, AppState> {
         pendingImageElementId: this.state.pendingImageElementId,
       });
 
+    const allElementsMap = this.scene.getNonDeletedElementsMap();
+
     const shouldBlockPointerEvents =
       !(
         this.state.editingElement && isLinearElement(this.state.editingElement)
@@ -1628,6 +1630,7 @@ class App extends React.Component<AppProps, AppState> {
                           canvas={this.canvas}
                           rc={this.rc}
                           elementsMap={elementsMap}
+                          allElementsMap={allElementsMap}
                           visibleElements={visibleElements}
                           versionNonce={versionNonce}
                           selectionNonce={
@@ -3869,7 +3872,11 @@ class App extends React.Component<AppProps, AppState> {
             if (!isTextElement(selectedElement)) {
               container = selectedElement as ExcalidrawTextContainer;
             }
-            const midPoint = getContainerCenter(selectedElement, this.state);
+            const midPoint = getContainerCenter(
+              selectedElement,
+              this.state,
+              this.scene.getNonDeletedElementsMap(),
+            );
             const sceneX = midPoint.x;
             const sceneY = midPoint.y;
             this.startTextEditing({
@@ -4333,6 +4340,7 @@ class App extends React.Component<AppProps, AppState> {
         this.frameNameBoundsCache,
         x,
         y,
+        this.scene.getNonDeletedElementsMap(),
       )
         ? allHitElements[allHitElements.length - 2]
         : elementWithHighestZIndex;
@@ -4362,7 +4370,14 @@ class App extends React.Component<AppProps, AppState> {
             );
 
     return getElementsAtPosition(elements, (element) =>
-      hitTest(element, this.state, this.frameNameBoundsCache, x, y),
+      hitTest(
+        element,
+        this.state,
+        this.frameNameBoundsCache,
+        x,
+        y,
+        this.scene.getNonDeletedElementsMap(),
+      ),
     ).filter((element) => {
       // hitting a frame's element from outside the frame is not considered a hit
       const containingFrame = getContainingFrame(element);
@@ -4399,7 +4414,10 @@ class App extends React.Component<AppProps, AppState> {
         container,
       );
     if (container && parentCenterPosition) {
-      const boundTextElementToContainer = getBoundTextElement(container);
+      const boundTextElementToContainer = getBoundTextElement(
+        container,
+        this.scene.getNonDeletedElementsMap(),
+      );
       if (!boundTextElementToContainer) {
         shouldBindToContainer = true;
       }
@@ -4412,7 +4430,10 @@ class App extends React.Component<AppProps, AppState> {
       if (isTextElement(selectedElements[0])) {
         existingTextElement = selectedElements[0];
       } else if (container) {
-        existingTextElement = getBoundTextElement(selectedElements[0]);
+        existingTextElement = getBoundTextElement(
+          selectedElements[0],
+          this.scene.getNonDeletedElementsMap(),
+        );
       } else {
         existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
       }
@@ -4621,7 +4642,11 @@ class App extends React.Component<AppProps, AppState> {
             [sceneX, sceneY],
           )
         ) {
-          const midPoint = getContainerCenter(container, this.state);
+          const midPoint = getContainerCenter(
+            container,
+            this.state,
+            this.scene.getNonDeletedElementsMap(),
+          );
 
           sceneX = midPoint.x;
           sceneY = midPoint.y;
@@ -5257,8 +5282,8 @@ class App extends React.Component<AppProps, AppState> {
     const element = LinearElementEditor.getElement(
       linearElementEditor.elementId,
     );
-
-    const boundTextElement = getBoundTextElement(element);
+    const elementsMap = this.scene.getNonDeletedElementsMap();
+    const boundTextElement = getBoundTextElement(element, elementsMap);
 
     if (!element) {
       return;
@@ -5285,6 +5310,7 @@ class App extends React.Component<AppProps, AppState> {
             linearElementEditor,
             { x: scenePointerX, y: scenePointerY },
             this.state,
+            this.scene.getNonDeletedElementsMap(),
           );
 
         if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
@@ -5300,6 +5326,7 @@ class App extends React.Component<AppProps, AppState> {
           this.frameNameBoundsCache,
           scenePointerX,
           scenePointerY,
+          elementsMap,
         )
       ) {
         setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
@@ -5311,6 +5338,7 @@ class App extends React.Component<AppProps, AppState> {
           this.frameNameBoundsCache,
           scenePointerX,
           scenePointerY,
+          this.scene.getNonDeletedElementsMap(),
         )
       ) {
         setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
@@ -6060,6 +6088,7 @@ class App extends React.Component<AppProps, AppState> {
             this.history,
             pointerDownState.origin,
             linearElementEditor,
+            this.scene.getNonDeletedElementsMap(),
           );
           if (ret.hitElement) {
             pointerDownState.hit.element = ret.hitElement;
@@ -6995,6 +7024,7 @@ class App extends React.Component<AppProps, AppState> {
             );
           },
           linearElementEditor,
+          this.scene.getNonDeletedElementsMap(),
         );
         if (didDrag) {
           pointerDownState.lastCoords.x = pointerCoords.x;
@@ -7713,7 +7743,10 @@ class App extends React.Component<AppProps, AppState> {
                     groupIds: [],
                   });
 
-                  removeElementsFromFrame([linearElement]);
+                  removeElementsFromFrame(
+                    [linearElement],
+                    this.scene.getNonDeletedElementsMap(),
+                  );
 
                   this.scene.informMutation();
                 }
@@ -7866,6 +7899,7 @@ class App extends React.Component<AppProps, AppState> {
               this.state,
             ),
             frame,
+            this,
           );
         }
 
@@ -8093,6 +8127,7 @@ class App extends React.Component<AppProps, AppState> {
             this.frameNameBoundsCache,
             pointerDownState.origin.x,
             pointerDownState.origin.y,
+            this.scene.getNonDeletedElementsMap(),
           )) ||
           (!hitElement &&
             pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
@@ -9334,7 +9369,11 @@ class App extends React.Component<AppProps, AppState> {
       let elementCenterX = container.x + container.width / 2;
       let elementCenterY = container.y + container.height / 2;
 
-      const elementCenter = getContainerCenter(container, appState);
+      const elementCenter = getContainerCenter(
+        container,
+        appState,
+        this.scene.getNonDeletedElementsMap(),
+      );
       if (elementCenter) {
         elementCenterX = elementCenter.x;
         elementCenterY = elementCenter.y;

+ 6 - 1
packages/excalidraw/components/canvases/StaticCanvas.tsx

@@ -7,13 +7,17 @@ import type {
   RenderableElementsMap,
   StaticCanvasRenderConfig,
 } from "../../scene/types";
-import type { NonDeletedExcalidrawElement } from "../../element/types";
+import type {
+  NonDeletedExcalidrawElement,
+  NonDeletedSceneElementsMap,
+} from "../../element/types";
 import { isRenderThrottlingEnabled } from "../../reactUtils";
 
 type StaticCanvasProps = {
   canvas: HTMLCanvasElement;
   rc: RoughCanvas;
   elementsMap: RenderableElementsMap;
+  allElementsMap: NonDeletedSceneElementsMap;
   visibleElements: readonly NonDeletedExcalidrawElement[];
   versionNonce: number | undefined;
   selectionNonce: number | undefined;
@@ -67,6 +71,7 @@ const StaticCanvas = (props: StaticCanvasProps) => {
         rc: props.rc,
         scale: props.scale,
         elementsMap: props.elementsMap,
+        allElementsMap: props.allElementsMap,
         visibleElements: props.visibleElements,
         appState: props.appState,
         renderConfig: props.renderConfig,

+ 4 - 1
packages/excalidraw/data/transform.ts

@@ -24,6 +24,7 @@ import {
   normalizeText,
 } from "../element/textElement";
 import {
+  ElementsMap,
   ExcalidrawArrowElement,
   ExcalidrawBindableElement,
   ExcalidrawElement,
@@ -42,7 +43,7 @@ import {
   VerticalAlign,
 } from "../element/types";
 import { MarkOptional } from "../utility-types";
-import { assertNever, cloneJSON, getFontString } from "../utils";
+import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils";
 import { getSizeFromPoints } from "../points";
 import { randomId } from "../random";
 
@@ -202,6 +203,7 @@ const DEFAULT_DIMENSION = 100;
 const bindTextToContainer = (
   container: ExcalidrawElement,
   textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
+  elementsMap: ElementsMap,
 ) => {
   const textElement: ExcalidrawTextElement = newTextElement({
     x: 0,
@@ -623,6 +625,7 @@ export const convertToExcalidrawElements = (
           let [container, text] = bindTextToContainer(
             excalidrawElement,
             element?.label,
+            arrayToMap(elementStore.getElements()),
           );
           elementStore.add(container);
           elementStore.add(text);

+ 3 - 2
packages/excalidraw/distribute.ts

@@ -1,7 +1,7 @@
-import { ExcalidrawElement } from "./element/types";
 import { newElementWith } from "./element/mutateElement";
 import { getMaximumGroups } from "./groups";
 import { getCommonBoundingBox } from "./element/bounds";
+import type { ElementsMap, ExcalidrawElement } from "./element/types";
 
 export interface Distribution {
   space: "between";
@@ -10,6 +10,7 @@ export interface Distribution {
 
 export const distributeElements = (
   selectedElements: ExcalidrawElement[],
+  elementsMap: ElementsMap,
   distribution: Distribution,
 ): ExcalidrawElement[] => {
   const [start, mid, end, extent] =
@@ -18,7 +19,7 @@ export const distributeElements = (
       : (["minY", "midY", "maxY", "height"] as const);
 
   const bounds = getCommonBoundingBox(selectedElements);
-  const groups = getMaximumGroups(selectedElements)
+  const groups = getMaximumGroups(selectedElements, elementsMap)
     .map((group) => [group, getCommonBoundingBox(group)] as const)
     .sort((a, b) => a[1][mid] - b[1][mid]);
 

+ 7 - 4
packages/excalidraw/element/binding.ts

@@ -321,9 +321,9 @@ export const updateBoundElements = (
   const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
     simultaneouslyUpdated,
   );
-
+  const scene = Scene.getScene(changedElement)!;
   getNonDeletedElements(
-    Scene.getScene(changedElement)!,
+    scene,
     boundLinearElements.map((el) => el.id),
   ).forEach((element) => {
     if (!isLinearElement(element)) {
@@ -362,9 +362,12 @@ export const updateBoundElements = (
       endBinding,
       changedElement as ExcalidrawBindableElement,
     );
-    const boundText = getBoundTextElement(element);
+    const boundText = getBoundTextElement(
+      element,
+      scene.getNonDeletedElementsMap(),
+    );
     if (boundText) {
-      handleBindTextResize(element, false);
+      handleBindTextResize(element, scene.getNonDeletedElementsMap(), false);
     }
   });
 };

+ 18 - 9
packages/excalidraw/element/bounds.ts

@@ -6,6 +6,7 @@ import {
   NonDeleted,
   ExcalidrawTextElementWithContainer,
   ElementsMapOrArray,
+  ElementsMap,
 } from "./types";
 import { distance2d, rotate, rotatePoint } from "../math";
 import rough from "roughjs/bin/rough";
@@ -74,13 +75,16 @@ export class ElementBounds {
     ) {
       return cachedBounds.bounds;
     }
-
-    const bounds = ElementBounds.calculateBounds(element);
+    const scene = Scene.getScene(element);
+    const bounds = ElementBounds.calculateBounds(
+      element,
+      scene?.getNonDeletedElementsMap() || new Map(),
+    );
 
     // hack to ensure that downstream checks could retrieve element Scene
     // so as to have correctly calculated bounds
     // FIXME remove when we get rid of all the id:Scene / element:Scene mapping
-    const shouldCache = Scene.getScene(element);
+    const shouldCache = !!scene;
 
     if (shouldCache) {
       ElementBounds.boundsCache.set(element, {
@@ -92,7 +96,10 @@ export class ElementBounds {
     return bounds;
   }
 
-  private static calculateBounds(element: ExcalidrawElement): Bounds {
+  private static calculateBounds(
+    element: ExcalidrawElement,
+    elementsMap: ElementsMap,
+  ): Bounds {
     let bounds: Bounds;
 
     const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
@@ -111,7 +118,7 @@ export class ElementBounds {
         maxY + element.y,
       ];
     } else if (isLinearElement(element)) {
-      bounds = getLinearElementRotatedBounds(element, cx, cy);
+      bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
     } else if (element.type === "diamond") {
       const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
       const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
@@ -154,16 +161,17 @@ export const getElementAbsoluteCoords = (
   element: ExcalidrawElement,
   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)) {
     return LinearElementEditor.getElementAbsoluteCoords(
       element,
+      elementsMap,
       includeBoundText,
     );
   } else if (isTextElement(element)) {
-    const elementsMap =
-      Scene.getScene(element)?.getElementsMapIncludingDeleted();
     const container = elementsMap
       ? getContainerElement(element, elementsMap)
       : null;
@@ -677,7 +685,10 @@ const getLinearElementRotatedBounds = (
   element: ExcalidrawLinearElement,
   cx: number,
   cy: number,
+  elementsMap: ElementsMap,
 ): Bounds => {
+  const boundTextElement = getBoundTextElement(element, elementsMap);
+
   if (element.points.length < 2) {
     const [pointX, pointY] = element.points[0];
     const [x, y] = rotate(
@@ -689,7 +700,6 @@ const getLinearElementRotatedBounds = (
     );
 
     let coords: Bounds = [x, y, x, y];
-    const boundTextElement = getBoundTextElement(element);
     if (boundTextElement) {
       const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
         element,
@@ -714,7 +724,6 @@ const getLinearElementRotatedBounds = (
     rotate(element.x + x, element.y + y, cx, cy, element.angle);
   const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
   let coords: Bounds = [res[0], res[1], res[2], res[3]];
-  const boundTextElement = getBoundTextElement(element);
   if (boundTextElement) {
     const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
       element,

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

@@ -28,6 +28,7 @@ import {
   StrokeRoundness,
   ExcalidrawFrameLikeElement,
   ExcalidrawIframeLikeElement,
+  ElementsMap,
 } from "./types";
 
 import {
@@ -78,6 +79,7 @@ export const hitTest = (
   frameNameBoundsCache: FrameNameBoundsCache,
   x: number,
   y: number,
+  elementsMap: ElementsMap,
 ): boolean => {
   // How many pixels off the shape boundary we still consider a hit
   const threshold = 10 / appState.zoom.value;
@@ -95,7 +97,7 @@ export const hitTest = (
     );
   }
 
-  const boundTextElement = getBoundTextElement(element);
+  const boundTextElement = getBoundTextElement(element, elementsMap);
   if (boundTextElement) {
     const isHittingBoundTextElement = hitTest(
       boundTextElement,
@@ -103,6 +105,7 @@ export const hitTest = (
       frameNameBoundsCache,
       x,
       y,
+      elementsMap,
     );
     if (isHittingBoundTextElement) {
       return true;
@@ -122,15 +125,16 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
   frameNameBoundsCache: FrameNameBoundsCache,
   x: number,
   y: number,
+  elementsMap: ElementsMap,
 ): boolean => {
   const threshold = 10 / appState.zoom.value;
 
   // So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element
   // eg for linear elements text can be outside the element bounding box
-  const boundTextElement = getBoundTextElement(element);
+  const boundTextElement = getBoundTextElement(element, elementsMap);
   if (
     boundTextElement &&
-    hitTest(boundTextElement, appState, frameNameBoundsCache, x, y)
+    hitTest(boundTextElement, appState, frameNameBoundsCache, x, y, elementsMap)
   ) {
     return false;
   }

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

@@ -57,7 +57,10 @@ export const dragSelectedElements = (
       // skip arrow labels since we calculate its position during render
       !isArrowElement(element)
     ) {
-      const textElement = getBoundTextElement(element);
+      const textElement = getBoundTextElement(
+        element,
+        scene.getNonDeletedElementsMap(),
+      );
       if (textElement) {
         updateElementCoords(pointerDownState, textElement, adjustedOffset);
       }

+ 19 - 6
packages/excalidraw/element/linearElementEditor.ts

@@ -5,6 +5,7 @@ import {
   PointBinding,
   ExcalidrawBindableElement,
   ExcalidrawTextElementWithContainer,
+  ElementsMap,
 } from "./types";
 import {
   distance2d,
@@ -193,6 +194,7 @@ export class LinearElementEditor {
       pointSceneCoords: { x: number; y: number }[],
     ) => void,
     linearElementEditor: LinearElementEditor,
+    elementsMap: ElementsMap,
   ): boolean {
     if (!linearElementEditor) {
       return false;
@@ -272,9 +274,9 @@ export class LinearElementEditor {
         );
       }
 
-      const boundTextElement = getBoundTextElement(element);
+      const boundTextElement = getBoundTextElement(element, elementsMap);
       if (boundTextElement) {
-        handleBindTextResize(element, false);
+        handleBindTextResize(element, elementsMap, false);
       }
 
       // suggest bindings for first and last point if selected
@@ -404,9 +406,10 @@ export class LinearElementEditor {
 
   static getEditorMidPoints = (
     element: NonDeleted<ExcalidrawLinearElement>,
+    elementsMap: ElementsMap,
     appState: InteractiveCanvasAppState,
   ): typeof editorMidPointsCache["points"] => {
-    const boundText = getBoundTextElement(element);
+    const boundText = getBoundTextElement(element, elementsMap);
 
     // Since its not needed outside editor unless 2 pointer lines or bound text
     if (
@@ -465,6 +468,7 @@ export class LinearElementEditor {
     linearElementEditor: LinearElementEditor,
     scenePointer: { x: number; y: number },
     appState: AppState,
+    elementsMap: ElementsMap,
   ) => {
     const { elementId } = linearElementEditor;
     const element = LinearElementEditor.getElement(elementId);
@@ -503,7 +507,7 @@ export class LinearElementEditor {
     }
     let index = 0;
     const midPoints: typeof editorMidPointsCache["points"] =
-      LinearElementEditor.getEditorMidPoints(element, appState);
+      LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
     while (index < midPoints.length) {
       if (midPoints[index] !== null) {
         const distance = distance2d(
@@ -581,6 +585,7 @@ export class LinearElementEditor {
     linearElementEditor: LinearElementEditor,
     appState: AppState,
     midPoint: Point,
+    elementsMap: ElementsMap,
   ) {
     const element = LinearElementEditor.getElement(
       linearElementEditor.elementId,
@@ -588,7 +593,11 @@ export class LinearElementEditor {
     if (!element) {
       return -1;
     }
-    const midPoints = LinearElementEditor.getEditorMidPoints(element, appState);
+    const midPoints = LinearElementEditor.getEditorMidPoints(
+      element,
+      elementsMap,
+      appState,
+    );
     let index = 0;
     while (index < midPoints.length) {
       if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) {
@@ -605,6 +614,7 @@ export class LinearElementEditor {
     history: History,
     scenePointer: { x: number; y: number },
     linearElementEditor: LinearElementEditor,
+    elementsMap: ElementsMap,
   ): {
     didAddPoint: boolean;
     hitElement: NonDeleted<ExcalidrawElement> | null;
@@ -630,6 +640,7 @@ export class LinearElementEditor {
       linearElementEditor,
       scenePointer,
       appState,
+      elementsMap,
     );
     let segmentMidpointIndex = null;
     if (segmentMidpoint) {
@@ -637,6 +648,7 @@ export class LinearElementEditor {
         linearElementEditor,
         appState,
         segmentMidpoint,
+        elementsMap,
       );
     }
     if (event.altKey && appState.editingLinearElement) {
@@ -1418,6 +1430,7 @@ export class LinearElementEditor {
 
   static getElementAbsoluteCoords = (
     element: ExcalidrawLinearElement,
+    elementsMap: ElementsMap,
     includeBoundText: boolean = false,
   ): [number, number, number, number, number, number] => {
     let coords: [number, number, number, number, number, number];
@@ -1462,7 +1475,7 @@ export class LinearElementEditor {
     if (!includeBoundText) {
       return coords;
     }
-    const boundTextElement = getBoundTextElement(element);
+    const boundTextElement = getBoundTextElement(element, elementsMap);
     if (boundTextElement) {
       coords = LinearElementEditor.getMinMaxXYWithBoundText(
         element,

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

@@ -342,7 +342,7 @@ export const refreshTextDimensions = (
     text = wrapText(
       text,
       getFontString(textElement),
-      getBoundTextMaxWidth(container),
+      getBoundTextMaxWidth(container, textElement),
     );
   }
   const dimensions = getAdjustedDimensions(textElement, text);

+ 9 - 6
packages/excalidraw/element/resizeElements.ts

@@ -126,6 +126,7 @@ export const transformElements = (
       rotateMultipleElements(
         originalElements,
         selectedElements,
+        elementsMap,
         pointerX,
         pointerY,
         shouldRotateWithDiscreteAngle,
@@ -219,7 +220,7 @@ const measureFontSizeFromWidth = (
   if (hasContainer) {
     const container = getContainerElement(element, elementsMap);
     if (container) {
-      width = getBoundTextMaxWidth(container);
+      width = getBoundTextMaxWidth(container, element);
     }
   }
   const nextFontSize = element.fontSize * (nextWidth / width);
@@ -394,7 +395,7 @@ export const resizeSingleElement = (
   let scaleY = atStartBoundsHeight / boundsCurrentHeight;
 
   let boundTextFont: { fontSize?: number; baseline?: number } = {};
-  const boundTextElement = getBoundTextElement(element);
+  const boundTextElement = getBoundTextElement(element, elementsMap);
 
   if (transformHandleDirection.includes("e")) {
     scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
@@ -458,7 +459,7 @@ export const resizeSingleElement = (
       const nextFont = measureFontSizeFromWidth(
         boundTextElement,
         elementsMap,
-        getBoundTextMaxWidth(updatedElement),
+        getBoundTextMaxWidth(updatedElement, boundTextElement),
         getBoundTextMaxHeight(updatedElement, boundTextElement),
       );
       if (nextFont === null) {
@@ -640,6 +641,7 @@ export const resizeSingleElement = (
     }
     handleBindTextResize(
       element,
+      elementsMap,
       transformHandleDirection,
       shouldMaintainAspectRatio,
     );
@@ -882,7 +884,7 @@ export const resizeMultipleElements = (
       newSize: { width, height },
     });
 
-    const boundTextElement = getBoundTextElement(element);
+    const boundTextElement = getBoundTextElement(element, elementsMap);
     if (boundTextElement && boundTextFontSize) {
       mutateElement(
         boundTextElement,
@@ -892,7 +894,7 @@ export const resizeMultipleElements = (
         },
         false,
       );
-      handleBindTextResize(element, transformHandleType, true);
+      handleBindTextResize(element, elementsMap, transformHandleType, true);
     }
   }
 
@@ -902,6 +904,7 @@ export const resizeMultipleElements = (
 const rotateMultipleElements = (
   originalElements: PointerDownState["originalElements"],
   elements: readonly NonDeletedExcalidrawElement[],
+  elementsMap: ElementsMap,
   pointerX: number,
   pointerY: number,
   shouldRotateWithDiscreteAngle: boolean,
@@ -941,7 +944,7 @@ const rotateMultipleElements = (
       );
       updateBoundElements(element, { simultaneouslyUpdated: elements });
 
-      const boundText = getBoundTextElement(element);
+      const boundText = getBoundTextElement(element, elementsMap);
       if (boundText && !isArrowElement(element)) {
         mutateElement(
           boundText,

+ 3 - 3
packages/excalidraw/element/textElement.test.ts

@@ -319,17 +319,17 @@ describe("Test measureText", () => {
 
     it("should return max width when container is rectangle", () => {
       const container = API.createElement({ type: "rectangle", ...params });
-      expect(getBoundTextMaxWidth(container)).toBe(168);
+      expect(getBoundTextMaxWidth(container, null)).toBe(168);
     });
 
     it("should return max width when container is ellipse", () => {
       const container = API.createElement({ type: "ellipse", ...params });
-      expect(getBoundTextMaxWidth(container)).toBe(116);
+      expect(getBoundTextMaxWidth(container, null)).toBe(116);
     });
 
     it("should return max width when container is diamond", () => {
       const container = API.createElement({ type: "diamond", ...params });
-      expect(getBoundTextMaxWidth(container)).toBe(79);
+      expect(getBoundTextMaxWidth(container, null)).toBe(79);
     });
   });
 

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

@@ -23,7 +23,6 @@ import {
   VERTICAL_ALIGN,
 } from "../constants";
 import { MaybeTransformHandleType } from "./transformHandles";
-import Scene from "../scene/Scene";
 import { isTextElement } from ".";
 import { isBoundToContainer, isArrowElement } from "./typeChecks";
 import { LinearElementEditor } from "./linearElementEditor";
@@ -89,7 +88,7 @@ export const redrawTextBoundingBox = (
       container,
       textElement as ExcalidrawTextElementWithContainer,
     );
-    const maxContainerWidth = getBoundTextMaxWidth(container);
+    const maxContainerWidth = getBoundTextMaxWidth(container, textElement);
 
     if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
       const nextHeight = computeContainerDimensionForBoundText(
@@ -162,6 +161,7 @@ export const bindTextToShapeAfterDuplication = (
 
 export const handleBindTextResize = (
   container: NonDeletedExcalidrawElement,
+  elementsMap: ElementsMap,
   transformHandleType: MaybeTransformHandleType,
   shouldMaintainAspectRatio = false,
 ) => {
@@ -170,25 +170,17 @@ export const handleBindTextResize = (
     return;
   }
   resetOriginalContainerCache(container.id);
-  let textElement = Scene.getScene(container)!.getElement(
-    boundTextElementId,
-  ) as ExcalidrawTextElement;
+  const textElement = getBoundTextElement(container, elementsMap);
   if (textElement && textElement.text) {
     if (!container) {
       return;
     }
 
-    textElement = Scene.getScene(container)!.getElement(
-      boundTextElementId,
-    ) as ExcalidrawTextElement;
     let text = textElement.text;
     let nextHeight = textElement.height;
     let nextWidth = textElement.width;
-    const maxWidth = getBoundTextMaxWidth(container);
-    const maxHeight = getBoundTextMaxHeight(
-      container,
-      textElement as ExcalidrawTextElementWithContainer,
-    );
+    const maxWidth = getBoundTextMaxWidth(container, textElement);
+    const maxHeight = getBoundTextMaxHeight(container, textElement);
     let containerHeight = container.height;
     let nextBaseLine = textElement.baseline;
     if (
@@ -243,10 +235,7 @@ export const handleBindTextResize = (
     if (!isArrowElement(container)) {
       mutateElement(
         textElement,
-        computeBoundTextPosition(
-          container,
-          textElement as ExcalidrawTextElementWithContainer,
-        ),
+        computeBoundTextPosition(container, textElement),
       );
     }
   }
@@ -264,7 +253,7 @@ export const computeBoundTextPosition = (
   }
   const containerCoords = getContainerCoords(container);
   const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
-  const maxContainerWidth = getBoundTextMaxWidth(container);
+  const maxContainerWidth = getBoundTextMaxWidth(container, boundTextElement);
 
   let x;
   let y;
@@ -667,17 +656,18 @@ export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
     : null;
 };
 
-export const getBoundTextElement = (element: ExcalidrawElement | null) => {
+export const getBoundTextElement = (
+  element: ExcalidrawElement | null,
+  elementsMap: ElementsMap,
+) => {
   if (!element) {
     return null;
   }
   const boundTextElementId = getBoundTextElementId(element);
+
   if (boundTextElementId) {
-    return (
-      (Scene.getScene(element)?.getElement(
-        boundTextElementId,
-      ) as ExcalidrawTextElementWithContainer) || null
-    );
+    return (elementsMap.get(boundTextElementId) ||
+      null) as ExcalidrawTextElementWithContainer | null;
   }
   return null;
 };
@@ -699,6 +689,7 @@ export const getContainerElement = (
 export const getContainerCenter = (
   container: ExcalidrawElement,
   appState: AppState,
+  elementsMap: ElementsMap,
 ) => {
   if (!isArrowElement(container)) {
     return {
@@ -718,6 +709,7 @@ export const getContainerCenter = (
   const index = container.points.length / 2 - 1;
   let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints(
     container,
+    elementsMap,
     appState,
   )[index];
   if (!midSegmentMidpoint) {
@@ -877,9 +869,7 @@ export const computeContainerDimensionForBoundText = (
 
 export const getBoundTextMaxWidth = (
   container: ExcalidrawElement,
-  boundTextElement: ExcalidrawTextElement | null = getBoundTextElement(
-    container,
-  ),
+  boundTextElement: ExcalidrawTextElement | null,
 ) => {
   const { width } = container;
   if (isArrowElement(container)) {

+ 8 - 2
packages/excalidraw/element/textWysiwyg.tsx

@@ -34,6 +34,7 @@ import {
   computeContainerDimensionForBoundText,
   detectLineHeight,
   computeBoundTextPosition,
+  getBoundTextElement,
 } from "./textElement";
 import {
   actionDecreaseFontSize,
@@ -196,7 +197,8 @@ export const textWysiwyg = ({
           }
         }
 
-        maxWidth = getBoundTextMaxWidth(container);
+        maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
+
         maxHeight = getBoundTextMaxHeight(
           container,
           updatedTextElement as ExcalidrawTextElementWithContainer,
@@ -361,10 +363,14 @@ export const textWysiwyg = ({
         fontFamily: app.state.currentItemFontFamily,
       });
       if (container) {
+        const boundTextElement = getBoundTextElement(
+          container,
+          app.scene.getNonDeletedElementsMap(),
+        );
         const wrappedText = wrapText(
           `${editable.value}${data}`,
           font,
-          getBoundTextMaxWidth(container),
+          getBoundTextMaxWidth(container, boundTextElement),
         );
         const width = getTextWidth(wrappedText, font);
         editable.style.width = `${width}px`;

+ 10 - 0
packages/excalidraw/element/types.ts

@@ -279,6 +279,16 @@ export type NonDeletedElementsMap = Map<
 export type SceneElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement> &
   MakeBrand<"SceneElementsMap">;
 
+/**
+ * Map of all non-deleted Scene elements.
+ * Not a subset. Use this type when you need access to current Scene elements.
+ */
+export type NonDeletedSceneElementsMap = Map<
+  ExcalidrawElement["id"],
+  NonDeletedExcalidrawElement
+> &
+  MakeBrand<"NonDeletedSceneElementsMap">;
+
 export type ElementsMapOrArray =
   | readonly ExcalidrawElement[]
   | Readonly<ElementsMap>;

+ 7 - 4
packages/excalidraw/frame.ts

@@ -444,6 +444,7 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
   elementsToAdd: NonDeletedExcalidrawElement[],
   frame: ExcalidrawFrameLikeElement,
 ): T => {
+  const elementsMap = arrayToMap(allElements);
   const currTargetFrameChildrenMap = new Map<ExcalidrawElement["id"], true>();
   for (const element of allElements.values()) {
     if (element.frameId === frame.id) {
@@ -481,7 +482,7 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
       finalElementsToAdd.push(element);
     }
 
-    const boundTextElement = getBoundTextElement(element);
+    const boundTextElement = getBoundTextElement(element, elementsMap);
     if (
       boundTextElement &&
       !suppliedElementsToAddSet.has(boundTextElement.id) &&
@@ -506,6 +507,7 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
 
 export const removeElementsFromFrame = (
   elementsToRemove: ReadonlySetLike<NonDeletedExcalidrawElement>,
+  elementsMap: ElementsMap,
 ) => {
   const _elementsToRemove = new Map<
     ExcalidrawElement["id"],
@@ -524,7 +526,7 @@ export const removeElementsFromFrame = (
       const arr = toRemoveElementsByFrame.get(element.frameId) || [];
       arr.push(element);
 
-      const boundTextElement = getBoundTextElement(element);
+      const boundTextElement = getBoundTextElement(element, elementsMap);
       if (boundTextElement) {
         _elementsToRemove.set(boundTextElement.id, boundTextElement);
         arr.push(boundTextElement);
@@ -550,7 +552,7 @@ export const removeAllElementsFromFrame = <T extends ExcalidrawElement>(
   frame: ExcalidrawFrameLikeElement,
 ) => {
   const elementsInFrame = getFrameChildren(allElements, frame.id);
-  removeElementsFromFrame(elementsInFrame);
+  removeElementsFromFrame(elementsInFrame, arrayToMap(allElements));
   return allElements;
 };
 
@@ -558,6 +560,7 @@ export const replaceAllElementsInFrame = <T extends ExcalidrawElement>(
   allElements: readonly T[],
   nextElementsInFrame: ExcalidrawElement[],
   frame: ExcalidrawFrameLikeElement,
+  app: AppClassProperties,
 ): T[] => {
   return addElementsToFrame(
     removeAllElementsFromFrame(allElements, frame),
@@ -608,7 +611,7 @@ export const updateFrameMembershipOfSelectedElements = <
   });
 
   if (elementsToRemove.size > 0) {
-    removeElementsFromFrame(elementsToRemove);
+    removeElementsFromFrame(elementsToRemove, elementsMap);
   }
   return allElements;
 };

+ 3 - 2
packages/excalidraw/groups.ts

@@ -4,6 +4,7 @@ import {
   NonDeleted,
   NonDeletedExcalidrawElement,
   ElementsMapOrArray,
+  ElementsMap,
 } from "./element/types";
 import {
   AppClassProperties,
@@ -329,12 +330,12 @@ export const removeFromSelectedGroups = (
 
 export const getMaximumGroups = (
   elements: ExcalidrawElement[],
+  elementsMap: ElementsMap,
 ): ExcalidrawElement[][] => {
   const groups: Map<String, ExcalidrawElement[]> = new Map<
     String,
     ExcalidrawElement[]
   >();
-
   elements.forEach((element: ExcalidrawElement) => {
     const groupId =
       element.groupIds.length === 0
@@ -344,7 +345,7 @@ export const getMaximumGroups = (
     const currentGroupMembers = groups.get(groupId) || [];
 
     // Include bound text if present when grouping
-    const boundTextElement = getBoundTextElement(element);
+    const boundTextElement = getBoundTextElement(element, elementsMap);
     if (boundTextElement) {
       currentGroupMembers.push(boundTextElement);
     }

+ 21 - 7
packages/excalidraw/renderer/renderElement.ts

@@ -6,6 +6,7 @@ import {
   ExcalidrawImageElement,
   ExcalidrawTextElementWithContainer,
   ExcalidrawFrameLikeElement,
+  NonDeletedSceneElementsMap,
 } from "../element/types";
 import {
   isTextElement,
@@ -190,6 +191,7 @@ const cappedElementCanvasSize = (
 
 const generateElementCanvas = (
   element: NonDeletedExcalidrawElement,
+  elementsMap: RenderableElementsMap,
   zoom: Zoom,
   renderConfig: StaticCanvasRenderConfig,
   appState: StaticCanvasAppState,
@@ -247,7 +249,8 @@ const generateElementCanvas = (
     zoomValue: zoom.value,
     canvasOffsetX,
     canvasOffsetY,
-    boundTextElementVersion: getBoundTextElement(element)?.version || null,
+    boundTextElementVersion:
+      getBoundTextElement(element, elementsMap)?.version || null,
     containingFrameOpacity: getContainingFrame(element)?.opacity || 100,
   };
 };
@@ -407,6 +410,7 @@ export const elementWithCanvasCache = new WeakMap<
 
 const generateElementWithCanvas = (
   element: NonDeletedExcalidrawElement,
+  elementsMap: RenderableElementsMap,
   renderConfig: StaticCanvasRenderConfig,
   appState: StaticCanvasAppState,
 ) => {
@@ -416,7 +420,9 @@ const generateElementWithCanvas = (
     prevElementWithCanvas &&
     prevElementWithCanvas.zoomValue !== zoom.value &&
     !appState?.shouldCacheIgnoreZoom;
-  const boundTextElementVersion = getBoundTextElement(element)?.version || null;
+  const boundTextElementVersion =
+    getBoundTextElement(element, elementsMap)?.version || null;
+
   const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
 
   if (
@@ -428,6 +434,7 @@ const generateElementWithCanvas = (
   ) {
     const elementWithCanvas = generateElementCanvas(
       element,
+      elementsMap,
       zoom,
       renderConfig,
       appState,
@@ -445,6 +452,7 @@ const drawElementFromCanvas = (
   context: CanvasRenderingContext2D,
   renderConfig: StaticCanvasRenderConfig,
   appState: StaticCanvasAppState,
+  allElementsMap: NonDeletedSceneElementsMap,
 ) => {
   const element = elementWithCanvas.element;
   const padding = getCanvasPadding(element);
@@ -464,7 +472,8 @@ const drawElementFromCanvas = (
 
   context.save();
   context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
-  const boundTextElement = getBoundTextElement(element);
+
+  const boundTextElement = getBoundTextElement(element, allElementsMap);
 
   if (isArrowElement(element) && boundTextElement) {
     const tempCanvas = document.createElement("canvas");
@@ -511,7 +520,6 @@ const drawElementFromCanvas = (
       offsetY -
       padding * zoom;
     tempCanvasContext.translate(-shiftX, -shiftY);
-
     // Clear the bound text area
     tempCanvasContext.clearRect(
       -(boundTextElement.width / 2 + BOUND_TEXT_PADDING) *
@@ -573,6 +581,7 @@ const drawElementFromCanvas = (
     ) {
       const textElement = getBoundTextElement(
         element,
+        allElementsMap,
       ) as ExcalidrawTextElementWithContainer;
       const coords = getContainerCoords(element);
       context.strokeStyle = "#c92a2a";
@@ -580,7 +589,7 @@ const drawElementFromCanvas = (
       context.strokeRect(
         (coords.x + appState.scrollX) * window.devicePixelRatio,
         (coords.y + appState.scrollY) * window.devicePixelRatio,
-        getBoundTextMaxWidth(element) * window.devicePixelRatio,
+        getBoundTextMaxWidth(element, textElement) * window.devicePixelRatio,
         getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
       );
     }
@@ -616,6 +625,7 @@ export const renderSelectionElement = (
 export const renderElement = (
   element: NonDeletedExcalidrawElement,
   elementsMap: RenderableElementsMap,
+  allElementsMap: NonDeletedSceneElementsMap,
   rc: RoughCanvas,
   context: CanvasRenderingContext2D,
   renderConfig: StaticCanvasRenderConfig,
@@ -687,6 +697,7 @@ export const renderElement = (
       } else {
         const elementWithCanvas = generateElementWithCanvas(
           element,
+          elementsMap,
           renderConfig,
           appState,
         );
@@ -695,6 +706,7 @@ export const renderElement = (
           context,
           renderConfig,
           appState,
+          allElementsMap,
         );
       }
 
@@ -737,7 +749,7 @@ export const renderElement = (
         if (shouldResetImageFilter(element, renderConfig, appState)) {
           context.filter = "none";
         }
-        const boundTextElement = getBoundTextElement(element);
+        const boundTextElement = getBoundTextElement(element, elementsMap);
 
         if (isArrowElement(element) && boundTextElement) {
           const tempCanvas = document.createElement("canvas");
@@ -820,6 +832,7 @@ export const renderElement = (
       } else {
         const elementWithCanvas = generateElementWithCanvas(
           element,
+          elementsMap,
           renderConfig,
           appState,
         );
@@ -851,6 +864,7 @@ export const renderElement = (
           context,
           renderConfig,
           appState,
+          allElementsMap,
         );
 
         // reset
@@ -1096,7 +1110,7 @@ export const renderElementToSvg = (
     }
     case "line":
     case "arrow": {
-      const boundText = getBoundTextElement(element);
+      const boundText = getBoundTextElement(element, elementsMap);
       const maskPath = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask");
       if (boundText) {
         maskPath.setAttribute("id", `mask-${element.id}`);

+ 15 - 1
packages/excalidraw/renderer/renderScene.ts

@@ -246,6 +246,7 @@ const renderLinearPointHandles = (
   context: CanvasRenderingContext2D,
   appState: InteractiveCanvasAppState,
   element: NonDeleted<ExcalidrawLinearElement>,
+  elementsMap: RenderableElementsMap,
 ) => {
   if (!appState.selectedLinearElement) {
     return;
@@ -269,6 +270,7 @@ const renderLinearPointHandles = (
   //Rendering segment mid points
   const midPoints = LinearElementEditor.getEditorMidPoints(
     element,
+    elementsMap,
     appState,
   ).filter((midPoint) => midPoint !== null) as Point[];
 
@@ -485,7 +487,12 @@ const _renderInteractiveScene = ({
   });
 
   if (editingLinearElement) {
-    renderLinearPointHandles(context, appState, editingLinearElement);
+    renderLinearPointHandles(
+      context,
+      appState,
+      editingLinearElement,
+      elementsMap,
+    );
   }
 
   // Paint selection element
@@ -528,6 +535,7 @@ const _renderInteractiveScene = ({
       context,
       appState,
       selectedElements[0] as NonDeleted<ExcalidrawLinearElement>,
+      elementsMap,
     );
   }
 
@@ -553,6 +561,7 @@ const _renderInteractiveScene = ({
         context,
         appState,
         selectedElements[0] as ExcalidrawLinearElement,
+        elementsMap,
       );
     }
     const selectionColor = renderConfig.selectionColor || oc.black;
@@ -891,6 +900,7 @@ const _renderStaticScene = ({
   canvas,
   rc,
   elementsMap,
+  allElementsMap,
   visibleElements,
   scale,
   appState,
@@ -972,6 +982,7 @@ const _renderStaticScene = ({
           renderElement(
             element,
             elementsMap,
+            allElementsMap,
             rc,
             context,
             renderConfig,
@@ -982,6 +993,7 @@ const _renderStaticScene = ({
           renderElement(
             element,
             elementsMap,
+            allElementsMap,
             rc,
             context,
             renderConfig,
@@ -1005,6 +1017,7 @@ const _renderStaticScene = ({
           renderElement(
             element,
             elementsMap,
+            allElementsMap,
             rc,
             context,
             renderConfig,
@@ -1024,6 +1037,7 @@ const _renderStaticScene = ({
             renderElement(
               label,
               elementsMap,
+              allElementsMap,
               rc,
               context,
               renderConfig,

+ 5 - 4
packages/excalidraw/scene/Scene.ts

@@ -4,8 +4,8 @@ import {
   NonDeleted,
   ExcalidrawFrameLikeElement,
   ElementsMapOrArray,
-  NonDeletedElementsMap,
   SceneElementsMap,
+  NonDeletedSceneElementsMap,
 } from "../element/types";
 import { isNonDeletedElement } from "../element";
 import { LinearElementEditor } from "../element/linearElementEditor";
@@ -27,7 +27,7 @@ type SelectionHash = string & { __brand: "selectionHash" };
 const getNonDeletedElements = <T extends ExcalidrawElement>(
   allElements: readonly T[],
 ) => {
-  const elementsMap = new Map() as NonDeletedElementsMap;
+  const elementsMap = new Map() as NonDeletedSceneElementsMap;
   const elements: T[] = [];
   for (const element of allElements) {
     if (!element.isDeleted) {
@@ -120,8 +120,9 @@ class Scene {
   private callbacks: Set<SceneStateCallback> = new Set();
 
   private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
-  private nonDeletedElementsMap: NonDeletedElementsMap =
-    new Map() as NonDeletedElementsMap;
+  private nonDeletedElementsMap = toBrandedType<NonDeletedSceneElementsMap>(
+    new Map(),
+  );
   private elements: readonly ExcalidrawElement[] = [];
   private nonDeletedFramesLikes: readonly NonDeleted<ExcalidrawFrameLikeElement>[] =
     [];

+ 7 - 5
packages/excalidraw/scene/export.ts

@@ -4,6 +4,7 @@ import {
   ExcalidrawFrameLikeElement,
   ExcalidrawTextElement,
   NonDeletedExcalidrawElement,
+  NonDeletedSceneElementsMap,
 } from "../element/types";
 import {
   Bounds,
@@ -248,14 +249,15 @@ export const exportToCanvas = async (
     files,
   });
 
-  const elementsMap = toBrandedType<RenderableElementsMap>(
-    arrayToMap(elementsForRender),
-  );
-
   renderStaticScene({
     canvas,
     rc: rough.canvas(canvas),
-    elementsMap,
+    elementsMap: toBrandedType<RenderableElementsMap>(
+      arrayToMap(elementsForRender),
+    ),
+    allElementsMap: toBrandedType<NonDeletedSceneElementsMap>(
+      arrayToMap(elements),
+    ),
     visibleElements: elementsForRender,
     scale,
     appState: {

+ 2 - 0
packages/excalidraw/scene/types.ts

@@ -4,6 +4,7 @@ import {
   ExcalidrawTextElement,
   NonDeletedElementsMap,
   NonDeletedExcalidrawElement,
+  NonDeletedSceneElementsMap,
 } from "../element/types";
 import {
   AppClassProperties,
@@ -66,6 +67,7 @@ export type StaticSceneRenderConfig = {
   canvas: HTMLCanvasElement;
   rc: RoughCanvas;
   elementsMap: RenderableElementsMap;
+  allElementsMap: NonDeletedSceneElementsMap;
   visibleElements: readonly NonDeletedExcalidrawElement[];
   scale: number;
   appState: StaticCanvasAppState;

+ 6 - 2
packages/excalidraw/snapping.ts

@@ -16,6 +16,7 @@ import { KEYS } from "./keys";
 import { rangeIntersection, rangesOverlap, rotatePoint } from "./math";
 import { getVisibleAndNonSelectedElements } from "./scene/selection";
 import { AppState, KeyboardModifiersObject, Point } from "./types";
+import { arrayToMap } from "./utils";
 
 const SNAP_DISTANCE = 8;
 
@@ -286,7 +287,10 @@ export const getVisibleGaps = (
     appState,
   );
 
-  const referenceBounds = getMaximumGroups(referenceElements)
+  const referenceBounds = getMaximumGroups(
+    referenceElements,
+    arrayToMap(elements),
+  )
     .filter(
       (elementsGroup) =>
         !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
@@ -572,7 +576,7 @@ export const getReferenceSnapPoints = (
     appState,
   );
 
-  return getMaximumGroups(referenceElements)
+  return getMaximumGroups(referenceElements, arrayToMap(elements))
     .filter(
       (elementsGroup) =>
         !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),

+ 73 - 15
packages/excalidraw/tests/linearElementEditor.test.tsx

@@ -24,6 +24,7 @@ import {
 import * as textElementUtils from "../element/textElement";
 import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
 import { vi } from "vitest";
+import { arrayToMap } from "../utils";
 
 const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
 const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
@@ -307,6 +308,7 @@ describe("Test Linear Elements", () => {
 
       const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints(
         line,
+        h.app.scene.getNonDeletedElementsMap(),
         h.state,
       );
 
@@ -320,6 +322,7 @@ describe("Test Linear Elements", () => {
 
       const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
         h.elements[0] as ExcalidrawLinearElement,
+        h.app.scene.getNonDeletedElementsMap(),
         h.state,
       );
       expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]);
@@ -351,7 +354,11 @@ describe("Test Linear Elements", () => {
       const points = LinearElementEditor.getPointsGlobalCoordinates(line);
       expect([line.x, line.y]).toEqual(points[0]);
 
-      const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
+      const midPoints = LinearElementEditor.getEditorMidPoints(
+        line,
+        h.app.scene.getNonDeletedElementsMap(),
+        h.state,
+      );
 
       const startPoint = centerPoint(points[0], midPoints[0] as Point);
       const deltaX = 50;
@@ -373,6 +380,7 @@ describe("Test Linear Elements", () => {
 
       const newMidPoints = LinearElementEditor.getEditorMidPoints(
         line,
+        h.app.scene.getNonDeletedElementsMap(),
         h.state,
       );
       expect(midPoints[0]).not.toEqual(newMidPoints[0]);
@@ -458,7 +466,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 midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
+        const midPoints = LinearElementEditor.getEditorMidPoints(
+          line,
+          h.app.scene.getNonDeletedElementsMap(),
+          h.state,
+        );
 
         const hitCoords: Point = [points[0][0], points[0][1]];
 
@@ -478,6 +490,7 @@ describe("Test Linear Elements", () => {
 
         const newMidPoints = LinearElementEditor.getEditorMidPoints(
           line,
+          h.app.scene.getNonDeletedElementsMap(),
           h.state,
         );
 
@@ -487,7 +500,11 @@ describe("Test Linear Elements", () => {
 
       it("should hide midpoints in the segment when points moved close", async () => {
         const points = LinearElementEditor.getPointsGlobalCoordinates(line);
-        const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
+        const midPoints = LinearElementEditor.getEditorMidPoints(
+          line,
+          h.app.scene.getNonDeletedElementsMap(),
+          h.state,
+        );
 
         const hitCoords: Point = [points[0][0], points[0][1]];
 
@@ -507,6 +524,7 @@ describe("Test Linear Elements", () => {
 
         const newMidPoints = LinearElementEditor.getEditorMidPoints(
           line,
+          h.app.scene.getNonDeletedElementsMap(),
           h.state,
         );
         // This midpoint is hidden since the points are too close
@@ -526,7 +544,11 @@ describe("Test Linear Elements", () => {
         ]);
         expect(line.points.length).toEqual(4);
 
-        const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
+        const midPoints = LinearElementEditor.getEditorMidPoints(
+          line,
+          h.app.scene.getNonDeletedElementsMap(),
+          h.state,
+        );
 
         // delete 3rd point
         deletePoint(points[2]);
@@ -538,6 +560,7 @@ describe("Test Linear Elements", () => {
 
         const newMidPoints = LinearElementEditor.getEditorMidPoints(
           line,
+          h.app.scene.getNonDeletedElementsMap(),
           h.state,
         );
         expect(newMidPoints.length).toEqual(2);
@@ -615,7 +638,11 @@ describe("Test Linear Elements", () => {
 
       it("should update all the midpoints when its point is dragged", async () => {
         const points = LinearElementEditor.getPointsGlobalCoordinates(line);
-        const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
+        const midPoints = LinearElementEditor.getEditorMidPoints(
+          line,
+          h.app.scene.getNonDeletedElementsMap(),
+          h.state,
+        );
 
         const hitCoords: Point = [points[0][0], points[0][1]];
 
@@ -630,6 +657,7 @@ describe("Test Linear Elements", () => {
 
         const newMidPoints = LinearElementEditor.getEditorMidPoints(
           line,
+          h.app.scene.getNonDeletedElementsMap(),
           h.state,
         );
 
@@ -651,7 +679,11 @@ describe("Test Linear Elements", () => {
 
       it("should hide midpoints in the segment when points moved close", async () => {
         const points = LinearElementEditor.getPointsGlobalCoordinates(line);
-        const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
+        const midPoints = LinearElementEditor.getEditorMidPoints(
+          line,
+          h.app.scene.getNonDeletedElementsMap(),
+          h.state,
+        );
 
         const hitCoords: Point = [points[0][0], points[0][1]];
 
@@ -671,6 +703,7 @@ describe("Test Linear Elements", () => {
 
         const newMidPoints = LinearElementEditor.getEditorMidPoints(
           line,
+          h.app.scene.getNonDeletedElementsMap(),
           h.state,
         );
         // This mid point is hidden due to point being too close
@@ -685,7 +718,11 @@ describe("Test Linear Elements", () => {
         ]);
         expect(line.points.length).toEqual(4);
 
-        const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
+        const midPoints = LinearElementEditor.getEditorMidPoints(
+          line,
+          h.app.scene.getNonDeletedElementsMap(),
+          h.state,
+        );
         const points = LinearElementEditor.getPointsGlobalCoordinates(line);
 
         // delete 3rd point
@@ -694,6 +731,7 @@ describe("Test Linear Elements", () => {
 
         const newMidPoints = LinearElementEditor.getEditorMidPoints(
           line,
+          h.app.scene.getNonDeletedElementsMap(),
           h.state,
         );
         expect(newMidPoints.length).toEqual(2);
@@ -762,7 +800,7 @@ describe("Test Linear Elements", () => {
         type: "text",
         x: 0,
         y: 0,
-        text: wrapText(text, font, getBoundTextMaxWidth(container)),
+        text: wrapText(text, font, getBoundTextMaxWidth(container, null)),
         containerId: container.id,
         width: 30,
         height: 20,
@@ -986,8 +1024,13 @@ describe("Test Linear Elements", () => {
         collaboration made 
         easy"
       `);
-      expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
-        .toMatchInlineSnapshot(`
+      expect(
+        LinearElementEditor.getElementAbsoluteCoords(
+          container,
+          h.app.scene.getNonDeletedElementsMap(),
+          true,
+        ),
+      ).toMatchInlineSnapshot(`
           [
             20,
             20,
@@ -1020,8 +1063,13 @@ describe("Test Linear Elements", () => {
           "Online whiteboard 
           collaboration made easy"
         `);
-      expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
-        .toMatchInlineSnapshot(`
+      expect(
+        LinearElementEditor.getElementAbsoluteCoords(
+          container,
+          h.app.scene.getNonDeletedElementsMap(),
+          true,
+        ),
+      ).toMatchInlineSnapshot(`
           [
             20,
             35,
@@ -1121,7 +1169,11 @@ describe("Test Linear Elements", () => {
       expect(rect.x).toBe(400);
       expect(rect.y).toBe(0);
       expect(
-        wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
+        wrapText(
+          textElement.originalText,
+          font,
+          getBoundTextMaxWidth(arrow, null),
+        ),
       ).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made easy"
@@ -1140,11 +1192,17 @@ describe("Test Linear Elements", () => {
       expect(rect.x).toBe(200);
       expect(rect.y).toBe(0);
       expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
-        h.elements[1],
+        h.elements[0],
+        arrayToMap(h.elements),
+        "nw",
         false,
       );
       expect(
-        wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
+        wrapText(
+          textElement.originalText,
+          font,
+          getBoundTextMaxWidth(arrow, null),
+        ),
       ).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made