[skip ci] Stats binding behavior changes and test updates

This commit is contained in:
Mark Tolmacs 2025-03-29 16:46:44 +01:00
parent ce10087edc
commit db9e501d35
5 changed files with 50 additions and 18 deletions

View file

@ -541,7 +541,7 @@ const isLinearElementSimple = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
): boolean => linearElement.points.length < 3; ): boolean => linearElement.points.length < 3;
const unbindLinearElement = ( export const unbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
): ExcalidrawBindableElement["id"] | null => { ): ExcalidrawBindableElement["id"] | null => {

View file

@ -130,7 +130,7 @@ export const isLinearElementType = (
export const isBindingElement = ( export const isBindingElement = (
element?: ExcalidrawElement | null, element?: ExcalidrawElement | null,
includeLocked = true, includeLocked = true,
): element is ExcalidrawLinearElement => { ): element is ExcalidrawArrowElement => {
return ( return (
element != null && element != null &&
(!element.locked || includeLocked === true) && (!element.locked || includeLocked === true) &&

View file

@ -12,7 +12,7 @@ import type { ExcalidrawElement } from "@excalidraw/element/types";
import { angleIcon } from "../icons"; import { angleIcon } from "../icons";
import DragInput from "./DragInput"; import DragInput from "./DragInput";
import { getStepSizedValue, isPropertyEditable } from "./utils"; import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
import type { DragInputCallbackType } from "./DragInput"; import type { DragInputCallbackType } from "./DragInput";
import type Scene from "../../scene/Scene"; import type Scene from "../../scene/Scene";
@ -47,6 +47,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
mutateElement(latestElement, { mutateElement(latestElement, {
angle: nextAngle, angle: nextAngle,
}); });
updateBindings(latestElement, elementsMap);
const boundTextElement = getBoundTextElement(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) { if (boundTextElement && !isArrowElement(latestElement)) {
@ -72,6 +73,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
mutateElement(latestElement, { mutateElement(latestElement, {
angle: nextAngle, angle: nextAngle,
}); });
updateBindings(latestElement, elementsMap);
const boundTextElement = getBoundTextElement(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) { if (boundTextElement && !isArrowElement(latestElement)) {

View file

@ -140,7 +140,9 @@ describe("binding with linear elements", () => {
expect(linear.startBinding).not.toBe(null); 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 linear = h.elements[1] as ExcalidrawLinearElement;
const inputAngle = UI.queryStatsProperty("A")?.querySelector( const inputAngle = UI.queryStatsProperty("A")?.querySelector(
".drag-input", ".drag-input",
@ -148,7 +150,7 @@ describe("binding with linear elements", () => {
expect(linear.startBinding).not.toBe(null); expect(linear.startBinding).not.toBe(null);
UI.updateInput(inputAngle, String("1")); 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 () => { it("should unbind linear element on large position change", async () => {

View file

@ -3,6 +3,8 @@ import { pointFrom, pointRotateRads } from "@excalidraw/math";
import { mutateElement } from "@excalidraw/element/mutateElement"; import { mutateElement } from "@excalidraw/element/mutateElement";
import { getBoundTextElement } from "@excalidraw/element/textElement"; import { getBoundTextElement } from "@excalidraw/element/textElement";
import { import {
isBindableElement,
isBindingElement,
isFrameLikeElement, isFrameLikeElement,
isTextElement, isTextElement,
} from "@excalidraw/element/typeChecks"; } from "@excalidraw/element/typeChecks";
@ -13,6 +15,11 @@ import {
isInGroup, isInGroup,
} from "@excalidraw/element/groups"; } from "@excalidraw/element/groups";
import {
unbindLinearElement,
updateBoundElements,
} from "@excalidraw/element/binding";
import type { Radians } from "@excalidraw/math"; import type { Radians } from "@excalidraw/math";
import type { import type {
@ -152,6 +159,8 @@ export const moveElement = (
shouldInformMutation, shouldInformMutation,
); );
updateBindings(latestElement, elementsMap);
const boundTextElement = getBoundTextElement( const boundTextElement = getBoundTextElement(
originalElement, originalElement,
originalElementsMap, originalElementsMap,
@ -190,3 +199,22 @@ export const getAtomicUnits = (
}); });
return _atomicUnits; 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);
}
};