Переглянути джерело

Move linear element handling out of App.tsx

Signed-off-by: Mark Tolmacs <[email protected]>
Mark Tolmacs 4 місяців тому
батько
коміт
3068787ac4

+ 1 - 11
packages/common/src/utils.ts

@@ -1,14 +1,7 @@
-import {
-  average,
-  type GlobalPoint,
-  type LocalPoint,
-  pointTranslate,
-  vector,
-} from "@excalidraw/math";
+import { average } from "@excalidraw/math";
 
 import type {
   ExcalidrawBindableElement,
-  ExcalidrawElement,
   FontFamilyValues,
   FontString,
 } from "@excalidraw/element/types";
@@ -1208,6 +1201,3 @@ export const escapeDoubleQuotes = (str: string) => {
 
 export const castArray = <T>(value: T | T[]): T[] =>
   Array.isArray(value) ? value : [value];
-
-export const toLocalPoint = (p: GlobalPoint, element: ExcalidrawElement) =>
-  pointTranslate<GlobalPoint, LocalPoint>(p, vector(-element.x, -element.y));

+ 11 - 8
packages/element/src/binding.ts

@@ -6,7 +6,6 @@ import {
   invariant,
   isDevEnv,
   isTestEnv,
-  toLocalPoint,
 } from "@excalidraw/common";
 
 import {
@@ -527,14 +526,18 @@ export const bindLinearElement = (
   const points = Array.from(linearElement.points);
 
   if (isArrowElement(linearElement)) {
-    points[edgePointIndex] = toLocalPoint(
-      bindPointToSnapToElementOutline(
-        linearElement,
-        hoveredElement,
-        startOrEnd,
-        elementsMap,
-      ),
+    const [x, y] = bindPointToSnapToElementOutline(
       linearElement,
+      hoveredElement,
+      startOrEnd,
+      elementsMap,
+    );
+    points[edgePointIndex] = LinearElementEditor.createPointAt(
+      linearElement,
+      elementsMap,
+      x,
+      y,
+      null,
     );
   }
 

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

@@ -100,7 +100,6 @@ import {
   arrayToMap,
   type EXPORT_IMAGE_TYPES,
   randomInteger,
-  toLocalPoint,
 } from "@excalidraw/common";
 
 import {
@@ -114,7 +113,6 @@ import {
   fixBindingsAfterDeletion,
   getHoveredElementForBinding,
   isBindingEnabled,
-  isLinearElementSimpleAndAlreadyBound,
   maybeBindLinearElement,
   shouldEnableBindingForPointerEvent,
   updateBoundElements,
@@ -172,7 +170,6 @@ import {
 } from "@excalidraw/element/typeChecks";
 
 import {
-  getLockedLinearCursorAlignSize,
   getNormalizedDimensions,
   isElementCompletelyInViewport,
   isElementInViewport,
@@ -307,7 +304,6 @@ import { isNonDeletedElement } from "@excalidraw/element";
 import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
 
 import type {
-  ExcalidrawBindableElement,
   ExcalidrawElement,
   ExcalidrawFreeDrawElement,
   ExcalidrawGenericElement,
@@ -466,6 +462,14 @@ import { isMaybeMermaidDefinition } from "../mermaid";
 
 import { LassoTrail } from "../lasso";
 
+import {
+  handleCanvasPointerMoveForLinearElement,
+  handleDoubleClickForLinearElement,
+  maybeSuggestBindingsForLinearElementAtCoords,
+  onPointerMoveFromPointerDownOnLinearElement,
+  onPointerUpFromPointerDownOnLinearElementHandler,
+} from "../linear";
+
 import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
 import BraveMeasureTextError from "./BraveMeasureTextError";
 import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
@@ -5447,75 +5451,16 @@ class App extends React.Component<AppProps, AppState> {
 
     if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
       if (
-        event[KEYS.CTRL_OR_CMD] &&
-        (!this.state.editingLinearElement ||
-          this.state.editingLinearElement.elementId !==
-            selectedElements[0].id) &&
-        !isElbowArrow(selectedElements[0])
+        handleDoubleClickForLinearElement(
+          this,
+          this.store,
+          selectedElements[0],
+          event,
+          sceneX,
+          sceneY,
+        )
       ) {
-        this.store.shouldCaptureIncrement();
-        this.setState({
-          editingLinearElement: new LinearElementEditor(selectedElements[0]),
-        });
         return;
-      } else if (
-        this.state.selectedLinearElement &&
-        isElbowArrow(selectedElements[0])
-      ) {
-        const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords(
-          this.state.selectedLinearElement,
-          { x: sceneX, y: sceneY },
-          this.state,
-          this.scene.getNonDeletedElementsMap(),
-        );
-        const midPoint = hitCoords
-          ? LinearElementEditor.getSegmentMidPointIndex(
-              this.state.selectedLinearElement,
-              this.state,
-              hitCoords,
-              this.scene.getNonDeletedElementsMap(),
-            )
-          : -1;
-
-        if (midPoint && midPoint > -1) {
-          this.store.shouldCaptureIncrement();
-          LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
-
-          const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
-            {
-              ...this.state.selectedLinearElement,
-              segmentMidPointHoveredCoords: null,
-            },
-            { x: sceneX, y: sceneY },
-            this.state,
-            this.scene.getNonDeletedElementsMap(),
-          );
-          const nextIndex = nextCoords
-            ? LinearElementEditor.getSegmentMidPointIndex(
-                this.state.selectedLinearElement,
-                this.state,
-                nextCoords,
-                this.scene.getNonDeletedElementsMap(),
-              )
-            : null;
-
-          this.setState({
-            selectedLinearElement: {
-              ...this.state.selectedLinearElement,
-              pointerDownState: {
-                ...this.state.selectedLinearElement.pointerDownState,
-                segmentMidpoint: {
-                  index: nextIndex,
-                  value: hitCoords,
-                  added: false,
-                },
-              },
-              segmentMidPointHoveredCoords: nextCoords,
-            },
-          });
-
-          return;
-        }
       }
     }
 
@@ -5896,9 +5841,10 @@ class App extends React.Component<AppProps, AppState> {
       // and point
       const { newElement } = this.state;
       if (isBindingElement(newElement, false)) {
-        this.maybeSuggestBindingsForLinearElementAtCoords(
+        maybeSuggestBindingsForLinearElementAtCoords(
           newElement,
           [scenePointer],
+          this,
           this.state.startBoundElement,
         );
       } else {
@@ -5908,121 +5854,14 @@ class App extends React.Component<AppProps, AppState> {
 
     if (this.state.multiElement) {
       const { multiElement } = this.state;
-      const { x: rx, y: ry } = multiElement;
-
-      const { points, lastCommittedPoint } = multiElement;
-      const lastPoint = points[points.length - 1];
-
-      setCursorForShape(this.interactiveCanvas, this.state);
-
-      if (lastPoint === lastCommittedPoint) {
-        // if we haven't yet created a temp point and we're beyond commit-zone
-        // threshold, add a point
-        if (
-          pointDistance(
-            pointFrom(scenePointerX - rx, scenePointerY - ry),
-            lastPoint,
-          ) >= LINE_CONFIRM_THRESHOLD
-        ) {
-          mutateElement(
-            multiElement,
-            {
-              points: [
-                ...points,
-                pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
-              ],
-            },
-            false,
-          );
-        } else {
-          setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
-          // in this branch, we're inside the commit zone, and no uncommitted
-          // point exists. Thus do nothing (don't add/remove points).
-        }
-      } else if (
-        points.length > 2 &&
-        lastCommittedPoint &&
-        pointDistance(
-          pointFrom(scenePointerX - rx, scenePointerY - ry),
-          lastCommittedPoint,
-        ) < LINE_CONFIRM_THRESHOLD
-      ) {
-        setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
-        mutateElement(
-          multiElement,
-          {
-            points: points.slice(0, -1),
-          },
-          false,
-        );
-      } else {
-        const [gridX, gridY] = getGridPoint(
-          scenePointerX,
-          scenePointerY,
-          event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement)
-            ? null
-            : this.getEffectiveGridSize(),
-        );
-
-        const [lastCommittedX, lastCommittedY] =
-          multiElement?.lastCommittedPoint ?? [0, 0];
-
-        let dxFromLastCommitted = gridX - rx - lastCommittedX;
-        let dyFromLastCommitted = gridY - ry - lastCommittedY;
-
-        if (shouldRotateWithDiscreteAngle(event)) {
-          ({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
-            getLockedLinearCursorAlignSize(
-              // actual coordinate of the last committed point
-              lastCommittedX + rx,
-              lastCommittedY + ry,
-              // cursor-grid coordinate
-              gridX,
-              gridY,
-            ));
-        }
-
-        if (isPathALoop(points, this.state.zoom.value)) {
-          setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
-        }
-        // update last uncommitted point
-        mutateElement(
-          multiElement,
-          {
-            points: [
-              ...points.slice(0, -1),
-              isArrowElement(multiElement)
-                ? toLocalPoint(
-                    getOutlineAvoidingPoint(
-                      multiElement,
-                      pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
-                      multiElement.points.length - 1,
-                      this.scene,
-                      this.state.zoom,
-                      pointFrom<GlobalPoint>(
-                        multiElement.x + lastCommittedX + dxFromLastCommitted,
-                        multiElement.y + lastCommittedY + dyFromLastCommitted,
-                      ),
-                    ),
-                    multiElement,
-                  )
-                : pointFrom<LocalPoint>(
-                    lastCommittedX + dxFromLastCommitted,
-                    lastCommittedY + dyFromLastCommitted,
-                  ),
-            ],
-          },
-          false,
-          {
-            isDragging: true,
-          },
-        );
-
-        // in this path, we're mutating multiElement to reflect
-        // how it will be after adding pointer position as the next point
-        // trigger update here so that new element canvas renders again to reflect this
-        this.triggerRender(false);
-      }
+      handleCanvasPointerMoveForLinearElement(
+        multiElement,
+        this,
+        scenePointerX,
+        scenePointerY,
+        event,
+        this.triggerRender,
+      );
 
       return;
     }
@@ -8301,9 +8140,10 @@ class App extends React.Component<AppProps, AppState> {
           pointerCoords.x,
           pointerCoords.y,
           (element, pointsSceneCoords) => {
-            this.maybeSuggestBindingsForLinearElementAtCoords(
+            maybeSuggestBindingsForLinearElementAtCoords(
               element,
               pointsSceneCoords,
+              this,
             );
           },
           linearElementEditor,
@@ -8691,120 +8531,14 @@ class App extends React.Component<AppProps, AppState> {
             });
           }
         } else if (isLinearElement(newElement)) {
-          pointerDownState.drag.hasOccurred = true;
-          const points = newElement.points;
-          const [gridX, gridY] = getGridPoint(
-            pointerCoords.x,
-            pointerCoords.y,
-            event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
-          );
-          let dx = gridX - newElement.x;
-          let dy = gridY - newElement.y;
-
-          if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
-            ({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
-              newElement.x,
-              newElement.y,
-              pointerCoords.x,
-              pointerCoords.y,
-            ));
-          }
-
-          if (points.length === 1) {
-            mutateElement(
-              newElement,
-              {
-                points: [
-                  ...points,
-                  isArrowElement(newElement)
-                    ? toLocalPoint(
-                        getOutlineAvoidingPoint(
-                          newElement,
-                          pointFrom<GlobalPoint>(
-                            pointerCoords.x,
-                            pointerCoords.y,
-                          ),
-                          newElement.points.length - 1,
-                          this.scene,
-                          this.state.zoom,
-                          pointFrom<GlobalPoint>(
-                            newElement.x + dx,
-                            newElement.y + dy,
-                          ),
-                        ),
-                        newElement,
-                      )
-                    : pointFrom<LocalPoint>(dx, dy),
-                ],
-              },
-              false,
-            );
-          } else if (
-            points.length === 2 ||
-            (points.length > 1 && isElbowArrow(newElement))
-          ) {
-            mutateElement(
-              newElement,
-              {
-                points: [
-                  ...points.slice(0, -1),
-                  isArrowElement(newElement)
-                    ? toLocalPoint(
-                        getOutlineAvoidingPoint(
-                          newElement,
-                          pointFrom<GlobalPoint>(
-                            pointerCoords.x,
-                            pointerCoords.y,
-                          ),
-                          newElement.points.length - 1,
-                          this.scene,
-                          this.state.zoom,
-                          pointFrom<GlobalPoint>(
-                            newElement.x + dx,
-                            newElement.y + dy,
-                          ),
-                        ),
-                        newElement,
-                      )
-                    : pointFrom<LocalPoint>(dx, dy),
-                ],
-              },
-              false,
-              { isDragging: true },
-            );
-            LinearElementEditor.movePoints(newElement, [
-              {
-                index: 0,
-                isDragging: false,
-                point: toLocalPoint(
-                  getOutlineAvoidingPoint(
-                    newElement,
-                    pointFrom<GlobalPoint>(
-                      pointerDownState.origin.x,
-                      pointerDownState.origin.y,
-                    ),
-                    0,
-                    this.scene,
-                    this.state.zoom,
-                  ),
-                  newElement,
-                ),
-              },
-            ]);
-          }
-
-          this.setState({
+          onPointerMoveFromPointerDownOnLinearElement(
             newElement,
-          });
-
-          if (isBindingElement(newElement, false)) {
-            // When creating a linear element by dragging
-            this.maybeSuggestBindingsForLinearElementAtCoords(
-              newElement,
-              [pointerCoords],
-              this.state.startBoundElement,
-            );
-          }
+            this,
+            pointerDownState,
+            pointerCoords,
+            event,
+            elementsMap,
+          );
         } else {
           pointerDownState.lastCoords.x = pointerCoords.x;
           pointerDownState.lastCoords.y = pointerCoords.y;
@@ -9170,65 +8904,15 @@ class App extends React.Component<AppProps, AppState> {
       }
 
       if (isLinearElement(newElement)) {
-        if (newElement!.points.length > 1) {
-          this.store.shouldCaptureIncrement();
-        }
-        const pointerCoords = viewportCoordsToSceneCoords(
+        onPointerUpFromPointerDownOnLinearElementHandler(
+          newElement,
+          multiElement,
+          this,
+          this.store,
+          pointerDownState,
           childEvent,
-          this.state,
+          activeTool,
         );
-
-        if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
-          mutateElement(newElement, {
-            points: [
-              ...newElement.points,
-              pointFrom<LocalPoint>(
-                pointerCoords.x - newElement.x,
-                pointerCoords.y - newElement.y,
-              ),
-            ],
-          });
-          this.setState({
-            multiElement: newElement,
-            newElement,
-          });
-        } else if (pointerDownState.drag.hasOccurred && !multiElement) {
-          if (
-            isBindingEnabled(this.state) &&
-            isBindingElement(newElement, false)
-          ) {
-            maybeBindLinearElement(
-              newElement,
-              this.state,
-              this.scene.getNonDeletedElementsMap(),
-              this.scene.getNonDeletedElements(),
-            );
-          }
-          this.setState({ suggestedBindings: [], startBoundElement: null });
-          if (!activeTool.locked) {
-            resetCursor(this.interactiveCanvas);
-            this.setState((prevState) => ({
-              newElement: null,
-              activeTool: updateActiveTool(this.state, {
-                type: "selection",
-              }),
-              selectedElementIds: makeNextSelectedElementIds(
-                {
-                  ...prevState.selectedElementIds,
-                  [newElement.id]: true,
-                },
-                prevState,
-              ),
-              selectedLinearElement: new LinearElementEditor(newElement),
-            }));
-          } else {
-            this.setState((prevState) => ({
-              newElement: null,
-            }));
-          }
-          // so that the scene gets rendered again to display the newly drawn linear as well
-          this.scene.triggerUpdate();
-        }
         return;
       }
 
@@ -10294,49 +9978,6 @@ class App extends React.Component<AppProps, AppState> {
     });
   };
 
-  private maybeSuggestBindingsForLinearElementAtCoords = (
-    linearElement: NonDeleted<ExcalidrawLinearElement>,
-    /** scene coords */
-    pointerCoords: {
-      x: number;
-      y: number;
-    }[],
-    // During line creation the start binding hasn't been written yet
-    // into `linearElement`
-    oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
-  ): void => {
-    if (!pointerCoords.length) {
-      return;
-    }
-
-    const suggestedBindings = pointerCoords.reduce(
-      (acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
-        const hoveredBindableElement = getHoveredElementForBinding(
-          coords,
-          this.scene.getNonDeletedElements(),
-          this.scene.getNonDeletedElementsMap(),
-          this.state.zoom,
-          isElbowArrow(linearElement),
-          isElbowArrow(linearElement),
-        );
-        if (
-          hoveredBindableElement != null &&
-          !isLinearElementSimpleAndAlreadyBound(
-            linearElement,
-            oppositeBindingBoundElement?.id,
-            hoveredBindableElement,
-          )
-        ) {
-          acc.push(hoveredBindableElement);
-        }
-        return acc;
-      },
-      [],
-    );
-
-    this.setState({ suggestedBindings });
-  };
-
   private clearSelection(hitElement: ExcalidrawElement | null): void {
     this.setState((prevState) => ({
       selectedElementIds: makeNextSelectedElementIds({}, prevState),

+ 478 - 0
packages/excalidraw/linear.ts

@@ -0,0 +1,478 @@
+import {
+  CURSOR_TYPE,
+  getGridPoint,
+  KEYS,
+  LINE_CONFIRM_THRESHOLD,
+  shouldRotateWithDiscreteAngle,
+  updateActiveTool,
+  viewportCoordsToSceneCoords,
+} from "@excalidraw/common";
+
+import { getLockedLinearCursorAlignSize } from "@excalidraw/element/sizeHelpers";
+
+import {
+  isArrowElement,
+  isBindingElement,
+  isElbowArrow,
+} from "@excalidraw/element/typeChecks";
+import {
+  getHoveredElementForBinding,
+  getOutlineAvoidingPoint,
+  isBindingEnabled,
+  isLinearElementSimpleAndAlreadyBound,
+  maybeBindLinearElement,
+} from "@excalidraw/element/binding";
+
+import { pointDistance, pointFrom } from "@excalidraw/math";
+import { mutateElement } from "@excalidraw/element/mutateElement";
+
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+
+import { isPathALoop } from "@excalidraw/element/shapes";
+
+import { makeNextSelectedElementIds } from "@excalidraw/element/selection";
+
+import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
+
+import type {
+  ExcalidrawBindableElement,
+  ExcalidrawLinearElement,
+  NonDeleted,
+  NonDeletedSceneElementsMap,
+} from "@excalidraw/element/types";
+
+import { resetCursor, setCursor, setCursorForShape } from "./cursor";
+
+import type App from "./components/App";
+
+import type { ActiveTool, PointerDownState } from "./types";
+
+/**
+ * This function is called when the user drags the pointer to create a new linear element.
+ */
+export function onPointerMoveFromPointerDownOnLinearElement(
+  newElement: ExcalidrawLinearElement,
+  app: App,
+  pointerDownState: PointerDownState,
+  pointerCoords: { x: number; y: number },
+  event: PointerEvent,
+  elementsMap: NonDeletedSceneElementsMap,
+) {
+  pointerDownState.drag.hasOccurred = true;
+  const points = newElement.points;
+  const [gridX, gridY] = getGridPoint(
+    pointerCoords.x,
+    pointerCoords.y,
+    event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
+  );
+  let dx = gridX - newElement.x;
+  let dy = gridY - newElement.y;
+
+  if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
+    ({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
+      newElement.x,
+      newElement.y,
+      pointerCoords.x,
+      pointerCoords.y,
+    ));
+  }
+
+  if (points.length === 1) {
+    let x = newElement.x + dx;
+    let y = newElement.y + dy;
+    if (isArrowElement(newElement)) {
+      [x, y] = getOutlineAvoidingPoint(
+        newElement,
+        pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
+        newElement.points.length - 1,
+        app.scene,
+        app.state.zoom,
+        pointFrom<GlobalPoint>(newElement.x + dx, newElement.y + dy),
+      );
+    }
+
+    mutateElement(
+      newElement,
+      {
+        points: [
+          ...points,
+          LinearElementEditor.createPointAt(
+            newElement,
+            elementsMap,
+            x,
+            y,
+            app.getEffectiveGridSize(),
+          ),
+        ],
+      },
+      false,
+    );
+  } else if (
+    points.length === 2 ||
+    (points.length > 1 && isElbowArrow(newElement))
+  ) {
+    const targets = [
+      {
+        index: points.length - 1,
+        isDragging: true,
+        point: pointFrom<LocalPoint>(dx, dy),
+      },
+    ];
+
+    if (isArrowElement(newElement)) {
+      const [x, y] = getOutlineAvoidingPoint(
+        newElement,
+        pointFrom<GlobalPoint>(
+          pointerDownState.origin.x,
+          pointerDownState.origin.y,
+        ),
+        0,
+        app.scene,
+        app.state.zoom,
+      );
+      targets.unshift({
+        index: 0,
+        isDragging: false,
+        point: LinearElementEditor.createPointAt(
+          newElement,
+          elementsMap,
+          x,
+          y,
+          app.getEffectiveGridSize(),
+        ),
+      });
+    }
+
+    LinearElementEditor.movePoints(newElement, targets);
+  }
+
+  app.setState({
+    newElement,
+  });
+
+  if (isBindingElement(newElement, false)) {
+    // When creating a linear element by dragging
+    maybeSuggestBindingsForLinearElementAtCoords(
+      newElement,
+      [pointerCoords],
+      app,
+      app.state.startBoundElement,
+    );
+  }
+}
+
+/**
+ *
+ */
+export function handleCanvasPointerMoveForLinearElement(
+  multiElement: NonDeleted<ExcalidrawLinearElement>,
+  app: App,
+  scenePointerX: number,
+  scenePointerY: number,
+  event: React.PointerEvent<HTMLCanvasElement>,
+  triggerRender: (forceUpdate?: boolean) => void,
+) {
+  const { x: rx, y: ry } = multiElement;
+
+  const { points, lastCommittedPoint } = multiElement;
+  const lastPoint = points[points.length - 1];
+
+  setCursorForShape(app.interactiveCanvas, app.state);
+
+  if (lastPoint === lastCommittedPoint) {
+    // if we haven't yet created a temp point and we're beyond commit-zone
+    // threshold, add a point
+    if (
+      pointDistance(
+        pointFrom(scenePointerX - rx, scenePointerY - ry),
+        lastPoint,
+      ) >= LINE_CONFIRM_THRESHOLD
+    ) {
+      mutateElement(
+        multiElement,
+        {
+          points: [
+            ...points,
+            pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
+          ],
+        },
+        false,
+      );
+    } else {
+      setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER);
+      // in this branch, we're inside the commit zone, and no uncommitted
+      // point exists. Thus do nothing (don't add/remove points).
+    }
+  } else if (
+    points.length > 2 &&
+    lastCommittedPoint &&
+    pointDistance(
+      pointFrom(scenePointerX - rx, scenePointerY - ry),
+      lastCommittedPoint,
+    ) < LINE_CONFIRM_THRESHOLD
+  ) {
+    setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER);
+    mutateElement(
+      multiElement,
+      {
+        points: points.slice(0, -1),
+      },
+      false,
+    );
+  } else {
+    const [gridX, gridY] = getGridPoint(
+      scenePointerX,
+      scenePointerY,
+      event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement)
+        ? null
+        : app.getEffectiveGridSize(),
+    );
+    console.log(points);
+    const [lastCommittedX, lastCommittedY] =
+      multiElement?.lastCommittedPoint ?? [0, 0];
+
+    let dxFromLastCommitted = gridX - rx - lastCommittedX;
+    let dyFromLastCommitted = gridY - ry - lastCommittedY;
+
+    if (shouldRotateWithDiscreteAngle(event)) {
+      ({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
+        getLockedLinearCursorAlignSize(
+          // actual coordinate of the last committed point
+          lastCommittedX + rx,
+          lastCommittedY + ry,
+          // cursor-grid coordinate
+          gridX,
+          gridY,
+        ));
+    }
+
+    if (isPathALoop(points, app.state.zoom.value)) {
+      setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER);
+    }
+
+    let x = multiElement.x + lastCommittedX + dxFromLastCommitted;
+    let y = multiElement.y + lastCommittedY + dyFromLastCommitted;
+
+    if (isArrowElement(multiElement)) {
+      [x, y] = getOutlineAvoidingPoint(
+        multiElement,
+        pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
+        multiElement.points.length - 1,
+        app.scene,
+        app.state.zoom,
+        pointFrom<GlobalPoint>(x, y),
+      );
+    }
+
+    // update last uncommitted point
+    LinearElementEditor.movePoints(multiElement, [
+      {
+        index: points.length - 1,
+        point: LinearElementEditor.createPointAt(
+          multiElement,
+          app.scene.getNonDeletedElementsMap(),
+          x,
+          y,
+          app.getEffectiveGridSize(),
+        ),
+        isDragging: true,
+      },
+    ]);
+
+    // in this path, we're mutating multiElement to reflect
+    // how it will be after adding pointer position as the next point
+    // trigger update here so that new element canvas renders again to reflect this
+    triggerRender(false);
+  }
+}
+
+export function onPointerUpFromPointerDownOnLinearElementHandler(
+  newElement: ExcalidrawLinearElement,
+  multiElement: NonDeleted<ExcalidrawLinearElement> | null,
+  app: App,
+  store: App["store"],
+  pointerDownState: PointerDownState,
+  childEvent: PointerEvent,
+  activeTool: {
+    lastActiveTool: ActiveTool | null;
+    locked: boolean;
+    fromSelection: boolean;
+  } & ActiveTool,
+) {
+  if (newElement!.points.length > 1) {
+    store.shouldCaptureIncrement();
+  }
+  const pointerCoords = viewportCoordsToSceneCoords(childEvent, app.state);
+
+  if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
+    mutateElement(newElement, {
+      points: [
+        ...newElement.points,
+        pointFrom<LocalPoint>(
+          pointerCoords.x - newElement.x,
+          pointerCoords.y - newElement.y,
+        ),
+      ],
+    });
+    app.setState({
+      multiElement: newElement,
+      newElement,
+    });
+  } else if (pointerDownState.drag.hasOccurred && !multiElement) {
+    if (isBindingEnabled(app.state) && isBindingElement(newElement, false)) {
+      maybeBindLinearElement(
+        newElement,
+        app.state,
+        app.scene.getNonDeletedElementsMap(),
+        app.scene.getNonDeletedElements(),
+      );
+    }
+    app.setState({ suggestedBindings: [], startBoundElement: null });
+    if (!activeTool.locked) {
+      resetCursor(app.interactiveCanvas);
+      app.setState((prevState) => ({
+        newElement: null,
+        activeTool: updateActiveTool(app.state, {
+          type: "selection",
+        }),
+        selectedElementIds: makeNextSelectedElementIds(
+          {
+            ...prevState.selectedElementIds,
+            [newElement.id]: true,
+          },
+          prevState,
+        ),
+        selectedLinearElement: new LinearElementEditor(newElement),
+      }));
+    } else {
+      app.setState((prevState) => ({
+        newElement: null,
+      }));
+    }
+    // so that the scene gets rendered again to display the newly drawn linear as well
+    app.scene.triggerUpdate();
+  }
+}
+
+/**
+ * Handles double click on a linear element to edit it or delete a segment
+ */
+export function handleDoubleClickForLinearElement(
+  app: App,
+  store: App["store"],
+  selectedElement: NonDeleted<ExcalidrawLinearElement>,
+  event: React.MouseEvent<HTMLCanvasElement>,
+  sceneX: number,
+  sceneY: number,
+) {
+  if (
+    event[KEYS.CTRL_OR_CMD] &&
+    (!app.state.editingLinearElement ||
+      app.state.editingLinearElement.elementId !== selectedElement.id) &&
+    !isElbowArrow(selectedElement)
+  ) {
+    store.shouldCaptureIncrement();
+    app.setState({
+      editingLinearElement: new LinearElementEditor(selectedElement),
+    });
+  } else if (app.state.selectedLinearElement && isElbowArrow(selectedElement)) {
+    const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords(
+      app.state.selectedLinearElement,
+      { x: sceneX, y: sceneY },
+      app.state,
+      app.scene.getNonDeletedElementsMap(),
+    );
+    const midPoint = hitCoords
+      ? LinearElementEditor.getSegmentMidPointIndex(
+          app.state.selectedLinearElement,
+          app.state,
+          hitCoords,
+          app.scene.getNonDeletedElementsMap(),
+        )
+      : -1;
+
+    if (midPoint && midPoint > -1) {
+      store.shouldCaptureIncrement();
+      LinearElementEditor.deleteFixedSegment(selectedElement, midPoint);
+
+      const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
+        {
+          ...app.state.selectedLinearElement,
+          segmentMidPointHoveredCoords: null,
+        },
+        { x: sceneX, y: sceneY },
+        app.state,
+        app.scene.getNonDeletedElementsMap(),
+      );
+      const nextIndex = nextCoords
+        ? LinearElementEditor.getSegmentMidPointIndex(
+            app.state.selectedLinearElement,
+            app.state,
+            nextCoords,
+            app.scene.getNonDeletedElementsMap(),
+          )
+        : null;
+
+      app.setState({
+        selectedLinearElement: {
+          ...app.state.selectedLinearElement,
+          pointerDownState: {
+            ...app.state.selectedLinearElement.pointerDownState,
+            segmentMidpoint: {
+              index: nextIndex,
+              value: hitCoords,
+              added: false,
+            },
+          },
+          segmentMidPointHoveredCoords: nextCoords,
+        },
+      });
+
+      return true;
+    }
+  }
+}
+
+export function maybeSuggestBindingsForLinearElementAtCoords(
+  linearElement: NonDeleted<ExcalidrawLinearElement>,
+  /** scene coords */
+  pointerCoords: {
+    x: number;
+    y: number;
+  }[],
+  app: App,
+  // During line creation the start binding hasn't been written yet
+  // into `linearElement`
+  oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
+) {
+  if (!pointerCoords.length) {
+    return;
+  }
+
+  const suggestedBindings = pointerCoords.reduce(
+    (acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
+      const hoveredBindableElement = getHoveredElementForBinding(
+        coords,
+        app.scene.getNonDeletedElements(),
+        app.scene.getNonDeletedElementsMap(),
+        app.state.zoom,
+        isElbowArrow(linearElement),
+        isElbowArrow(linearElement),
+      );
+      if (
+        hoveredBindableElement != null &&
+        !isLinearElementSimpleAndAlreadyBound(
+          linearElement,
+          oppositeBindingBoundElement?.id,
+          hoveredBindableElement,
+        )
+      ) {
+        acc.push(hoveredBindableElement);
+      }
+      return acc;
+    },
+    [],
+  );
+
+  app.setState({ suggestedBindings });
+}