Stats unbonds arrows when used

This commit is contained in:
Mark Tolmacs 2025-04-02 21:09:57 +02:00
parent ccda36a0e3
commit eecabccf8d
8 changed files with 102 additions and 163 deletions

View file

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

View file

@ -3,20 +3,13 @@ import { degreesToRadians, radiansToDegrees } 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 { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
isArrowElement,
isBindableElement,
isElbowArrow,
} from "@excalidraw/element/typeChecks";
import { import { getSuggestedBindingsForArrows } from "@excalidraw/element/binding";
getSuggestedBindingsForArrows,
updateBoundElements,
} from "@excalidraw/element/binding";
import type { AppState } from "@excalidraw/excalidraw/types"; 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"; import type { ExcalidrawElement } from "@excalidraw/element/types";
@ -63,9 +56,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
angle: nextAngle, angle: nextAngle,
}); });
if (isBindableElement(latestElement)) { updateBindings(latestElement, elementsMap, scene);
updateBoundElements(latestElement, elementsMap);
}
const boundTextElement = getBoundTextElement(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) { if (boundTextElement && !isArrowElement(latestElement)) {
@ -92,9 +83,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
angle: nextAngle, angle: nextAngle,
}); });
if (isBindableElement(latestElement)) { updateBindings(latestElement, elementsMap, scene);
updateBoundElements(latestElement, elementsMap);
}
const boundTextElement = getBoundTextElement(latestElement, elementsMap); const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) { if (boundTextElement && !isArrowElement(latestElement)) {
@ -112,46 +101,11 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
}; };
const handleFinished: DragFinishedCallbackType<AngleProps["property"]> = ({ const handleFinished: DragFinishedCallbackType<AngleProps["property"]> = ({
originalElements,
originalAppState,
scene,
accumulatedChange,
setAppState, setAppState,
setInputValue,
}) => { }) => {
const elementsMap = scene.getNonDeletedElementsMap(); setAppState({
const origElement = originalElements[0]; suggestedBindings: [],
});
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;
}; };
const Angle = ({ element, scene, appState, property }: AngleProps) => { const Angle = ({ element, scene, appState, property }: AngleProps) => {

View file

@ -12,7 +12,7 @@ import { isImageElement } from "@excalidraw/element/typeChecks";
import type { ExcalidrawElement } from "@excalidraw/element/types"; import type { ExcalidrawElement } from "@excalidraw/element/types";
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";
@ -118,6 +118,9 @@ const handleDimensionChange: DragInputCallbackType<
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth), width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
}); });
updateBindings(element, elementsMap, scene);
return; return;
} }
@ -150,6 +153,8 @@ const handleDimensionChange: DragInputCallbackType<
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight), height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
}); });
updateBindings(element, elementsMap, scene);
return; return;
} }
@ -184,6 +189,8 @@ const handleDimensionChange: DragInputCallbackType<
}, },
); );
updateBindings(origElement, elementsMap, scene);
return; return;
} }
const changeInWidth = property === "width" ? accumulatedChange : 0; const changeInWidth = property === "width" ? accumulatedChange : 0;
@ -230,6 +237,8 @@ const handleDimensionChange: DragInputCallbackType<
shouldMaintainAspectRatio: keepAspectRatio, shouldMaintainAspectRatio: keepAspectRatio,
}, },
); );
updateBindings(origElement, elementsMap, scene);
} }
}; };

View file

@ -49,7 +49,7 @@ export type DragFinishedCallbackType<
accumulatedChange: number; accumulatedChange: number;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
setInputValue: (value: number) => void; setInputValue: (value: number) => void;
}) => boolean; }) => void;
interface StatsDragInputProps< interface StatsDragInputProps<
T extends StatsInputProperty, T extends StatsInputProperty,
@ -156,7 +156,7 @@ const StatsDragInput = <
setInputValue: (value) => setInputValue(String(value)), setInputValue: (value) => setInputValue(String(value)),
setAppState, setAppState,
}); });
const commit = dragFinishedCallback?.({ dragFinishedCallback?.({
originalElements: elements, originalElements: elements,
originalElementsMap, originalElementsMap,
scene, scene,
@ -166,11 +166,9 @@ const StatsDragInput = <
setAppState, setAppState,
setInputValue: (value) => setInputValue(String(value)), setInputValue: (value) => setInputValue(String(value)),
}); });
if (commit) { app.syncActionResult({
app.syncActionResult({ captureUpdate: CaptureUpdateAction.IMMEDIATELY,
captureUpdate: CaptureUpdateAction.IMMEDIATELY, });
});
}
} }
}; };
@ -315,27 +313,23 @@ const StatsDragInput = <
false, false,
); );
let commit = true;
if (originalElements !== null && originalElementsMap !== null) { if (originalElements !== null && originalElementsMap !== null) {
commit = dragFinishedCallback?.({
dragFinishedCallback?.({ originalElements,
originalElements, originalElementsMap,
originalElementsMap, scene,
scene, originalAppState,
originalAppState, property,
property, accumulatedChange,
accumulatedChange, setAppState,
setAppState, setInputValue: (value) => setInputValue(String(value)),
setInputValue: (value) => setInputValue(String(value)),
}) ?? true;
}
if (commit) {
app.syncActionResult({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
}); });
} }
app.syncActionResult({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
});
lastPointer = null; lastPointer = null;
accumulatedChange = 0; accumulatedChange = 0;
stepChange = 0; stepChange = 0;

