Browse Source

Start grid point arrow align

Mark Tolmacs 6 months ago
parent
commit
1a87aa8e55
2 changed files with 148 additions and 117 deletions
  1. 12 32
      packages/element/src/linearElementEditor.ts
  2. 136 85
      packages/excalidraw/components/App.tsx

+ 12 - 32
packages/element/src/linearElementEditor.ts

@@ -239,14 +239,15 @@ export class LinearElementEditor {
     });
   }
 
-  static getOutlineAvoidingPointOrNull(
+  static getOutlineAvoidingPoint(
     element: NonDeleted<ExcalidrawLinearElement>,
-    coords: { x: number; y: number },
+    coords: GlobalPoint,
     pointIndex: number,
     app: AppClassProperties,
-  ) {
+    fallback?: GlobalPoint,
+  ): GlobalPoint {
     const hoveredElement = getHoveredElementForBinding(
-      coords,
+      { x: coords[0], y: coords[1] },
       app.scene.getNonDeletedElements(),
       app.scene.getNonDeletedElementsMap(),
       app.state.zoom,
@@ -255,11 +256,10 @@ export class LinearElementEditor {
     );
 
     if (hoveredElement) {
-      const p = pointFrom<GlobalPoint>(coords.x, coords.y);
       const newPoints = Array.from(element.points);
       newPoints[pointIndex] = pointFrom<LocalPoint>(
-        p[0] - element.x,
-        p[1] - element.y,
+        coords[0] - element.x,
+        coords[1] - element.y,
       );
 
       return bindPointToSnapToElementOutline(
@@ -273,27 +273,7 @@ export class LinearElementEditor {
       );
     }
 
-    return null;
-  }
-
-  static getOutlineAvoidingPoint(
-    element: NonDeleted<ExcalidrawLinearElement>,
-    coords: { x: number; y: number },
-    pointIndex: number,
-    app: AppClassProperties,
-  ): GlobalPoint {
-    const p = LinearElementEditor.getOutlineAvoidingPointOrNull(
-      element,
-      coords,
-      pointIndex,
-      app,
-    );
-
-    if (p) {
-      return p;
-    }
-
-    return pointFrom<GlobalPoint>(coords.x, coords.y);
+    return fallback ?? coords;
   }
 
   /**
@@ -412,10 +392,10 @@ export class LinearElementEditor {
                 globalNewPointPosition =
                   LinearElementEditor.getOutlineAvoidingPoint(
                     element,
-                    {
-                      x: element.x + element.points[pointIndex][0] + deltaX,
-                      y: element.y + element.points[pointIndex][1] + deltaY,
-                    },
+                    pointFrom<GlobalPoint>(
+                      element.x + element.points[pointIndex][0] + deltaX,
+                      element.y + element.points[pointIndex][1] + deltaY,
+                    ),
                     pointIndex,
                     app,
                   );

+ 136 - 85
packages/excalidraw/components/App.tsx

@@ -5992,32 +5992,26 @@ class App extends React.Component<AppProps, AppState> {
           setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
         }
 
-        const outlineGlobalPoint =
-          LinearElementEditor.getOutlineAvoidingPointOrNull(
-            multiElement,
-            {
-              x: scenePointerX,
-              y: scenePointerY,
-            },
-            multiElement.points.length - 1,
-            this,
-          );
-
-        const nextPoint = outlineGlobalPoint
-          ? pointFrom<LocalPoint>(
-              outlineGlobalPoint[0] - rx,
-              outlineGlobalPoint[1] - ry,
-            )
-          : pointFrom<LocalPoint>(
-              lastCommittedX + dxFromLastCommitted,
-              lastCommittedY + dyFromLastCommitted,
-            );
-
         // update last uncommitted point
         mutateElement(
           multiElement,
           {
-            points: [...points.slice(0, -1), nextPoint],
+            points: [
+              ...points.slice(0, -1),
+              pointTranslate<GlobalPoint, LocalPoint>(
+                LinearElementEditor.getOutlineAvoidingPoint(
+                  multiElement,
+                  pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
+                  multiElement.points.length - 1,
+                  this,
+                  pointFrom<GlobalPoint>(
+                    multiElement.x + lastCommittedX + dxFromLastCommitted,
+                    multiElement.y + lastCommittedY + dyFromLastCommitted,
+                  ),
+                ),
+                vector(-multiElement.x, -multiElement.y),
+              ),
+            ],
           },
           false,
           {
@@ -7838,53 +7832,93 @@ class App extends React.Component<AppProps, AppState> {
           ? [currentItemStartArrowhead, currentItemEndArrowhead]
           : [null, null];
 
-      const element =
-        elementType === "arrow"
-          ? newArrowElement({
-              type: elementType,
-              x: gridX,
-              y: gridY,
-              strokeColor: this.state.currentItemStrokeColor,
-              backgroundColor: this.state.currentItemBackgroundColor,
-              fillStyle: this.state.currentItemFillStyle,
-              strokeWidth: this.state.currentItemStrokeWidth,
-              strokeStyle: this.state.currentItemStrokeStyle,
-              roughness: this.state.currentItemRoughness,
-              opacity: this.state.currentItemOpacity,
-              roundness:
-                this.state.currentItemArrowType === ARROW_TYPE.round
-                  ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
-                  : // note, roundness doesn't have any effect for elbow arrows,
-                    // but it's best to set it to null as well
-                    null,
-              startArrowhead,
-              endArrowhead,
-              locked: false,
-              frameId: topLayerFrame ? topLayerFrame.id : null,
-              elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
-              fixedSegments:
-                this.state.currentItemArrowType === ARROW_TYPE.elbow
-                  ? []
-                  : null,
-            })
-          : newLinearElement({
-              type: elementType,
-              x: gridX,
-              y: gridY,
-              strokeColor: this.state.currentItemStrokeColor,
-              backgroundColor: this.state.currentItemBackgroundColor,
-              fillStyle: this.state.currentItemFillStyle,
-              strokeWidth: this.state.currentItemStrokeWidth,
-              strokeStyle: this.state.currentItemStrokeStyle,
-              roughness: this.state.currentItemRoughness,
-              opacity: this.state.currentItemOpacity,
-              roundness:
-                this.state.currentItemRoundness === "round"
-                  ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
-                  : null,
-              locked: false,
-              frameId: topLayerFrame ? topLayerFrame.id : null,
-            });
+      let element: NonDeleted<ExcalidrawLinearElement>;
+      if (elementType === "arrow") {
+        const arrow: Mutable<NonDeleted<ExcalidrawArrowElement>> =
+          newArrowElement({
+            type: "arrow",
+            x: gridX,
+            y: gridY,
+            strokeColor: this.state.currentItemStrokeColor,
+            backgroundColor: this.state.currentItemBackgroundColor,
+            fillStyle: this.state.currentItemFillStyle,
+            strokeWidth: this.state.currentItemStrokeWidth,
+            strokeStyle: this.state.currentItemStrokeStyle,
+            roughness: this.state.currentItemRoughness,
+            opacity: this.state.currentItemOpacity,
+            roundness:
+              this.state.currentItemArrowType === ARROW_TYPE.round
+                ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
+                : // note, roundness doesn't have any effect for elbow arrows,
+                  // but it's best to set it to null as well
+                  null,
+            startArrowhead,
+            endArrowhead,
+            locked: false,
+            frameId: topLayerFrame ? topLayerFrame.id : null,
+            elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
+            fixedSegments:
+              this.state.currentItemArrowType === ARROW_TYPE.elbow ? [] : null,
+          });
+
+        const hoveredElement = getHoveredElementForBinding(
+          { x: gridX, y: gridY },
+          this.scene.getNonDeletedElements(),
+          this.scene.getNonDeletedElementsMap(),
+          this.state.zoom,
+          true,
+          this.state.currentItemArrowType === ARROW_TYPE.elbow,
+        );
+
+        if (hoveredElement) {
+          [arrow.x, arrow.y] =
+            intersectElementWithLineSegment(
+              hoveredElement,
+              lineSegment(
+                pointFrom<GlobalPoint>(gridX, gridY),
+                pointFrom<GlobalPoint>(
+                  gridX,
+                  hoveredElement.y + hoveredElement.height / 2,
+                ),
+              ),
+              2 * FIXED_BINDING_DISTANCE,
+            )[0] ??
+            intersectElementWithLineSegment(
+              hoveredElement,
+              lineSegment(
+                pointFrom<GlobalPoint>(gridX, gridY),
+                pointFrom<GlobalPoint>(
+                  hoveredElement.x + hoveredElement.width / 2,
+                  gridY,
+                ),
+              ),
+              2 * FIXED_BINDING_DISTANCE,
+            )[0] ??
+            pointFrom<GlobalPoint>(gridX, gridY);
+        }
+
+        element = arrow;
+      } else {
+        element = newLinearElement({
+          type: elementType,
+          x: gridX,
+          y: gridY,
+          strokeColor: this.state.currentItemStrokeColor,
+          backgroundColor: this.state.currentItemBackgroundColor,
+          fillStyle: this.state.currentItemFillStyle,
+          strokeWidth: this.state.currentItemStrokeWidth,
+          strokeStyle: this.state.currentItemStrokeStyle,
+          roughness: this.state.currentItemRoughness,
+          opacity: this.state.currentItemOpacity,
+          roundness:
+            this.state.currentItemRoundness === "round"
+              ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
+              : null,
+          locked: false,
+          frameId: topLayerFrame ? topLayerFrame.id : null,
+        });
+      }
+
       this.setState((prevState) => {
         const nextSelectedElementIds = {
           ...prevState.selectedElementIds,
@@ -8199,12 +8233,6 @@ class App extends React.Component<AppProps, AppState> {
         this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y);
       }
 
-      const [gridX, gridY] = getGridPoint(
-        pointerCoords.x,
-        pointerCoords.y,
-        event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
-      );
-
       // for arrows/lines, don't start dragging until a given threshold
       // to ensure we don't create a 2-point arrow by mistake when
       // user clicks mouse in a way that it moves a tiny bit (thus
@@ -8691,6 +8719,11 @@ 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;
 
@@ -8707,7 +8740,22 @@ class App extends React.Component<AppProps, AppState> {
             mutateElement(
               newElement,
               {
-                points: [...points, pointFrom<LocalPoint>(dx, dy)],
+                points: [
+                  ...points,
+                  pointTranslate<GlobalPoint, LocalPoint>(
+                    LinearElementEditor.getOutlineAvoidingPoint(
+                      newElement,
+                      pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
+                      newElement.points.length - 1,
+                      this,
+                      pointFrom<GlobalPoint>(
+                        newElement.x + dx,
+                        newElement.y + dy,
+                      ),
+                    ),
+                    vector(-newElement.x, -newElement.y),
+                  ),
+                ],
               },
               false,
             );
@@ -8715,20 +8763,23 @@ class App extends React.Component<AppProps, AppState> {
             points.length === 2 ||
             (points.length > 1 && isElbowArrow(newElement))
           ) {
-            const globalPoint = LinearElementEditor.getOutlineAvoidingPoint(
-              newElement,
-              { x: newElement.x + dx, y: newElement.y + dy },
-              1,
-              this,
-            );
             mutateElement(
               newElement,
               {
                 points: [
                   ...points.slice(0, -1),
-                  pointFrom<LocalPoint>(
-                    globalPoint[0] - newElement.x,
-                    globalPoint[1] - newElement.y,
+                  pointTranslate<GlobalPoint, LocalPoint>(
+                    LinearElementEditor.getOutlineAvoidingPoint(
+                      newElement,
+                      pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
+                      newElement.points.length - 1,
+                      this,
+                      pointFrom<GlobalPoint>(
+                        newElement.x + dx,
+                        newElement.y + dy,
+                      ),
+                    ),
+                    vector(-newElement.x, -newElement.y),
                   ),
                 ],
               },