diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index c8dd3a4688..87b64c0023 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -25,6 +25,7 @@ import { doBoundsIntersect, getCenterForBounds, getElementBounds, + pointInsideBounds, } from "./bounds"; import { getAllHoveredElementAtPoint, @@ -47,6 +48,7 @@ import { isBindableElement, isBoundToContainer, isElbowArrow, + isFrameLikeElement, isRectanguloidElement, isTextElement, } from "./typeChecks"; @@ -553,6 +555,65 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = ( return { start, end }; } + // Handle binding to shapes where the frame cuts out a part of the shape + { + const globalPoint = LinearElementEditor.getPointGlobalCoordinates( + arrow, + draggingPoints.get(startDragged ? startIdx : endIdx)!.point, + elementsMap, + ); + const hoveredElement = getHoveredElementForBinding( + globalPoint, + elements, + elementsMap, + ); + const intersectionPoint = + hoveredElement && + hoveredElement.frameId && + bindPointToSnapToElementOutline( + arrow, + hoveredElement, + startDragged ? "start" : "end", + elementsMap, + undefined, + true, + ); + if (intersectionPoint) { + const enclosingFrame = elementsMap.get(hoveredElement.frameId); + if (enclosingFrame && isFrameLikeElement(enclosingFrame)) { + const enclosingFrameBounds = getElementBounds( + enclosingFrame, + elementsMap, + ); + if (!pointInsideBounds(intersectionPoint, enclosingFrameBounds)) { + if (isElbowArrow(arrow)) { + return { + start: { mode: startDragged ? null : start.mode }, + end: { mode: endDragged ? null : end.mode }, + }; + } + + return { + start: startDragged + ? { + mode: "inside", + element: hoveredElement, + focusPoint: globalPoint, + } + : start, + end: endDragged + ? { + mode: "inside", + element: hoveredElement, + focusPoint: globalPoint, + } + : end, + }; + } + } + } + } + // Handle simpler elbow arrow binding if (isElbowArrow(arrow)) { return bindingStrategyForElbowArrowEndpointDragging( @@ -901,6 +962,7 @@ export const bindPointToSnapToElementOutline = ( startOrEnd: "start" | "end", elementsMap: ElementsMap, customIntersector?: LineSegment, + ignoreFrameCutouts?: boolean, ): GlobalPoint => { const aabb = aabbForElement(bindableElement, elementsMap); const localPoint = @@ -1020,6 +1082,21 @@ export const bindPointToSnapToElementOutline = ( return edgePoint; } + // Frames can cut out bindables, so ignore the intersection if + // it isn't in the frame + if (!ignoreFrameCutouts && bindableElement.frameId) { + const enclosingFrame = elementsMap.get(bindableElement.frameId); + if (enclosingFrame && isFrameLikeElement(enclosingFrame)) { + const enclosingFrameBounds = getElementBounds( + enclosingFrame, + elementsMap, + ); + if (!pointInsideBounds(intersection, enclosingFrameBounds)) { + return edgePoint; + } + } + } + return intersection; }; diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index a70db89430..17dc9b1987 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -36,6 +36,7 @@ import { getCubicBezierCurveBound, getDiamondPoints, getElementBounds, + pointInsideBounds, } from "./bounds"; import { hasBoundTextElement, @@ -226,6 +227,20 @@ const bindingBorderTest = ( return false; } + // If the element is inside a frame, we should clip the element + if (element.frameId) { + const enclosingFrame = elementsMap.get(element.frameId); + if (enclosingFrame && isFrameLikeElement(enclosingFrame)) { + const enclosingFrameBounds = getElementBounds( + enclosingFrame, + elementsMap, + ); + if (!pointInsideBounds(p, enclosingFrameBounds)) { + return false; + } + } + } + // Do the intersection test against the element since it's close enough const intersections = intersectElementWithLineSegment( element, diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 345b7dd71a..394db8670d 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -2147,7 +2147,7 @@ const pointDraggingUpdates = ( ), }; - if (endIsDragged) { + if (endIsDragged && updates.endBinding.mode === "orbit") { updates.suggestedBinding = end.element; } } else if (endIsDragged) { diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 4dfeb81f2b..7404ebacab 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -209,6 +209,35 @@ const renderBindingHighlightForBindableElement = ( const opacity = clamp((1 / BIND_MODE_TIMEOUT) * remainingTime, 0.0001, 1); const offset = element.strokeWidth / 2; + const enclosingFrame = element.frameId && allElementsMap.get(element.frameId); + if (enclosingFrame && isFrameLikeElement(enclosingFrame)) { + context.translate( + enclosingFrame.x + appState.scrollX, + enclosingFrame.y + appState.scrollY, + ); + + context.beginPath(); + + if (FRAME_STYLE.radius && context.roundRect) { + context.roundRect( + -1, + -1, + enclosingFrame.width + 1, + enclosingFrame.height + 1, + FRAME_STYLE.radius / appState.zoom.value, + ); + } else { + context.rect(-1, -1, enclosingFrame.width + 1, enclosingFrame.height + 1); + } + + context.clip(); + + context.translate( + -(enclosingFrame.x + appState.scrollX), + -(enclosingFrame.y + appState.scrollY), + ); + } + switch (element.type) { case "magicframe": case "frame":