View file

@ -24,7 +24,13 @@ import type {
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import DragInput from "./DragInput"; import DragInput from "./DragInput";
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils"; import {
getAtomicUnits,
getStepSizedValue,
isPropertyEditable,
updateBindings,
updateSelectionBindings,
} from "./utils";
import { getElementsInAtomicUnit } from "./utils"; import { getElementsInAtomicUnit } from "./utils";
import type { DragInputCallbackType } from "./DragInput"; import type { DragInputCallbackType } from "./DragInput";
@ -118,6 +124,7 @@ const resizeGroup = (
originalElements: ExcalidrawElement[], originalElements: ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
originalElementsMap: ElementsMap, originalElementsMap: ElementsMap,
scene: Scene,
) => { ) => {
// keep aspect ratio for groups // keep aspect ratio for groups
if (property === "width") { if (property === "width") {
@ -143,6 +150,8 @@ const resizeGroup = (
originalElementsMap, originalElementsMap,
); );
} }
updateSelectionBindings(originalElements, elementsMap, scene);
}; };
const handleDimensionChange: DragInputCallbackType< const handleDimensionChange: DragInputCallbackType<
@ -194,6 +203,7 @@ const handleDimensionChange: DragInputCallbackType<
originalElements, originalElements,
elementsMap, elementsMap,
originalElementsMap, originalElementsMap,
scene,
); );
} else { } else {
const [el] = elementsInUnit; const [el] = elementsInUnit;
@ -242,6 +252,8 @@ const handleDimensionChange: DragInputCallbackType<
shouldInformMutation: false, shouldInformMutation: false,
}, },
); );
updateBindings(latestElement, elementsMap, scene);
} }
} }
} }
@ -301,6 +313,7 @@ const handleDimensionChange: DragInputCallbackType<
originalElements, originalElements,
elementsMap, elementsMap,
originalElementsMap, originalElementsMap,
scene,
); );
} else { } else {
const [el] = elementsInUnit; const [el] = elementsInUnit;

View file

@ -12,7 +12,12 @@ import type {
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import StatsDragInput from "./DragInput"; import StatsDragInput from "./DragInput";
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils"; import {
getAtomicUnits,
getStepSizedValue,
isPropertyEditable,
updateSelectionBindings,
} from "./utils";
import { getElementsInAtomicUnit, moveElement } from "./utils"; import { getElementsInAtomicUnit, moveElement } from "./utils";
import type { DragInputCallbackType } from "./DragInput"; import type { DragInputCallbackType } from "./DragInput";
@ -39,6 +44,7 @@ const moveElements = (
originalElements: readonly ExcalidrawElement[], originalElements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
originalElementsMap: ElementsMap, originalElementsMap: ElementsMap,
scene: Scene,
) => { ) => {
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
const origElement = originalElements[i]; const origElement = originalElements[i];
@ -68,6 +74,8 @@ const moveElements = (
false, false,
); );
} }
updateSelectionBindings(elements, elementsMap, scene);
}; };
const moveGroupTo = ( const moveGroupTo = (
@ -76,6 +84,7 @@ const moveGroupTo = (
originalElements: ExcalidrawElement[], originalElements: ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
originalElementsMap: ElementsMap, originalElementsMap: ElementsMap,
scene: Scene,
) => { ) => {
const [x1, y1, ,] = getCommonBounds(originalElements); const [x1, y1, ,] = getCommonBounds(originalElements);
const offsetX = nextX - x1; const offsetX = nextX - x1;
@ -112,6 +121,8 @@ const moveGroupTo = (
); );
} }
} }
updateSelectionBindings(originalElements, elementsMap, scene);
}; };
const handlePositionChange: DragInputCallbackType< const handlePositionChange: DragInputCallbackType<
@ -152,6 +163,7 @@ const handlePositionChange: DragInputCallbackType<
elementsInUnit.map((el) => el.original), elementsInUnit.map((el) => el.original),
elementsMap, elementsMap,
originalElementsMap, originalElementsMap,
scene,
); );
} else { } else {
const origElement = elementsInUnit[0]?.original; const origElement = elementsInUnit[0]?.original;
@ -204,6 +216,7 @@ const handlePositionChange: DragInputCallbackType<
originalElements, originalElements,
elementsMap, elementsMap,
originalElementsMap, originalElementsMap,
scene,
); );
scene.triggerUpdate(); scene.triggerUpdate();

