mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Stats: Angle setting now works properly and resets on unconnected bindings
This commit is contained in:
parent
4efa6f69e5
commit
88d4c4fe8d
6 changed files with 155 additions and 61 deletions
|
@ -3,20 +3,36 @@ import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
|||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBindableElement,
|
||||
isElbowArrow,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import type { Degrees } from "@excalidraw/math";
|
||||
import {
|
||||
getSuggestedBindingsForArrows,
|
||||
updateBoundElements,
|
||||
} from "@excalidraw/element/binding";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { Degrees, Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { angleIcon } from "../icons";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type {
|
||||
DragFinishedCallbackType,
|
||||
DragInputCallbackType,
|
||||
} from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface AngleProps {
|
||||
element: ExcalidrawElement;
|
||||
|
@ -33,6 +49,8 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
shouldChangeByStepSize,
|
||||
nextValue,
|
||||
scene,
|
||||
setAppState,
|
||||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const origElement = originalElements[0];
|
||||
|
@ -47,13 +65,24 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap);
|
||||
|
||||
if (isBindableElement(latestElement)) {
|
||||
updateBoundElements(latestElement, elementsMap);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
|
||||
setAppState({
|
||||
suggestedBindings: getSuggestedBindingsForArrows(
|
||||
[latestElement] as NonDeletedExcalidrawElement[],
|
||||
elementsMap,
|
||||
originalAppState.zoom,
|
||||
),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -73,12 +102,62 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap);
|
||||
|
||||
if (isBindableElement(latestElement)) {
|
||||
updateBoundElements(latestElement, elementsMap);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
|
||||
setAppState({
|
||||
suggestedBindings: getSuggestedBindingsForArrows(
|
||||
[latestElement] as NonDeletedExcalidrawElement[],
|
||||
elementsMap,
|
||||
originalAppState.zoom,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinished: DragFinishedCallbackType = ({
|
||||
originalElements,
|
||||
originalAppState,
|
||||
scene,
|
||||
accumulatedChange,
|
||||
setAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const origElement = originalElements[0];
|
||||
|
||||
if (origElement) {
|
||||
const latestElement = elementsMap.get(origElement.id);
|
||||
|
||||
if (latestElement) {
|
||||
updateBindings(latestElement, elementsMap, originalAppState.zoom, () => {
|
||||
const revertAngle = (latestElement.angle -
|
||||
degreesToRadians(accumulatedChange as Degrees)) as Radians;
|
||||
|
||||
mutateElement(latestElement, {
|
||||
angle: revertAngle,
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
latestElement,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: revertAngle });
|
||||
}
|
||||
|
||||
setAppState({
|
||||
suggestedBindings: [],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -90,6 +169,7 @@ const Angle = ({ element, scene, appState, property }: AngleProps) => {
|
|||
value={Math.round((radiansToDegrees(element.angle) % 360) * 100) / 100}
|
||||
elements={[element]}
|
||||
dragInputCallback={handleDegreeChange}
|
||||
dragFinishedCallback={handleFinished}
|
||||
editable={isPropertyEditable(element, "angle")}
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
|||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { CaptureUpdateAction } from "../../store";
|
||||
import { useApp } from "../App";
|
||||
import { useApp, useExcalidrawSetAppState } from "../App";
|
||||
import { InlineIcon } from "../InlineIcon";
|
||||
|
||||
import { SMALLEST_DELTA } from "./utils";
|
||||
|
@ -34,6 +34,16 @@ export type DragInputCallbackType<
|
|||
property: P;
|
||||
originalAppState: AppState;
|
||||
setInputValue: (value: number) => void;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
}) => void;
|
||||
|
||||
export type DragFinishedCallbackType<E = ExcalidrawElement> = (props: {
|
||||
originalElements: readonly E[];
|
||||
originalElementsMap: ElementsMap;
|
||||
scene: Scene;
|
||||
originalAppState: AppState;
|
||||
accumulatedChange: number;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
}) => void;
|
||||
|
||||
interface StatsDragInputProps<
|
||||
|
@ -47,6 +57,7 @@ interface StatsDragInputProps<
|
|||
editable?: boolean;
|
||||
shouldKeepAspectRatio?: boolean;
|
||||
dragInputCallback: DragInputCallbackType<T, E>;
|
||||
dragFinishedCallback?: DragFinishedCallbackType<E>;
|
||||
property: T;
|
||||
scene: Scene;
|
||||
appState: AppState;
|
||||
|
@ -61,6 +72,7 @@ const StatsDragInput = <
|
|||
label,
|
||||
icon,
|
||||
dragInputCallback,
|
||||
dragFinishedCallback,
|
||||
value,
|
||||
elements,
|
||||
editable = true,
|
||||
|
@ -71,6 +83,7 @@ const StatsDragInput = <
|
|||
sensitivity = 1,
|
||||
}: StatsDragInputProps<T, E>) => {
|
||||
const app = useApp();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const labelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -135,6 +148,7 @@ const StatsDragInput = <
|
|||
property,
|
||||
originalAppState: appState,
|
||||
setInputValue: (value) => setInputValue(String(value)),
|
||||
setAppState,
|
||||
});
|
||||
app.syncActionResult({
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
|
@ -262,6 +276,7 @@ const StatsDragInput = <
|
|||
scene,
|
||||
originalAppState,
|
||||
setInputValue: (value) => setInputValue(String(value)),
|
||||
setAppState,
|
||||
});
|
||||
|
||||
stepChange = 0;
|
||||
|
@ -282,6 +297,17 @@ const StatsDragInput = <
|
|||
false,
|
||||
);
|
||||
|
||||
if (originalElements !== null && originalElementsMap !== null) {
|
||||
dragFinishedCallback?.({
|
||||
originalElements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
originalAppState,
|
||||
accumulatedChange,
|
||||
setAppState,
|
||||
});
|
||||
}
|
||||
|
||||
app.syncActionResult({
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@ import { getCommonBounds } from "@excalidraw/element/bounds";
|
|||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
|
@ -40,7 +39,6 @@ const moveElements = (
|
|||
originalElements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const origElement = originalElements[i];
|
||||
|
@ -66,8 +64,6 @@ const moveElements = (
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
|
@ -79,9 +75,7 @@ const moveGroupTo = (
|
|||
nextY: number,
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const [x1, y1, ,] = getCommonBounds(originalElements);
|
||||
const offsetX = nextX - x1;
|
||||
|
@ -113,8 +107,6 @@ const moveGroupTo = (
|
|||
topLeftY + offsetY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
|
@ -135,7 +127,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
|
||||
if (nextValue !== undefined) {
|
||||
for (const atomicUnit of getAtomicUnits(
|
||||
|
@ -160,9 +151,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||
newTopLeftY,
|
||||
elementsInUnit.map((el) => el.original),
|
||||
elementsMap,
|
||||
elements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const origElement = elementsInUnit[0]?.original;
|
||||
|
@ -189,8 +178,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
|
@ -217,7 +204,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
|
||||
scene.triggerUpdate();
|
||||
|
|
|
@ -38,7 +38,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
const [cx, cy] = [
|
||||
origElement.x + origElement.width / 2,
|
||||
|
@ -134,8 +133,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
return;
|
||||
|
@ -167,8 +164,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
};
|
||||
|
|
|
@ -128,27 +128,19 @@ describe("binding with linear elements", () => {
|
|||
restoreOriginalGetBoundingClientRect();
|
||||
});
|
||||
|
||||
// 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 even" +
|
||||
" on small position change",
|
||||
async () => {
|
||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
||||
const inputX = UI.queryStatsProperty("X")?.querySelector(
|
||||
".drag-input",
|
||||
) as HTMLInputElement;
|
||||
it("should remain bound to linear element even on small position change", async () => {
|
||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
||||
const inputX = UI.queryStatsProperty("X")?.querySelector(
|
||||
".drag-input",
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
expect(inputX).not.toBeNull();
|
||||
UI.updateInput(inputX, String("204"));
|
||||
expect(linear.startBinding).toBe(null);
|
||||
},
|
||||
);
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
expect(inputX).not.toBeNull();
|
||||
UI.updateInput(inputX, String("204"));
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
});
|
||||
|
||||
// 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 () => {
|
||||
it("should 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",
|
||||
|
@ -156,7 +148,7 @@ describe("binding with linear elements", () => {
|
|||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
UI.updateInput(inputAngle, String("1"));
|
||||
expect(linear.startBinding).toBe(null);
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
});
|
||||
|
||||
it("should unbind linear element on large position change", async () => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from "@excalidraw/element/groups";
|
||||
|
||||
import {
|
||||
getOriginalBindingsIfStillCloseToArrowEnds,
|
||||
unbindLinearElement,
|
||||
updateBoundElements,
|
||||
} from "@excalidraw/element/binding";
|
||||
|
@ -29,7 +30,6 @@ import type {
|
|||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
export type StatsInputProperty =
|
||||
|
@ -122,8 +122,6 @@ export const moveElement = (
|
|||
newTopLeftY: number,
|
||||
originalElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
originalElementsMap: ElementsMap,
|
||||
shouldInformMutation = true,
|
||||
) => {
|
||||
|
@ -159,7 +157,9 @@ export const moveElement = (
|
|||
shouldInformMutation,
|
||||
);
|
||||
|
||||
updateBindings(latestElement, elementsMap);
|
||||
if (isBindableElement(latestElement)) {
|
||||
updateBoundElements(latestElement, elementsMap);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
originalElement,
|
||||
|
@ -203,18 +203,33 @@ export const getAtomicUnits = (
|
|||
export const updateBindings = (
|
||||
latestElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
},
|
||||
zoom?: AppState["zoom"],
|
||||
remainedBound?: () => void,
|
||||
) => {
|
||||
if (isBindingElement(latestElement)) {
|
||||
if (latestElement.startBinding) {
|
||||
unbindLinearElement(latestElement, "start");
|
||||
const [start, end] = getOriginalBindingsIfStillCloseToArrowEnds(
|
||||
latestElement,
|
||||
elementsMap,
|
||||
zoom,
|
||||
);
|
||||
|
||||
if (
|
||||
(latestElement.startBinding && start) ||
|
||||
(latestElement.endBinding && end)
|
||||
) {
|
||||
remainedBound?.();
|
||||
} else {
|
||||
if (latestElement.startBinding && !start) {
|
||||
unbindLinearElement(latestElement, "start");
|
||||
}
|
||||
|
||||
if (latestElement.endBinding && !end) {
|
||||
unbindLinearElement(latestElement, "end");
|
||||
}
|
||||
}
|
||||
if (latestElement.endBinding) {
|
||||
unbindLinearElement(latestElement, "end");
|
||||
}
|
||||
} else if (isBindableElement(latestElement)) {
|
||||
updateBoundElements(latestElement, elementsMap, options);
|
||||
|
||||
// else if (end) {
|
||||
// updateBoundElements(end, elementsMap);
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue