1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2024-11-10 11:35:52 +01:00

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

Co-authored-by: Ryan <diweihao@bytedance.com>
This commit is contained in:
Ryan Di 2022-08-05 06:42:31 +08:00 committed by GitHub
parent 4359e2935d
commit b818df1098
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 151 additions and 56 deletions

@ -4130,6 +4130,7 @@ class App extends React.Component<AppProps, AppState> {
const linearElementEditor = const linearElementEditor =
this.state.editingLinearElement || this.state.selectedLinearElement; this.state.editingLinearElement || this.state.selectedLinearElement;
const didDrag = LinearElementEditor.handlePointDragging( const didDrag = LinearElementEditor.handlePointDragging(
event,
this.state, this.state,
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
@ -4555,7 +4556,10 @@ class App extends React.Component<AppProps, AppState> {
if (linearElementEditor !== this.state.selectedLinearElement) { if (linearElementEditor !== this.state.selectedLinearElement) {
this.setState({ this.setState({
selectedLinearElement: linearElementEditor, selectedLinearElement: {
...linearElementEditor,
selectedPointsIndices: null,
},
suggestedBindings: [], suggestedBindings: [],
}); });
} }
@ -4891,9 +4895,9 @@ class App extends React.Component<AppProps, AppState> {
isLinearElement(hitElement) && isLinearElement(hitElement) &&
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1. // 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 // 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) ? new LinearElementEditor(hitElement, this.scene)
: this.state.selectedLinearElement, : prevState.selectedLinearElement,
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
), ),

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