diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 954518837d..09faac5935 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -569,7 +569,7 @@ const isLinearElementSimple = ( linearElement: NonDeleted, ): boolean => linearElement.points.length < 3; -export const unbindLinearElement = ( +const unbindLinearElement = ( linearElement: NonDeleted, startOrEnd: "start" | "end", ): ExcalidrawBindableElement["id"] | null => { diff --git a/packages/excalidraw/components/Stats/Angle.tsx b/packages/excalidraw/components/Stats/Angle.tsx index 301cf8b84f..a074042fad 100644 --- a/packages/excalidraw/components/Stats/Angle.tsx +++ b/packages/excalidraw/components/Stats/Angle.tsx @@ -3,20 +3,13 @@ import { degreesToRadians, radiansToDegrees } from "@excalidraw/math"; import { mutateElement } from "@excalidraw/element/mutateElement"; import { getBoundTextElement } from "@excalidraw/element/textElement"; -import { - isArrowElement, - isBindableElement, - isElbowArrow, -} from "@excalidraw/element/typeChecks"; +import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks"; -import { - getSuggestedBindingsForArrows, - updateBoundElements, -} from "@excalidraw/element/binding"; +import { getSuggestedBindingsForArrows } from "@excalidraw/element/binding"; import type { AppState } from "@excalidraw/excalidraw/types"; -import type { Degrees, Radians } from "@excalidraw/math"; +import type { Degrees } from "@excalidraw/math"; import type { ExcalidrawElement } from "@excalidraw/element/types"; @@ -63,9 +56,7 @@ const handleDegreeChange: DragInputCallbackType = ({ angle: nextAngle, }); - if (isBindableElement(latestElement)) { - updateBoundElements(latestElement, elementsMap); - } + updateBindings(latestElement, elementsMap, scene); const boundTextElement = getBoundTextElement(latestElement, elementsMap); if (boundTextElement && !isArrowElement(latestElement)) { @@ -92,9 +83,7 @@ const handleDegreeChange: DragInputCallbackType = ({ angle: nextAngle, }); - if (isBindableElement(latestElement)) { - updateBoundElements(latestElement, elementsMap); - } + updateBindings(latestElement, elementsMap, scene); const boundTextElement = getBoundTextElement(latestElement, elementsMap); if (boundTextElement && !isArrowElement(latestElement)) { @@ -112,46 +101,11 @@ const handleDegreeChange: DragInputCallbackType = ({ }; const handleFinished: DragFinishedCallbackType = ({ - originalElements, - originalAppState, - scene, - accumulatedChange, setAppState, - setInputValue, }) => { - const elementsMap = scene.getNonDeletedElementsMap(); - const origElement = originalElements[0]; - - if (origElement) { - const latestElement = elementsMap.get(origElement.id); - - if (latestElement) { - setAppState({ - suggestedBindings: [], - }); - - const success = updateBindings( - latestElement, - elementsMap, - originalAppState.zoom, - ); - - if (!success) { - const change = degreesToRadians(accumulatedChange as Degrees); - const angle = (latestElement.angle - change) as Radians; - - mutateElement(latestElement, { - angle, - }); - - setInputValue(angle); - - return false; - } - } - } - - return true; + setAppState({ + suggestedBindings: [], + }); }; const Angle = ({ element, scene, appState, property }: AngleProps) => { diff --git a/packages/excalidraw/components/Stats/Dimension.tsx b/packages/excalidraw/components/Stats/Dimension.tsx index 142abc4074..d93cf18880 100644 --- a/packages/excalidraw/components/Stats/Dimension.tsx +++ b/packages/excalidraw/components/Stats/Dimension.tsx @@ -12,7 +12,7 @@ import { isImageElement } from "@excalidraw/element/typeChecks"; import type { ExcalidrawElement } from "@excalidraw/element/types"; 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"; @@ -118,6 +118,9 @@ const handleDimensionChange: DragInputCallbackType< width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), }); + + updateBindings(element, elementsMap, scene); + return; } @@ -150,6 +153,8 @@ const handleDimensionChange: DragInputCallbackType< height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), }); + updateBindings(element, elementsMap, scene); + return; } @@ -184,6 +189,8 @@ const handleDimensionChange: DragInputCallbackType< }, ); + updateBindings(origElement, elementsMap, scene); + return; } const changeInWidth = property === "width" ? accumulatedChange : 0; @@ -230,6 +237,8 @@ const handleDimensionChange: DragInputCallbackType< shouldMaintainAspectRatio: keepAspectRatio, }, ); + + updateBindings(origElement, elementsMap, scene); } }; diff --git a/packages/excalidraw/components/Stats/DragInput.tsx b/packages/excalidraw/components/Stats/DragInput.tsx index 65e8adaea5..31f6a6dbc5 100644 --- a/packages/excalidraw/components/Stats/DragInput.tsx +++ b/packages/excalidraw/components/Stats/DragInput.tsx @@ -49,7 +49,7 @@ export type DragFinishedCallbackType< accumulatedChange: number; setAppState: React.Component["setState"]; setInputValue: (value: number) => void; -}) => boolean; +}) => void; interface StatsDragInputProps< T extends StatsInputProperty, @@ -156,7 +156,7 @@ const StatsDragInput = < setInputValue: (value) => setInputValue(String(value)), setAppState, }); - const commit = dragFinishedCallback?.({ + dragFinishedCallback?.({ originalElements: elements, originalElementsMap, scene, @@ -166,11 +166,9 @@ const StatsDragInput = < setAppState, setInputValue: (value) => setInputValue(String(value)), }); - if (commit) { - app.syncActionResult({ - captureUpdate: CaptureUpdateAction.IMMEDIATELY, - }); - } + app.syncActionResult({ + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); } }; @@ -315,27 +313,23 @@ const StatsDragInput = < false, ); - let commit = true; if (originalElements !== null && originalElementsMap !== null) { - commit = - dragFinishedCallback?.({ - originalElements, - originalElementsMap, - scene, - originalAppState, - property, - accumulatedChange, - setAppState, - setInputValue: (value) => setInputValue(String(value)), - }) ?? true; - } - - if (commit) { - app.syncActionResult({ - captureUpdate: CaptureUpdateAction.IMMEDIATELY, + dragFinishedCallback?.({ + originalElements, + originalElementsMap, + scene, + originalAppState, + property, + accumulatedChange, + setAppState, + setInputValue: (value) => setInputValue(String(value)), }); } + app.syncActionResult({ + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }); + lastPointer = null; accumulatedChange = 0; stepChange = 0; diff --git a/packages/excalidraw/components/Stats/MultiDimension.tsx b/packages/excalidraw/components/Stats/MultiDimension.tsx index 6dd941e153..b037001f77 100644 --- a/packages/excalidraw/components/Stats/MultiDimension.tsx +++ b/packages/excalidraw/components/Stats/MultiDimension.tsx @@ -24,7 +24,13 @@ import type { } from "@excalidraw/element/types"; import DragInput from "./DragInput"; -import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils"; +import { + getAtomicUnits, + getStepSizedValue, + isPropertyEditable, + updateBindings, + updateSelectionBindings, +} from "./utils"; import { getElementsInAtomicUnit } from "./utils"; import type { DragInputCallbackType } from "./DragInput"; @@ -118,6 +124,7 @@ const resizeGroup = ( originalElements: ExcalidrawElement[], elementsMap: NonDeletedSceneElementsMap, originalElementsMap: ElementsMap, + scene: Scene, ) => { // keep aspect ratio for groups if (property === "width") { @@ -143,6 +150,8 @@ const resizeGroup = ( originalElementsMap, ); } + + updateSelectionBindings(originalElements, elementsMap, scene); }; const handleDimensionChange: DragInputCallbackType< @@ -194,6 +203,7 @@ const handleDimensionChange: DragInputCallbackType< originalElements, elementsMap, originalElementsMap, + scene, ); } else { const [el] = elementsInUnit; @@ -242,6 +252,8 @@ const handleDimensionChange: DragInputCallbackType< shouldInformMutation: false, }, ); + + updateBindings(latestElement, elementsMap, scene); } } } @@ -301,6 +313,7 @@ const handleDimensionChange: DragInputCallbackType< originalElements, elementsMap, originalElementsMap, + scene, ); } else { const [el] = elementsInUnit; diff --git a/packages/excalidraw/components/Stats/MultiPosition.tsx b/packages/excalidraw/components/Stats/MultiPosition.tsx index 243260925b..3709e86bb2 100644 --- a/packages/excalidraw/components/Stats/MultiPosition.tsx +++ b/packages/excalidraw/components/Stats/MultiPosition.tsx @@ -12,7 +12,12 @@ import type { } from "@excalidraw/element/types"; import StatsDragInput from "./DragInput"; -import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils"; +import { + getAtomicUnits, + getStepSizedValue, + isPropertyEditable, + updateSelectionBindings, +} from "./utils"; import { getElementsInAtomicUnit, moveElement } from "./utils"; import type { DragInputCallbackType } from "./DragInput"; @@ -39,6 +44,7 @@ const moveElements = ( originalElements: readonly ExcalidrawElement[], elementsMap: NonDeletedSceneElementsMap, originalElementsMap: ElementsMap, + scene: Scene, ) => { for (let i = 0; i < elements.length; i++) { const origElement = originalElements[i]; @@ -68,6 +74,8 @@ const moveElements = ( false, ); } + + updateSelectionBindings(elements, elementsMap, scene); }; const moveGroupTo = ( @@ -76,6 +84,7 @@ const moveGroupTo = ( originalElements: ExcalidrawElement[], elementsMap: NonDeletedSceneElementsMap, originalElementsMap: ElementsMap, + scene: Scene, ) => { const [x1, y1, ,] = getCommonBounds(originalElements); const offsetX = nextX - x1; @@ -112,6 +121,8 @@ const moveGroupTo = ( ); } } + + updateSelectionBindings(originalElements, elementsMap, scene); }; const handlePositionChange: DragInputCallbackType< @@ -152,6 +163,7 @@ const handlePositionChange: DragInputCallbackType< elementsInUnit.map((el) => el.original), elementsMap, originalElementsMap, + scene, ); } else { const origElement = elementsInUnit[0]?.original; @@ -204,6 +216,7 @@ const handlePositionChange: DragInputCallbackType< originalElements, elementsMap, originalElementsMap, + scene, ); scene.triggerUpdate(); diff --git a/packages/excalidraw/components/Stats/Position.tsx b/packages/excalidraw/components/Stats/Position.tsx index ad6ff16b2c..46a1f1036d 100644 --- a/packages/excalidraw/components/Stats/Position.tsx +++ b/packages/excalidraw/components/Stats/Position.tsx @@ -7,8 +7,6 @@ import { import { mutateElement } from "@excalidraw/element/mutateElement"; import { isImageElement } from "@excalidraw/element/typeChecks"; -import { getSuggestedBindingsForArrows } from "@excalidraw/element/binding"; - import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types"; import StatsDragInput from "./DragInput"; @@ -110,6 +108,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({ crop: nextCrop, }); + updateBindings(element, elementsMap, scene); + return; } @@ -128,13 +128,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({ crop: nextCrop, }); - setAppState({ - suggestedBindings: getSuggestedBindingsForArrows( - [origElement], - elementsMap, - originalAppState.zoom, - ), - }); + updateBindings(element, elementsMap, scene); return; } @@ -150,6 +144,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({ originalElementsMap, ); + updateBindings(origElement, elementsMap, scene); + return; } @@ -182,60 +178,15 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({ originalElementsMap, ); - if (origElement) { - const latestElement = elementsMap.get(origElement.id); - - if (latestElement) { - setAppState({ - suggestedBindings: getSuggestedBindingsForArrows( - [latestElement], - elementsMap, - originalAppState.zoom, - ), - }); - } - } + updateBindings(origElement, elementsMap, scene); }; const handleFinished: DragFinishedCallbackType<"x" | "y"> = ({ - originalElements, - originalAppState, - scene, - accumulatedChange, - property, setAppState, - setInputValue, -}): boolean => { - const elementsMap = scene.getNonDeletedElementsMap(); - const origElement = originalElements[0]; - - if (origElement) { - const latestElement = elementsMap.get(origElement.id); - - if (latestElement) { - setAppState({ - suggestedBindings: [], - }); - - const success = updateBindings( - latestElement, - elementsMap, - originalAppState.zoom, - ); - - if (!success) { - mutateElement(latestElement, { - [property]: latestElement[property] - accumulatedChange, - }); - - setInputValue(latestElement[property] - accumulatedChange); - } - - return false; - } - } - - return true; +}) => { + setAppState({ + suggestedBindings: [], + }); }; const Position = ({ property, element, scene, appState }: PositionProps) => { diff --git a/packages/excalidraw/components/Stats/utils.ts b/packages/excalidraw/components/Stats/utils.ts index bbba3df2bc..3926fb7466 100644 --- a/packages/excalidraw/components/Stats/utils.ts +++ b/packages/excalidraw/components/Stats/utils.ts @@ -16,8 +16,7 @@ import { } from "@excalidraw/element/groups"; import { - getOriginalBindingsIfStillCloseToArrowEnds, - unbindLinearElement, + bindOrUnbindLinearElement, updateBoundElements, } from "@excalidraw/element/binding"; @@ -30,6 +29,8 @@ import type { NonDeletedSceneElementsMap, } from "@excalidraw/element/types"; +import type Scene from "@excalidraw/excalidraw/scene/Scene"; + import type { AppState } from "../../types"; export type StatsInputProperty = @@ -203,30 +204,34 @@ export const getAtomicUnits = ( export const updateBindings = ( latestElement: ExcalidrawElement, elementsMap: NonDeletedSceneElementsMap, - zoom?: AppState["zoom"], -): boolean => { + scene: Scene, +) => { if (isBindingElement(latestElement)) { - const [start, end] = getOriginalBindingsIfStillCloseToArrowEnds( - latestElement, - elementsMap, - zoom, - ); - - if ( - (latestElement.startBinding && start) || - (latestElement.endBinding && end) - ) { - return false; + if (latestElement.startBinding || latestElement.endBinding) { + bindOrUnbindLinearElement(latestElement, null, null, elementsMap, scene); } + } else if (isBindableElement(latestElement)) { + updateBoundElements(latestElement, elementsMap); + } +}; - if (latestElement.startBinding && !start) { - unbindLinearElement(latestElement, "start"); - } +export const updateSelectionBindings = ( + elements: readonly ExcalidrawElement[], + elementsMap: NonDeletedSceneElementsMap, + scene: Scene, +) => { + for (const element of elements) { + // Only preserve bindings if the bound element is in the selection + if (isBindingElement(element)) { + if (elements.find((el) => el.id !== element.startBinding?.elementId)) { + bindOrUnbindLinearElement(element, null, "keep", elementsMap, scene); + } - if (latestElement.endBinding && !end) { - unbindLinearElement(latestElement, "end"); + if (elements.find((el) => el.id !== element.endBinding?.elementId)) { + bindOrUnbindLinearElement(element, "keep", null, elementsMap, scene); + } + } else if (isBindableElement(element)) { + updateBoundElements(element, elementsMap); } } - - return true; };