FEAT: No binding to frame cutout

This commit is contained in:
Mark Tolmacs
2025-10-05 20:46:02 +02:00
parent 9551f2f8fb
commit 539e805e91
4 changed files with 122 additions and 1 deletions

View File

@@ -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<GlobalPoint>,
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;
};

View File

@@ -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,

View File

@@ -2147,7 +2147,7 @@ const pointDraggingUpdates = (
),
};
if (endIsDragged) {
if (endIsDragged && updates.endBinding.mode === "orbit") {
updates.suggestedBinding = end.element;
}
} else if (endIsDragged) {

View File

@@ -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":