Refactor dragging

This commit is contained in:
Mark Tolmacs
2025-08-29 17:40:10 +02:00
parent df8c98d946
commit 1668a9736b
2 changed files with 180 additions and 245 deletions

View File

@@ -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]);