mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: Orthogonal (elbow) arrows for diagramming (#8299)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
a133a70e87
commit
15e019706d
69 changed files with 5415 additions and 1144 deletions
|
@ -1,6 +1,6 @@
|
|||
import { mutateElement } from "../../element/mutateElement";
|
||||
import { getBoundTextElement } from "../../element/textElement";
|
||||
import { isArrowElement } from "../../element/typeChecks";
|
||||
import { isArrowElement, isElbowArrow } from "../../element/typeChecks";
|
||||
import type { ExcalidrawElement } from "../../element/types";
|
||||
import { degreeToRadian, radianToDegree } from "../../math";
|
||||
import { angleIcon } from "../icons";
|
||||
|
@ -27,8 +27,9 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
scene,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
if (origElement) {
|
||||
if (origElement && !isElbowArrow(origElement)) {
|
||||
const latestElement = elementsMap.get(origElement.id);
|
||||
if (!latestElement) {
|
||||
return;
|
||||
|
@ -39,7 +40,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap);
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
|
@ -65,7 +66,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap);
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
|
|
|
@ -31,6 +31,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
scene,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
if (origElement) {
|
||||
const keepAspectRatio =
|
||||
|
@ -61,6 +62,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
keepAspectRatio,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
);
|
||||
|
||||
return;
|
||||
|
@ -103,6 +106,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
keepAspectRatio,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -25,9 +25,9 @@ export type DragInputCallbackType<
|
|||
originalElementsMap: ElementsMap;
|
||||
shouldKeepAspectRatio: boolean;
|
||||
shouldChangeByStepSize: boolean;
|
||||
scene: Scene;
|
||||
nextValue?: number;
|
||||
property: P;
|
||||
scene: Scene;
|
||||
originalAppState: AppState;
|
||||
}) => void;
|
||||
|
||||
|
@ -122,9 +122,9 @@ const StatsDragInput = <
|
|||
originalElementsMap: app.scene.getNonDeletedElementsMap(),
|
||||
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
||||
shouldChangeByStepSize: false,
|
||||
scene,
|
||||
nextValue: rounded,
|
||||
property,
|
||||
scene,
|
||||
originalAppState: appState,
|
||||
});
|
||||
app.syncActionResult({ storeAction: StoreAction.CAPTURE });
|
||||
|
|
|
@ -66,8 +66,10 @@ const resizeElementInGroup = (
|
|||
origElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
||||
const { width: oldWidth, height: oldHeight } = latestElement;
|
||||
|
||||
mutateElement(latestElement, updates, false);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
|
@ -76,8 +78,8 @@ const resizeElementInGroup = (
|
|||
);
|
||||
if (boundTextElement) {
|
||||
const newFontSize = boundTextElement.fontSize * scale;
|
||||
updateBoundElements(latestElement, elementsMap, {
|
||||
newSize: { width: updates.width, height: updates.height },
|
||||
updateBoundElements(latestElement, elementsMap, scene, {
|
||||
oldSize: { width: oldWidth, height: oldHeight },
|
||||
});
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||
|
@ -109,6 +111,7 @@ const resizeGroup = (
|
|||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
// keep aspect ratio for groups
|
||||
if (property === "width") {
|
||||
|
@ -132,6 +135,7 @@ const resizeGroup = (
|
|||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -149,6 +153,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
property,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const atomicUnits = getAtomicUnits(originalElements, originalAppState);
|
||||
if (nextValue !== undefined) {
|
||||
for (const atomicUnit of atomicUnits) {
|
||||
|
@ -185,6 +190,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
|
@ -227,6 +233,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
false,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
@ -288,6 +296,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
|
@ -320,7 +329,15 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
||||
|
||||
resizeElement(nextWidth, nextHeight, false, origElement, elementsMap);
|
||||
resizeElement(
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
false,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../../element/types";
|
||||
import { rotate } from "../../math";
|
||||
|
@ -33,6 +34,7 @@ const moveElements = (
|
|||
originalElements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const origElement = originalElements[i];
|
||||
|
@ -60,6 +62,8 @@ const moveElements = (
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
|
@ -71,6 +75,7 @@ const moveGroupTo = (
|
|||
nextY: number,
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
|
@ -106,6 +111,8 @@ const moveGroupTo = (
|
|||
topLeftY + offsetY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
|
@ -126,6 +133,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
|
||||
if (nextValue !== undefined) {
|
||||
for (const atomicUnit of getAtomicUnits(
|
||||
|
@ -150,6 +158,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||
newTopLeftY,
|
||||
elementsInUnit.map((el) => el.original),
|
||||
elementsMap,
|
||||
elements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
|
@ -180,6 +189,8 @@ const handlePositionChange: DragInputCallbackType<
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
|
@ -206,6 +217,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
|
||||
scene.triggerUpdate();
|
||||
|
|
|
@ -26,6 +26,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
scene,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
const [cx, cy] = [
|
||||
origElement.x + origElement.width / 2,
|
||||
|
@ -47,6 +48,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
return;
|
||||
|
@ -78,6 +81,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
};
|
||||
|
@ -104,9 +109,9 @@ const Position = ({
|
|||
label={property === "x" ? "X" : "Y"}
|
||||
elements={[element]}
|
||||
dragInputCallback={handlePositionChange}
|
||||
scene={scene}
|
||||
value={value}
|
||||
property={property}
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ import type Scene from "../../scene/Scene";
|
|||
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
||||
import { getAtomicUnits } from "./utils";
|
||||
import { STATS_PANELS } from "../../constants";
|
||||
import { isElbowArrow } from "../../element/typeChecks";
|
||||
|
||||
interface StatsProps {
|
||||
scene: Scene;
|
||||
|
@ -209,12 +210,14 @@ export const StatsInner = memo(
|
|||
scene={scene}
|
||||
appState={appState}
|
||||
/>
|
||||
<Angle
|
||||
property="angle"
|
||||
element={singleElement}
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
/>
|
||||
{!isElbowArrow(singleElement) && (
|
||||
<Angle
|
||||
property="angle"
|
||||
element={singleElement}
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
/>
|
||||
)}
|
||||
<FontSize
|
||||
property="fontSize"
|
||||
element={singleElement}
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
isInGroup,
|
||||
} from "../../groups";
|
||||
import { rotate } from "../../math";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
import { getFontString } from "../../utils";
|
||||
|
||||
|
@ -124,6 +125,8 @@ export const resizeElement = (
|
|||
keepAspectRatio: boolean,
|
||||
origElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
shouldInformMutation = true,
|
||||
) => {
|
||||
const latestElement = elementsMap.get(origElement.id);
|
||||
|
@ -146,6 +149,8 @@ export const resizeElement = (
|
|||
nextHeight = Math.max(nextHeight, minHeight);
|
||||
}
|
||||
|
||||
const { width: oldWidth, height: oldHeight } = latestElement;
|
||||
|
||||
mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
|
@ -164,7 +169,7 @@ export const resizeElement = (
|
|||
},
|
||||
shouldInformMutation,
|
||||
);
|
||||
updateBindings(latestElement, elementsMap, {
|
||||
updateBindings(latestElement, elementsMap, elements, scene, {
|
||||
newSize: {
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
|
@ -193,6 +198,10 @@ export const resizeElement = (
|
|||
}
|
||||
}
|
||||
|
||||
updateBoundElements(latestElement, elementsMap, scene, {
|
||||
oldSize: { width: oldWidth, height: oldHeight },
|
||||
});
|
||||
|
||||
if (boundTextElement && boundTextFont) {
|
||||
mutateElement(boundTextElement, {
|
||||
fontSize: boundTextFont.fontSize,
|
||||
|
@ -206,6 +215,8 @@ export const moveElement = (
|
|||
newTopLeftY: number,
|
||||
originalElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
originalElementsMap: ElementsMap,
|
||||
shouldInformMutation = true,
|
||||
) => {
|
||||
|
@ -244,7 +255,7 @@ export const moveElement = (
|
|||
},
|
||||
shouldInformMutation,
|
||||
);
|
||||
updateBindings(latestElement, elementsMap);
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
originalElement,
|
||||
|
@ -288,14 +299,23 @@ export const getAtomicUnits = (
|
|||
export const updateBindings = (
|
||||
latestElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
},
|
||||
) => {
|
||||
if (isLinearElement(latestElement)) {
|
||||
bindOrUnbindLinearElements([latestElement], elementsMap, true, []);
|
||||
bindOrUnbindLinearElements(
|
||||
[latestElement],
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
true,
|
||||
[],
|
||||
);
|
||||
} else {
|
||||
updateBoundElements(latestElement, elementsMap, options);
|
||||
updateBoundElements(latestElement, elementsMap, scene, options);
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue