From da9d0999936d4736d6e95624f24151ae8466d969 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Sat, 16 Aug 2025 20:54:16 +0200 Subject: [PATCH] Fix all tests Signed-off-by: Mark Tolmacs fix(transform): Fix group resize and rotate fix(binding): Harmonize binding param usage fix: Center focus point Signed-off-by: Mark Tolmacs chore: Trigger build Remove binding gap Signed-off-by: Mark Tolmacs --- packages/element/src/binding.ts | 219 ++----- packages/element/src/collision.ts | 76 +-- packages/element/src/elbowArrow.ts | 17 +- packages/element/src/linearElementEditor.ts | 9 +- packages/element/src/resizeElements.ts | 56 +- packages/excalidraw/components/App.tsx | 12 - .../excalidraw/renderer/interactiveScene.ts | 21 +- .../tests/__snapshots__/history.test.tsx.snap | 606 +++++------------- packages/excalidraw/tests/history.test.tsx | 101 ++- 9 files changed, 349 insertions(+), 768 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 117fd2220c..758a59502a 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -35,10 +35,8 @@ import { import { bindingBorderTest, getHoveredElementForBinding, - getHoveredElementForBindingAndIfItsPrecise, hitElementItself, intersectElementWithLineSegment, - maxBindingDistanceFromOutline, } from "./collision"; import { distanceToElement } from "./distance"; import { @@ -114,7 +112,6 @@ export type BindingStrategy = }; export const FIXED_BINDING_DISTANCE = 5; -export const BINDING_HIGHLIGHT_THICKNESS = 10; export const shouldEnableBindingForPointerEvent = ( event: React.PointerEvent, @@ -202,7 +199,6 @@ const bindOrUnbindBindingElementEdge = ( const getOriginalBindingsIfStillCloseToBindingEnds = ( linearElement: NonDeleted, elementsMap: NonDeletedSceneElementsMap, - zoom?: AppState["zoom"], ): (NonDeleted | null)[] => (["start", "end"] as const).map((edge) => { const coors = tupleToCoors( @@ -224,7 +220,6 @@ const getOriginalBindingsIfStillCloseToBindingEnds = ( element, pointFrom(coors.x, coors.y), elementsMap, - zoom, ) ) { return element; @@ -328,30 +323,16 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = ( draggingPoints.get(startDragged ? startIdx : endIdx)!.point, elementsMap, ); - const { hovered, hit } = getHoveredElementForBindingAndIfItsPrecise( - point, - elements, - elementsMap, - appState.zoom, - true, - ); + const hit = getHoveredElementForBinding(point, elements, elementsMap); // With new arrows this handles the binding at arrow creation if (startDragged) { - if (hovered) { - if (hit) { - start = { - element: hovered, - mode: "inside", - focusPoint: point, - }; - } else { - start = { - element: hovered, - mode: "orbit", - focusPoint: point, - }; - } + if (hit) { + start = { + element: hit, + mode: "inside", + focusPoint: point, + }; } else { start = { mode: null }; } @@ -365,41 +346,24 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = ( appState?.selectedLinearElement?.pointerDownState.arrowOriginalStartPoint; // Inside -> inside binding - if (hovered && hit && arrow.startBinding?.elementId === hovered.id) { + if (hit && arrow.startBinding?.elementId === hit.id) { const center = pointFrom( - hovered.x + hovered.width / 2, - hovered.y + hovered.height / 2, + hit.x + hit.width / 2, + hit.y + hit.height / 2, ); return { start: { mode: "inside", - element: hovered, + element: hit, focusPoint: arrowOriginalStartPoint ?? center, }, - end: { mode: "inside", element: hovered, focusPoint: point }, - }; - } - - // Inside -> orbit binding - if (hovered && !hit && arrow.startBinding?.elementId === hovered.id) { - const center = pointFrom( - hovered.x + hovered.width / 2, - hovered.y + hovered.height / 2, - ); - - return { - start: { - mode: globalBindMode === "inside" ? "inside" : "orbit", - element: hovered, - focusPoint: arrowOriginalStartPoint ?? center, - }, - end: { mode: null }, + end: { mode: "inside", element: hit, focusPoint: point }, }; } // Inside -> outside binding - if (arrow.startBinding && arrow.startBinding.elementId !== hovered?.id) { + if (arrow.startBinding && arrow.startBinding.elementId !== hit?.id) { const otherElement = elementsMap.get( arrow.startBinding.elementId, ) as ExcalidrawBindableElement; @@ -411,21 +375,26 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = ( const other: BindingStrategy = { mode: otherIsInsideBinding ? "inside" : "orbit", element: otherElement, - focusPoint: snapToCenter( - otherElement, - elementsMap, - arrowOriginalStartPoint ?? pointFrom(arrow.x, arrow.y), - ), + focusPoint: otherIsInsideBinding + ? arrowOriginalStartPoint ?? pointFrom(arrow.x, arrow.y) + : snapToCenter( + otherElement, + elementsMap, + arrowOriginalStartPoint ?? + pointFrom(arrow.x, arrow.y), + ), }; // We are hovering another element with the end point let current: BindingStrategy; - if (hovered) { + if (hit) { const isInsideBinding = globalBindMode === "inside"; current = { mode: isInsideBinding ? "inside" : "orbit", - element: hovered, - focusPoint: snapToCenter(hovered, elementsMap, point), + element: hit, + focusPoint: isInsideBinding + ? point + : snapToCenter(hit, elementsMap, point), }; } else { current = { mode: null }; @@ -439,13 +408,13 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = ( // No start binding if (!arrow.startBinding) { - if (hovered) { + if (hit) { const isInsideBinding = - globalBindMode === "inside" || isAlwaysInsideBinding(hovered); + globalBindMode === "inside" || isAlwaysInsideBinding(hit); end = { mode: isInsideBinding ? "inside" : "orbit", - element: hovered, + element: hit, focusPoint: point, }; } else { @@ -466,7 +435,6 @@ const bindingStrategyForSimpleArrowEndpointDragging = ( oppositeBinding: FixedPointBinding | null, elementsMap: NonDeletedSceneElementsMap, elements: readonly Ordered[], - zoom: AppState["zoom"], globalBindMode?: AppState["bindMode"], opts?: { newArrow?: boolean; @@ -476,23 +444,14 @@ const bindingStrategyForSimpleArrowEndpointDragging = ( let current: BindingStrategy = { mode: undefined }; let other: BindingStrategy = { mode: undefined }; - const { hovered, hit } = getHoveredElementForBindingAndIfItsPrecise( - point, - elements, - elementsMap, - zoom, - true, - ); + const hit = getHoveredElementForBinding(point, elements, elementsMap); // If the global bind mode is in free binding mode, just bind // where the pointer is and keep the other end intact - if ( - globalBindMode === "inside" || - (hovered && isAlwaysInsideBinding(hovered)) - ) { - current = hovered + if (globalBindMode === "inside" || (hit && isAlwaysInsideBinding(hit))) { + current = hit ? { - element: hovered, + element: hit, focusPoint: point, mode: "inside", } @@ -503,90 +462,58 @@ const bindingStrategyForSimpleArrowEndpointDragging = ( // Dragged point is outside of any bindable element // so we break any existing binding - if (!hovered) { + if (!hit) { return { current: { mode: null }, other }; } - // Dragged point is on the binding gap of a bindable element - if (!hit) { - // If the opposite binding (if exists) is on the same element - if (oppositeBinding) { - if (oppositeBinding.elementId === hovered.id) { - return { current: { mode: null }, other }; - } - // The opposite binding is on a different element - // eslint-disable-next-line no-else-return - else { - current = { - element: hovered, - mode: "orbit", - focusPoint: opts?.newArrow - ? pointFrom( - hovered.x + hovered.width / 2, - hovered.y + hovered.height / 2, - ) - : point, - }; - - return { current, other }; - } - } - - // No opposite binding or the opposite binding is on a different element - current = { element: hovered, mode: "orbit", focusPoint: point }; - } // The dragged point is inside the hovered bindable element - else { - // The opposite binding is on the same element - // eslint-disable-next-line no-lonely-if - if (oppositeBinding) { - if (oppositeBinding.elementId === hovered.id) { - // The opposite binding is on the binding gap of the same element - if (oppositeBinding.mode !== "inside") { - current = { element: hovered, mode: "orbit", focusPoint: point }; - other = { mode: null }; - return { current, other }; - } - // The opposite binding is inside the same element - // eslint-disable-next-line no-else-return - else { - current = { element: hovered, mode: "inside", focusPoint: point }; + // The opposite binding is on the same element + // eslint-disable-next-line no-lonely-if + if (oppositeBinding) { + if (oppositeBinding.elementId === hit.id) { + // The opposite binding is on the binding gap of the same element + if (oppositeBinding.mode !== "inside") { + current = { element: hit, mode: "orbit", focusPoint: point }; + other = { mode: null }; - return { current, other }; - } + return { current, other }; } - // The opposite binding is on a different element + // The opposite binding is inside the same element // eslint-disable-next-line no-else-return else { - current = { - element: hovered, - mode: "orbit", - focusPoint: opts?.newArrow - ? pointFrom( - hovered.x + hovered.width / 2, - hovered.y + hovered.height / 2, - ) - : point, - }; + current = { element: hit, mode: "inside", focusPoint: point }; return { current, other }; } } - // The opposite binding is on a different element or no binding + // The opposite binding is on a different element + // eslint-disable-next-line no-else-return else { current = { - element: hovered, + element: hit, mode: "orbit", focusPoint: opts?.newArrow ? pointFrom( - hovered.x + hovered.width / 2, - hovered.y + hovered.height / 2, + hit.x + hit.width / 2, + hit.y + hit.height / 2, ) : point, }; + + return { current, other }; } } + // The opposite binding is on a different element or no binding + else { + current = { + element: hit, + mode: "orbit", + focusPoint: opts?.newArrow + ? pointFrom(hit.x + hit.width / 2, hit.y + hit.height / 2) + : point, + }; + } // Must return as only one endpoint is dragged, therefore // the end binding strategy might accidentally gets overriden @@ -649,7 +576,6 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = ( p, elements, elementsMap, - appState.zoom, ); const current: BindingStrategy = hoveredElement ? { @@ -697,7 +623,6 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = ( arrow.endBinding, elementsMap, elements, - appState.zoom, globalBindMode, opts, ); @@ -719,7 +644,6 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = ( arrow.startBinding, elementsMap, elements, - appState.zoom, globalBindMode, opts, ); @@ -748,7 +672,6 @@ export const bindOrUnbindBindingElements = ( export const getSuggestedBindingsForBindingElements = ( selectedElements: NonDeleted[], elementsMap: NonDeletedSceneElementsMap, - zoom: AppState["zoom"], ): SuggestedBinding[] => { // HOT PATH: Bail out if selected elements list is too large if (selectedElements.length > 50) { @@ -759,11 +682,7 @@ export const getSuggestedBindingsForBindingElements = ( selectedElements .filter(isArrowElement) .flatMap((element) => - getOriginalBindingsIfStillCloseToBindingEnds( - element, - elementsMap, - zoom, - ), + getOriginalBindingsIfStillCloseToBindingEnds(element, elementsMap), ) .filter( (element): element is NonDeleted => @@ -785,7 +704,6 @@ export const maybeSuggestBindingsForBindingElementAtCoords = ( linearElement: NonDeleted, startOrEndOrBoth: "start" | "end" | "both", scene: Scene, - zoom: AppState["zoom"], ): ExcalidrawBindableElement[] => { const startCoords = LinearElementEditor.getPointAtIndexGlobalCoordinates( linearElement, @@ -801,13 +719,11 @@ export const maybeSuggestBindingsForBindingElementAtCoords = ( startCoords, scene.getNonDeletedElements(), scene.getNonDeletedElementsMap(), - zoom, ); const endHovered = getHoveredElementForBinding( endCoords, scene.getNonDeletedElements(), scene.getNonDeletedElementsMap(), - zoom, ); const suggestedBindings = []; @@ -1075,7 +991,6 @@ export const getHeadingForElbowArrowSnap = ( aabb: Bounds | undefined | null, origPoint: GlobalPoint, elementsMap: ElementsMap, - zoom?: AppState["zoom"], ): Heading => { const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p)); @@ -1084,14 +999,8 @@ export const getHeadingForElbowArrowSnap = ( } const d = distanceToElement(bindableElement, elementsMap, origPoint); - const bindDistance = maxBindingDistanceFromOutline( - bindableElement, - bindableElement.width, - bindableElement.height, - zoom, - ); - const distance = d > bindDistance ? null : d; + const distance = d > 0 ? null : d; if (!distance) { return vectorToHeading( diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index a7cda59d43..306b64bd33 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -25,7 +25,7 @@ import type { Radians, } from "@excalidraw/math"; -import type { AppState, FrameNameBounds } from "@excalidraw/excalidraw/types"; +import type { FrameNameBounds } from "@excalidraw/excalidraw/types"; import { isPathALoop } from "./utils"; import { @@ -58,8 +58,6 @@ import { LinearElementEditor } from "./linearElementEditor"; import { distanceToElement } from "./distance"; -import { BINDING_HIGHLIGHT_THICKNESS, FIXED_BINDING_DISTANCE } from "./binding"; - import type { ElementsMap, ExcalidrawBindableElement, @@ -206,40 +204,12 @@ export const hitElementBoundText = ( return isPointInElement(point, boundTextElement, elementsMap); }; -export const maxBindingDistanceFromOutline = ( - element: ExcalidrawElement, - elementWidth: number, - elementHeight: number, - zoom?: AppState["zoom"], -): number => { - const zoomValue = zoom?.value && zoom.value < 1 ? zoom.value : 1; - - // Aligns diamonds with rectangles - const shapeRatio = element.type === "diamond" ? 1 / Math.sqrt(2) : 1; - const smallerDimension = shapeRatio * Math.min(elementWidth, elementHeight); - - return Math.max( - 16, - // bigger bindable boundary for bigger elements - Math.min(0.25 * smallerDimension, 32), - // keep in sync with the zoomed highlight - BINDING_HIGHLIGHT_THICKNESS / zoomValue + FIXED_BINDING_DISTANCE, - ); -}; - export const bindingBorderTest = ( element: NonDeleted, [x, y]: Readonly, elementsMap: NonDeletedSceneElementsMap, - zoom?: AppState["zoom"], ): boolean => { const p = pointFrom(x, y); - const threshold = maxBindingDistanceFromOutline( - element, - element.width, - element.height, - zoom, - ); const shouldTestInside = // disable fullshape snapping for frame elements so we // can bind to frame children @@ -247,12 +217,7 @@ export const bindingBorderTest = ( // PERF: Run a cheap test to see if the binding element // is even close to the element - const bounds = [ - x - threshold, - y - threshold, - x + threshold, - y + threshold, - ] as Bounds; + const bounds = [x - 1, y - 1, x + 1, y + 1] as Bounds; const elementBounds = getElementBounds(element, elementsMap); if (!doBoundsIntersect(bounds, elementBounds)) { return false; @@ -267,15 +232,14 @@ export const bindingBorderTest = ( const distance = distanceToElement(element, elementsMap, p); return shouldTestInside - ? intersections.length === 0 || distance <= threshold - : intersections.length > 0 && distance <= threshold; + ? intersections.length === 0 + : intersections.length > 0 && distance <= 1; }; export const getHoveredElementForBinding = ( point: Readonly, elements: readonly Ordered[], elementsMap: NonDeletedSceneElementsMap, - zoom?: AppState["zoom"], ): NonDeleted | null => { const candidateElements: NonDeleted[] = []; // We need to to hit testing from front (end of the array) to back (beginning of the array) @@ -291,7 +255,7 @@ export const getHoveredElementForBinding = ( if ( isBindableElement(element, false) && - bindingBorderTest(element, point, elementsMap, zoom) + bindingBorderTest(element, point, elementsMap) ) { candidateElements.push(element); } @@ -313,36 +277,6 @@ export const getHoveredElementForBinding = ( .pop() as NonDeleted; }; -export const getHoveredElementForBindingAndIfItsPrecise = ( - point: GlobalPoint, - elements: readonly Ordered[], - elementsMap: NonDeletedSceneElementsMap, - zoom: AppState["zoom"], - shouldTestInside: boolean = true, -): { - hovered: NonDeleted | null; - hit: boolean; -} => { - const hoveredElement = getHoveredElementForBinding( - point, - elements, - elementsMap, - zoom, - ); - // TODO: Optimize this to avoid recalculating the point - element distance - const hit = - !!hoveredElement && - hitElementItself({ - element: hoveredElement, - elementsMap, - point, - threshold: 0, - overrideShouldTestInside: shouldTestInside, - }); - - return { hovered: hoveredElement, hit }; -}; - /** * Intersect a line with an element for binding test * diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index 4348e81b58..f19d257c6a 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -1216,19 +1216,9 @@ const getElbowArrowData = ( if (options?.isDragging) { const elements = Array.from(elementsMap.values()); hoveredStartElement = - getHoveredElement( - origStartGlobalPoint, - elementsMap, - elements, - options?.zoom, - ) || null; + getHoveredElement(origStartGlobalPoint, elementsMap, elements) || null; hoveredEndElement = - getHoveredElement( - origEndGlobalPoint, - elementsMap, - elements, - options?.zoom, - ) || null; + getHoveredElement(origEndGlobalPoint, elementsMap, elements) || null; } else { hoveredStartElement = arrow.startBinding ? getBindableElementForId(arrow.startBinding.elementId, elementsMap) || @@ -2262,9 +2252,8 @@ const getHoveredElement = ( origPoint: GlobalPoint, elementsMap: NonDeletedSceneElementsMap, elements: readonly Ordered[], - zoom?: AppState["zoom"], ) => { - return getHoveredElementForBinding(origPoint, elements, elementsMap, zoom); + return getHoveredElementForBinding(origPoint, elements, elementsMap); }; const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean => diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 53a85178f7..b1236d9eb0 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -465,7 +465,6 @@ export class LinearElementEditor { ? "start" : "end", app.scene, - app.state.zoom, ); } } @@ -2001,7 +2000,6 @@ const pointDraggingUpdates = ( newGlobalPointPosition, elements, elementsMap, - app.state.zoom, ); const otherGlobalPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( @@ -2011,12 +2009,7 @@ const pointDraggingUpdates = ( ); const otherPointInsideElement = !!hoveredElement && - !!bindingBorderTest( - hoveredElement, - otherGlobalPoint, - elementsMap, - app.state.zoom, - ); + !!bindingBorderTest(hoveredElement, otherGlobalPoint, elementsMap); if ( isBindingEnabled(app.state) && diff --git a/packages/element/src/resizeElements.ts b/packages/element/src/resizeElements.ts index feb52d177a..f043619567 100644 --- a/packages/element/src/resizeElements.ts +++ b/packages/element/src/resizeElements.ts @@ -20,7 +20,11 @@ import type { PointerDownState } from "@excalidraw/excalidraw/types"; import type { Mutable } from "@excalidraw/common/utility-types"; -import { getArrowLocalFixedPoints, updateBoundElements } from "./binding"; +import { + getArrowLocalFixedPoints, + unbindBindingElement, + updateBoundElements, +} from "./binding"; import { getElementAbsoluteCoords, getCommonBounds, @@ -229,12 +233,18 @@ const rotateSingleElement = ( if (isBindingElement(element)) { update = { ...update, - startBinding: null, - endBinding: null, } as ElementUpdate; + + if (element.startBinding) { + unbindBindingElement(element, "start", scene); + } + if (element.endBinding) { + unbindBindingElement(element, "end", scene); + } } scene.mutateElement(element, update); + if (boundTextElementId) { const textElement = scene.getElement(boundTextElementId); @@ -399,6 +409,11 @@ const rotateMultipleElements = ( centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE; } + const rotatedElementsMap = new Map< + ExcalidrawElement["id"], + NonDeletedExcalidrawElement + >(elements.map((element) => [element.id, element])); + for (const element of elements) { if (!isFrameLikeElement(element)) { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); @@ -429,6 +444,19 @@ const rotateMultipleElements = ( simultaneouslyUpdated: elements, }); + if (isBindingElement(element)) { + if (element.startBinding) { + if (!rotatedElementsMap.has(element.startBinding.elementId)) { + unbindBindingElement(element, "start", scene); + } + } + if (element.endBinding) { + if (!rotatedElementsMap.has(element.endBinding.elementId)) { + unbindBindingElement(element, "end", scene); + } + } + } + const boundText = getBoundTextElement(element, elementsMap); if (boundText && !isArrowElement(element)) { scene.mutateElement(boundText, { @@ -845,8 +873,11 @@ export const resizeSingleElement = ( if (latestElement.startBinding) { updates = { ...updates, - startBinding: null, } as ElementUpdate; + + if (latestElement.startBinding) { + unbindBindingElement(latestElement, "start", scene); + } } if (latestElement.endBinding) { @@ -1408,6 +1439,10 @@ export const resizeMultipleElements = ( } const elementsToUpdate = elementsAndUpdates.map(({ element }) => element); + const resizedElementsMap = new Map< + ExcalidrawElement["id"], + NonDeletedExcalidrawElement + >(elementsAndUpdates.map(({ element }) => [element.id, element])); for (const { element, @@ -1421,6 +1456,19 @@ export const resizeMultipleElements = ( simultaneouslyUpdated: elementsToUpdate, }); + if (isBindingElement(element)) { + if (element.startBinding) { + if (!resizedElementsMap.has(element.startBinding.elementId)) { + unbindBindingElement(element, "start", scene); + } + } + if (element.endBinding) { + if (!resizedElementsMap.has(element.endBinding.elementId)) { + unbindBindingElement(element, "end", scene); + } + } + } + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement && boundTextFontSize) { scene.mutateElement(boundTextElement, { diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index f850455c4f..d78f5ac50f 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -4537,7 +4537,6 @@ class App extends React.Component { (element) => element.id !== elbowArrow?.id || step !== 0, ), this.scene.getNonDeletedElementsMap(), - this.state.zoom, ), }); @@ -4766,7 +4765,6 @@ class App extends React.Component { pointFrom(scenePointer.x, scenePointer.y), this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - this.state.zoom, ); if (hoveredElement && !this.bindModeHandler) { @@ -6098,7 +6096,6 @@ class App extends React.Component { newElement, "end", this.scene, - this.state.zoom, ), }); } else { @@ -6203,7 +6200,6 @@ class App extends React.Component { pointFrom(scenePointerX, scenePointerY), this.scene.getNonDeletedElements(), elementsMap, - this.state.zoom, ); // Timed bind mode handler for arrow elements @@ -7991,7 +7987,6 @@ class App extends React.Component { ), this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - this.state.zoom, ); this.setState({ @@ -8197,7 +8192,6 @@ class App extends React.Component { lastGlobalPoint, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - this.state.zoom, ); // clicking inside commit zone → finalize arrow @@ -8318,7 +8312,6 @@ class App extends React.Component { point, this.scene.getNonDeletedElements(), elementsMap, - this.state.zoom, ); this.scene.mutateElement(element, { @@ -8764,7 +8757,6 @@ class App extends React.Component { pointFrom(pointerCoords.x, pointerCoords.y), this.scene.getNonDeletedElements(), elementsMap, - this.state.zoom, ); // Timed bind mode handler for arrow elements @@ -9141,7 +9133,6 @@ class App extends React.Component { suggestedBindings: getSuggestedBindingsForBindingElements( selectedElements, this.scene.getNonDeletedElementsMap(), - this.state.zoom, ), }); } @@ -9451,7 +9442,6 @@ class App extends React.Component { newElement, "end", this.scene, - this.state.zoom, ), }); } @@ -11002,7 +10992,6 @@ class App extends React.Component { pointFrom(pointerCoords.x, pointerCoords.y), this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - this.state.zoom, ); this.setState({ suggestedBindings: @@ -11625,7 +11614,6 @@ class App extends React.Component { const suggestedBindings = getSuggestedBindingsForBindingElements( selectedElements, this.scene.getNonDeletedElementsMap(), - this.state.zoom, ); const elementsToHighlight = new Set(); diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index dc1411b4e7..cdbe2a9939 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -16,10 +16,7 @@ import { throttleRAF, } from "@excalidraw/common"; -import { - FIXED_BINDING_DISTANCE, - maxBindingDistanceFromOutline, -} from "@excalidraw/element"; +import { FIXED_BINDING_DISTANCE } from "@excalidraw/element"; import { LinearElementEditor } from "@excalidraw/element"; import { getOmitSidesForDevice, @@ -197,12 +194,7 @@ const renderBindingHighlightForBindableElement = ( elementsMap: ElementsMap, zoom: InteractiveCanvasAppState["zoom"], ) => { - const padding = maxBindingDistanceFromOutline( - element, - element.width, - element.height, - zoom, - ); + const padding = 5; context.fillStyle = "rgba(0,0,0,.05)"; @@ -251,14 +243,9 @@ const renderBindingHighlightForSuggestedPointBinding = ( elementsMap: ElementsMap, zoom: InteractiveCanvasAppState["zoom"], ) => { - const [element, startOrEnd, bindableElement] = suggestedBinding; + const [element, startOrEnd] = suggestedBinding; - const threshold = maxBindingDistanceFromOutline( - bindableElement, - bindableElement.width, - bindableElement.height, - zoom, - ); + const threshold = 0; context.strokeStyle = "rgba(0,0,0,0)"; context.fillStyle = "rgba(0,0,0,.05)"; diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 47fe286a4b..fd7077c204 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -143,7 +143,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 5, + "version": 13, "width": 100, "x": -100, "y": -50, @@ -173,7 +173,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, + "version": 9, "width": 100, "x": 100, "y": -50, @@ -188,11 +188,18 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "customData": undefined, "elbowed": false, "endArrowhead": "arrow", - "endBinding": null, + "endBinding": { + "elementId": "id15", + "fixedPoint": [ + "0.50000", + 1, + ], + "mode": "orbit", + }, "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.03787", + "height": "113.98784", "id": "id4", "index": "a2", "isDeleted": false, @@ -208,7 +215,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], [ 95, - "0.03787", + "113.98784", ], ], "roughness": 1, @@ -222,23 +229,58 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 1, "0.50010", ], - "mode": "orbit", + "mode": "inside", }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 16, + "version": 37, "width": 95, - "x": 5, - "y": "0.01199", + "x": 0, + "y": "0.01000", } `; -exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of elements 1`] = `3`; +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] element 3 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id4", + "type": "arrow", + }, + ], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 50, + "id": "id15", + "index": "a3", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 10, + "width": 50, + "x": 100, + "y": 100, +} +`; -exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of renders 1`] = `12`; +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of elements 1`] = `4`; + +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of renders 1`] = `21`; exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] redo stack 1`] = ` [ @@ -256,7 +298,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "id1": { "deleted": { "boundElements": [], - "version": 4, + "version": 9, }, "inserted": { "boundElements": [ @@ -265,13 +307,35 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], - "version": 3, + "version": 8, + }, + }, + "id15": { + "deleted": { + "boundElements": [ + { + "id": "id4", + "type": "arrow", + }, + ], + "version": 9, + }, + "inserted": { + "boundElements": [], + "version": 8, }, }, "id4": { "deleted": { - "endBinding": null, - "height": "0.88851", + "endBinding": { + "elementId": "id15", + "fixedPoint": [ + "0.50000", + 1, + ], + "mode": "orbit", + }, + "height": "100.79596", "points": [ [ 0, @@ -279,29 +343,30 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], [ 90, - "0.88851", + "100.79596", ], ], "startBinding": { "elementId": "id0", "fixedPoint": [ 1, - "0.50010", + "0.60000", ], "mode": "orbit", }, - "version": 14, + "version": 36, + "width": 90, }, "inserted": { "endBinding": { "elementId": "id1", "fixedPoint": [ 0, - "0.50010", + "0.60000", ], "mode": "orbit", }, - "height": "0.00047", + "height": "0.00000", "points": [ [ 0, @@ -309,23 +374,24 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], [ 90, - "0.00047", + "0.00000", ], ], "startBinding": { "elementId": "id0", "fixedPoint": [ 1, - "0.50010", + "0.60000", ], "mode": "orbit", }, - "version": 12, + "version": 33, + "width": 90, }, }, }, }, - "id": "id17", + "id": "id22", }, { "appState": AppStateDelta { @@ -340,7 +406,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "updated": { "id4": { "deleted": { - "height": "0.03787", + "height": "113.98784", "points": [ [ 0, @@ -348,7 +414,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], [ 95, - "0.03787", + "113.98784", ], ], "startBinding": { @@ -357,13 +423,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 1, "0.50010", ], - "mode": "orbit", + "mode": "inside", }, - "version": 16, + "version": 37, "width": 95, + "x": 0, + "y": "0.01000", }, "inserted": { - "height": "0.88851", + "height": "100.79596", "points": [ [ 0, @@ -371,24 +439,26 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], [ 90, - "0.88851", + "100.79596", ], ], "startBinding": { "elementId": "id0", "fixedPoint": [ 1, - "0.50010", + "0.60000", ], "mode": "orbit", }, - "version": 14, + "version": 36, "width": 90, + "x": 5, + "y": "15.52629", }, }, }, }, - "id": "id18", + "id": "id23", }, ] `; @@ -570,136 +640,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "id": "id6", }, - { - "appState": AppStateDelta { - "delta": Delta { - "deleted": {}, - "inserted": {}, - }, - }, - "elements": { - "added": {}, - "removed": {}, - "updated": { - "id4": { - "deleted": { - "height": "0.95000", - "points": [ - [ - 0, - 0, - ], - [ - 95, - "-0.95000", - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "inside", - }, - "version": 7, - "width": 95, - "x": 5, - "y": "0.95000", - }, - "inserted": { - "height": 0, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "inside", - }, - "version": 6, - "width": 100, - "x": 0, - "y": 0, - }, - }, - }, - }, - "id": "id9", - }, - { - "appState": AppStateDelta { - "delta": Delta { - "deleted": {}, - "inserted": {}, - }, - }, - "elements": { - "added": {}, - "removed": {}, - "updated": { - "id4": { - "deleted": { - "height": "0.00950", - "points": [ - [ - 0, - 0, - ], - [ - 95, - "-0.00950", - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "orbit", - }, - "version": 9, - "y": "0.00950", - }, - "inserted": { - "height": "0.95000", - "points": [ - [ - 0, - 0, - ], - [ - 95, - "-0.95000", - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "inside", - }, - "version": 7, - "y": "0.95000", - }, - }, - }, - }, - "id": "id11", - }, ] `; @@ -846,9 +786,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 5, + "version": 14, "width": 100, - "x": -100, + "x": 150, "y": -50, } `; @@ -876,9 +816,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, + "version": 9, "width": 100, - "x": 100, + "x": 150, "y": -50, } `; @@ -895,7 +835,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "0.88851", + "height": "0.01000", "id": "id4", "index": "a2", "isDeleted": false, @@ -910,8 +850,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - 90, - "0.88851", + 0, + "-0.01000", ], ], "roughness": 1, @@ -925,23 +865,23 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 1, "0.50010", ], - "mode": "orbit", + "mode": "inside", }, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 18, - "width": 90, - "x": 5, - "y": "0.05936", + "version": 30, + "width": 0, + "x": 250, + "y": "0.01000", } `; exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of elements 1`] = `3`; -exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of renders 1`] = `14`; +exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of renders 1`] = `23`; exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] redo stack 1`] = ` [ @@ -959,7 +899,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "id1": { "deleted": { "boundElements": [], - "version": 4, + "version": 9, }, "inserted": { "boundElements": [ @@ -968,62 +908,66 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], - "version": 3, + "version": 8, }, }, "id4": { "deleted": { "endBinding": null, - "height": "0.00900", + "height": "3.00000", "points": [ [ 0, 0, ], [ - 90, - "-0.00900", + -45, + "-3.00000", ], ], "startBinding": { "elementId": "id0", "fixedPoint": [ 1, - "0.50010", + "0.60000", ], "mode": "orbit", }, - "version": 16, + "version": 29, + "width": 45, + "y": "3.00000", }, "inserted": { "endBinding": { "elementId": "id1", "fixedPoint": [ 0, - "0.50010", + "0.60000", ], "mode": "orbit", }, - "height": "0.04676", + "height": 0, "points": [ [ 0, 0, ], [ - 90, - "-0.04676", + 0, + 0, ], ], "startBinding": { "elementId": "id0", "fixedPoint": [ 1, - "0.50010", + "0.60000", ], "mode": "orbit", }, - "version": 14, + "version": 28, + "width": 0, + "y": "9.99861", }, }, }, @@ -1043,15 +987,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "updated": { "id4": { "deleted": { - "height": "0.88851", + "height": "0.01000", "points": [ [ 0, 0, ], [ - 90, - "0.88851", + 0, + "-0.01000", ], ], "startBinding": { @@ -1060,33 +1004,37 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 1, "0.50010", ], - "mode": "orbit", + "mode": "inside", }, - "version": 18, - "y": "0.05936", + "version": 30, + "width": 0, + "x": 250, + "y": "0.01000", }, "inserted": { - "height": "0.00900", + "height": "3.00000", "points": [ [ 0, 0, ], [ - 90, - "-0.00900", + -45, + "-3.00000", ], ], "startBinding": { "elementId": "id0", "fixedPoint": [ 1, - "0.50010", + "0.60000", ], "mode": "orbit", }, - "version": 16, - "y": "0.00950", + "version": 29, + "width": 45, + "x": 145, + "y": "3.00000", }, }, }, @@ -1273,262 +1221,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "id": "id6", }, - { - "appState": AppStateDelta { - "delta": Delta { - "deleted": {}, - "inserted": {}, - }, - }, - "elements": { - "added": {}, - "removed": {}, - "updated": { - "id4": { - "deleted": { - "height": "0.95000", - "points": [ - [ - 0, - 0, - ], - [ - 95, - "-0.95000", - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "inside", - }, - "version": 7, - "width": 95, - "x": 5, - "y": "0.95000", - }, - "inserted": { - "height": 0, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "inside", - }, - "version": 6, - "width": 100, - "x": 0, - "y": 0, - }, - }, - }, - }, - "id": "id9", - }, - { - "appState": AppStateDelta { - "delta": Delta { - "deleted": {}, - "inserted": {}, - }, - }, - "elements": { - "added": {}, - "removed": {}, - "updated": { - "id4": { - "deleted": { - "height": 0, - "points": [ - [ - 0, - 0, - ], - [ - 95, - 0, - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "inside", - }, - "version": 8, - "y": 0, - }, - "inserted": { - "height": "0.95000", - "points": [ - [ - 0, - 0, - ], - [ - 95, - "-0.95000", - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "inside", - }, - "version": 7, - "y": "0.95000", - }, - }, - }, - }, - "id": "id11", - }, - { - "appState": AppStateDelta { - "delta": Delta { - "deleted": {}, - "inserted": {}, - }, - }, - "elements": { - "added": {}, - "removed": {}, - "updated": { - "id4": { - "deleted": { - "height": "0.00950", - "points": [ - [ - 0, - 0, - ], - [ - 95, - "-0.00950", - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "orbit", - }, - "version": 10, - "y": "0.00950", - }, - "inserted": { - "height": 0, - "points": [ - [ - 0, - 0, - ], - [ - 95, - 0, - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "inside", - }, - "version": 8, - "y": 0, - }, - }, - }, - }, - "id": "id13", - }, - { - "appState": AppStateDelta { - "delta": Delta { - "deleted": {}, - "inserted": {}, - }, - }, - "elements": { - "added": {}, - "removed": {}, - "updated": { - "id4": { - "deleted": { - "height": "0.93837", - "points": [ - [ - 0, - 0, - ], - [ - 90, - "0.93837", - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "orbit", - }, - "version": 11, - "width": 90, - }, - "inserted": { - "height": "0.00950", - "points": [ - [ - 0, - 0, - ], - [ - 95, - "-0.00950", - ], - ], - "startBinding": { - "elementId": "id0", - "fixedPoint": [ - 1, - "0.50010", - ], - "mode": "orbit", - }, - "version": 10, - "width": 95, - }, - }, - }, - }, - "id": "id16", - }, ] `; diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index 4fe1096dfc..b0d82f80ee 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -4557,16 +4557,30 @@ describe("history", () => { // create start binding mouse.downAt(0, 0); - mouse.moveTo(0, 1); - mouse.moveTo(0, 0); + mouse.moveTo(0, 10); + mouse.moveTo(0, 10); mouse.up(); // create end binding mouse.downAt(100, 0); - mouse.moveTo(100, 1); - mouse.moveTo(100, 0); + mouse.moveTo(100, 10); + mouse.moveTo(100, 10); mouse.up(); + expect( + (h.elements[2] as ExcalidrawElbowArrowElement).startBinding + ?.fixedPoint, + ).not.toEqual([1, 0.5001]); + expect( + (h.elements[2] as ExcalidrawElbowArrowElement).startBinding?.mode, + ).toBe("orbit"); + expect( + (h.elements[2] as ExcalidrawElbowArrowElement).endBinding, + ).not.toEqual([1, 0.5001]); + expect( + (h.elements[2] as ExcalidrawElbowArrowElement).endBinding?.mode, + ).toBe("orbit"); + expect(h.elements).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -4606,12 +4620,16 @@ describe("history", () => { expect(h.elements).toEqual([ expect.objectContaining({ id: rect1.id, - boundElements: [], + boundElements: [{ id: arrowId, type: "arrow" }], }), expect.objectContaining({ id: rect2.id, boundElements: [] }), expect.objectContaining({ id: arrowId, - startBinding: null, + startBinding: expect.objectContaining({ + elementId: rect1.id, + fixedPoint: [1, 0.5001], + mode: "inside", + }), endBinding: null, }), ]); @@ -4656,13 +4674,13 @@ describe("history", () => { id: arrowId, startBinding: expect.objectContaining({ elementId: rect1.id, - focus: expect.toBeNonNaNNumber(), - gap: expect.toBeNonNaNNumber(), + fixedPoint: [1, 0.6], + mode: "orbit", }), endBinding: expect.objectContaining({ elementId: rect2.id, - focus: expect.toBeNonNaNNumber(), - gap: expect.toBeNonNaNNumber(), + fixedPoint: [0, 0.6], + mode: "orbit", }), }), ]), @@ -4675,12 +4693,21 @@ describe("history", () => { expect(h.elements).toEqual([ expect.objectContaining({ id: rect1.id, - boundElements: [], + boundElements: [ + expect.objectContaining({ + id: arrowId, + type: "arrow", + }), + ], }), expect.objectContaining({ id: rect2.id, boundElements: [] }), expect.objectContaining({ id: arrowId, - startBinding: null, + startBinding: expect.objectContaining({ + elementId: rect1.id, + fixedPoint: [1, 0.5001], + mode: "inside", + }), endBinding: null, }), ]); @@ -4699,13 +4726,13 @@ describe("history", () => { // create start binding mouse.downAt(0, 0); - mouse.moveTo(0, 1); - mouse.upAt(0, 0); + mouse.moveTo(0, 10); + mouse.upAt(0, 10); // create end binding mouse.downAt(100, 0); - mouse.moveTo(100, 1); - mouse.upAt(100, 0); + mouse.moveTo(100, 10); + mouse.upAt(100, 10); expect(h.elements).toEqual( expect.arrayContaining([ @@ -4746,12 +4773,21 @@ describe("history", () => { expect(h.elements).toEqual([ expect.objectContaining({ id: rect1.id, - boundElements: [], + boundElements: [ + expect.objectContaining({ + id: arrowId, + type: "arrow", + }), + ], }), expect.objectContaining({ id: rect2.id, boundElements: [] }), expect.objectContaining({ id: arrowId, - startBinding: null, + startBinding: expect.objectContaining({ + elementId: rect1.id, + fixedPoint: [1, 0.5001], + mode: "inside", + }), endBinding: null, }), ]); @@ -4799,14 +4835,14 @@ describe("history", () => { id: arrowId, startBinding: expect.objectContaining({ elementId: rect1.id, - focus: expect.toBeNonNaNNumber(), - gap: expect.toBeNonNaNNumber(), + fixedPoint: [1, 0.6], + mode: "orbit", }), // rebound with previous rectangle endBinding: expect.objectContaining({ elementId: rect2.id, - focus: expect.toBeNonNaNNumber(), - gap: expect.toBeNonNaNNumber(), + fixedPoint: [0, 0.6], + mode: "orbit", }), }), expect.objectContaining({ @@ -4824,7 +4860,12 @@ describe("history", () => { expect.arrayContaining([ expect.objectContaining({ id: rect1.id, - boundElements: [], + boundElements: [ + expect.objectContaining({ + id: arrowId, + type: "arrow", + }), + ], }), expect.objectContaining({ id: rect2.id, @@ -4832,16 +4873,16 @@ describe("history", () => { }), expect.objectContaining({ id: arrowId, - startBinding: null, + startBinding: expect.objectContaining({ + elementId: rect1.id, + fixedPoint: [1, 0.5001], + mode: "inside", + }), endBinding: expect.objectContaining({ // now we are back in the previous state! elementId: remoteContainer.id, - fixedPoint: [ - expect.toBeNonNaNNumber(), - expect.toBeNonNaNNumber(), - ], - focus: expect.toBeNonNaNNumber(), - gap: expect.toBeNonNaNNumber(), + fixedPoint: [0.5, 1], + mode: "orbit", }), }), expect.objectContaining({