chore: More point dragging centralization

This commit is contained in:
Mark Tolmacs
2025-08-28 11:49:14 +02:00
parent cfa8631b0f
commit ab6353b2fa
2 changed files with 174 additions and 206 deletions

View File

@@ -43,6 +43,7 @@ import type {
} from "@excalidraw/excalidraw/types";
import {
calculateFixedPointForNonElbowArrowBinding,
getBindingStrategyForDraggingBindingElementEndpoints,
getStartGlobalEndLocalPointsForSimpleArrowBinding,
maybeSuggestBindingsForBindingElementAtCoords,
@@ -117,6 +118,12 @@ const getNormalizedPoints = ({
};
};
type PointMoveOtherUpdates = {
startBinding?: FixedPointBinding | null;
endBinding?: FixedPointBinding | null;
moveMidPointsWithElement?: boolean | null;
};
export class LinearElementEditor {
public readonly elementId: ExcalidrawElement["id"] & {
_brand: "excalidrawLinearElementId";
@@ -282,7 +289,7 @@ export class LinearElementEditor {
scenePointerX: number,
scenePointerY: number,
linearElementEditor: LinearElementEditor,
): Pick<AppState, keyof AppState> | null {
): Pick<AppState, "suggestedBinding" | "selectedLinearElement"> | null {
if (!linearElementEditor) {
return null;
}
@@ -356,94 +363,48 @@ export class LinearElementEditor {
const deltaX = reference[0] - draggingPoint[0];
const deltaY = reference[1] - draggingPoint[1];
LinearElementEditor.movePoints(
const { positions, updates } = pointDraggingUpdates(
selectedPointsIndices,
deltaX,
deltaY,
elementsMap,
element,
app.scene,
pointDraggingUpdates(
selectedPointsIndices,
deltaX,
deltaY,
elementsMap,
element,
elements,
app,
),
elements,
app,
);
} else {
const scenePointer = pointFrom<GlobalPoint>(
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(),
);
// Do not allow dragging the bound arrow closer to the shape than
// the dragging threshold
let allowDrag = true;
if (isSimpleArrow(element)) {
if (selectedPointsIndices.includes(0) && element.startBinding) {
const boundElement = elementsMap.get(
element.startBinding.elementId,
)!;
const dist = distanceToElement(
boundElement,
elementsMap,
scenePointer,
);
const inside = isPointInElement(
scenePointer,
boundElement,
elementsMap,
);
allowDrag = allowDrag && (dist > DRAGGING_THRESHOLD || inside);
if (allowDrag) {
unbindBindingElement(element, "start", app.scene);
}
}
if (
selectedPointsIndices.includes(element.points.length - 1) &&
element.endBinding
) {
const boundElement = elementsMap.get(element.endBinding.elementId)!;
const dist = distanceToElement(
boundElement,
elementsMap,
scenePointer,
);
const inside = isPointInElement(
scenePointer,
boundElement,
elementsMap,
);
allowDrag = allowDrag && (dist > DRAGGING_THRESHOLD || inside);
if (allowDrag) {
unbindBindingElement(element, "end", app.scene);
}
}
}
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
if (allowDrag) {
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,
pointDraggingUpdates(
selectedPointsIndices,
deltaX,
deltaY,
elementsMap,
element,
elements,
app,
),
);
}
LinearElementEditor.movePoints(element, app.scene, positions, updates);
}
const boundTextElement = getBoundTextElement(element, elementsMap);
@@ -1987,7 +1948,10 @@ export const pointDraggingUpdates = (
element: NonDeleted<ExcalidrawLinearElement>,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
app: AppClassProperties,
): PointsPositionUpdates => {
): {
positions: PointsPositionUpdates;
updates?: PointMoveOtherUpdates;
} => {
const naiveDraggingPoints = new Map(
selectedPointsIndices.map((pointIndex) => {
return [
@@ -2005,7 +1969,9 @@ export const pointDraggingUpdates = (
// Linear elements have no special logic
if (!isArrowElement(element) || isElbowArrow(element)) {
return naiveDraggingPoints;
return {
positions: naiveDraggingPoints,
};
}
const startIsDragged = selectedPointsIndices.includes(0);
@@ -2014,7 +1980,9 @@ export const pointDraggingUpdates = (
);
if (startIsDragged === endIsDragged) {
return naiveDraggingPoints;
return {
positions: naiveDraggingPoints,
};
}
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
@@ -2090,16 +2058,75 @@ export const pointDraggingUpdates = (
new Set([0, element.points.length - 1, ...selectedPointsIndices]),
);
return new Map(
indices.map((idx) => {
return [
idx,
idx === 0
? { point: startLocalPoint, isDragging: true }
: idx === element.points.length - 1
? { point: finalEndLocalPoint, isDragging: true }
: naiveDraggingPoints.get(idx)!,
];
}),
);
return {
updates: start.mode
? {
startBinding: {
elementId: start.element.id,
mode: start.mode,
...calculateFixedPointForNonElbowArrowBinding(
element,
start.element,
"start",
elementsMap,
),
},
}
: undefined,
positions: new Map(
indices.map((idx) => {
return [
idx,
idx === 0
? { point: startLocalPoint, isDragging: true }
: idx === element.points.length - 1
? { point: finalEndLocalPoint, isDragging: true }
: naiveDraggingPoints.get(idx)!,
];
}),
),
};
};
const shouldAllowDraggingPoint = (
element: ExcalidrawLinearElement,
scenePointerX: number,
scenePointerY: number,
selectedPointsIndices: readonly number[],
elementsMap: Readonly<NonDeletedSceneElementsMap>,
app: AppClassProperties,
) => {
if (!isSimpleArrow(element)) {
return true;
}
const scenePointer = pointFrom<GlobalPoint>(scenePointerX, scenePointerY);
// Do not allow dragging the bound arrow closer to the shape than
// the dragging threshold
let allowDrag = true;
if (isSimpleArrow(element)) {
if (selectedPointsIndices.includes(0) && element.startBinding) {
const boundElement = elementsMap.get(element.startBinding.elementId)!;
const dist = distanceToElement(boundElement, elementsMap, scenePointer);
const inside = isPointInElement(scenePointer, boundElement, elementsMap);
allowDrag = allowDrag && (dist > DRAGGING_THRESHOLD || inside);
if (allowDrag) {
unbindBindingElement(element, "start", app.scene);
}
}
if (
selectedPointsIndices.includes(element.points.length - 1) &&
element.endBinding
) {
const boundElement = elementsMap.get(element.endBinding.elementId)!;
const dist = distanceToElement(boundElement, elementsMap, scenePointer);
const inside = isPointInElement(scenePointer, boundElement, elementsMap);
allowDrag = allowDrag && (dist > DRAGGING_THRESHOLD || inside);
if (allowDrag) {
unbindBindingElement(element, "end", app.scene);
}
}
}
return allowDrag;
};

View File

@@ -243,7 +243,6 @@ import {
getBindingStrategyForDraggingBindingElementEndpoints,
getStartGlobalEndLocalPointsForSimpleArrowBinding,
mutateElement,
pointDraggingUpdates,
} from "@excalidraw/element";
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
@@ -965,34 +964,17 @@ class App extends React.Component<AppProps, AppState> {
});
});
const elementsMap = this.scene.getNonDeletedElementsMap();
const elements = this.scene.getNonDeletedElements();
const newDraggingPointPosition = LinearElementEditor.createPointAt(
arrow,
elementsMap,
const event =
this.lastPointerMoveEvent ?? this.lastPointerDownEvent?.nativeEvent;
invariant(event, "Last event must exist");
const newState = LinearElementEditor.handlePointDragging(
event,
this,
x - this.state.selectedLinearElement.pointerOffset.x,
y - this.state.selectedLinearElement.pointerOffset.y,
this.getEffectiveGridSize(),
);
const draggingPoint =
arrow.points[
this.state.selectedLinearElement.pointerDownState.lastClickedPoint
];
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
LinearElementEditor.movePoints(
arrow,
this.scene,
pointDraggingUpdates(
startDragged ? [0] : endDragged ? [arrow.points.length - 1] : [],
deltaX,
deltaY,
elementsMap,
arrow,
elements,
this,
),
this.state.selectedLinearElement,
);
this.setState(newState);
}
};
@@ -8419,26 +8401,31 @@ class App extends React.Component<AppProps, AppState> {
this.setState((prevState) => {
let linearElementEditor = null;
let nextSelectedElementIds = prevState.selectedElementIds;
if (isBindingElement(element)) {
const linearElement = new LinearElementEditor(
if (isLinearElement(element)) {
linearElementEditor = new LinearElementEditor(
element,
this.scene.getNonDeletedElementsMap(),
);
linearElementEditor = {
...linearElement,
pointerDownState: {
...linearElement.pointerDownState,
arrowOriginalStartPoint: pointFrom<GlobalPoint>(
pointerDownState.origin.x,
pointerDownState.origin.y,
),
},
selectedPointsIndices: [1],
};
nextSelectedElementIds = makeNextSelectedElementIds(
{ [element.id]: true },
prevState,
);
if (isBindingElement(element)) {
linearElementEditor = {
...linearElementEditor,
selectedPointsIndices: [1],
pointerDownState: {
...linearElementEditor.pointerDownState,
lastClickedIsEndPoint: true,
lastClickedPoint: 1,
arrowOriginalStartPoint: pointFrom<GlobalPoint>(
pointerDownState.origin.x,
pointerDownState.origin.y,
),
},
};
nextSelectedElementIds = makeNextSelectedElementIds(
{ [element.id]: true },
prevState,
);
}
}
return {
@@ -9297,83 +9284,37 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.drag.hasOccurred = true;
const points = newElement.points;
// Update arrow points
let startBinding = newElement.startBinding;
let startGlobalPoint =
this.state.selectedLinearElement?.pointerDownState
?.arrowOriginalStartPoint ??
LinearElementEditor.getPointAtIndexGlobalCoordinates(
newElement,
0,
elementsMap,
);
let endLocalPoint = pointFrom<LocalPoint>(
gridX - newElement.x,
gridY - newElement.y,
);
// Simple arrows need both their start and end points adjusted
if (isBindingElement(newElement) && !isElbowArrow(newElement)) {
const point = pointFrom<LocalPoint>(
pointerCoords.x - newElement.x,
pointerCoords.y - newElement.y,
);
const { start, end } =
getBindingStrategyForDraggingBindingElementEndpoints(
newElement,
new Map([
[newElement.points.length - 1, { point, isDragging: true }],
]),
elementsMap,
this.scene.getNonDeletedElements(),
this.state,
{ newArrow: !!this.state.newElement },
);
if (start.mode) {
startBinding = {
elementId: start.element.id,
mode: start.mode,
...calculateFixedPointForNonElbowArrowBinding(
newElement,
start.element,
"start",
elementsMap,
),
};
}
[startGlobalPoint, endLocalPoint] =
getStartGlobalEndLocalPointsForSimpleArrowBinding(
newElement,
start,
end,
startGlobalPoint,
endLocalPoint,
elementsMap,
);
}
invariant(
points.length > 1,
"Do not create linear elements with less than 2 points",
);
if (isElbowArrow(newElement) || points.length === 2) {
this.scene.mutateElement(
let linearElementEditor = this.state.selectedLinearElement;
if (!linearElementEditor) {
linearElementEditor = new LinearElementEditor(
newElement,
{
x: startGlobalPoint[0],
y: startGlobalPoint[1],
points: [pointFrom<LocalPoint>(0, 0), endLocalPoint],
startBinding,
},
{ isDragging: true, informMutation: false },
this.scene.getNonDeletedElementsMap(),
);
linearElementEditor = {
...linearElementEditor,
selectedPointsIndices: [1],
pointerDownState: {
...linearElementEditor.pointerDownState,
lastClickedIsEndPoint: true,
lastClickedPoint: 1,
},
};
}
this.setState({
newElement,
...LinearElementEditor.handlePointDragging(
event,
this,
gridX,
gridY,
linearElementEditor,
)!,
});
if (isBindingElement(newElement, false)) {