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

View file

@ -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<AngleProps["property"]> = ({
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<AngleProps["property"]> = ({
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<AngleProps["property"]> = ({
};
const handleFinished: DragFinishedCallbackType<AngleProps["property"]> = ({
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;
};
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 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);
}
};

View file

@ -49,7 +49,7 @@ export type DragFinishedCallbackType<
accumulatedChange: number;
setAppState: React.Component<any, AppState>["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,12 +166,10 @@ const StatsDragInput = <
setAppState,
setInputValue: (value) => setInputValue(String(value)),
});
if (commit) {
app.syncActionResult({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
});
}
}
};
const callbacksRef = useRef<
@ -315,9 +313,7 @@ const StatsDragInput = <
false,
);
let commit = true;
if (originalElements !== null && originalElementsMap !== null) {
commit =
dragFinishedCallback?.({
originalElements,
originalElementsMap,
@ -327,14 +323,12 @@ const StatsDragInput = <
accumulatedChange,
setAppState,
setInputValue: (value) => setInputValue(String(value)),
}) ?? true;
});
}
if (commit) {
app.syncActionResult({
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
});
}
lastPointer = null;
accumulatedChange = 0;

View file

@ -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;

View file

@ -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();

View file

@ -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;
};
const Position = ({ property, element, scene, appState }: PositionProps) => {

View file

@ -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);
}
};
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 (elements.find((el) => el.id !== element.endBinding?.elementId)) {
bindOrUnbindLinearElement(element, "keep", null, elementsMap, scene);
}
} else if (isBindableElement(element)) {
updateBoundElements(element, elementsMap);
}
if (latestElement.startBinding && !start) {
unbindLinearElement(latestElement, "start");
}
if (latestElement.endBinding && !end) {
unbindLinearElement(latestElement, "end");
}
}
return true;
};