feat: Nested shapes handling

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs
2025-09-04 17:03:19 +02:00
parent 6ea0102b0a
commit 109ff756f5
2 changed files with 67 additions and 2 deletions

View File

@@ -32,6 +32,7 @@ import {
getElementBounds, getElementBounds,
} from "./bounds"; } from "./bounds";
import { import {
getAllHoveredElementAtPoint,
getHoveredElementForBinding, getHoveredElementForBinding,
hitElementItself, hitElementItself,
intersectElementWithLineSegment, intersectElementWithLineSegment,
@@ -301,6 +302,54 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
}; };
} }
// Check and handle nested shapes
if (arrow.startBinding) {
const otherElement = elementsMap.get(
arrow.startBinding.elementId,
) as ExcalidrawBindableElement;
invariant(otherElement, "Other element must be in the elements map");
const startFocusElements = getAllHoveredElementAtPoint(
getGlobalFixedPointForBindableElement(
arrow.startBinding.fixedPoint,
otherElement,
elementsMap,
),
elements,
elementsMap,
);
const startHoverElements = getAllHoveredElementAtPoint(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
arrow,
0,
elementsMap,
),
elements,
elementsMap,
);
if (
hit &&
otherElement.id !== hit.id &&
(startHoverElements.find((el) => el.id === hit.id) ||
startFocusElements.find((el) => el.id === hit.id))
) {
return {
start: isMultiPoint
? { mode: undefined }
: {
mode: "orbit",
element: otherElement,
focusPoint: snapToCenter(
otherElement,
elementsMap,
origin ?? pointFrom<GlobalPoint>(arrow.x, arrow.y),
),
},
end: { mode: "inside", element: hit, focusPoint: point },
};
}
}
// Inside -> outside binding // Inside -> outside binding
if (arrow.startBinding && arrow.startBinding.elementId !== hit?.id) { if (arrow.startBinding && arrow.startBinding.elementId !== hit?.id) {
const otherElement = elementsMap.get( const otherElement = elementsMap.get(

View File

@@ -238,12 +238,12 @@ const bindingBorderTest = (
: intersections.length > 0 && distance <= t; : intersections.length > 0 && distance <= t;
}; };
export const getHoveredElementForBinding = ( export const getAllHoveredElementAtPoint = (
point: Readonly<GlobalPoint>, point: Readonly<GlobalPoint>,
elements: readonly Ordered<NonDeletedExcalidrawElement>[], elements: readonly Ordered<NonDeletedExcalidrawElement>[],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
toleranceFn?: (element: ExcalidrawBindableElement) => number, toleranceFn?: (element: ExcalidrawBindableElement) => number,
): NonDeleted<ExcalidrawBindableElement> | null => { ): NonDeleted<ExcalidrawBindableElement>[] => {
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = []; const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
// We need to to hit testing from front (end of the array) to back (beginning of the array) // We need to to hit testing from front (end of the array) to back (beginning of the array)
// because array is ordered from lower z-index to highest and we want element z-index // because array is ordered from lower z-index to highest and we want element z-index
@@ -264,6 +264,22 @@ export const getHoveredElementForBinding = (
} }
} }
return candidateElements;
};
export const getHoveredElementForBinding = (
point: Readonly<GlobalPoint>,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
elementsMap: NonDeletedSceneElementsMap,
toleranceFn?: (element: ExcalidrawBindableElement) => number,
): NonDeleted<ExcalidrawBindableElement> | null => {
const candidateElements = getAllHoveredElementAtPoint(
point,
elements,
elementsMap,
toleranceFn,
);
if (!candidateElements || candidateElements.length === 0) { if (!candidateElements || candidateElements.length === 0) {
return null; return null;
} }