Refactor how element border tests work

This commit is contained in:
Mark Tolmacs 2025-04-16 20:57:53 +02:00
parent 3054be4c20
commit 43561b6631
No known key found for this signature in database
3 changed files with 68 additions and 54 deletions

View file

@ -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,7 +562,8 @@ export const getHoveredElementForBinding = (
let cullRest = false;
const candidateElements = getAllElementsAtPositionForBinding(
elements,
(element) =>
(element) => {
const result =
isBindableElement(element, false) &&
bindingBorderTest(
element,
@ -580,7 +577,10 @@ export const getHoveredElementForBinding = (
// 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<GlobalPoint>,
bindableElement: ExcalidrawBindableElement,
fullShape: boolean,
zoom?: AppState["zoom"],
) => {
const distance = distanceToBindableElement(bindableElement, point);
@ -911,12 +917,14 @@ const getDistanceForBinding = (
bindableElement.height,
zoom,
);
const isInside = isPointInShape(
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<GlobalPoint>(
x + width / 4 - sqrtFixedDistance,
y + height / 4 - sqrtFixedDistance,
x + width / 4 - distance,
y + height / 4 - distance,
);
const topRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + sqrtFixedDistance,
y + height / 4 - sqrtFixedDistance,
x + (3 * width) / 4 + distance,
y + height / 4 - distance,
);
const bottomLeft = pointFrom<GlobalPoint>(
x + width / 4 - sqrtFixedDistance,
y + (3 * height) / 4 + sqrtFixedDistance,
x + width / 4 - distance,
y + (3 * height) / 4 + distance,
);
const bottomRight = pointFrom<GlobalPoint>(
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 = (

View file

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

View file

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