mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-23 09:21:00 +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]);
|
||||
|
@@ -8787,54 +8787,54 @@ class App extends React.Component<AppProps, AppState> {
|
||||
!linearElementEditor.pointerDownState.segmentMidpoint.added
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
if (isBindingElement(element)) {
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
|
||||
this.scene.getNonDeletedElements(),
|
||||
} else if (linearElementEditor.pointerDownState.lastClickedPoint > -1) {
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
this.handleDelayedBindModeChange(element, hoveredElement);
|
||||
}
|
||||
if (isBindingElement(element)) {
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
|
||||
this.scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const newState = LinearElementEditor.handlePointDragging(
|
||||
event,
|
||||
this,
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
linearElementEditor,
|
||||
);
|
||||
if (newState) {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||
pointerDownState.drag.hasOccurred = true;
|
||||
|
||||
// NOTE: Optimize setState calls because it
|
||||
// affects history and performance
|
||||
if (
|
||||
newState.suggestedBinding !== this.state.suggestedBinding ||
|
||||
!isShallowEqual(
|
||||
newState.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||
) ||
|
||||
newState.selectedLinearElement?.hoverPointIndex !==
|
||||
this.state.selectedLinearElement?.hoverPointIndex ||
|
||||
newState.selectedLinearElement?.customLineAngle !==
|
||||
this.state.selectedLinearElement?.customLineAngle ||
|
||||
this.state.selectedLinearElement.isDragging !==
|
||||
newState.selectedLinearElement?.isDragging
|
||||
) {
|
||||
this.setState(newState);
|
||||
this.handleDelayedBindModeChange(element, hoveredElement);
|
||||
}
|
||||
|
||||
return;
|
||||
const newState = LinearElementEditor.handlePointDragging(
|
||||
event,
|
||||
this,
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
linearElementEditor,
|
||||
);
|
||||
if (newState) {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||
pointerDownState.drag.hasOccurred = true;
|
||||
|
||||
// NOTE: Optimize setState calls because it
|
||||
// affects history and performance
|
||||
if (
|
||||
newState.suggestedBinding !== this.state.suggestedBinding ||
|
||||
!isShallowEqual(
|
||||
newState.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||
) ||
|
||||
newState.selectedLinearElement?.hoverPointIndex !==
|
||||
this.state.selectedLinearElement?.hoverPointIndex ||
|
||||
newState.selectedLinearElement?.customLineAngle !==
|
||||
this.state.selectedLinearElement?.customLineAngle ||
|
||||
this.state.selectedLinearElement.isDragging !==
|
||||
newState.selectedLinearElement?.isDragging
|
||||
) {
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9007,13 +9007,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const nextCrop = {
|
||||
...crop,
|
||||
x: clamp(
|
||||
crop.x -
|
||||
crop.x +
|
||||
offsetVector[0] * Math.sign(croppingElement.scale[0]),
|
||||
0,
|
||||
image.naturalWidth - crop.width,
|
||||
),
|
||||
y: clamp(
|
||||
crop.y -
|
||||
crop.y +
|
||||
offsetVector[1] * Math.sign(croppingElement.scale[1]),
|
||||
0,
|
||||
image.naturalHeight - crop.height,
|
||||
@@ -9316,18 +9316,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
linearElementEditor,
|
||||
)!,
|
||||
});
|
||||
|
||||
// if (isBindingElement(newElement, false)) {
|
||||
// // When creating a linear element by dragging
|
||||
// this.setState({
|
||||
// suggestedBinding: maybeSuggestBindingsForBindingElementAtCoords(
|
||||
// newElement,
|
||||
// "end",
|
||||
// this.scene,
|
||||
// pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
|
||||
// ),
|
||||
// });
|
||||
// }
|
||||
} else {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||
|
Reference in New Issue
Block a user