View file

@ -7,8 +7,6 @@ import {
import { mutateElement } from "@excalidraw/element/mutateElement"; import { mutateElement } from "@excalidraw/element/mutateElement";
import { isImageElement } from "@excalidraw/element/typeChecks"; import { isImageElement } from "@excalidraw/element/typeChecks";
import { getSuggestedBindingsForArrows } from "@excalidraw/element/binding";
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types"; import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
import StatsDragInput from "./DragInput"; import StatsDragInput from "./DragInput";
@ -110,6 +108,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
crop: nextCrop, crop: nextCrop,
}); });
updateBindings(element, elementsMap, scene);
return; return;
} }
@ -128,13 +128,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
crop: nextCrop, crop: nextCrop,
}); });
setAppState({ updateBindings(element, elementsMap, scene);
suggestedBindings: getSuggestedBindingsForArrows(
[origElement],
elementsMap,
originalAppState.zoom,
),
});
return; return;
} }
@ -150,6 +144,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
originalElementsMap, originalElementsMap,
); );
updateBindings(origElement, elementsMap, scene);
return; return;
} }
@ -182,60 +178,15 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
originalElementsMap, originalElementsMap,
); );
if (origElement) { updateBindings(origElement, elementsMap, scene);
const latestElement = elementsMap.get(origElement.id);
if (latestElement) {
setAppState({
suggestedBindings: getSuggestedBindingsForArrows(
[latestElement],
elementsMap,
originalAppState.zoom,
),
});
}
}
}; };
const handleFinished: DragFinishedCallbackType<"x" | "y"> = ({ const handleFinished: DragFinishedCallbackType<"x" | "y"> = ({
originalElements,
originalAppState,
scene,
accumulatedChange,
property,
setAppState, setAppState,
setInputValue, }) => {
}): boolean => { setAppState({
const elementsMap = scene.getNonDeletedElementsMap(); suggestedBindings: [],
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;
}; };
const Position = ({ property, element, scene, appState }: PositionProps) => { const Position = ({ property, element, scene, appState }: PositionProps) => {

View file

@ -16,8 +16,7 @@ import {
} from "@excalidraw/element/groups"; } from "@excalidraw/element/groups";
import { import {
getOriginalBindingsIfStillCloseToArrowEnds, bindOrUnbindLinearElement,
unbindLinearElement,
updateBoundElements, updateBoundElements,
} from "@excalidraw/element/binding"; } from "@excalidraw/element/binding";
@ -30,6 +29,8 @@ import type {
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import type { AppState } from "../../types"; import type { AppState } from "../../types";
export type StatsInputProperty = export type StatsInputProperty =
@ -203,30 +204,34 @@ export const getAtomicUnits = (
export const updateBindings = ( export const updateBindings = (
latestElement: ExcalidrawElement, latestElement: ExcalidrawElement,
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"], scene: Scene,
): boolean => { ) => {
if (isBindingElement(latestElement)) { if (isBindingElement(latestElement)) {
const [start, end] = getOriginalBindingsIfStillCloseToArrowEnds( if (latestElement.startBinding || latestElement.endBinding) {
latestElement, bindOrUnbindLinearElement(latestElement, null, null, elementsMap, scene);
elementsMap,
zoom,
);
if (
(latestElement.startBinding && start) ||
(latestElement.endBinding && end)
) {
return false;
} }
} else if (isBindableElement(latestElement)) {
updateBoundElements(latestElement, elementsMap);
}
};
if (latestElement.startBinding && !start) { export const updateSelectionBindings = (
unbindLinearElement(latestElement, "start"); 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) { if (elements.find((el) => el.id !== element.endBinding?.elementId)) {
unbindLinearElement(latestElement, "end"); bindOrUnbindLinearElement(element, "keep", null, elementsMap, scene);
}
} else if (isBindableElement(element)) {
updateBoundElements(element, elementsMap);
} }
} }
return true;
}; };