diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 927f55ae08..8420b5df5a 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -27,7 +27,7 @@ import { PRECISION, } from "@excalidraw/math"; -import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision"; +import { isPointInShape } from "@excalidraw/utils/collision"; import { getEllipseShape, getPolygonShape } from "@excalidraw/utils/shape"; @@ -64,7 +64,7 @@ import { isTextElement, } from "./typeChecks"; -import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes"; +import { aabbForElement } from "./shapes"; import { updateElbowArrowPoints } from "./elbowArrow"; import type Scene from "./Scene"; @@ -108,7 +108,7 @@ export const isBindingEnabled = (appState: AppState): boolean => { }; export const FIXED_BINDING_DISTANCE = 5; -export const BINDING_HIGHLIGHT_THICKNESS = 10; +const BINDING_HIGHLIGHT_THICKNESS = 10; export const BINDING_HIGHLIGHT_OFFSET = 4; const getNonDeletedElements = ( @@ -442,19 +442,15 @@ const normalizePointBinding = ( binding: { focus: number; gap: number }, hoveredElement: ExcalidrawBindableElement, ) => { - let gap = binding.gap; const maxGap = maxBindingGap( hoveredElement, hoveredElement.width, hoveredElement.height, ); - if (gap > maxGap) { - gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET; - } return { ...binding, - gap, + gap: Math.min(binding.gap, maxGap), }; }; @@ -566,21 +562,25 @@ export const getHoveredElementForBinding = ( let cullRest = false; const candidateElements = getAllElementsAtPositionForBinding( elements, - (element) => - isBindableElement(element, false) && - bindingBorderTest( - element, - pointerCoords, - elementsMap, - zoom, - (fullShape || - !isBindingFallthroughEnabled( - element as ExcalidrawBindableElement, - )) && - // disable fullshape snapping for frame elements so we - // can bind to frame children - !isFrameLikeElement(element), - ), + (element) => { + const result = + isBindableElement(element, false) && + bindingBorderTest( + element, + pointerCoords, + elementsMap, + zoom, + (fullShape || + !isBindingFallthroughEnabled( + element as ExcalidrawBindableElement, + )) && + // disable fullshape snapping for frame elements so we + // can bind to frame children + !isFrameLikeElement(element), + ); + + return result; + }, ).filter((element) => { if (cullRest) { return false; @@ -888,7 +888,12 @@ export const getHeadingForElbowArrowSnap = ( return otherPointHeading; } - const distance = getDistanceForBinding(origPoint, bindableElement, zoom); + const distance = getDistanceForBinding( + origPoint, + bindableElement, + true, + zoom, + ); if (!distance) { return vectorToHeading( @@ -899,9 +904,10 @@ export const getHeadingForElbowArrowSnap = ( return headingForPointFromElement(bindableElement, aabb, p); }; -const getDistanceForBinding = ( +export const getDistanceForBinding = ( point: Readonly, bindableElement: ExcalidrawBindableElement, + fullShape: boolean, zoom?: AppState["zoom"], ) => { const distance = distanceToBindableElement(bindableElement, point); @@ -911,12 +917,14 @@ const getDistanceForBinding = ( bindableElement.height, zoom, ); - const isInside = isPointInShape( - point, - bindableElement.type === "ellipse" - ? getEllipseShape(bindableElement) - : getPolygonShape(bindableElement), - ); + const isInside = fullShape + ? isPointInShape( + point, + bindableElement.type === "ellipse" + ? getEllipseShape(bindableElement) + : getPolygonShape(bindableElement), + ) + : false; return distance > bindDistance && !isInside ? null : distance; }; @@ -1172,22 +1180,22 @@ export const snapToMid = ( angle, ); } else if (element.type === "diamond") { - const sqrtFixedDistance = Math.sqrt(FIXED_BINDING_DISTANCE); + const distance = FIXED_BINDING_DISTANCE - 1; const topLeft = pointFrom( - x + width / 4 - sqrtFixedDistance, - y + height / 4 - sqrtFixedDistance, + x + width / 4 - distance, + y + height / 4 - distance, ); const topRight = pointFrom( - x + (3 * width) / 4 + sqrtFixedDistance, - y + height / 4 - sqrtFixedDistance, + x + (3 * width) / 4 + distance, + y + height / 4 - distance, ); const bottomLeft = pointFrom( - x + width / 4 - sqrtFixedDistance, - y + (3 * height) / 4 + sqrtFixedDistance, + x + width / 4 - distance, + y + (3 * height) / 4 + distance, ); const bottomRight = pointFrom( - x + (3 * width) / 4 + sqrtFixedDistance, - y + (3 * height) / 4 + sqrtFixedDistance, + x + (3 * width) / 4 + distance, + y + (3 * height) / 4 + distance, ); if ( pointDistance(topLeft, nonRotated) < @@ -1553,14 +1561,14 @@ export const bindingBorderTest = ( zoom?: AppState["zoom"], fullShape?: boolean, ): boolean => { - const threshold = maxBindingGap(element, element.width, element.height, zoom); - - const shape = getElementShape(element, elementsMap); - return ( - isPointOnShape(pointFrom(x, y), shape, threshold) || - (fullShape === true && - pointInsideBounds(pointFrom(x, y), aabbForElement(element))) + const distance = getDistanceForBinding( + pointFrom(x, y), + element, + !!fullShape, + zoom, ); + + return !!distance; }; export const maxBindingGap = ( diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index 95a2aa8ef5..22ae427136 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -31,6 +31,7 @@ import { getGlobalFixedPointForBindableElement, snapToMid, getHoveredElementForBinding, + getDistanceForBinding, } from "./binding"; import { distanceToBindableElement } from "./distance"; import { @@ -1255,6 +1256,7 @@ const getElbowArrowData = ( origStartGlobalPoint, hoveredStartElement, options?.isDragging, + options?.zoom, ); const endGlobalPoint = getGlobalPoint( { @@ -1268,6 +1270,7 @@ const getElbowArrowData = ( origEndGlobalPoint, hoveredEndElement, options?.isDragging, + options?.zoom, ); const startHeading = getBindPointHeading( startGlobalPoint, @@ -2211,16 +2214,14 @@ const getGlobalPoint = ( initialPoint: GlobalPoint, element?: ExcalidrawBindableElement | null, isDragging?: boolean, + zoom?: AppState["zoom"], ): GlobalPoint => { if (isDragging) { - if (element) { - const snapPoint = bindPointToSnapToElementOutline( - arrow, + if (element && getDistanceForBinding(initialPoint, element, true, zoom)) { + return snapToMid( element, - startOrEnd, + bindPointToSnapToElementOutline(arrow, element, startOrEnd), ); - - return snapToMid(element, snapPoint); } return initialPoint; diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index 63751d1f6f..9e3f69107f 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -148,7 +148,12 @@ export const actionFinalize = register({ } } - if (isBindingElement(element) && !isLoop && element.points.length > 1) { + if ( + isBindingElement(element) && + !isLoop && + element.points.length > 1 && + !appState.selectedElementIds[element.id] + ) { const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates( element, -1,