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, PRECISION,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision"; import { isPointInShape } from "@excalidraw/utils/collision";
import { getEllipseShape, getPolygonShape } from "@excalidraw/utils/shape"; import { getEllipseShape, getPolygonShape } from "@excalidraw/utils/shape";
@ -64,7 +64,7 @@ import {
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes"; import { aabbForElement } from "./shapes";
import { updateElbowArrowPoints } from "./elbowArrow"; import { updateElbowArrowPoints } from "./elbowArrow";
import type Scene from "./Scene"; import type Scene from "./Scene";
@ -108,7 +108,7 @@ export const isBindingEnabled = (appState: AppState): boolean => {
}; };
export const FIXED_BINDING_DISTANCE = 5; export const FIXED_BINDING_DISTANCE = 5;
export const BINDING_HIGHLIGHT_THICKNESS = 10; const BINDING_HIGHLIGHT_THICKNESS = 10;
export const BINDING_HIGHLIGHT_OFFSET = 4; export const BINDING_HIGHLIGHT_OFFSET = 4;
const getNonDeletedElements = ( const getNonDeletedElements = (
@ -442,19 +442,15 @@ const normalizePointBinding = (
binding: { focus: number; gap: number }, binding: { focus: number; gap: number },
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
) => { ) => {
let gap = binding.gap;
const maxGap = maxBindingGap( const maxGap = maxBindingGap(
hoveredElement, hoveredElement,
hoveredElement.width, hoveredElement.width,
hoveredElement.height, hoveredElement.height,
); );
if (gap > maxGap) {
gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET;
}
return { return {
...binding, ...binding,
gap, gap: Math.min(binding.gap, maxGap),
}; };
}; };
@ -566,7 +562,8 @@ export const getHoveredElementForBinding = (
let cullRest = false; let cullRest = false;
const candidateElements = getAllElementsAtPositionForBinding( const candidateElements = getAllElementsAtPositionForBinding(
elements, elements,
(element) => (element) => {
const result =
isBindableElement(element, false) && isBindableElement(element, false) &&
bindingBorderTest( bindingBorderTest(
element, element,
@ -580,7 +577,10 @@ export const getHoveredElementForBinding = (
// disable fullshape snapping for frame elements so we // disable fullshape snapping for frame elements so we
// can bind to frame children // can bind to frame children
!isFrameLikeElement(element), !isFrameLikeElement(element),
), );
return result;
},
).filter((element) => { ).filter((element) => {
if (cullRest) { if (cullRest) {
return false; return false;
@ -888,7 +888,12 @@ export const getHeadingForElbowArrowSnap = (
return otherPointHeading; return otherPointHeading;
} }
const distance = getDistanceForBinding(origPoint, bindableElement, zoom); const distance = getDistanceForBinding(
origPoint,
bindableElement,
true,
zoom,
);
if (!distance) { if (!distance) {
return vectorToHeading( return vectorToHeading(
@ -899,9 +904,10 @@ export const getHeadingForElbowArrowSnap = (
return headingForPointFromElement(bindableElement, aabb, p); return headingForPointFromElement(bindableElement, aabb, p);
}; };
const getDistanceForBinding = ( export const getDistanceForBinding = (
point: Readonly<GlobalPoint>, point: Readonly<GlobalPoint>,
bindableElement: ExcalidrawBindableElement, bindableElement: ExcalidrawBindableElement,
fullShape: boolean,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
) => { ) => {
const distance = distanceToBindableElement(bindableElement, point); const distance = distanceToBindableElement(bindableElement, point);
@ -911,12 +917,14 @@ const getDistanceForBinding = (
bindableElement.height, bindableElement.height,
zoom, zoom,
); );
const isInside = isPointInShape( const isInside = fullShape
? isPointInShape(
point, point,
bindableElement.type === "ellipse" bindableElement.type === "ellipse"
? getEllipseShape(bindableElement) ? getEllipseShape(bindableElement)
: getPolygonShape(bindableElement), : getPolygonShape(bindableElement),
); )
: false;
return distance > bindDistance && !isInside ? null : distance; return distance > bindDistance && !isInside ? null : distance;
}; };
@ -1172,22 +1180,22 @@ export const snapToMid = (
angle, angle,
); );
} else if (element.type === "diamond") { } else if (element.type === "diamond") {
const sqrtFixedDistance = Math.sqrt(FIXED_BINDING_DISTANCE); const distance = FIXED_BINDING_DISTANCE - 1;
const topLeft = pointFrom<GlobalPoint>( const topLeft = pointFrom<GlobalPoint>(
x + width / 4 - sqrtFixedDistance, x + width / 4 - distance,
y + height / 4 - sqrtFixedDistance, y + height / 4 - distance,
); );
const topRight = pointFrom<GlobalPoint>( const topRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + sqrtFixedDistance, x + (3 * width) / 4 + distance,
y + height / 4 - sqrtFixedDistance, y + height / 4 - distance,
); );
const bottomLeft = pointFrom<GlobalPoint>( const bottomLeft = pointFrom<GlobalPoint>(
x + width / 4 - sqrtFixedDistance, x + width / 4 - distance,
y + (3 * height) / 4 + sqrtFixedDistance, y + (3 * height) / 4 + distance,
); );
const bottomRight = pointFrom<GlobalPoint>( const bottomRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + sqrtFixedDistance, x + (3 * width) / 4 + distance,
y + (3 * height) / 4 + sqrtFixedDistance, y + (3 * height) / 4 + distance,
); );
if ( if (
pointDistance(topLeft, nonRotated) < pointDistance(topLeft, nonRotated) <
@ -1553,14 +1561,14 @@ export const bindingBorderTest = (
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
fullShape?: boolean, fullShape?: boolean,
): boolean => { ): boolean => {
const threshold = maxBindingGap(element, element.width, element.height, zoom); const distance = getDistanceForBinding(
pointFrom(x, y),
const shape = getElementShape(element, elementsMap); element,
return ( !!fullShape,
isPointOnShape(pointFrom(x, y), shape, threshold) || zoom,
(fullShape === true &&
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
); );
return !!distance;
}; };
export const maxBindingGap = ( export const maxBindingGap = (

View file

@ -31,6 +31,7 @@ import {
getGlobalFixedPointForBindableElement, getGlobalFixedPointForBindableElement,
snapToMid, snapToMid,
getHoveredElementForBinding, getHoveredElementForBinding,
getDistanceForBinding,
} from "./binding"; } from "./binding";
import { distanceToBindableElement } from "./distance"; import { distanceToBindableElement } from "./distance";
import { import {
@ -1255,6 +1256,7 @@ const getElbowArrowData = (
origStartGlobalPoint, origStartGlobalPoint,
hoveredStartElement, hoveredStartElement,
options?.isDragging, options?.isDragging,
options?.zoom,
); );
const endGlobalPoint = getGlobalPoint( const endGlobalPoint = getGlobalPoint(
{ {
@ -1268,6 +1270,7 @@ const getElbowArrowData = (
origEndGlobalPoint, origEndGlobalPoint,
hoveredEndElement, hoveredEndElement,
options?.isDragging, options?.isDragging,
options?.zoom,
); );
const startHeading = getBindPointHeading( const startHeading = getBindPointHeading(
startGlobalPoint, startGlobalPoint,
@ -2211,16 +2214,14 @@ const getGlobalPoint = (
initialPoint: GlobalPoint, initialPoint: GlobalPoint,
element?: ExcalidrawBindableElement | null, element?: ExcalidrawBindableElement | null,
isDragging?: boolean, isDragging?: boolean,
zoom?: AppState["zoom"],
): GlobalPoint => { ): GlobalPoint => {
if (isDragging) { if (isDragging) {
if (element) { if (element && getDistanceForBinding(initialPoint, element, true, zoom)) {
const snapPoint = bindPointToSnapToElementOutline( return snapToMid(
arrow,
element, element,
startOrEnd, bindPointToSnapToElementOutline(arrow, element, startOrEnd),
); );
return snapToMid(element, snapPoint);
} }
return initialPoint; 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( const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element, element,
-1, -1,