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:
parent
4359e2935d
commit
b818df1098
@ -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 = (
|
||||||
|
Loading…
Reference in New Issue
Block a user