Repository: excalidraw/excalidraw (2025-07-18T18:37:52Z)

Issue:
deleting all points from within line editor throws
1. create line
2. enter line editor
3. box-select all points
4. delete
Bug: [regression in master] Line editor point remains hovered
Seems to be a regression from #9642. Happens because `handleHoverSelectedLinearElement` was used to handle pointer away and now it doesn't fire when the pointer is away while line-editing.

https://github.com/user-attachments/assets/b9297af2-c1a5-4637-9e74-0ad895a3d984
Excessive re-renders on pointer move during line editing
There are two things that cause unnecessary re-renders while moving the pointer while in the line editor:
- A new `editingLinearElement` is returned each time [here](https://github.com/excalidraw/excalidraw/blob/678dff25eddfc43319299495556ab9f6029bc719/packages/element/src/linearElementEditor.ts#L1043-L1047) even if nothing's changed. The new reference of `editingLinearElement` is then checked against the one in appState [here](https://github.com/excalidraw/excalidraw/blob/678dff25eddfc43319299495556ab9f6029bc719/packages/excalidraw/components/App.tsx#L5860-L5863).
- Always executing `setState` [here](https://github.com/excalidraw/excalidraw/blob/678dff25eddfc43319299495556ab9f6029bc719/packages/excalidraw/components/App.tsx#L5878-L5883) even if nothing's changed.
Bug: "suggested bindings box" shows in line editor (not arrow) while pressing alt to add a point
The type of the element is not checked [here](https://github.com/excalidraw/excalidraw/blob/678dff25eddfc43319299495556ab9f6029bc719/packages/excalidraw/components/App.tsx#L5873-L5877)

https://github.com/user-attachments/assets/140aa0a0-050c-44f4-8983-3234b33fea78


Discussion:
Maybe an extra check for `selectedPointsIndices.length` against `element.points.length` here would be sufficient since the `selectedPointsIndices` does not include duplicates?: https://github.com/excalidraw/excalidraw/blob/e058a08b33239ab4024e6958d490a238693beea3/packages/excalidraw/actions/actionDeleteSelected.tsx#L229

Can reproduce on staging.



I can reproduce both on staging and prod



Patch:
diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts
index 3f666c412c6c..26e59a98ac60 100644
--- a/packages/element/src/linearElementEditor.ts
+++ b/packages/element/src/linearElementEditor.ts
@@ -1040,10 +1040,12 @@ export class LinearElementEditor {
       if (lastPoint === lastUncommittedPoint) {
         LinearElementEditor.deletePoints(element, app, [points.length - 1]);
       }
-      return {
-        ...appState.editingLinearElement,
-        lastUncommittedPoint: null,
-      };
+      return appState.editingLinearElement.lastUncommittedPoint
+        ? {
+            ...appState.editingLinearElement,
+            lastUncommittedPoint: null,
+          }
+        : appState.editingLinearElement;
     }
 
     let newPoint: LocalPoint;
diff --git a/packages/excalidraw/actions/actionDeleteSelected.tsx b/packages/excalidraw/actions/actionDeleteSelected.tsx
index 20d7d129fd11..c0e43d3dc1b3 100644
--- a/packages/excalidraw/actions/actionDeleteSelected.tsx
+++ b/packages/excalidraw/actions/actionDeleteSelected.tsx
@@ -225,8 +225,11 @@ export const actionDeleteSelected = register({
         return false;
       }
 
-      // case: deleting last remaining point
-      if (element.points.length < 2) {
+      // case: deleting all points
+      if (
+        element.points.length < 2 ||
+        selectedPointsIndices.length === element.points.length
+      ) {
         const nextElements = elements.map((el) => {
           if (el.id === element.id) {
             return newElementWith(el, { isDeleted: true });
diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx
index 1397a8b7c2c2..e0495cde7174 100644
--- a/packages/excalidraw/components/App.tsx
+++ b/packages/excalidraw/components/App.tsx
@@ -5867,6 +5867,9 @@ class App extends React.Component<AppProps, AppState> {
         scenePointerY,
         this,
       );
+      const linearElement = editingLinearElement
+        ? this.scene.getElement(editingLinearElement.elementId)
+        : null;
 
       if (
         editingLinearElement &&
@@ -5881,16 +5884,17 @@ class App extends React.Component<AppProps, AppState> {
           });
         });
       }
-      if (editingLinearElement?.lastUncommittedPoint != null) {
+      if (
+        editingLinearElement?.lastUncommittedPoint != null &&
+        linearElement &&
+        isBindingElementType(linearElement.type)
+      ) {
         this.maybeSuggestBindingAtCursor(
           scenePointer,
           editingLinearElement.elbowed,
         );
-      } else {
-        // causes stack overflow if not sync
-        flushSync(() => {
-          this.setState({ suggestedBindings: [] });
-        });
+      } else if (this.state.suggestedBindings.length) {
+        this.setState({ suggestedBindings: [] });
       }
     }
 
@@ -6154,15 +6158,6 @@ class App extends React.Component<AppProps, AppState> {
         setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
       } else if (isOverScrollBar) {
         setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
-      } else if (
-        this.state.selectedLinearElement &&
-        hitElement?.id === this.state.selectedLinearElement.elementId
-      ) {
-        this.handleHoverSelectedLinearElement(
-          this.state.selectedLinearElement,
-          scenePointerX,
-          scenePointerY,
-        );
       } else if (
         // if using cmd/ctrl, we're not dragging
         !event[KEYS.CTRL_OR_CMD]
@@ -6204,6 +6199,14 @@ class App extends React.Component<AppProps, AppState> {
       } else {
         setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
       }
+
+      if (this.state.selectedLinearElement) {
+        this.handleHoverSelectedLinearElement(
+          this.state.selectedLinearElement,
+          scenePointerX,
+          scenePointerY,
+        );
+      }
     }
 
     if (this.state.openDialog?.name === "elementLinkSelector" && hitElement) {


Test patch:
diff --git a/packages/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx
index 4b957022c6d3..a442afe6b519 100644
--- a/packages/element/tests/linearElementEditor.test.tsx
+++ b/packages/element/tests/linearElementEditor.test.tsx
@@ -367,7 +367,7 @@ describe("Test Linear Elements", () => {
       // drag line from midpoint
       drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
       expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-        `12`,
+        `11`,
       );
       expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
 
@@ -469,7 +469,7 @@ describe("Test Linear Elements", () => {
       drag(startPoint, endPoint);
 
       expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-        `12`,
+        `11`,
       );
       expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
 
@@ -537,7 +537,7 @@ describe("Test Linear Elements", () => {
         );
 
         expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-          `16`,
+          `14`,
         );
         expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
 
@@ -588,7 +588,7 @@ describe("Test Linear Elements", () => {
         drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
 
         expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-          `12`,
+          `11`,
         );
         expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
 
@@ -629,7 +629,7 @@ describe("Test Linear Elements", () => {
         drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
 
         expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-          `12`,
+          `11`,
         );
         expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
 
@@ -677,7 +677,7 @@ describe("Test Linear Elements", () => {
         deletePoint(points[2]);
         expect(line.points.length).toEqual(3);
         expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-          `18`,
+          `17`,
         );
         expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
 
@@ -735,7 +735,7 @@ describe("Test Linear Elements", () => {
           ),
         );
         expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-          `16`,
+          `14`,
         );
         expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
         expect(line.points.length).toEqual(5);
@@ -833,7 +833,7 @@ describe("Test Linear Elements", () => {
         drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
 
         expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
-          `12`,
+          `11`,
         );
         expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);