mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-22 17:00:40 +02:00
Refactor dragging
This commit is contained in:
@@ -19,7 +19,6 @@ import {
|
||||
shouldRotateWithDiscreteAngle,
|
||||
getGridPoint,
|
||||
invariant,
|
||||
tupleToCoors,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
@@ -290,207 +289,146 @@ export class LinearElementEditor {
|
||||
scenePointerY: number,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
): Pick<AppState, "suggestedBinding" | "selectedLinearElement"> | null {
|
||||
if (!linearElementEditor) {
|
||||
return null;
|
||||
}
|
||||
const { elementId } = linearElementEditor;
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
const elements = app.scene.getNonDeletedElements();
|
||||
const { elbowed, elementId, pointerDownState, selectedPointsIndices } =
|
||||
linearElementEditor;
|
||||
const { lastClickedPoint } = pointerDownState;
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
let customLineAngle = linearElementEditor.customLineAngle;
|
||||
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
invariant(
|
||||
selectedPointsIndices,
|
||||
"There must be selected points in order to drag them",
|
||||
);
|
||||
|
||||
const elbowed = isElbowArrow(element);
|
||||
invariant(
|
||||
lastClickedPoint > -1 && selectedPointsIndices.includes(lastClickedPoint),
|
||||
"There must be a valid lastClickedPoint in order to drag it",
|
||||
);
|
||||
|
||||
if (
|
||||
elbowed &&
|
||||
!linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
|
||||
linearElementEditor.pointerDownState.lastClickedPoint !== 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
invariant(element, "Element being dragged must exist in the scene");
|
||||
|
||||
const selectedPointsIndices = elbowed
|
||||
? [
|
||||
!!linearElementEditor.selectedPointsIndices?.includes(0)
|
||||
? 0
|
||||
: undefined,
|
||||
!!linearElementEditor.selectedPointsIndices?.find((idx) => idx > 0)
|
||||
? element.points.length - 1
|
||||
: undefined,
|
||||
].filter((idx): idx is number => idx !== undefined)
|
||||
: linearElementEditor.selectedPointsIndices;
|
||||
const lastClickedPoint = elbowed
|
||||
? linearElementEditor.pointerDownState.lastClickedPoint > 0
|
||||
? element.points.length - 1
|
||||
: 0
|
||||
: linearElementEditor.pointerDownState.lastClickedPoint;
|
||||
invariant(element.points.length > 1, "Element must have at least 2 points");
|
||||
|
||||
invariant(
|
||||
!elbowed ||
|
||||
selectedPointsIndices?.filter(
|
||||
(idx) => idx !== 0 && idx !== element.points.length - 1,
|
||||
).length === 0,
|
||||
"Only start and end points can be selected for elbow arrows",
|
||||
);
|
||||
|
||||
// point that's being dragged (out of all selected points)
|
||||
const draggingPoint = element.points[lastClickedPoint];
|
||||
// The adjacent point to the one dragged point
|
||||
const pivotPoint =
|
||||
element.points[lastClickedPoint === 0 ? 1 : lastClickedPoint - 1];
|
||||
const singlePointDragged = selectedPointsIndices.length === 1;
|
||||
const customLineAngle =
|
||||
linearElementEditor.customLineAngle ??
|
||||
determineCustomLinearAngle(pivotPoint, element.points[lastClickedPoint]);
|
||||
const startIsSelected = selectedPointsIndices.includes(0);
|
||||
const endIsSelected = selectedPointsIndices.includes(
|
||||
element.points.length - 1,
|
||||
);
|
||||
|
||||
if (selectedPointsIndices && draggingPoint) {
|
||||
const elements = app.scene.getNonDeletedElements();
|
||||
|
||||
if (
|
||||
shouldRotateWithDiscreteAngle(event) &&
|
||||
selectedPointsIndices.length === 1 &&
|
||||
element.points.length > 1
|
||||
) {
|
||||
const selectedIndex = selectedPointsIndices[0];
|
||||
const referencePoint =
|
||||
element.points[selectedIndex === 0 ? 1 : selectedIndex - 1];
|
||||
customLineAngle =
|
||||
linearElementEditor.customLineAngle ??
|
||||
Math.atan2(
|
||||
element.points[selectedIndex][1] - referencePoint[1],
|
||||
element.points[selectedIndex][0] - referencePoint[0],
|
||||
);
|
||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||
element,
|
||||
elementsMap,
|
||||
referencePoint,
|
||||
pointFrom(scenePointerX, scenePointerY),
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
customLineAngle,
|
||||
);
|
||||
const reference = pointFrom<LocalPoint>(
|
||||
width + referencePoint[0],
|
||||
height + referencePoint[1],
|
||||
);
|
||||
const deltaX = reference[0] - draggingPoint[0];
|
||||
const deltaY = reference[1] - draggingPoint[1];
|
||||
|
||||
const { positions, updates } = pointDraggingUpdates(
|
||||
selectedPointsIndices,
|
||||
deltaX,
|
||||
deltaY,
|
||||
elementsMap,
|
||||
element,
|
||||
elements,
|
||||
app,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(element, app.scene, positions, updates);
|
||||
} else if (
|
||||
shouldAllowDraggingPoint(
|
||||
element,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
selectedPointsIndices,
|
||||
elementsMap,
|
||||
app,
|
||||
)
|
||||
) {
|
||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
||||
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
||||
|
||||
const { positions, updates } = pointDraggingUpdates(
|
||||
selectedPointsIndices,
|
||||
deltaX,
|
||||
deltaY,
|
||||
elementsMap,
|
||||
element,
|
||||
elements,
|
||||
app,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(element, app.scene, positions, updates);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
handleBindTextResize(element, app.scene, false);
|
||||
}
|
||||
|
||||
// suggest bindings for first and last point if selected
|
||||
let suggestedBinding: AppState["suggestedBinding"] = null;
|
||||
if (isBindingElement(element, false)) {
|
||||
const firstIndexIsSelected = selectedPointsIndices[0] === 0;
|
||||
const lastIndexIsSelected =
|
||||
selectedPointsIndices[selectedPointsIndices.length - 1] ===
|
||||
element.points.length - 1;
|
||||
const coords: { x: number; y: number }[] = [];
|
||||
|
||||
if (firstIndexIsSelected !== lastIndexIsSelected) {
|
||||
if (firstIndexIsSelected) {
|
||||
coords.push(
|
||||
tupleToCoors(
|
||||
LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
element.points[0],
|
||||
elementsMap,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (lastIndexIsSelected) {
|
||||
coords.push(
|
||||
tupleToCoors(
|
||||
LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
element.points[
|
||||
selectedPointsIndices[selectedPointsIndices.length - 1]
|
||||
],
|
||||
elementsMap,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (coords.length) {
|
||||
suggestedBinding = maybeSuggestBindingsForBindingElementAtCoords(
|
||||
element,
|
||||
firstIndexIsSelected && lastIndexIsSelected
|
||||
? "both"
|
||||
: firstIndexIsSelected
|
||||
? "start"
|
||||
: "end",
|
||||
app.scene,
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newLinearElementEditor = {
|
||||
...linearElementEditor,
|
||||
selectedPointsIndices,
|
||||
segmentMidPointHoveredCoords:
|
||||
lastClickedPoint !== 0 &&
|
||||
lastClickedPoint !== element.points.length - 1
|
||||
? this.getPointGlobalCoordinates(
|
||||
element,
|
||||
draggingPoint,
|
||||
elementsMap,
|
||||
)
|
||||
: null,
|
||||
hoverPointIndex:
|
||||
lastClickedPoint === 0 ||
|
||||
lastClickedPoint === element.points.length - 1
|
||||
? lastClickedPoint
|
||||
: -1,
|
||||
isDragging: true,
|
||||
// Determine if point movement should happen and how much
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
if (shouldRotateWithDiscreteAngle(event) && singlePointDragged) {
|
||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||
element,
|
||||
elementsMap,
|
||||
pivotPoint,
|
||||
pointFrom(scenePointerX, scenePointerY),
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
customLineAngle,
|
||||
};
|
||||
);
|
||||
const target = pointFrom<LocalPoint>(
|
||||
width + pivotPoint[0],
|
||||
height + pivotPoint[1],
|
||||
);
|
||||
|
||||
return {
|
||||
selectedLinearElement: newLinearElementEditor,
|
||||
suggestedBinding,
|
||||
} as Pick<AppState, keyof AppState>;
|
||||
deltaX = target[0] - draggingPoint[0];
|
||||
deltaY = target[1] - draggingPoint[1];
|
||||
} else if (
|
||||
shouldAllowDraggingPoint(
|
||||
element,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
selectedPointsIndices,
|
||||
elementsMap,
|
||||
app,
|
||||
)
|
||||
) {
|
||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
||||
deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
// Apply the point movement if needed
|
||||
if (deltaX || deltaY) {
|
||||
const { positions, updates } = pointDraggingUpdates(
|
||||
selectedPointsIndices,
|
||||
deltaX,
|
||||
deltaY,
|
||||
elementsMap,
|
||||
element,
|
||||
elements,
|
||||
app,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(element, app.scene, positions, updates);
|
||||
}
|
||||
|
||||
// Attached text might need to update if arrow dimensions change
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
handleBindTextResize(element, app.scene, false);
|
||||
}
|
||||
|
||||
// Suggest bindings for first and last point if selected
|
||||
let suggestedBinding: AppState["suggestedBinding"] = null;
|
||||
if (isBindingElement(element, false)) {
|
||||
if (startIsSelected || endIsSelected) {
|
||||
suggestedBinding = maybeSuggestBindingsForBindingElementAtCoords(
|
||||
element,
|
||||
startIsSelected && endIsSelected
|
||||
? "both"
|
||||
: startIsSelected
|
||||
? "start"
|
||||
: "end",
|
||||
app.scene,
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newLinearElementEditor = {
|
||||
...linearElementEditor,
|
||||
selectedPointsIndices,
|
||||
segmentMidPointHoveredCoords:
|
||||
lastClickedPoint !== 0 && lastClickedPoint !== element.points.length - 1
|
||||
? this.getPointGlobalCoordinates(element, draggingPoint, elementsMap)
|
||||
: null,
|
||||
hoverPointIndex:
|
||||
lastClickedPoint === 0 || lastClickedPoint === element.points.length - 1
|
||||
? lastClickedPoint
|
||||
: -1,
|
||||
isDragging: true,
|
||||
customLineAngle,
|
||||
};
|
||||
|
||||
return {
|
||||
selectedLinearElement: newLinearElementEditor,
|
||||
suggestedBinding,
|
||||
};
|
||||
}
|
||||
|
||||
static handlePointerUp(
|
||||
@@ -1841,7 +1779,10 @@ export class LinearElementEditor {
|
||||
x: number,
|
||||
y: number,
|
||||
scene: Scene,
|
||||
): LinearElementEditor {
|
||||
): Pick<
|
||||
LinearElementEditor,
|
||||
"segmentMidPointHoveredCoords" | "pointerDownState"
|
||||
> {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElement.elementId,
|
||||
@@ -2133,3 +2074,9 @@ const shouldAllowDraggingPoint = (
|
||||
|
||||
return allowDrag;
|
||||
};
|
||||
|
||||
const determineCustomLinearAngle = (
|
||||
pivotPoint: LocalPoint,
|
||||
draggedPoint: LocalPoint,
|
||||
) =>
|
||||
Math.atan2(draggedPoint[1] - pivotPoint[1], draggedPoint[0] - pivotPoint[0]);
|
||||
|
Reference in New Issue
Block a user