From 7541fadf9cd0cc8413f7d0184a63b496ea65f80a Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Wed, 23 Apr 2025 17:59:03 +1000 Subject: [PATCH] safe conversion between line, sharp, curved, and elbow --- packages/element/src/mutateElement.ts | 233 ++++++++++- packages/element/src/typeChecks.ts | 87 +--- packages/element/src/types.ts | 8 +- .../actions/actionToggleShapeSwitch.tsx | 7 +- packages/excalidraw/components/App.tsx | 6 +- .../excalidraw/components/ShapeSwitch.tsx | 374 ++++++++++-------- 6 files changed, 466 insertions(+), 249 deletions(-) diff --git a/packages/element/src/mutateElement.ts b/packages/element/src/mutateElement.ts index 4e4bc60f6..61b908cd3 100644 --- a/packages/element/src/mutateElement.ts +++ b/packages/element/src/mutateElement.ts @@ -3,12 +3,16 @@ import { randomInteger, getUpdatedTimestamp, toBrandedType, + isDevEnv, + ROUNDNESS, } from "@excalidraw/common"; // TODO: remove direct dependency on the scene, should be passed in or injected instead // eslint-disable-next-line @typescript-eslint/no-restricted-imports import Scene from "@excalidraw/excalidraw/scene/Scene"; +import type { AppClassProperties } from "@excalidraw/excalidraw/types"; + import type { Radians } from "@excalidraw/math"; import type { Mutable } from "@excalidraw/common/utility-types"; @@ -16,9 +20,26 @@ import type { Mutable } from "@excalidraw/common/utility-types"; import { ShapeCache } from "./ShapeCache"; import { updateElbowArrowPoints } from "./elbowArrow"; -import { isElbowArrow } from "./typeChecks"; +import { + isCurvedArrow, + isElbowArrow, + isSharpArrow, + isUsingAdaptiveRadius, +} from "./typeChecks"; -import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types"; +import type { + ConvertibleGenericTypes, + ConvertibleLinearTypes, + ExcalidrawArrowElement, + ExcalidrawDiamondElement, + ExcalidrawElbowArrowElement, + ExcalidrawElement, + ExcalidrawEllipseElement, + ExcalidrawLinearElement, + ExcalidrawRectangleElement, + ExcalidrawSelectionElement, + NonDeletedSceneElementsMap, +} from "./types"; export type ElementUpdate = Omit< Partial, @@ -212,3 +233,211 @@ export const bumpVersion = >( element.updated = getUpdatedTimestamp(); return element; }; + +// Declare the constant array with a read-only type so that its values can only be one of the valid union. +export const CONVERTIBLE_GENERIC_TYPES: readonly ConvertibleGenericTypes[] = [ + "rectangle", + "diamond", + "ellipse", +]; + +const ELBOW_ARROW_SPECIFIC_PROPERTIES: Array< + keyof ExcalidrawElbowArrowElement +> = ["elbowed", "fixedSegments", "startIsSpecial", "endIsSpecial"]; + +const ARROW_TO_LINE_CLEAR_PROPERTIES: Array = [ + "startArrowhead", + "endArrowhead", + "startBinding", + "endBinding", +]; + +export const CONVERTIBLE_LINEAR_TYPES: readonly ConvertibleLinearTypes[] = [ + "line", + "sharpArrow", + "curvedArrow", + "elbowArrow", +]; + +type NewElementType = ConvertibleGenericTypes | ConvertibleLinearTypes; + +export const isConvertibleGenericType = ( + elementType: string, +): elementType is ConvertibleGenericTypes => + CONVERTIBLE_GENERIC_TYPES.includes(elementType as ConvertibleGenericTypes); + +export const isConvertibleLinearType = ( + elementType: string, +): elementType is ConvertibleLinearTypes => + elementType === "arrow" || + CONVERTIBLE_LINEAR_TYPES.includes(elementType as ConvertibleLinearTypes); + +/** + * Converts an element to a new type, adding or removing properties as needed + * so that the element object is always valid. + * + * Valid conversions at this point: + * - switching between generic elements + * e.g. rectangle -> diamond + * - switching between linear elements + * e.g. elbow arrow -> line + */ +export const convertElementType = < + TElement extends Mutable< + Exclude + >, +>( + element: TElement, + newType: NewElementType, + app: AppClassProperties, + informMutation = true, +): ExcalidrawElement => { + if (!isValidConversion(element.type, newType)) { + if (isDevEnv()) { + throw Error(`Invalid conversion from ${element.type} to ${newType}.`); + } + return element; + } + + const startType = isSharpArrow(element) + ? "sharpArrow" + : isCurvedArrow(element) + ? "curvedArrow" + : isElbowArrow(element) + ? "elbowArrow" + : element.type; + + if (element.type === newType) { + return element; + } + + ShapeCache.delete(element); + + const update = () => { + (element as any).version++; + (element as any).versionNonce = randomInteger(); + (element as any).updated = getUpdatedTimestamp(); + + if (informMutation) { + app.scene.triggerUpdate(); + } + }; + + if ( + isConvertibleGenericType(startType) && + isConvertibleGenericType(newType) + ) { + (element as any).type = newType; + + if (newType === "diamond" && element.roundness) { + (element as any).roundness = { + type: isUsingAdaptiveRadius(newType) + ? ROUNDNESS.ADAPTIVE_RADIUS + : ROUNDNESS.PROPORTIONAL_RADIUS, + }; + } + + update(); + + switch (element.type) { + case "rectangle": + return element as ExcalidrawRectangleElement; + case "diamond": + return element as ExcalidrawDiamondElement; + case "ellipse": + return element as ExcalidrawEllipseElement; + } + } + + if (isConvertibleLinearType(element.type)) { + if (newType === "line") { + for (const key of ELBOW_ARROW_SPECIFIC_PROPERTIES) { + delete (element as any)[key]; + } + for (const key of ARROW_TO_LINE_CLEAR_PROPERTIES) { + if (key in element) { + (element as any)[key] = null; + } + } + + (element as any).type = newType; + } + + if (newType === "sharpArrow") { + if (startType === "elbowArrow") { + // drop elbow arrow specific properties + for (const key of ELBOW_ARROW_SPECIFIC_PROPERTIES) { + delete (element as any)[key]; + } + } + + (element as any).type = "arrow"; + (element as any).elbowed = false; + (element as any).roundness = null; + (element as any).startArrowhead = app.state.currentItemStartArrowhead; + (element as any).endArrowhead = app.state.currentItemEndArrowhead; + } + + if (newType === "curvedArrow") { + if (startType === "elbowArrow") { + // drop elbow arrow specific properties + for (const key of ELBOW_ARROW_SPECIFIC_PROPERTIES) { + delete (element as any)[key]; + } + } + (element as any).type = "arrow"; + (element as any).elbowed = false; + (element as any).roundness = { + type: ROUNDNESS.PROPORTIONAL_RADIUS, + }; + (element as any).startArrowhead = app.state.currentItemStartArrowhead; + (element as any).endArrowhead = app.state.currentItemEndArrowhead; + } + + if (newType === "elbowArrow") { + (element as any).type = "arrow"; + (element as any).elbowed = true; + (element as any).fixedSegments = null; + (element as any).startIsSpecial = null; + (element as any).endIsSpecial = null; + } + + update(); + + switch (newType) { + case "line": + return element as ExcalidrawLinearElement; + case "sharpArrow": + return element as ExcalidrawArrowElement; + case "curvedArrow": + return element as ExcalidrawArrowElement; + case "elbowArrow": + return element as ExcalidrawElbowArrowElement; + } + } + + return element; +}; + +const isValidConversion = ( + startType: string, + targetType: NewElementType, +): startType is NewElementType => { + if ( + isConvertibleGenericType(startType) && + isConvertibleGenericType(targetType) + ) { + return true; + } + + if ( + isConvertibleLinearType(startType) && + isConvertibleLinearType(targetType) + ) { + return true; + } + + // NOTE: add more conversions when needed + + return false; +}; diff --git a/packages/element/src/typeChecks.ts b/packages/element/src/typeChecks.ts index 7a0c6a3e4..fb321f209 100644 --- a/packages/element/src/typeChecks.ts +++ b/packages/element/src/typeChecks.ts @@ -119,6 +119,20 @@ export const isElbowArrow = ( return isArrowElement(element) && element.elbowed; }; +export const isSharpArrow = ( + element?: ExcalidrawElement, +): element is ExcalidrawArrowElement => { + return isArrowElement(element) && !element.elbowed && !element.roundness; +}; + +export const isCurvedArrow = ( + element?: ExcalidrawElement, +): element is ExcalidrawArrowElement => { + return ( + isArrowElement(element) && !element.elbowed && element.roundness !== null + ); +}; + export const isLinearElementType = ( elementType: ElementOrToolType, ): boolean => { @@ -338,76 +352,3 @@ export const isBounds = (box: unknown): box is Bounds => typeof box[1] === "number" && typeof box[2] === "number" && typeof box[3] === "number"; - -export const getSwitchableTypeFromElements = ( - elements: ExcalidrawElement[], -): - | { - generic: true; - linear: false; - } - | { - linear: true; - generic: false; - } - | { - generic: false; - linear: false; - } => { - if (elements.length === 0) { - return { - generic: false, - linear: false, - }; - } - - let onlyLinear = true; - for (const element of elements) { - if ( - element.type === "rectangle" || - element.type === "ellipse" || - element.type === "diamond" - ) { - return { - generic: true, - linear: false, - }; - } - if (element.type !== "arrow" && element.type !== "line") { - onlyLinear = false; - } - } - - if (onlyLinear) { - // check at least some linear element is switchable - // for a linear to be swtichable: - // - no labels - // - not bound to anything - - let linear = true; - - for (const element of elements) { - if ( - isArrowElement(element) && - (element.startBinding !== null || element.endBinding !== null) - ) { - linear = false; - } else if (element.boundElements && element.boundElements.length > 0) { - linear = false; - } else { - linear = true; - break; - } - } - - return { - linear, - generic: false, - }; - } - - return { - generic: false, - linear: false, - }; -}; diff --git a/packages/element/src/types.ts b/packages/element/src/types.ts index 120841536..166c91a00 100644 --- a/packages/element/src/types.ts +++ b/packages/element/src/types.ts @@ -413,5 +413,9 @@ export type ElementsMapOrArray = | readonly ExcalidrawElement[] | Readonly; -export type GenericSwitchableToolType = "rectangle" | "ellipse" | "diamond"; -export type LinearSwitchableToolType = "line" | "arrow"; +export type ConvertibleGenericTypes = "rectangle" | "diamond" | "ellipse"; +export type ConvertibleLinearTypes = + | "line" + | "sharpArrow" + | "curvedArrow" + | "elbowArrow"; diff --git a/packages/excalidraw/actions/actionToggleShapeSwitch.tsx b/packages/excalidraw/actions/actionToggleShapeSwitch.tsx index 90cae988a..475052f59 100644 --- a/packages/excalidraw/actions/actionToggleShapeSwitch.tsx +++ b/packages/excalidraw/actions/actionToggleShapeSwitch.tsx @@ -1,8 +1,9 @@ -import { getSwitchableTypeFromElements } from "@excalidraw/element/typeChecks"; - import type { ExcalidrawElement } from "@excalidraw/element/types"; -import { shapeSwitchAtom } from "../components/ShapeSwitch"; +import { + getSwitchableTypeFromElements, + shapeSwitchAtom, +} from "../components/ShapeSwitch"; import { editorJotaiStore } from "../editor-jotai"; import { CaptureUpdateAction } from "../store"; diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 6a1215e2a..856ceae35 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -166,7 +166,6 @@ import { isFlowchartNodeElement, isBindableElement, isTextElement, - getSwitchableTypeFromElements, } from "@excalidraw/element/typeChecks"; import { @@ -327,7 +326,7 @@ import type { MagicGenerationData, ExcalidrawNonSelectionElement, ExcalidrawArrowElement, - GenericSwitchableToolType, + ConvertibleGenericTypes, } from "@excalidraw/element/types"; import type { ValueOf } from "@excalidraw/common/utility-types"; @@ -464,6 +463,7 @@ import { isOverScrollBars } from "../scene/scrollbars"; import { isMaybeMermaidDefinition } from "../mermaid"; import ShapeSwitch, { + getSwitchableTypeFromElements, shapeSwitchAtom, shapeSwitchFontSizeAtom, switchShapes, @@ -4193,7 +4193,7 @@ class App extends React.Component { ...editorJotaiStore.get(shapeSwitchFontSizeAtom), [element.id]: { fontSize: boundText.fontSize, - elementType: element.type as GenericSwitchableToolType, + elementType: element.type as ConvertibleGenericTypes, }, }); } diff --git a/packages/excalidraw/components/ShapeSwitch.tsx b/packages/excalidraw/components/ShapeSwitch.tsx index e1cd1ffe0..496822578 100644 --- a/packages/excalidraw/components/ShapeSwitch.tsx +++ b/packages/excalidraw/components/ShapeSwitch.tsx @@ -5,10 +5,11 @@ import { updateElbowArrowPoints } from "@excalidraw/element/elbowArrow"; import { pointFrom, pointRotateRads, type LocalPoint } from "@excalidraw/math"; import { - getSwitchableTypeFromElements, isArrowElement, + isCurvedArrow, isElbowArrow, - isUsingAdaptiveRadius, + isLinearElement, + isSharpArrow, } from "@excalidraw/element/typeChecks"; import { @@ -29,23 +30,31 @@ import { getFontString, updateActiveTool } from "@excalidraw/common"; import { measureText } from "@excalidraw/element/textMeasurements"; -import { ShapeCache } from "@excalidraw/element/ShapeCache"; - import { LinearElementEditor } from "@excalidraw/element/linearElementEditor"; +import { + convertElementType, + CONVERTIBLE_GENERIC_TYPES, + CONVERTIBLE_LINEAR_TYPES, + isConvertibleGenericType, + isConvertibleLinearType, +} from "@excalidraw/element/mutateElement"; + import type { + ConvertibleGenericTypes, + ConvertibleLinearTypes, ElementsMap, - ExcalidrawArrowElement, + ExcalidrawDiamondElement, ExcalidrawElement, + ExcalidrawEllipseElement, ExcalidrawLinearElement, + ExcalidrawRectangleElement, ExcalidrawTextContainer, ExcalidrawTextElementWithContainer, FixedSegment, - GenericSwitchableToolType, - LinearSwitchableToolType, } from "@excalidraw/element/types"; -import { mutateElement, ROUNDNESS, sceneCoordsToViewportCoords } from ".."; +import { mutateElement, sceneCoordsToViewportCoords } from ".."; import { getSelectedElements } from "../scene"; import { trackEvent } from "../analytics"; import { atom, editorJotaiStore, useAtom } from "../editor-jotai"; @@ -53,11 +62,13 @@ import { atom, editorJotaiStore, useAtom } from "../editor-jotai"; import "./ShapeSwitch.scss"; import { ToolButton } from "./ToolButton"; import { - ArrowIcon, DiamondIcon, + elbowArrowIcon, EllipseIcon, LineIcon, RectangleIcon, + roundArrowIcon, + sharpArrowIcon, } from "./icons"; import type App from "./App"; @@ -65,10 +76,6 @@ import type App from "./App"; const GAP_HORIZONTAL = 8; const GAP_VERTICAL = 10; -export const GENERIC_SWITCHABLE_SHAPES = ["rectangle", "diamond", "ellipse"]; - -export const LINEAR_SWITCHABLE_SHAPES = ["line", "arrow"]; - export const shapeSwitchAtom = atom<{ type: "panel"; } | null>(null); @@ -76,7 +83,7 @@ export const shapeSwitchAtom = atom<{ export const shapeSwitchFontSizeAtom = atom<{ [id: string]: { fontSize: number; - elementType: GenericSwitchableToolType; + elementType: ConvertibleGenericTypes; }; } | null>(null); @@ -142,7 +149,9 @@ const Panel = ({ (element) => element.type === genericElements[0].type, ) : linear - ? linearElements.every((element) => element.type === linearElements[0].type) + ? linearElements.every( + (element) => getArrowType(element) === getArrowType(linearElements[0]), + ) : false; const [panelPosition, setPanelPosition] = useState({ x: 0, y: 0 }); @@ -188,16 +197,18 @@ const Panel = ({ setPanelPosition({ x, y }); }, [genericElements, linearElements, app.scene, app.state]); - const SHAPES: [string, string, ReactNode][] = linear + const SHAPES: [string, ReactNode][] = linear ? [ - ["arrow", "5", ArrowIcon], - ["line", "6", LineIcon], + ["line", LineIcon], + ["sharpArrow", sharpArrowIcon], + ["curvedArrow", roundArrowIcon], + ["elbowArrow", elbowArrowIcon], ] : generic ? [ - ["rectangle", "2", RectangleIcon], - ["diamond", "3", DiamondIcon], - ["ellipse", "4", EllipseIcon], + ["rectangle", RectangleIcon], + ["diamond", DiamondIcon], + ["ellipse", EllipseIcon], ] : []; @@ -215,16 +226,16 @@ const Panel = ({ }} className="ShapeSwitch__Panel" > - {SHAPES.map(([type, shortcut, icon]) => { + {SHAPES.map(([type, icon]) => { const isSelected = sameType && ((generic && genericElements[0].type === type) || - (linear && linearElements[0].type === type)); + (linear && getArrowType(linearElements[0]) === type)); return ( @@ -312,7 +323,7 @@ export const switchShapes = ( }: { generic?: boolean; linear?: boolean; - nextType?: GenericSwitchableToolType | LinearSwitchableToolType; + nextType?: ConvertibleGenericTypes | ConvertibleLinearTypes; direction?: "left" | "right"; } = {}, ): boolean => { @@ -341,165 +352,108 @@ export const switchShapes = ( ); const index = sameType - ? GENERIC_SWITCHABLE_SHAPES.indexOf( + ? CONVERTIBLE_GENERIC_TYPES.indexOf( selectedGenericSwitchableElements[0].type, ) : -1; nextType = nextType ?? - (GENERIC_SWITCHABLE_SHAPES[ - (index + GENERIC_SWITCHABLE_SHAPES.length + advancement) % - GENERIC_SWITCHABLE_SHAPES.length - ] as GenericSwitchableToolType); + CONVERTIBLE_GENERIC_TYPES[ + (index + CONVERTIBLE_GENERIC_TYPES.length + advancement) % + CONVERTIBLE_GENERIC_TYPES.length + ]; - selectedGenericSwitchableElements.forEach((element) => { - ShapeCache.delete(element); + if (nextType && isConvertibleGenericType(nextType)) { + for (const element of selectedGenericSwitchableElements) { + convertElementType(element, nextType, app, false); - mutateElement( - element, - { - type: nextType as GenericSwitchableToolType, - roundness: - nextType === "diamond" && element.roundness - ? { - type: isUsingAdaptiveRadius(nextType) - ? ROUNDNESS.ADAPTIVE_RADIUS - : ROUNDNESS.PROPORTIONAL_RADIUS, - value: ROUNDNESS.PROPORTIONAL_RADIUS, - } - : element.roundness, - }, - false, - ); - - const boundText = getBoundTextElement( - element, - app.scene.getNonDeletedElementsMap(), - ); - if (boundText) { - if ( - editorJotaiStore.get(shapeSwitchFontSizeAtom)?.[element.id] - ?.elementType === nextType - ) { - mutateElement( - boundText, - { - fontSize: - editorJotaiStore.get(shapeSwitchFontSizeAtom)?.[element.id] - ?.fontSize ?? boundText.fontSize, - }, - false, - ); - } - - adjustBoundTextSize( - element as ExcalidrawTextContainer, - boundText, + const boundText = getBoundTextElement( + element, app.scene.getNonDeletedElementsMap(), ); - } - }); + if (boundText) { + if ( + editorJotaiStore.get(shapeSwitchFontSizeAtom)?.[element.id] + ?.elementType === nextType + ) { + mutateElement( + boundText, + { + fontSize: + editorJotaiStore.get(shapeSwitchFontSizeAtom)?.[element.id] + ?.fontSize ?? boundText.fontSize, + }, + false, + ); + } - app.setState((prevState) => { - return { - selectedElementIds, - activeTool: updateActiveTool(prevState, { - type: "selection", - }), - }; - }); + adjustBoundTextSize( + element as ExcalidrawTextContainer, + boundText, + app.scene.getNonDeletedElementsMap(), + ); + } + } + + app.setState((prevState) => { + return { + selectedElementIds, + activeTool: updateActiveTool(prevState, { + type: "selection", + }), + }; + }); + } } if (linear) { - const selectedLinearSwitchableElements = - getLinearSwitchableElements(selectedElements); + const selectedLinearSwitchableElements = getLinearSwitchableElements( + selectedElements, + ) as ExcalidrawLinearElement[]; + const arrowType = getArrowType(selectedLinearSwitchableElements[0]); const sameType = selectedLinearSwitchableElements.every( - (element) => element.type === selectedLinearSwitchableElements[0].type, + (element) => getArrowType(element) === arrowType, ); - const index = sameType - ? LINEAR_SWITCHABLE_SHAPES.indexOf( - selectedLinearSwitchableElements[0].type, - ) - : -1; + + const index = sameType ? CONVERTIBLE_LINEAR_TYPES.indexOf(arrowType) : -1; nextType = nextType ?? - (LINEAR_SWITCHABLE_SHAPES[ - (index + LINEAR_SWITCHABLE_SHAPES.length + advancement) % - LINEAR_SWITCHABLE_SHAPES.length - ] as LinearSwitchableToolType); + CONVERTIBLE_LINEAR_TYPES[ + (index + CONVERTIBLE_LINEAR_TYPES.length + advancement) % + CONVERTIBLE_LINEAR_TYPES.length + ]; - selectedLinearSwitchableElements.forEach((element) => { - ShapeCache.delete(element); + if (nextType && isConvertibleLinearType(nextType)) { + for (const element of selectedLinearSwitchableElements) { + convertElementType(element, nextType, app, false); - // TODO: maybe add a separate function for safe type conversion - // without overloading mutateElement - if (nextType === "arrow") { - mutateElement( - element as ExcalidrawArrowElement, - { - type: "arrow", - startArrowhead: app.state.currentItemStartArrowhead, - endArrowhead: app.state.currentItemEndArrowhead, - startBinding: null, - endBinding: null, - ...(app.state.currentItemArrowType === "elbow" - ? { elbowed: true } - : {}), - }, - false, - ); - } - - if (nextType === "line") { if (isElbowArrow(element)) { - mutateElement( - element as ExcalidrawLinearElement, + const nextPoints = convertLineToElbow(element); + + const fixedSegments: FixedSegment[] = []; + + for (let i = 0; i < nextPoints.length - 1; i++) { + fixedSegments.push({ + start: nextPoints[i], + end: nextPoints[i + 1], + index: i, + }); + } + + const updates = updateElbowArrowPoints( + element, + app.scene.getNonDeletedElementsMap(), { - type: "line", - startArrowhead: null, - endArrowhead: null, - startBinding: null, - endBinding: null, - }, - false, - { - propertiesToDrop: [ - "elbowed", - "startIsSpecial", - "endIsSpecial", - "fixedSegments", - ], + points: nextPoints, + fixedSegments, }, ); + mutateElement(element, updates, false); } } - - if (isElbowArrow(element)) { - const nextPoints = convertLineToElbow(element); - - const fixedSegments: FixedSegment[] = []; - - for (let i = 0; i < nextPoints.length - 1; i++) { - fixedSegments.push({ - start: nextPoints[i], - end: nextPoints[i + 1], - index: i, - }); - } - - const updates = updateElbowArrowPoints( - element, - app.scene.getNonDeletedElementsMap(), - { - points: nextPoints, - fixedSegments, - }, - ); - mutateElement(element, updates, false); - } - }); + } const firstElement = selectedLinearSwitchableElements[0]; app.setState((prevState) => ({ @@ -517,21 +471,109 @@ export const switchShapes = ( return true; }; +export const getSwitchableTypeFromElements = ( + elements: ExcalidrawElement[], +): + | { + generic: true; + linear: false; + } + | { + linear: true; + generic: false; + } + | { + generic: false; + linear: false; + } => { + if (elements.length === 0) { + return { + generic: false, + linear: false, + }; + } + + let onlyLinear = true; + for (const element of elements) { + if ( + element.type === "rectangle" || + element.type === "ellipse" || + element.type === "diamond" + ) { + return { + generic: true, + linear: false, + }; + } + if (element.type !== "arrow" && element.type !== "line") { + onlyLinear = false; + } + } + + if (onlyLinear) { + // check at least some linear element is switchable + // for a linear to be swtichable: + // - no labels + // - not bound to anything + + let linear = true; + + for (const element of elements) { + if ( + isArrowElement(element) && + (element.startBinding !== null || element.endBinding !== null) + ) { + linear = false; + } else if (element.boundElements && element.boundElements.length > 0) { + linear = false; + } else { + linear = true; + break; + } + } + + return { + linear, + generic: false, + }; + } + + return { + generic: false, + linear: false, + }; +}; + +const getArrowType = (element: ExcalidrawLinearElement) => { + if (isSharpArrow(element)) { + return "sharpArrow"; + } + if (isCurvedArrow(element)) { + return "curvedArrow"; + } + if (isElbowArrow(element)) { + return "elbowArrow"; + } + return "line"; +}; + const getGenericSwitchableElements = (elements: ExcalidrawElement[]) => - elements.filter((element) => - GENERIC_SWITCHABLE_SHAPES.includes(element.type), - ); + elements.filter((element) => isConvertibleGenericType(element.type)) as Array< + | ExcalidrawRectangleElement + | ExcalidrawDiamondElement + | ExcalidrawEllipseElement + >; const getLinearSwitchableElements = (elements: ExcalidrawElement[]) => elements.filter( (element) => - LINEAR_SWITCHABLE_SHAPES.includes(element.type) && + isLinearElement(element) && !( isArrowElement(element) && (element.startBinding !== null || element.endBinding !== null) ) && (!element.boundElements || element.boundElements.length === 0), - ); + ) as ExcalidrawLinearElement[]; const convertLineToElbow = (line: ExcalidrawLinearElement) => { const linePoints = sanitizePoints(line.points);