فهرست منبع

fix: make LinearElementEditor independent of scene (#7670)

* fix: make LinearElementEditor independent of scene

* more fixes

* pass elements and elementsMap to maybeBindBindableElement,getHoveredElementForBinding,bindingBorderTest,getElligibleElementsForBindableElementAndWhere,isLinearElementEligibleForNewBindingByBindable

* replace `ElementsMap` with `NonDeletedSceneElementsMap` & remove unused params

* fix lint

---------

Co-authored-by: dwelle <[email protected]>
Aakansha Doshi 1 سال پیش
والد
کامیت
9013c84524

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

@@ -107,7 +107,7 @@ export const actionCut = register({
   trackEvent: { category: "element" },
   perform: (elements, appState, event: ClipboardEvent | null, app) => {
     actionCopy.perform(elements, appState, event, app);
-    return actionDeleteSelected.perform(elements, appState);
+    return actionDeleteSelected.perform(elements, appState, null, app);
   },
   contextItemLabel: "labels.cut",
   keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,

+ 3 - 2
packages/excalidraw/actions/actionDeleteSelected.tsx

@@ -73,7 +73,7 @@ const handleGroupEditingState = (
 export const actionDeleteSelected = register({
   name: "deleteSelectedElements",
   trackEvent: { category: "element", action: "delete" },
-  perform: (elements, appState) => {
+  perform: (elements, appState, formData, app) => {
     if (appState.editingLinearElement) {
       const {
         elementId,
@@ -81,7 +81,8 @@ export const actionDeleteSelected = register({
         startBindingElement,
         endBindingElement,
       } = appState.editingLinearElement;
-      const element = LinearElementEditor.getElement(elementId);
+      const elementsMap = app.scene.getNonDeletedElementsMap();
+      const element = LinearElementEditor.getElement(elementId, elementsMap);
       if (!element) {
         return false;
       }

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

@@ -35,10 +35,14 @@ import {
 export const actionDuplicateSelection = register({
   name: "duplicateSelection",
   trackEvent: { category: "element" },
-  perform: (elements, appState) => {
+  perform: (elements, appState, formData, app) => {
+    const elementsMap = app.scene.getNonDeletedElementsMap();
     // duplicate selected point(s) if editing a line
     if (appState.editingLinearElement) {
-      const ret = LinearElementEditor.duplicateSelectedPoints(appState);
+      const ret = LinearElementEditor.duplicateSelectedPoints(
+        appState,
+        elementsMap,
+      );
 
       if (!ret) {
         return false;

+ 3 - 3
packages/excalidraw/actions/actionFinalize.tsx

@@ -26,12 +26,12 @@ export const actionFinalize = register({
     _,
     { interactiveCanvas, focusContainer, scene },
   ) => {
-    const elementsMap = arrayToMap(elements);
+    const elementsMap = scene.getNonDeletedElementsMap();
 
     if (appState.editingLinearElement) {
       const { elementId, startBindingElement, endBindingElement } =
         appState.editingLinearElement;
-      const element = LinearElementEditor.getElement(elementId);
+      const element = LinearElementEditor.getElement(elementId, elementsMap);
 
       if (element) {
         if (isBindingElement(element)) {
@@ -191,7 +191,7 @@ export const actionFinalize = register({
         // To select the linear element when user has finished mutipoint editing
         selectedLinearElement:
           multiPointElement && isLinearElement(multiPointElement)
-            ? new LinearElementEditor(multiPointElement, scene)
+            ? new LinearElementEditor(multiPointElement)
             : appState.selectedLinearElement,
         pendingImageElementId: null,
       },

+ 7 - 6
packages/excalidraw/actions/actionFlip.ts

@@ -4,7 +4,6 @@ import { getNonDeletedElements } from "../element";
 import {
   ExcalidrawElement,
   NonDeleted,
-  NonDeletedElementsMap,
   NonDeletedSceneElementsMap,
 } from "../element/types";
 import { resizeMultipleElements } from "../element/resizeElements";
@@ -68,7 +67,7 @@ export const actionFlipVertical = register({
 
 const flipSelectedElements = (
   elements: readonly ExcalidrawElement[],
-  elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
   appState: Readonly<AppState>,
   flipDirection: "horizontal" | "vertical",
 ) => {
@@ -83,6 +82,7 @@ const flipSelectedElements = (
 
   const updatedElements = flipElements(
     selectedElements,
+    elements,
     elementsMap,
     appState,
     flipDirection,
@@ -97,7 +97,8 @@ const flipSelectedElements = (
 
 const flipElements = (
   selectedElements: NonDeleted<ExcalidrawElement>[],
-  elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
+  elements: readonly ExcalidrawElement[],
+  elementsMap: NonDeletedSceneElementsMap,
   appState: AppState,
   flipDirection: "horizontal" | "vertical",
 ): ExcalidrawElement[] => {
@@ -113,9 +114,9 @@ const flipElements = (
     flipDirection === "horizontal" ? minY : maxY,
   );
 
-  (isBindingEnabled(appState)
-    ? bindOrUnbindSelectedElements
-    : unbindLinearElements)(selectedElements, elementsMap);
+  isBindingEnabled(appState)
+    ? bindOrUnbindSelectedElements(selectedElements, elements, elementsMap)
+    : unbindLinearElements(selectedElements, elementsMap);
 
   return selectedElements;
 };

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

@@ -24,7 +24,7 @@ export const actionToggleLinearEditor = register({
     const editingLinearElement =
       appState.editingLinearElement?.elementId === selectedElement.id
         ? null
-        : new LinearElementEditor(selectedElement, app.scene);
+        : new LinearElementEditor(selectedElement);
     return {
       appState: {
         ...appState,

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

@@ -43,7 +43,7 @@ export const actionSelectAll = register({
           // single linear element selected
           Object.keys(selectedElementIds).length === 1 &&
           isLinearElement(elements[0])
-            ? new LinearElementEditor(elements[0], app.scene)
+            ? new LinearElementEditor(elements[0])
             : null,
       },
       commitToHistory: true,

+ 42 - 38
packages/excalidraw/components/App.tsx

@@ -2603,7 +2603,7 @@ class App extends React.Component<AppProps, AppState> {
   componentDidUpdate(prevProps: AppProps, prevState: AppState) {
     this.updateEmbeddables();
     const elements = this.scene.getElementsIncludingDeleted();
-    const elementsMap = this.scene.getElementsMapIncludingDeleted();
+    const elementsMap = this.scene.getNonDeletedElementsMap();
 
     if (!this.state.showWelcomeScreen && !elements.length) {
       this.setState({ showWelcomeScreen: true });
@@ -3860,7 +3860,6 @@ class App extends React.Component<AppProps, AppState> {
                 this.setState({
                   editingLinearElement: new LinearElementEditor(
                     selectedElement,
-                    this.scene,
                   ),
                 });
               }
@@ -4013,7 +4012,11 @@ class App extends React.Component<AppProps, AppState> {
       const selectedElements = this.scene.getSelectedElements(this.state);
       const elementsMap = this.scene.getNonDeletedElementsMap();
       isBindingEnabled(this.state)
-        ? bindOrUnbindSelectedElements(selectedElements, elementsMap)
+        ? bindOrUnbindSelectedElements(
+            selectedElements,
+            this.scene.getNonDeletedElements(),
+            elementsMap,
+          )
         : unbindLinearElements(selectedElements, elementsMap);
       this.setState({ suggestedBindings: [] });
     }
@@ -4578,10 +4581,7 @@ class App extends React.Component<AppProps, AppState> {
       ) {
         this.history.resumeRecording();
         this.setState({
-          editingLinearElement: new LinearElementEditor(
-            selectedElements[0],
-            this.scene,
-          ),
+          editingLinearElement: new LinearElementEditor(selectedElements[0]),
         });
         return;
       } else if (
@@ -5305,10 +5305,12 @@ class App extends React.Component<AppProps, AppState> {
     scenePointerX: number,
     scenePointerY: number,
   ) {
+    const elementsMap = this.scene.getNonDeletedElementsMap();
+
     const element = LinearElementEditor.getElement(
       linearElementEditor.elementId,
+      elementsMap,
     );
-    const elementsMap = this.scene.getNonDeletedElementsMap();
     const boundTextElement = getBoundTextElement(element, elementsMap);
 
     if (!element) {
@@ -6122,7 +6124,8 @@ class App extends React.Component<AppProps, AppState> {
             this.history,
             pointerDownState.origin,
             linearElementEditor,
-            this.scene.getNonDeletedElementsMap(),
+            this.scene.getNonDeletedElements(),
+            elementsMap,
           );
           if (ret.hitElement) {
             pointerDownState.hit.element = ret.hitElement;
@@ -6459,7 +6462,8 @@ class App extends React.Component<AppProps, AppState> {
 
     const boundElement = getHoveredElementForBinding(
       pointerDownState.origin,
-      this.scene,
+      this.scene.getNonDeletedElements(),
+      this.scene.getNonDeletedElementsMap(),
     );
     this.scene.addNewElement(element);
     this.setState({
@@ -6727,7 +6731,8 @@ class App extends React.Component<AppProps, AppState> {
       });
       const boundElement = getHoveredElementForBinding(
         pointerDownState.origin,
-        this.scene,
+        this.scene.getNonDeletedElements(),
+        this.scene.getNonDeletedElementsMap(),
       );
 
       this.scene.addNewElement(element);
@@ -6997,6 +7002,7 @@ class App extends React.Component<AppProps, AppState> {
           return true;
         }
       }
+      const elementsMap = this.scene.getNonDeletedElementsMap();
 
       if (this.state.selectedLinearElement) {
         const linearElementEditor =
@@ -7007,6 +7013,7 @@ class App extends React.Component<AppProps, AppState> {
             this.state.selectedLinearElement,
             pointerCoords,
             this.state,
+            elementsMap,
           )
         ) {
           const ret = LinearElementEditor.addMidpoint(
@@ -7014,7 +7021,7 @@ class App extends React.Component<AppProps, AppState> {
             pointerCoords,
             this.state,
             !event[KEYS.CTRL_OR_CMD],
-            this.scene.getNonDeletedElementsMap(),
+            elementsMap,
           );
           if (!ret) {
             return;
@@ -7435,10 +7442,7 @@ class App extends React.Component<AppProps, AppState> {
               selectedLinearElement:
                 elementsWithinSelection.length === 1 &&
                 isLinearElement(elementsWithinSelection[0])
-                  ? new LinearElementEditor(
-                      elementsWithinSelection[0],
-                      this.scene,
-                    )
+                  ? new LinearElementEditor(elementsWithinSelection[0])
                   : null,
               showHyperlinkPopup:
                 elementsWithinSelection.length === 1 &&
@@ -7539,6 +7543,7 @@ class App extends React.Component<AppProps, AppState> {
             childEvent,
             this.state.editingLinearElement,
             this.state,
+            this.scene.getNonDeletedElements(),
             elementsMap,
           );
           if (editingLinearElement !== this.state.editingLinearElement) {
@@ -7563,6 +7568,7 @@ class App extends React.Component<AppProps, AppState> {
             childEvent,
             this.state.selectedLinearElement,
             this.state,
+            this.scene.getNonDeletedElements(),
             elementsMap,
           );
 
@@ -7732,10 +7738,7 @@ class App extends React.Component<AppProps, AppState> {
                 },
                 prevState,
               ),
-              selectedLinearElement: new LinearElementEditor(
-                draggingElement,
-                this.scene,
-              ),
+              selectedLinearElement: new LinearElementEditor(draggingElement),
             }));
           } else {
             this.setState((prevState) => ({
@@ -7975,10 +7978,7 @@ class App extends React.Component<AppProps, AppState> {
         // the one we've hit
         if (selectedELements.length === 1) {
           this.setState({
-            selectedLinearElement: new LinearElementEditor(
-              hitElement,
-              this.scene,
-            ),
+            selectedLinearElement: new LinearElementEditor(hitElement),
           });
         }
       }
@@ -8091,10 +8091,7 @@ class App extends React.Component<AppProps, AppState> {
                   selectedLinearElement:
                     newSelectedElements.length === 1 &&
                     isLinearElement(newSelectedElements[0])
-                      ? new LinearElementEditor(
-                          newSelectedElements[0],
-                          this.scene,
-                        )
+                      ? new LinearElementEditor(newSelectedElements[0])
                       : prevState.selectedLinearElement,
                 };
               });
@@ -8168,7 +8165,7 @@ class App extends React.Component<AppProps, AppState> {
               // Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
               // Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
               prevState.selectedLinearElement?.elementId !== hitElement.id
-                ? new LinearElementEditor(hitElement, this.scene)
+                ? new LinearElementEditor(hitElement)
                 : prevState.selectedLinearElement,
           }));
         }
@@ -8232,12 +8229,16 @@ class App extends React.Component<AppProps, AppState> {
       }
 
       if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
-        (isBindingEnabled(this.state)
-          ? bindOrUnbindSelectedElements
-          : unbindLinearElements)(
-          this.scene.getSelectedElements(this.state),
-          elementsMap,
-        );
+        isBindingEnabled(this.state)
+          ? bindOrUnbindSelectedElements(
+              this.scene.getSelectedElements(this.state),
+              this.scene.getNonDeletedElements(),
+              elementsMap,
+            )
+          : unbindLinearElements(
+              this.scene.getSelectedElements(this.state),
+              elementsMap,
+            );
       }
 
       if (activeTool.type === "laser") {
@@ -8714,7 +8715,8 @@ class App extends React.Component<AppProps, AppState> {
   }): void => {
     const hoveredBindableElement = getHoveredElementForBinding(
       pointerCoords,
-      this.scene,
+      this.scene.getNonDeletedElements(),
+      this.scene.getNonDeletedElementsMap(),
     );
     this.setState({
       suggestedBindings:
@@ -8741,7 +8743,8 @@ class App extends React.Component<AppProps, AppState> {
       (acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
         const hoveredBindableElement = getHoveredElementForBinding(
           coords,
-          this.scene,
+          this.scene.getNonDeletedElements(),
+          this.scene.getNonDeletedElementsMap(),
         );
         if (
           hoveredBindableElement != null &&
@@ -8769,6 +8772,7 @@ class App extends React.Component<AppProps, AppState> {
     }
     const suggestedBindings = getEligibleElementsForBinding(
       selectedElements,
+      this.scene.getNonDeletedElements(),
       this.scene.getNonDeletedElementsMap(),
     );
     this.setState({ suggestedBindings });
@@ -9037,7 +9041,7 @@ class App extends React.Component<AppProps, AppState> {
                 this,
               ),
               selectedLinearElement: isLinearElement(element)
-                ? new LinearElementEditor(element, this.scene)
+                ? new LinearElementEditor(element)
                 : null,
             }
           : this.state),

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

@@ -39,11 +39,12 @@ import {
   ExcalidrawTextElement,
   FileId,
   FontFamilyValues,
+  NonDeletedSceneElementsMap,
   TextAlign,
   VerticalAlign,
 } from "../element/types";
 import { MarkOptional } from "../utility-types";
-import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils";
+import { assertNever, cloneJSON, getFontString, toBrandedType } from "../utils";
 import { getSizeFromPoints } from "../points";
 import { randomId } from "../random";
 
@@ -231,7 +232,7 @@ const bindLinearElementToElement = (
   start: ValidLinearElement["start"],
   end: ValidLinearElement["end"],
   elementStore: ElementStore,
-  elementsMap: ElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
 ): {
   linearElement: ExcalidrawLinearElement;
   startBoundElement?: ExcalidrawElement;
@@ -460,6 +461,10 @@ class ElementStore {
     return Array.from(this.excalidrawElements.values());
   };
 
+  getElementsMap = () => {
+    return toBrandedType<NonDeletedSceneElementsMap>(this.excalidrawElements);
+  };
+
   getElement = (id: string) => {
     return this.excalidrawElements.get(id);
   };
@@ -615,7 +620,7 @@ export const convertToExcalidrawElements = (
     }
   }
 
-  const elementsMap = arrayToMap(elementStore.getElements());
+  const elementsMap = elementStore.getElementsMap();
   // Add labels and arrow bindings
   for (const [id, element] of elementsWithIds) {
     const excalidrawElement = elementStore.getElement(id)!;

+ 67 - 43
packages/excalidraw/element/binding.ts

@@ -6,6 +6,7 @@ import {
   PointBinding,
   ExcalidrawElement,
   ElementsMap,
+  NonDeletedSceneElementsMap,
 } from "./types";
 import { getElementAtPosition } from "../scene";
 import { AppState } from "../types";
@@ -67,7 +68,7 @@ export const bindOrUnbindLinearElement = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   startBindingElement: ExcalidrawBindableElement | null | "keep",
   endBindingElement: ExcalidrawBindableElement | null | "keep",
-  elementsMap: ElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
 ): void => {
   const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
   const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
@@ -115,7 +116,7 @@ const bindOrUnbindLinearElementEdge = (
   boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
   // Is mutated
   unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
-  elementsMap: ElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
 ): void => {
   if (bindableElement !== "keep") {
     if (bindableElement != null) {
@@ -151,7 +152,8 @@ const bindOrUnbindLinearElementEdge = (
 
 export const bindOrUnbindSelectedElements = (
   selectedElements: NonDeleted<ExcalidrawElement>[],
-  elementsMap: ElementsMap,
+  elements: readonly ExcalidrawElement[],
+  elementsMap: NonDeletedSceneElementsMap,
 ): void => {
   selectedElements.forEach((selectedElement) => {
     if (isBindingElement(selectedElement)) {
@@ -160,11 +162,13 @@ export const bindOrUnbindSelectedElements = (
         getElligibleElementForBindingElement(
           selectedElement,
           "start",
+          elements,
           elementsMap,
         ),
         getElligibleElementForBindingElement(
           selectedElement,
           "end",
+          elements,
           elementsMap,
         ),
         elementsMap,
@@ -177,16 +181,18 @@ export const bindOrUnbindSelectedElements = (
 
 const maybeBindBindableElement = (
   bindableElement: NonDeleted<ExcalidrawBindableElement>,
-  elementsMap: ElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
 ): void => {
-  getElligibleElementsForBindableElementAndWhere(bindableElement).forEach(
-    ([linearElement, where]) =>
-      bindOrUnbindLinearElement(
-        linearElement,
-        where === "end" ? "keep" : bindableElement,
-        where === "start" ? "keep" : bindableElement,
-        elementsMap,
-      ),
+  getElligibleElementsForBindableElementAndWhere(
+    bindableElement,
+    elementsMap,
+  ).forEach(([linearElement, where]) =>
+    bindOrUnbindLinearElement(
+      linearElement,
+      where === "end" ? "keep" : bindableElement,
+      where === "start" ? "keep" : bindableElement,
+      elementsMap,
+    ),
   );
 };
 
@@ -195,7 +201,7 @@ export const maybeBindLinearElement = (
   appState: AppState,
   scene: Scene,
   pointerCoords: { x: number; y: number },
-  elementsMap: ElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
 ): void => {
   if (appState.startBoundElement != null) {
     bindLinearElement(
@@ -205,7 +211,11 @@ export const maybeBindLinearElement = (
       elementsMap,
     );
   }
-  const hoveredElement = getHoveredElementForBinding(pointerCoords, scene);
+  const hoveredElement = getHoveredElementForBinding(
+    pointerCoords,
+    scene.getNonDeletedElements(),
+    elementsMap,
+  );
   if (
     hoveredElement != null &&
     !isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
@@ -222,7 +232,7 @@ export const bindLinearElement = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   hoveredElement: ExcalidrawBindableElement,
   startOrEnd: "start" | "end",
-  elementsMap: ElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
 ): void => {
   mutateElement(linearElement, {
     [startOrEnd === "start" ? "startBinding" : "endBinding"]: {
@@ -274,7 +284,7 @@ export const isLinearElementSimpleAndAlreadyBound = (
 
 export const unbindLinearElements = (
   elements: NonDeleted<ExcalidrawElement>[],
-  elementsMap: ElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
 ): void => {
   elements.forEach((element) => {
     if (isBindingElement(element)) {
@@ -301,17 +311,14 @@ export const getHoveredElementForBinding = (
     x: number;
     y: number;
   },
-  scene: Scene,
+  elements: readonly NonDeletedExcalidrawElement[],
+  elementsMap: NonDeletedSceneElementsMap,
 ): NonDeleted<ExcalidrawBindableElement> | null => {
   const hoveredElement = getElementAtPosition(
-    scene.getNonDeletedElements(),
+    elements,
     (element) =>
       isBindableElement(element, false) &&
-      bindingBorderTest(
-        element,
-        pointerCoords,
-        scene.getNonDeletedElementsMap(),
-      ),
+      bindingBorderTest(element, pointerCoords, elementsMap),
   );
   return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
 };
@@ -320,7 +327,7 @@ const calculateFocusAndGap = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   hoveredElement: ExcalidrawBindableElement,
   startOrEnd: "start" | "end",
-  elementsMap: ElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
 ): { focus: number; gap: number } => {
   const direction = startOrEnd === "start" ? -1 : 1;
   const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
@@ -539,33 +546,47 @@ const maybeCalculateNewGapWhenScaling = (
 
 // TODO: this is a bottleneck, optimise
 export const getEligibleElementsForBinding = (
-  elements: NonDeleted<ExcalidrawElement>[],
-  elementsMap: ElementsMap,
+  selectedElements: NonDeleted<ExcalidrawElement>[],
+  elements: readonly ExcalidrawElement[],
+  elementsMap: NonDeletedSceneElementsMap,
 ): SuggestedBinding[] => {
-  const includedElementIds = new Set(elements.map(({ id }) => id));
-  return elements.flatMap((element) =>
-    isBindingElement(element, false)
+  const includedElementIds = new Set(selectedElements.map(({ id }) => id));
+  return selectedElements.flatMap((selectedElement) =>
+    isBindingElement(selectedElement, false)
       ? (getElligibleElementsForBindingElement(
-          element as NonDeleted<ExcalidrawLinearElement>,
+          selectedElement as NonDeleted<ExcalidrawLinearElement>,
+          elements,
           elementsMap,
         ).filter(
           (element) => !includedElementIds.has(element.id),
         ) as SuggestedBinding[])
-      : isBindableElement(element, false)
-      ? getElligibleElementsForBindableElementAndWhere(element).filter(
-          (binding) => !includedElementIds.has(binding[0].id),
-        )
+      : isBindableElement(selectedElement, false)
+      ? getElligibleElementsForBindableElementAndWhere(
+          selectedElement,
+          elementsMap,
+        ).filter((binding) => !includedElementIds.has(binding[0].id))
       : [],
   );
 };
 
 const getElligibleElementsForBindingElement = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
-  elementsMap: ElementsMap,
+  elements: readonly ExcalidrawElement[],
+  elementsMap: NonDeletedSceneElementsMap,
 ): NonDeleted<ExcalidrawBindableElement>[] => {
   return [
-    getElligibleElementForBindingElement(linearElement, "start", elementsMap),
-    getElligibleElementForBindingElement(linearElement, "end", elementsMap),
+    getElligibleElementForBindingElement(
+      linearElement,
+      "start",
+      elements,
+      elementsMap,
+    ),
+    getElligibleElementForBindingElement(
+      linearElement,
+      "end",
+      elements,
+      elementsMap,
+    ),
   ].filter(
     (element): element is NonDeleted<ExcalidrawBindableElement> =>
       element != null,
@@ -575,18 +596,20 @@ const getElligibleElementsForBindingElement = (
 const getElligibleElementForBindingElement = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   startOrEnd: "start" | "end",
-  elementsMap: ElementsMap,
+  elements: readonly ExcalidrawElement[],
+  elementsMap: NonDeletedSceneElementsMap,
 ): NonDeleted<ExcalidrawBindableElement> | null => {
   return getHoveredElementForBinding(
     getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
-    Scene.getScene(linearElement)!,
+    elements,
+    elementsMap,
   );
 };
 
 const getLinearElementEdgeCoors = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   startOrEnd: "start" | "end",
-  elementsMap: ElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
 ): { x: number; y: number } => {
   const index = startOrEnd === "start" ? 0 : -1;
   return tupleToCoors(
@@ -600,6 +623,7 @@ const getLinearElementEdgeCoors = (
 
 const getElligibleElementsForBindableElementAndWhere = (
   bindableElement: NonDeleted<ExcalidrawBindableElement>,
+  elementsMap: NonDeletedSceneElementsMap,
 ): SuggestedPointBinding[] => {
   const scene = Scene.getScene(bindableElement)!;
   return scene
@@ -612,13 +636,13 @@ const getElligibleElementsForBindableElementAndWhere = (
         element,
         "start",
         bindableElement,
-        scene.getNonDeletedElementsMap(),
+        elementsMap,
       );
       const canBindEnd = isLinearElementEligibleForNewBindingByBindable(
         element,
         "end",
         bindableElement,
-        scene.getNonDeletedElementsMap(),
+        elementsMap,
       );
       if (!canBindStart && !canBindEnd) {
         return null;
@@ -636,7 +660,7 @@ const isLinearElementEligibleForNewBindingByBindable = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   startOrEnd: "start" | "end",
   bindableElement: NonDeleted<ExcalidrawBindableElement>,
-  elementsMap: ElementsMap,
+  elementsMap: NonDeletedSceneElementsMap,
 ): boolean => {
   const existingBinding =
     linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"];

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

@@ -6,6 +6,8 @@ import {
   ExcalidrawBindableElement,
   ExcalidrawTextElementWithContainer,
   ElementsMap,
+  NonDeletedExcalidrawElement,
+  NonDeletedSceneElementsMap,
 } from "./types";
 import {
   distance2d,
@@ -36,7 +38,6 @@ import {
 import { mutateElement } from "./mutateElement";
 import History from "../history";
 
-import Scene from "../scene/Scene";
 import {
   bindOrUnbindLinearElement,
   getHoveredElementForBinding,
@@ -86,11 +87,10 @@ export class LinearElementEditor {
   public readonly hoverPointIndex: number;
   public readonly segmentMidPointHoveredCoords: Point | null;
 
-  constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
+  constructor(element: NonDeleted<ExcalidrawLinearElement>) {
     this.elementId = element.id as string & {
       _brand: "excalidrawLinearElementId";
     };
-    Scene.mapElementToScene(this.elementId, scene);
     LinearElementEditor.normalizePoints(element);
 
     this.selectedPointsIndices = null;
@@ -123,8 +123,11 @@ export class LinearElementEditor {
    * @param id the `elementId` from the instance of this class (so that we can
    *  statically guarantee this method returns an ExcalidrawLinearElement)
    */
-  static getElement(id: InstanceType<typeof LinearElementEditor>["elementId"]) {
-    const element = Scene.getScene(id)?.getNonDeletedElement(id);
+  static getElement(
+    id: InstanceType<typeof LinearElementEditor>["elementId"],
+    elementsMap: ElementsMap,
+  ) {
+    const element = elementsMap.get(id);
     if (element) {
       return element as NonDeleted<ExcalidrawLinearElement>;
     }
@@ -135,7 +138,7 @@ export class LinearElementEditor {
     event: PointerEvent,
     appState: AppState,
     setState: React.Component<any, AppState>["setState"],
-    elementsMap: ElementsMap,
+    elementsMap: NonDeletedSceneElementsMap,
   ) {
     if (
       !appState.editingLinearElement ||
@@ -146,7 +149,7 @@ export class LinearElementEditor {
     const { editingLinearElement } = appState;
     const { selectedPointsIndices, elementId } = editingLinearElement;
 
-    const element = LinearElementEditor.getElement(elementId);
+    const element = LinearElementEditor.getElement(elementId, elementsMap);
     if (!element) {
       return false;
     }
@@ -197,13 +200,13 @@ export class LinearElementEditor {
       pointSceneCoords: { x: number; y: number }[],
     ) => void,
     linearElementEditor: LinearElementEditor,
-    elementsMap: ElementsMap,
+    elementsMap: NonDeletedSceneElementsMap,
   ): boolean {
     if (!linearElementEditor) {
       return false;
     }
     const { selectedPointsIndices, elementId } = linearElementEditor;
-    const element = LinearElementEditor.getElement(elementId);
+    const element = LinearElementEditor.getElement(elementId, elementsMap);
     if (!element) {
       return false;
     }
@@ -331,11 +334,12 @@ export class LinearElementEditor {
     event: PointerEvent,
     editingLinearElement: LinearElementEditor,
     appState: AppState,
-    elementsMap: ElementsMap,
+    elements: readonly NonDeletedExcalidrawElement[],
+    elementsMap: NonDeletedSceneElementsMap,
   ): LinearElementEditor {
     const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
       editingLinearElement;
-    const element = LinearElementEditor.getElement(elementId);
+    const element = LinearElementEditor.getElement(elementId, elementsMap);
     if (!element) {
       return editingLinearElement;
     }
@@ -376,7 +380,8 @@ export class LinearElementEditor {
                     elementsMap,
                   ),
                 ),
-                Scene.getScene(element)!,
+                elements,
+                elementsMap,
               )
             : null;
 
@@ -490,7 +495,7 @@ export class LinearElementEditor {
     elementsMap: ElementsMap,
   ) => {
     const { elementId } = linearElementEditor;
-    const element = LinearElementEditor.getElement(elementId);
+    const element = LinearElementEditor.getElement(elementId, elementsMap);
     if (!element) {
       return null;
     }
@@ -614,6 +619,7 @@ export class LinearElementEditor {
   ) {
     const element = LinearElementEditor.getElement(
       linearElementEditor.elementId,
+      elementsMap,
     );
     if (!element) {
       return -1;
@@ -639,7 +645,8 @@ export class LinearElementEditor {
     history: History,
     scenePointer: { x: number; y: number },
     linearElementEditor: LinearElementEditor,
-    elementsMap: ElementsMap,
+    elements: readonly NonDeletedExcalidrawElement[],
+    elementsMap: NonDeletedSceneElementsMap,
   ): {
     didAddPoint: boolean;
     hitElement: NonDeleted<ExcalidrawElement> | null;
@@ -656,7 +663,7 @@ export class LinearElementEditor {
     }
 
     const { elementId } = linearElementEditor;
-    const element = LinearElementEditor.getElement(elementId);
+    const element = LinearElementEditor.getElement(elementId, elementsMap);
 
     if (!element) {
       return ret;
@@ -709,7 +716,8 @@ export class LinearElementEditor {
         lastUncommittedPoint: null,
         endBindingElement: getHoveredElementForBinding(
           scenePointer,
-          Scene.getScene(element)!,
+          elements,
+          elementsMap,
         ),
       };
 
@@ -813,7 +821,7 @@ export class LinearElementEditor {
       return null;
     }
     const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
-    const element = LinearElementEditor.getElement(elementId);
+    const element = LinearElementEditor.getElement(elementId, elementsMap);
     if (!element) {
       return appState.editingLinearElement;
     }
@@ -1020,14 +1028,14 @@ export class LinearElementEditor {
     mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
   }
 
-  static duplicateSelectedPoints(appState: AppState) {
+  static duplicateSelectedPoints(appState: AppState, elementsMap: ElementsMap) {
     if (!appState.editingLinearElement) {
       return false;
     }
 
     const { selectedPointsIndices, elementId } = appState.editingLinearElement;
 
-    const element = LinearElementEditor.getElement(elementId);
+    const element = LinearElementEditor.getElement(elementId, elementsMap);
 
     if (!element || selectedPointsIndices === null) {
       return false;
@@ -1189,9 +1197,11 @@ export class LinearElementEditor {
     linearElementEditor: LinearElementEditor,
     pointerCoords: PointerCoords,
     appState: AppState,
+    elementsMap: ElementsMap,
   ) {
     const element = LinearElementEditor.getElement(
       linearElementEditor.elementId,
+      elementsMap,
     );
 
     if (!element) {
@@ -1234,6 +1244,7 @@ export class LinearElementEditor {
   ) {
     const element = LinearElementEditor.getElement(
       linearElementEditor.elementId,
+      elementsMap,
     );
     if (!element) {
       return;

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

@@ -354,7 +354,8 @@ const renderLinearElementPointHighlight = (
   ) {
     return;
   }
-  const element = LinearElementEditor.getElement(elementId);
+  const element = LinearElementEditor.getElement(elementId, elementsMap);
+
   if (!element) {
     return;
   }

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

@@ -1,4 +1,3 @@
-import React from "react";
 import ReactDOM from "react-dom";
 import { render, fireEvent } from "./test-utils";
 import { Excalidraw } from "../index";
@@ -13,7 +12,6 @@ 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")!);
@@ -76,7 +74,7 @@ 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);
+    const elementsMap = h.app.scene.getNonDeletedElementsMap();
     // bind line to two rectangles
     bindOrUnbindLinearElement(
       line.get() as NonDeleted<ExcalidrawLinearElement>,