فهرست منبع

feat: lock angle when editing linear elements with shift pressed (#5527)

Co-authored-by: Ryan <diweihao@bytedance.com>
Ryan Di 2 سال پیش
والد
کامیت
b818df1098
2فایلهای تغییر یافته به همراه151 افزوده شده و 56 حذف شده
  1. 7 3
      src/components/App.tsx
  2. 144 53
      src/element/linearElementEditor.ts

+ 7 - 3
src/components/App.tsx

@@ -4130,6 +4130,7 @@ class App extends React.Component<AppProps, AppState> {
         const linearElementEditor =
           this.state.editingLinearElement || this.state.selectedLinearElement;
         const didDrag = LinearElementEditor.handlePointDragging(
+          event,
           this.state,
           pointerCoords.x,
           pointerCoords.y,
@@ -4555,7 +4556,10 @@ class App extends React.Component<AppProps, AppState> {
 
           if (linearElementEditor !== this.state.selectedLinearElement) {
             this.setState({
-              selectedLinearElement: linearElementEditor,
+              selectedLinearElement: {
+                ...linearElementEditor,
+                selectedPointsIndices: null,
+              },
               suggestedBindings: [],
             });
           }
@@ -4891,9 +4895,9 @@ class App extends React.Component<AppProps, AppState> {
                   isLinearElement(hitElement) &&
                   // 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
-                  this.state.selectedLinearElement?.elementId !== hitElement.id
+                  prevState.selectedLinearElement?.elementId !== hitElement.id
                     ? new LinearElementEditor(hitElement, this.scene)
-                    : this.state.selectedLinearElement,
+                    : prevState.selectedLinearElement,
               },
               this.scene.getNonDeletedElements(),
             ),

+ 144 - 53
src/element/linearElementEditor.ts

@@ -5,8 +5,14 @@ import {
   PointBinding,
   ExcalidrawBindableElement,
 } from "./types";
-import { distance2d, rotate, isPathALoop, getGridPoint } from "../math";
-import { getElementAbsoluteCoords } from ".";
+import {
+  distance2d,
+  rotate,
+  isPathALoop,
+  getGridPoint,
+  rotatePoint,
+} from "../math";
+import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
 import { getElementPointsCoords } from "./bounds";
 import { Point, AppState } from "../types";
 import { mutateElement } from "./mutateElement";
@@ -20,27 +26,32 @@ import {
 } from "./binding";
 import { tupleToCoors } from "../utils";
 import { isBindingElement } from "./typeChecks";
+import { shouldRotateWithDiscreteAngle } from "../keys";
 
 export class LinearElementEditor {
-  public elementId: ExcalidrawElement["id"] & {
+  public readonly elementId: ExcalidrawElement["id"] & {
     _brand: "excalidrawLinearElementId";
   };
   /** indices */
-  public selectedPointsIndices: readonly number[] | null;
+  public readonly selectedPointsIndices: readonly number[] | null;
 
-  public pointerDownState: Readonly<{
+  public readonly pointerDownState: Readonly<{
     prevSelectedPointsIndices: readonly number[] | null;
     /** index */
     lastClickedPoint: number;
   }>;
 
   /** whether you're dragging a point */
-  public isDragging: boolean;
-  public lastUncommittedPoint: Point | null;
-  public pointerOffset: Readonly<{ x: number; y: number }>;
-  public startBindingElement: ExcalidrawBindableElement | null | "keep";
-  public endBindingElement: ExcalidrawBindableElement | null | "keep";
-  public hoverPointIndex: number;
+  public readonly isDragging: boolean;
+  public readonly lastUncommittedPoint: Point | null;
+  public readonly pointerOffset: Readonly<{ x: number; y: number }>;
+  public readonly startBindingElement:
+    | ExcalidrawBindableElement
+    | null
+    | "keep";
+  public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
+  public readonly hoverPointIndex: number;
+
   constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
     this.elementId = element.id as string & {
       _brand: "excalidrawLinearElementId";
@@ -133,6 +144,7 @@ export class LinearElementEditor {
 
   /** @returns whether point was dragged */
   static handlePointDragging(
+    event: PointerEvent,
     appState: AppState,
     scenePointerX: number,
     scenePointerY: number,
@@ -157,40 +169,72 @@ export class LinearElementEditor {
       linearElementEditor.pointerDownState.lastClickedPoint
     ] as [number, number] | undefined;
     if (selectedPointsIndices && draggingPoint) {
-      const newDraggingPointPosition = LinearElementEditor.createPointAt(
-        element,
-        scenePointerX - linearElementEditor.pointerOffset.x,
-        scenePointerY - linearElementEditor.pointerOffset.y,
-        appState.gridSize,
-      );
+      if (
+        shouldRotateWithDiscreteAngle(event) &&
+        selectedPointsIndices.length === 1 &&
+        element.points.length > 1
+      ) {
+        const selectedIndex = selectedPointsIndices[0];
+        const referencePoint =
+          element.points[selectedIndex === 0 ? 1 : selectedIndex - 1];
+
+        let [width, height] = LinearElementEditor._getShiftLockedDelta(
+          element,
+          referencePoint,
+          [scenePointerX, scenePointerY],
+          appState.gridSize,
+        );
 
-      const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
-      const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
+        // rounding to stop the dragged point from jiggling
+        width = Math.round(width);
+        height = Math.round(height);
 
-      LinearElementEditor.movePoints(
-        element,
-        selectedPointsIndices.map((pointIndex) => {
-          const newPointPosition =
-            pointIndex === linearElementEditor.pointerDownState.lastClickedPoint
-              ? LinearElementEditor.createPointAt(
-                  element,
-                  scenePointerX - linearElementEditor.pointerOffset.x,
-                  scenePointerY - linearElementEditor.pointerOffset.y,
-                  appState.gridSize,
-                )
-              : ([
-                  element.points[pointIndex][0] + deltaX,
-                  element.points[pointIndex][1] + deltaY,
-                ] as const);
-          return {
-            index: pointIndex,
-            point: newPointPosition,
+        LinearElementEditor.movePoints(element, [
+          {
+            index: selectedIndex,
+            point: [width + referencePoint[0], height + referencePoint[1]],
             isDragging:
-              pointIndex ===
+              selectedIndex ===
               linearElementEditor.pointerDownState.lastClickedPoint,
-          };
-        }),
-      );
+          },
+        ]);
+      } else {
+        const newDraggingPointPosition = LinearElementEditor.createPointAt(
+          element,
+          scenePointerX - linearElementEditor.pointerOffset.x,
+          scenePointerY - linearElementEditor.pointerOffset.y,
+          appState.gridSize,
+        );
+
+        const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
+        const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
+
+        LinearElementEditor.movePoints(
+          element,
+          selectedPointsIndices.map((pointIndex) => {
+            const newPointPosition =
+              pointIndex ===
+              linearElementEditor.pointerDownState.lastClickedPoint
+                ? LinearElementEditor.createPointAt(
+                    element,
+                    scenePointerX - linearElementEditor.pointerOffset.x,
+                    scenePointerY - linearElementEditor.pointerOffset.y,
+                    appState.gridSize,
+                  )
+                : ([
+                    element.points[pointIndex][0] + deltaX,
+                    element.points[pointIndex][1] + deltaY,
+                  ] as const);
+            return {
+              index: pointIndex,
+              point: newPointPosition,
+              isDragging:
+                pointIndex ===
+                linearElementEditor.pointerDownState.lastClickedPoint,
+            };
+          }),
+        );
+      }
 
       // suggest bindings for first and last point if selected
       if (isBindingElement(element, false)) {
@@ -244,10 +288,12 @@ export class LinearElementEditor {
       return editingLinearElement;
     }
 
-    const bindings: Partial<
-      Pick<
-        InstanceType<typeof LinearElementEditor>,
-        "startBindingElement" | "endBindingElement"
+    const bindings: Mutable<
+      Partial<
+        Pick<
+          InstanceType<typeof LinearElementEditor>,
+          "startBindingElement" | "endBindingElement"
+        >
       >
     > = {};
 
@@ -466,12 +512,30 @@ export class LinearElementEditor {
       return { ...linearElementEditor, lastUncommittedPoint: null };
     }
 
-    const newPoint = LinearElementEditor.createPointAt(
-      element,
-      scenePointerX - linearElementEditor.pointerOffset.x,
-      scenePointerY - linearElementEditor.pointerOffset.y,
-      gridSize,
-    );
+    let newPoint: Point;
+
+    if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
+      const lastCommittedPoint = points[points.length - 2];
+
+      const [width, height] = LinearElementEditor._getShiftLockedDelta(
+        element,
+        lastCommittedPoint,
+        [scenePointerX, scenePointerY],
+        gridSize,
+      );
+
+      newPoint = [
+        width + lastCommittedPoint[0],
+        height + lastCommittedPoint[1],
+      ];
+    } else {
+      newPoint = LinearElementEditor.createPointAt(
+        element,
+        scenePointerX - linearElementEditor.pointerOffset.x,
+        scenePointerY - linearElementEditor.pointerOffset.y,
+        gridSize,
+      );
+    }
 
     if (lastPoint === lastUncommittedPoint) {
       LinearElementEditor.movePoints(element, [
@@ -756,9 +820,9 @@ export class LinearElementEditor {
 
     if (selectedOriginPoint) {
       offsetX =
-        selectedOriginPoint.point[0] - points[selectedOriginPoint.index][0];
+        selectedOriginPoint.point[0] + points[selectedOriginPoint.index][0];
       offsetY =
-        selectedOriginPoint.point[1] - points[selectedOriginPoint.index][1];
+        selectedOriginPoint.point[1] + points[selectedOriginPoint.index][1];
     }
 
     const nextPoints = points.map((point, idx) => {
@@ -821,6 +885,33 @@ export class LinearElementEditor {
       y: element.y + rotated[1],
     });
   }
+
+  private static _getShiftLockedDelta(
+    element: NonDeleted<ExcalidrawLinearElement>,
+    referencePoint: Point,
+    scenePointer: Point,
+    gridSize: number | null,
+  ) {
+    const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
+      element,
+      referencePoint,
+    );
+
+    const [gridX, gridY] = getGridPoint(
+      scenePointer[0],
+      scenePointer[1],
+      gridSize,
+    );
+
+    const { width, height } = getLockedLinearCursorAlignSize(
+      referencePointCoords[0],
+      referencePointCoords[1],
+      gridX,
+      gridY,
+    );
+
+    return rotatePoint([width, height], [0, 0], -element.angle);
+  }
 }
 
 const normalizeSelectedPoints = (