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 =
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(),
),

@ -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,6 +169,36 @@ export class LinearElementEditor {
linearElementEditor.pointerDownState.lastClickedPoint
] as [number, number] | undefined;
if (selectedPointsIndices && draggingPoint) {
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,
);
// 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:
selectedIndex ===
linearElementEditor.pointerDownState.lastClickedPoint,
},
]);
} else {
const newDraggingPointPosition = LinearElementEditor.createPointAt(
element,
scenePointerX - linearElementEditor.pointerOffset.x,
@ -171,7 +213,8 @@ export class LinearElementEditor {
element,
selectedPointsIndices.map((pointIndex) => {
const newPointPosition =
pointIndex === linearElementEditor.pointerDownState.lastClickedPoint
pointIndex ===
linearElementEditor.pointerDownState.lastClickedPoint
? LinearElementEditor.createPointAt(
element,
scenePointerX - linearElementEditor.pointerOffset.x,
@ -191,6 +234,7 @@ export class LinearElementEditor {
};
}),
);
}
// suggest bindings for first and last point if selected
if (isBindingElement(element, false)) {
@ -244,11 +288,13 @@ export class LinearElementEditor {
return editingLinearElement;
}
const bindings: Partial<
const bindings: Mutable<
Partial<
Pick<
InstanceType<typeof LinearElementEditor>,
"startBindingElement" | "endBindingElement"
>
>
> = {};
if (isDragging && selectedPointsIndices) {
@ -466,12 +512,30 @@ export class LinearElementEditor {
return { ...linearElementEditor, lastUncommittedPoint: null };
}
const newPoint = LinearElementEditor.createPointAt(
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 = (