fix: Angle snapping around bindable objects incorrectly resolves (#10501)

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: zsviczian <viczian.zsolt@gmail.com>
This commit is contained in:
Márk Tolmács
2025-12-15 10:49:46 +01:00
committed by GitHub
parent bf4c65f483
commit f06484c6ab
4 changed files with 60 additions and 11 deletions

View File

@@ -146,6 +146,8 @@ export const isBindingEnabled = (appState: AppState): boolean => {
export const bindOrUnbindBindingElement = (
arrow: NonDeleted<ExcalidrawArrowElement>,
draggingPoints: PointsPositionUpdates,
scenePointerX: number,
scenePointerY: number,
scene: Scene,
appState: AppState,
opts?: {
@@ -158,6 +160,8 @@ export const bindOrUnbindBindingElement = (
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
arrow,
draggingPoints,
scenePointerX,
scenePointerY,
scene.getNonDeletedElementsMap(),
scene.getNonDeletedElements(),
appState,
@@ -557,6 +561,8 @@ const bindingStrategyForSimpleArrowEndpointDragging_complex = (
export const getBindingStrategyForDraggingBindingElementEndpoints = (
arrow: NonDeleted<ExcalidrawArrowElement>,
draggingPoints: PointsPositionUpdates,
screenPointerX: number,
screenPointerY: number,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
appState: AppState,
@@ -583,6 +589,8 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
return getBindingStrategyForDraggingBindingElementEndpoints_simple(
arrow,
draggingPoints,
screenPointerX,
screenPointerY,
elementsMap,
elements,
appState,
@@ -593,6 +601,8 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
arrow: NonDeleted<ExcalidrawArrowElement>,
draggingPoints: PointsPositionUpdates,
scenePointerX: number,
scenePointerY: number,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
appState: AppState,
@@ -670,7 +680,15 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
elementsMap,
(e) => maxBindingDistance_simple(appState.zoom),
);
const pointInElement = hit && isPointInElement(globalPoint, hit, elementsMap);
const pointInElement =
hit &&
(opts?.angleLocked
? isPointInElement(
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
hit,
elementsMap,
)
: isPointInElement(globalPoint, hit, elementsMap));
const otherBindableElement = otherBinding
? (elementsMap.get(
otherBinding.elementId,
@@ -944,6 +962,8 @@ export const bindOrUnbindBindingElements = (
bindOrUnbindBindingElement(
arrow,
new Map(), // No dragging points in this case
Infinity,
Infinity,
scene,
appState,
);
@@ -1146,7 +1166,14 @@ export const updateBindings = (
},
) => {
if (isArrowElement(latestElement)) {
bindOrUnbindBindingElement(latestElement, new Map(), scene, appState);
bindOrUnbindBindingElement(
latestElement,
new Map(),
Infinity,
Infinity,
scene,
appState,
);
} else {
updateBoundElements(latestElement, scene, {
...options,

View File

@@ -343,6 +343,8 @@ export class LinearElementEditor {
[idx],
deltaX,
deltaY,
scenePointerX,
scenePointerY,
elementsMap,
element,
elements,
@@ -498,7 +500,6 @@ export class LinearElementEditor {
width + pivotPoint[0],
height + pivotPoint[1],
);
deltaX = target[0] - draggingPoint[0];
deltaY = target[1] - draggingPoint[1];
} else {
@@ -519,6 +520,8 @@ export class LinearElementEditor {
selectedPointsIndices,
deltaX,
deltaY,
scenePointerX,
scenePointerY,
elementsMap,
element,
elements,
@@ -2066,6 +2069,8 @@ const pointDraggingUpdates = (
selectedPointsIndices: readonly number[],
deltaX: number,
deltaY: number,
scenePointerX: number,
scenePointerY: number,
elementsMap: NonDeletedSceneElementsMap,
element: NonDeleted<ExcalidrawLinearElement>,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
@@ -2106,6 +2111,8 @@ const pointDraggingUpdates = (
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
element,
naiveDraggingPoints,
scenePointerX,
scenePointerY,
elementsMap,
elements,
app.state,
@@ -2228,10 +2235,15 @@ const pointDraggingUpdates = (
// We need to use a custom intersector to ensure that if there is a big "jump"
// in the arrow's position, we can position it with outline avoidance
// pixel-perfectly and avoid "dancing" arrows.
const customIntersector =
// NOTE: Direction matters here, so we create two intersectors
const startCustomIntersector =
start.focusPoint && end.focusPoint
? lineSegment(start.focusPoint, end.focusPoint)
: undefined;
const endCustomIntersector =
start.focusPoint && end.focusPoint
? lineSegment(end.focusPoint, start.focusPoint)
: undefined;
// Needed to handle a special case where an existing arrow is dragged over
// the same element it is bound to on the other side
@@ -2268,7 +2280,7 @@ const pointDraggingUpdates = (
nextArrow.endBinding,
endBindable,
elementsMap,
customIntersector,
endCustomIntersector,
) || nextArrow.points[nextArrow.points.length - 1]
: nextArrow.points[nextArrow.points.length - 1];
@@ -2299,7 +2311,7 @@ const pointDraggingUpdates = (
nextArrow.startBinding,
startBindable,
elementsMap,
customIntersector,
startCustomIntersector,
) || nextArrow.points[0]
: nextArrow.points[0];

View File

@@ -103,11 +103,19 @@ export const actionFinalize = register<FormData>({
return map;
}, new Map()) ?? new Map();
bindOrUnbindBindingElement(element, draggedPoints, scene, appState, {
newArrow,
altKey: event.altKey,
angleLocked: shouldRotateWithDiscreteAngle(event),
});
bindOrUnbindBindingElement(
element,
draggedPoints,
sceneCoords.x,
sceneCoords.y,
scene,
appState,
{
newArrow,
altKey: event.altKey,
angleLocked: shouldRotateWithDiscreteAngle(event),
},
);
} else if (isLineElement(element)) {
if (
appState.selectedLinearElement?.isEditing &&

View File

@@ -8617,6 +8617,8 @@ class App extends React.Component<AppProps, AppState> {
},
],
]),
point[0],
point[1],
this.scene,
this.state,
{