From db9e501d350f591e7815715a321b9c7c12b9ea0d Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Sat, 29 Mar 2025 16:46:44 +0100 Subject: [PATCH] [skip ci] Stats binding behavior changes and test updates --- packages/element/src/binding.ts | 28 +++++++++---------- packages/element/src/typeChecks.ts | 2 +- .../excalidraw/components/Stats/Angle.tsx | 4 ++- .../components/Stats/stats.test.tsx | 6 ++-- packages/excalidraw/components/Stats/utils.ts | 28 +++++++++++++++++++ 5 files changed, 50 insertions(+), 18 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index ec7a2cbb37..02edb0db35 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -234,21 +234,21 @@ const getOriginalBindingsIfStillCloseToArrowEnds = ( edge as "start" | "end", elementsMap, ); - const elementId = - edge === "start" - ? linearElement.startBinding?.elementId - : linearElement.endBinding?.elementId; - if (elementId) { - const element = elementsMap.get(elementId); - if ( - isBindableElement(element) && - bindingBorderTest(element, coors, elementsMap, zoom) - ) { - return element; + const elementId = + edge === "start" + ? linearElement.startBinding?.elementId + : linearElement.endBinding?.elementId; + if (elementId) { + const element = elementsMap.get(elementId); + if ( + isBindableElement(element) && + bindingBorderTest(element, coors, elementsMap, zoom) + ) { + return element; + } } - } - return null; + return null; }); const getBindingStrategyForDraggingArrowEndpoints = ( @@ -541,7 +541,7 @@ const isLinearElementSimple = ( linearElement: NonDeleted, ): boolean => linearElement.points.length < 3; -const unbindLinearElement = ( +export const unbindLinearElement = ( linearElement: NonDeleted, startOrEnd: "start" | "end", ): ExcalidrawBindableElement["id"] | null => { diff --git a/packages/element/src/typeChecks.ts b/packages/element/src/typeChecks.ts index 54619726df..0eebd5385a 100644 --- a/packages/element/src/typeChecks.ts +++ b/packages/element/src/typeChecks.ts @@ -130,7 +130,7 @@ export const isLinearElementType = ( export const isBindingElement = ( element?: ExcalidrawElement | null, includeLocked = true, -): element is ExcalidrawLinearElement => { +): element is ExcalidrawArrowElement => { return ( element != null && (!element.locked || includeLocked === true) && diff --git a/packages/excalidraw/components/Stats/Angle.tsx b/packages/excalidraw/components/Stats/Angle.tsx index 20e2e4dd7f..b398a56467 100644 --- a/packages/excalidraw/components/Stats/Angle.tsx +++ b/packages/excalidraw/components/Stats/Angle.tsx @@ -12,7 +12,7 @@ import type { ExcalidrawElement } from "@excalidraw/element/types"; import { angleIcon } from "../icons"; import DragInput from "./DragInput"; -import { getStepSizedValue, isPropertyEditable } from "./utils"; +import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils"; import type { DragInputCallbackType } from "./DragInput"; import type Scene from "../../scene/Scene"; @@ -47,6 +47,7 @@ const handleDegreeChange: DragInputCallbackType = ({ mutateElement(latestElement, { angle: nextAngle, }); + updateBindings(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap); if (boundTextElement && !isArrowElement(latestElement)) { @@ -72,6 +73,7 @@ const handleDegreeChange: DragInputCallbackType = ({ mutateElement(latestElement, { angle: nextAngle, }); + updateBindings(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap); if (boundTextElement && !isArrowElement(latestElement)) { diff --git a/packages/excalidraw/components/Stats/stats.test.tsx b/packages/excalidraw/components/Stats/stats.test.tsx index fc94da0564..d21d1b1130 100644 --- a/packages/excalidraw/components/Stats/stats.test.tsx +++ b/packages/excalidraw/components/Stats/stats.test.tsx @@ -140,7 +140,9 @@ describe("binding with linear elements", () => { expect(linear.startBinding).not.toBe(null); }); - it("should remain bound to linear element on small angle change", async () => { + // UX RATIONALE: Since we force a fixed distance from elements angle changes + // would result in a "jump" the moment the bound object is moved + it("should not remain bound to linear element on any angle change", async () => { const linear = h.elements[1] as ExcalidrawLinearElement; const inputAngle = UI.queryStatsProperty("A")?.querySelector( ".drag-input", @@ -148,7 +150,7 @@ describe("binding with linear elements", () => { expect(linear.startBinding).not.toBe(null); UI.updateInput(inputAngle, String("1")); - expect(linear.startBinding).not.toBe(null); + expect(linear.startBinding).toBe(null); }); it("should unbind linear element on large position change", async () => { diff --git a/packages/excalidraw/components/Stats/utils.ts b/packages/excalidraw/components/Stats/utils.ts index e30b3c1da1..e4cab04130 100644 --- a/packages/excalidraw/components/Stats/utils.ts +++ b/packages/excalidraw/components/Stats/utils.ts @@ -3,6 +3,8 @@ import { pointFrom, pointRotateRads } from "@excalidraw/math"; import { mutateElement } from "@excalidraw/element/mutateElement"; import { getBoundTextElement } from "@excalidraw/element/textElement"; import { + isBindableElement, + isBindingElement, isFrameLikeElement, isTextElement, } from "@excalidraw/element/typeChecks"; @@ -13,6 +15,11 @@ import { isInGroup, } from "@excalidraw/element/groups"; +import { + unbindLinearElement, + updateBoundElements, +} from "@excalidraw/element/binding"; + import type { Radians } from "@excalidraw/math"; import type { @@ -152,6 +159,8 @@ export const moveElement = ( shouldInformMutation, ); + updateBindings(latestElement, elementsMap); + const boundTextElement = getBoundTextElement( originalElement, originalElementsMap, @@ -190,3 +199,22 @@ export const getAtomicUnits = ( }); return _atomicUnits; }; + +export const updateBindings = ( + latestElement: ExcalidrawElement, + elementsMap: NonDeletedSceneElementsMap, + options?: { + simultaneouslyUpdated?: readonly ExcalidrawElement[]; + }, +) => { + if (isBindingElement(latestElement)) { + if (latestElement.startBinding) { + unbindLinearElement(latestElement, "start"); + } + if (latestElement.endBinding) { + unbindLinearElement(latestElement, "end"); + } + } else if (isBindableElement(latestElement)) { + updateBoundElements(latestElement, elementsMap, options); + } +};