feat: Orthogonal (elbow) arrows for diagramming (#8299)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Márk Tolmács 2024-08-01 18:39:03 +02:00 committed by GitHub
parent a133a70e87
commit 15e019706d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 5415 additions and 1144 deletions

View file

@ -84,9 +84,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "ellipse-1",
"fixedPoint": null,
"focus": -0.008153707962747813,
"gap": 1,
},
@ -117,6 +119,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"startArrowhead": null,
"startBinding": {
"elementId": "id47",
"fixedPoint": null,
"focus": -0.08139534883720931,
"gap": 1,
},
@ -139,9 +142,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "ellipse-1",
"fixedPoint": null,
"focus": 0.10666666666666667,
"gap": 3.834326468444573,
},
@ -172,6 +177,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"startArrowhead": null,
"startBinding": {
"elementId": "diamond-1",
"fixedPoint": null,
"focus": 0,
"gap": 1,
},
@ -328,9 +334,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "text-2",
"fixedPoint": null,
"focus": 0,
"gap": 205,
},
@ -361,6 +369,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"startArrowhead": null,
"startBinding": {
"elementId": "text-1",
"fixedPoint": null,
"focus": 0,
"gap": 1,
},
@ -429,9 +438,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id40",
"fixedPoint": null,
"focus": 0,
"gap": 1,
},
@ -462,6 +473,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"startArrowhead": null,
"startBinding": {
"elementId": "id39",
"fixedPoint": null,
"focus": 0,
"gap": 1,
},
@ -604,9 +616,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id44",
"fixedPoint": null,
"focus": 0,
"gap": 1,
},
@ -637,6 +651,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"startArrowhead": null,
"startBinding": {
"elementId": "id43",
"fixedPoint": null,
"focus": 0,
"gap": 1,
},
@ -824,6 +839,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -871,6 +887,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"elbowed": false,
"endArrowhead": "triangle",
"endBinding": null,
"fillStyle": "solid",
@ -1463,9 +1480,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "Alice",
"fixedPoint": null,
"focus": 0,
"gap": 5.299874999999986,
},
@ -1498,6 +1517,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"startArrowhead": null,
"startBinding": {
"elementId": "Bob",
"fixedPoint": null,
"focus": 0,
"gap": 1,
},
@ -1525,9 +1545,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "B",
"fixedPoint": null,
"focus": 0,
"gap": 1,
},
@ -1556,6 +1578,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"startArrowhead": null,
"startBinding": {
"elementId": "Bob",
"fixedPoint": null,
"focus": 0,
"gap": 1,
},
@ -1837,6 +1860,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -1889,6 +1913,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -1941,6 +1966,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
@ -1993,6 +2019,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
},
],
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",

View file

@ -1,4 +1,5 @@
import type {
ExcalidrawArrowElement,
ExcalidrawElement,
ExcalidrawElementType,
ExcalidrawLinearElement,
@ -24,6 +25,7 @@ import {
} from "../element";
import {
isArrowElement,
isElbowArrow,
isLinearElement,
isTextElement,
isUsingAdaptiveRadius,
@ -92,11 +94,21 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
return DEFAULT_FONT_FAMILY;
};
const repairBinding = (binding: PointBinding | null) => {
const repairBinding = (
element: ExcalidrawLinearElement,
binding: PointBinding | null,
): PointBinding | null => {
if (!binding) {
return null;
}
return { ...binding, focus: binding.focus || 0 };
return {
...binding,
focus: binding.focus || 0,
fixedPoint: isElbowArrow(element)
? binding.fixedPoint ?? ([0, 0] as [number, number])
: null,
};
};
const restoreElementWithProperties = <
@ -242,11 +254,7 @@ const restoreElement = (
// @ts-ignore LEGACY type
// eslint-disable-next-line no-fallthrough
case "draw":
case "arrow": {
const {
startArrowhead = null,
endArrowhead = element.type === "arrow" ? "arrow" : null,
} = element;
const { startArrowhead = null, endArrowhead = null } = element;
let x = element.x;
let y = element.y;
let points = // migrate old arrow model to new one
@ -266,8 +274,8 @@ const restoreElement = (
(element.type as ExcalidrawElementType | "draw") === "draw"
? "line"
: element.type,
startBinding: repairBinding(element.startBinding),
endBinding: repairBinding(element.endBinding),
startBinding: repairBinding(element, element.startBinding),
endBinding: repairBinding(element, element.endBinding),
lastCommittedPoint: null,
startArrowhead,
endArrowhead,
@ -276,6 +284,36 @@ const restoreElement = (
y,
...getSizeFromPoints(points),
});
case "arrow": {
const { startArrowhead = null, endArrowhead = "arrow" } = element;
let x = element.x;
let y = element.y;
let points = // migrate old arrow model to new one
!Array.isArray(element.points) || element.points.length < 2
? [
[0, 0],
[element.width, element.height],
]
: element.points;
if (points[0][0] !== 0 || points[0][1] !== 0) {
({ points, x, y } = LinearElementEditor.getNormalizedPoints(element));
}
// TODO: Separate arrow from linear element
return restoreElementWithProperties(element as ExcalidrawArrowElement, {
type: element.type,
startBinding: repairBinding(element, element.startBinding),
endBinding: repairBinding(element, element.endBinding),
lastCommittedPoint: null,
startArrowhead,
endArrowhead,
points,
x,
y,
elbowed: (element as ExcalidrawArrowElement).elbowed,
...getSizeFromPoints(points),
});
}
// generic elements

View file

@ -771,6 +771,7 @@ describe("Test Transform", () => {
const [arrow, rect] = excalidrawElements;
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
elementId: "rect-1",
fixedPoint: null,
focus: 0,
gap: 205,
});

View file

@ -13,6 +13,7 @@ import {
import { bindLinearElement } from "../element/binding";
import type { ElementConstructorOpts } from "../element/newElement";
import {
newArrowElement,
newFrameElement,
newImageElement,
newMagicFrameElement,
@ -51,6 +52,7 @@ import { getSizeFromPoints } from "../points";
import { randomId } from "../random";
import { syncInvalidIndices } from "../fractionalIndex";
import { getLineHeight } from "../fonts";
import { isArrowElement } from "../element/typeChecks";
export type ValidLinearElement = {
type: "arrow" | "line";
@ -545,7 +547,7 @@ export const convertToExcalidrawElements = (
case "arrow": {
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
excalidrawElement = newLinearElement({
excalidrawElement = newArrowElement({
width,
height,
endArrowhead: "arrow",
@ -554,6 +556,7 @@ export const convertToExcalidrawElements = (
[width, height],
],
...element,
type: "arrow",
});
Object.assign(
@ -655,7 +658,7 @@ export const convertToExcalidrawElements = (
elementStore.add(container);
elementStore.add(text);
if (container.type === "arrow") {
if (isArrowElement(container)) {
const originalStart =
element.type === "arrow" ? element?.start : undefined;
const originalEnd =
@ -674,7 +677,7 @@ export const convertToExcalidrawElements = (
}
const { linearElement, startBoundElement, endBoundElement } =
bindLinearElementToElement(
container as ExcalidrawArrowElement,
container,
originalStart,
originalEnd,
elementStore,