fix: Restore arrow start point when self binding

This commit is contained in:
Mark Tolmacs
2025-11-08 15:45:43 +01:00
parent d93a6f09fe
commit 3ac0a3c3ef
3 changed files with 134 additions and 114 deletions

View File

@@ -665,41 +665,46 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
otherBinding.elementId,
) as NonDeleted<ExcalidrawBindableElement>)
: undefined;
const otherFocusPoint =
otherBinding &&
otherBindableElement &&
getGlobalFixedPointForBindableElement(
otherBinding.fixedPoint,
otherBindableElement,
elementsMap,
);
const otherPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
arrow,
startDragged ? -1 : 0,
elementsMap,
);
const otherFocusPointIsInElement =
otherBindableElement &&
otherFocusPoint &&
isPointInElement(otherFocusPoint, otherBindableElement, elementsMap);
// const otherFocusPoint =
// otherBinding &&
// otherBindableElement &&
// getGlobalFixedPointForBindableElement(
// otherBinding.fixedPoint,
// otherBindableElement,
// elementsMap,
// );
// const otherPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
// arrow,
// startDragged ? -1 : 0,
// elementsMap,
// );
// const otherFocusPointIsInElement =
// otherBindableElement &&
// otherFocusPoint &&
// isPointInElement(otherFocusPoint, otherBindableElement, elementsMap);
// Handle outside-outside binding with the same element
if (
otherBinding &&
otherBinding.elementId === hit?.id &&
!pointInElement &&
!otherFocusPointIsInElement
) {
// Handle outside-outside binding to the same element
if (otherBinding && otherBinding.elementId === hit?.id) {
const [startFixedPoint, endFixedPoint] = getGlobalFixedPoints(
arrow,
elementsMap,
);
invariant(
!opts?.newArrow || appState.selectedLinearElement?.initialState.origin,
"appState.selectedLinearElement.initialState.origin must be defined for new arrows",
);
return {
start: {
mode: "inside",
element: hit,
focusPoint: startDragged ? globalPoint : startFixedPoint,
focusPoint: startDragged
? globalPoint
: // NOTE: Can only affect the start point because new arrows always drag the end point
opts?.newArrow
? appState.selectedLinearElement!.initialState.origin!
: startFixedPoint,
},
end: {
mode: "inside",
@@ -709,8 +714,31 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
};
}
// Handle special alt key case to inside bind no matter what
if (opts?.altKey) {
return {
start:
startDragged && hit
? {
mode: "inside",
element: hit,
focusPoint: globalPoint,
}
: start,
end:
endDragged && hit
? {
mode: "inside",
element: hit,
focusPoint: globalPoint,
}
: end,
};
}
// Handle normal cases
const current: BindingStrategy = hit
? pointInElement || opts?.altKey
? pointInElement
? {
mode: "inside",
element: hit,
@@ -731,26 +759,12 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
: { mode: null };
const other: BindingStrategy =
!opts?.altKey && opts?.newArrow && otherBindableElement
? otherBindableElement.id === hit?.id
otherBindableElement &&
appState.selectedLinearElement?.initialState.altFocusPoint
? {
mode: "inside",
element: otherBindableElement,
focusPoint: otherPoint,
}
: {
mode: "orbit",
element: otherBindableElement,
focusPoint:
otherFocusPointIsInElement && !opts?.initialBinding
? otherFocusPoint
: projectFixedPointOntoDiagonal(
arrow,
otherPoint,
otherBindableElement,
startDragged ? "end" : "start",
elementsMap,
) || otherPoint,
focusPoint: appState.selectedLinearElement.initialState.altFocusPoint,
}
: { mode: undefined };
@@ -2128,7 +2142,7 @@ export class BindableElement {
}
export const getGlobalFixedPointForBindableElement = (
fixedPointRatio: [number, number],
fixedPointRatio: FixedPoint,
element: ExcalidrawBindableElement,
elementsMap: ElementsMap,
): GlobalPoint => {

View File

@@ -29,6 +29,7 @@ import {
getHoveredElementForBinding,
isPathALoop,
moveArrowAboveBindable,
projectFixedPointOntoDiagonal,
type Store,
} from "@excalidraw/element";
@@ -139,6 +140,7 @@ export class LinearElementEditor {
added: boolean;
};
arrowStartIsInside: boolean;
altFocusPoint: Readonly<GlobalPoint> | null;
}>;
/** whether you're dragging a point */
@@ -189,6 +191,7 @@ export class LinearElementEditor {
added: false,
},
arrowStartIsInside: false,
altFocusPoint: null,
};
this.hoverPointIndex = -1;
this.segmentMidPointHoveredCoords = null;
@@ -395,9 +398,28 @@ export class LinearElementEditor {
return null;
}
const startBindingElement =
isBindingElement(element) &&
element.startBinding &&
(elementsMap.get(
element.startBinding.elementId,
) as ExcalidrawBindableElement | null);
const newLinearElementEditor = {
...linearElementEditor,
customLineAngle,
initialState: {
...linearElementEditor.initialState,
altFocusPoint:
!linearElementEditor.initialState.altFocusPoint && startBindingElement
? projectFixedPointOntoDiagonal(
element,
pointFrom<GlobalPoint>(element.x, element.y),
startBindingElement,
"start",
elementsMap,
)
: linearElementEditor.initialState.altFocusPoint,
},
};
return {
@@ -582,6 +604,12 @@ export class LinearElementEditor {
: null;
const newHoverPointIndex = newLastClickedPoint;
const startBindingElement =
isBindingElement(element) &&
element.startBinding &&
(elementsMap.get(
element.startBinding.elementId,
) as ExcalidrawBindableElement | null);
const newLinearElementEditor = {
...linearElementEditor,
@@ -589,6 +617,16 @@ export class LinearElementEditor {
initialState: {
...linearElementEditor.initialState,
lastClickedPoint: newLastClickedPoint,
altFocusPoint:
!linearElementEditor.initialState.altFocusPoint && startBindingElement
? projectFixedPointOntoDiagonal(
element,
pointFrom<GlobalPoint>(element.x, element.y),
startBindingElement,
"start",
elementsMap,
)
: linearElementEditor.initialState.altFocusPoint,
},
segmentMidPointHoveredCoords: newSelectedMidPointHoveredCoords,
hoverPointIndex: newHoverPointIndex,
@@ -959,6 +997,7 @@ export class LinearElementEditor {
appState,
elementsMap,
);
const point = pointFrom<GlobalPoint>(scenePointer.x, scenePointer.y);
let segmentMidpointIndex = null;
if (segmentMidpoint) {
@@ -990,7 +1029,7 @@ export class LinearElementEditor {
initialState: {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: -1,
origin: pointFrom<GlobalPoint>(scenePointer.x, scenePointer.y),
origin: point,
segmentMidpoint: {
value: segmentMidpoint,
index: segmentMidpointIndex,
@@ -999,6 +1038,7 @@ export class LinearElementEditor {
arrowStartIsInside:
!!app.state.newElement &&
(app.state.bindMode === "inside" || app.state.bindMode === "skip"),
altFocusPoint: null,
},
selectedPointsIndices: [element.points.length - 1],
lastUncommittedPoint: null,
@@ -1051,7 +1091,7 @@ export class LinearElementEditor {
initialState: {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: clickedPointIndex,
origin: pointFrom<GlobalPoint>(scenePointer.x, scenePointer.y),
origin: point,
segmentMidpoint: {
value: segmentMidpoint,
index: segmentMidpointIndex,
@@ -1060,6 +1100,7 @@ export class LinearElementEditor {
arrowStartIsInside:
!!app.state.newElement &&
(app.state.bindMode === "inside" || app.state.bindMode === "skip"),
altFocusPoint: null,
},
selectedPointsIndices: nextSelectedPointsIndices,
pointerOffset: targetPoint
@@ -2280,7 +2321,8 @@ const pointDraggingUpdates = (
)! as ExcalidrawBindableElement)
: null;
const startLocalPoint = endIsDraggingOverStartElement
const startLocalPoint =
endIsDraggingOverStartElement && getFeatureFlag("COMPLEX_BINDINGS")
? nextArrow.points[0]
: startIsDraggingOverEndElement &&
app.state.bindMode !== "inside" &&
@@ -2337,49 +2379,6 @@ const pointDraggingUpdates = (
};
};
// 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 (selectedPointsIndices.includes(0) && element.startBinding) {
// const boundElement = elementsMap.get(
// element.startBinding.elementId,
// )! as ExcalidrawBindableElement;
// const dist = distanceToElement(boundElement, elementsMap, scenePointer);
// const inside = isPointInElement(scenePointer, boundElement, elementsMap);
// allowDrag =
// allowDrag && (dist > getFixedBindingDistance(boundElement) || inside);
// }
// if (
// selectedPointsIndices.includes(element.points.length - 1) &&
// element.endBinding
// ) {
// const boundElement = elementsMap.get(
// element.endBinding.elementId,
// )! as ExcalidrawBindableElement;
// const dist = distanceToElement(boundElement, elementsMap, scenePointer);
// const inside = isPointInElement(scenePointer, boundElement, elementsMap);
// allowDrag =
// allowDrag && (dist > getFixedBindingDistance(boundElement) || inside);
// }
// return allowDrag;
// };
const determineCustomLinearAngle = (
pivotPoint: LocalPoint,
draggedPoint: LocalPoint,

View File

@@ -31,6 +31,7 @@ import { elementCenterPoint, getDiamondPoints } from "./bounds";
import { generateLinearCollisionShape } from "./shape";
import { isPointInElement } from "./collision";
import { LinearElementEditor } from "./linearElementEditor";
import { isRectangularElement } from "./typeChecks";
@@ -557,14 +558,17 @@ export const projectFixedPointOntoDiagonal = (
element: ExcalidrawElement,
startOrEnd: "start" | "end",
elementsMap: ElementsMap,
) => {
): GlobalPoint | null => {
invariant(arrow.points.length >= 2, "Arrow must have at least two points");
if (arrow.width < 1 && arrow.height < 1) {
return null;
}
const [diagonalOne, diagonalTwo] = getDiagonalsForBindableElement(
element,
elementsMap,
);
invariant(arrow.points.length >= 2, "Arrow must have at least two points");
const a = LinearElementEditor.getPointAtIndexGlobalCoordinates(
arrow,
startOrEnd === "start" ? 1 : arrow.points.length - 2,
@@ -587,9 +591,12 @@ export const projectFixedPointOntoDiagonal = (
const d1 = p1 && pointDistance(a, p1);
const d2 = p2 && pointDistance(a, p2);
let p = null;
if (d1 != null && d2 != null) {
return d1 < d2 ? p1 : p2;
p = d1 < d2 ? p1 : p2;
} else {
p = p1 || p2 || null;
}
return p1 || p2 || null;
return p && isPointInElement(p, element, elementsMap) ? p : null;
};