From b818df1098440a1b330b53013717df8384ec4018 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Fri, 5 Aug 2022 06:42:31 +0800 Subject: [PATCH] feat: lock angle when editing linear elements with shift pressed (#5527) Co-authored-by: Ryan --- src/components/App.tsx | 10 +- src/element/linearElementEditor.ts | 197 +++++++++++++++++++++-------- 2 files changed, 151 insertions(+), 56 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 634f671b9..5b16fed3c 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -4130,6 +4130,7 @@ class App extends React.Component { 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 { if (linearElementEditor !== this.state.selectedLinearElement) { this.setState({ - selectedLinearElement: linearElementEditor, + selectedLinearElement: { + ...linearElementEditor, + selectedPointsIndices: null, + }, suggestedBindings: [], }); } @@ -4891,9 +4895,9 @@ class App extends React.Component { 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(), ), diff --git a/src/element/linearElementEditor.ts b/src/element/linearElementEditor.ts index f1f4cecc6..1de1e959f 100644 --- a/src/element/linearElementEditor.ts +++ b/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, 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]; - const deltaX = newDraggingPointPosition[0] - draggingPoint[0]; - const deltaY = newDraggingPointPosition[1] - draggingPoint[1]; + let [width, height] = LinearElementEditor._getShiftLockedDelta( + element, + referencePoint, + [scenePointerX, scenePointerY], + appState.gridSize, + ); - 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, + // rounding to stop the dragged point from jiggling + width = Math.round(width); + height = Math.round(height); + + 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, - "startBindingElement" | "endBindingElement" + const bindings: Mutable< + Partial< + Pick< + InstanceType, + "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, + 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 = (