From 109ff756f565294a4b8fdc79b7e3c5a8e3489d11 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Thu, 4 Sep 2025 17:03:19 +0200 Subject: [PATCH] feat: Nested shapes handling Signed-off-by: Mark Tolmacs --- packages/element/src/binding.ts | 49 +++++++++++++++++++++++++++++++ packages/element/src/collision.ts | 20 +++++++++++-- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index fa79eda26f..4f48c8a54f 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -32,6 +32,7 @@ import { getElementBounds, } from "./bounds"; import { + getAllHoveredElementAtPoint, getHoveredElementForBinding, hitElementItself, 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(arrow.x, arrow.y), + ), + }, + end: { mode: "inside", element: hit, focusPoint: point }, + }; + } + } + // Inside -> outside binding if (arrow.startBinding && arrow.startBinding.elementId !== hit?.id) { const otherElement = elementsMap.get( diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index 57566e3c52..ff65417c70 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -238,12 +238,12 @@ const bindingBorderTest = ( : intersections.length > 0 && distance <= t; }; -export const getHoveredElementForBinding = ( +export const getAllHoveredElementAtPoint = ( point: Readonly, elements: readonly Ordered[], elementsMap: NonDeletedSceneElementsMap, toleranceFn?: (element: ExcalidrawBindableElement) => number, -): NonDeleted | null => { +): NonDeleted[] => { const candidateElements: NonDeleted[] = []; // 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 @@ -264,6 +264,22 @@ export const getHoveredElementForBinding = ( } } + return candidateElements; +}; + +export const getHoveredElementForBinding = ( + point: Readonly, + elements: readonly Ordered[], + elementsMap: NonDeletedSceneElementsMap, + toleranceFn?: (element: ExcalidrawBindableElement) => number, +): NonDeleted | null => { + const candidateElements = getAllHoveredElementAtPoint( + point, + elements, + elementsMap, + toleranceFn, + ); + if (!candidateElements || candidateElements.length === 0) { return null; }