|
@@ -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 = (
|