diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 856ceae35..46305cb58 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -326,7 +326,6 @@ import type { MagicGenerationData, ExcalidrawNonSelectionElement, ExcalidrawArrowElement, - ConvertibleGenericTypes, } from "@excalidraw/element/types"; import type { ValueOf } from "@excalidraw/common/utility-types"; @@ -465,7 +464,6 @@ import { isMaybeMermaidDefinition } from "../mermaid"; import ShapeSwitch, { getSwitchableTypeFromElements, shapeSwitchAtom, - shapeSwitchFontSizeAtom, switchShapes, } from "./ShapeSwitch"; @@ -4182,23 +4180,6 @@ class App extends React.Component { editorJotaiStore.set(shapeSwitchAtom, { type: "panel", }); - if (!editorJotaiStore.get(shapeSwitchFontSizeAtom)) { - selectedElements.forEach((element) => { - const boundText = getBoundTextElement( - element, - this.scene.getNonDeletedElementsMap(), - ); - if (boundText && generic && element) { - editorJotaiStore.set(shapeSwitchFontSizeAtom, { - ...editorJotaiStore.get(shapeSwitchFontSizeAtom), - [element.id]: { - fontSize: boundText.fontSize, - elementType: element.type as ConvertibleGenericTypes, - }, - }); - } - }); - } } } @@ -6470,6 +6451,10 @@ class App extends React.Component { editorJotaiStore.set(searchItemInFocusAtom, null); } + if (editorJotaiStore.get(shapeSwitchAtom)) { + editorJotaiStore.set(shapeSwitchAtom, null); + } + // since contextMenu options are potentially evaluated on each render, // and an contextMenu action may depend on selection state, we must // close the contextMenu before we update the selection on pointerDown diff --git a/packages/excalidraw/components/ShapeSwitch.tsx b/packages/excalidraw/components/ShapeSwitch.tsx index 496822578..d0880104b 100644 --- a/packages/excalidraw/components/ShapeSwitch.tsx +++ b/packages/excalidraw/components/ShapeSwitch.tsx @@ -44,7 +44,9 @@ import type { ConvertibleGenericTypes, ConvertibleLinearTypes, ElementsMap, + ExcalidrawArrowElement, ExcalidrawDiamondElement, + ExcalidrawElbowArrowElement, ExcalidrawElement, ExcalidrawEllipseElement, ExcalidrawLinearElement, @@ -87,9 +89,19 @@ export const shapeSwitchFontSizeAtom = atom<{ }; } | null>(null); +export const shapeSwitchLinearAtom = atom<{ + [id: string]: { + properties: + | Partial + | Partial; + initialType: ConvertibleLinearTypes; + }; +} | null>(null); + const ShapeSwitch = ({ app }: { app: App }) => { const [shapeSwitch, setShapeSwitch] = useAtom(shapeSwitchAtom); const [, setShapeSwitchFontSize] = useAtom(shapeSwitchFontSizeAtom); + const [, setShapeSwitchLinear] = useAtom(shapeSwitchLinearAtom); const selectedElements = useMemo( () => getSelectedElements(app.scene.getNonDeletedElementsMap(), app.state), @@ -117,6 +129,7 @@ const ShapeSwitch = ({ app }: { app: App }) => { // clear if not active if (!shapeSwitch) { setShapeSwitchFontSize(null); + setShapeSwitchLinear(null); return null; } @@ -197,6 +210,56 @@ const Panel = ({ setPanelPosition({ x, y }); }, [genericElements, linearElements, app.scene, app.state]); + useEffect(() => { + if (editorJotaiStore.get(shapeSwitchLinearAtom)) { + return; + } + + for (const linearElement of linearElements) { + const initialType = getArrowType(linearElement); + const cachedProperties = + initialType === "line" + ? getLineProperties(linearElement) + : initialType === "sharpArrow" + ? getSharpArrowProperties(linearElement) + : initialType === "curvedArrow" + ? getCurvedArrowProperties(linearElement) + : initialType === "elbowArrow" + ? getElbowArrowProperties(linearElement) + : {}; + + editorJotaiStore.set(shapeSwitchLinearAtom, { + ...editorJotaiStore.get(shapeSwitchLinearAtom), + [linearElement.id]: { + properties: cachedProperties, + initialType, + }, + }); + } + }, [linearElements]); + + useEffect(() => { + if (editorJotaiStore.get(shapeSwitchFontSizeAtom)) { + return; + } + + for (const element of genericElements) { + const boundText = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); + if (boundText) { + editorJotaiStore.set(shapeSwitchFontSizeAtom, { + ...editorJotaiStore.get(shapeSwitchFontSizeAtom), + [element.id]: { + fontSize: boundText.fontSize, + elementType: element.type as ConvertibleGenericTypes, + }, + }); + } + } + }, [genericElements, app.scene]); + const SHAPES: [string, ReactNode][] = linear ? [ ["line", LineIcon], @@ -429,6 +492,19 @@ export const switchShapes = ( for (const element of selectedLinearSwitchableElements) { convertElementType(element, nextType, app, false); + const cachedLinear = editorJotaiStore.get(shapeSwitchLinearAtom)?.[ + element.id + ]; + + if (cachedLinear) { + const { properties, initialType } = cachedLinear; + + if (initialType === nextType) { + mutateElement(element, properties, false); + continue; + } + } + if (isElbowArrow(element)) { const nextPoints = convertLineToElbow(element); @@ -557,6 +633,72 @@ const getArrowType = (element: ExcalidrawLinearElement) => { return "line"; }; +const getLineProperties = ( + element: ExcalidrawLinearElement, +): Partial => { + if (element.type === "line") { + return { + points: element.points, + roundness: element.roundness, + }; + } + return {}; +}; + +const getSharpArrowProperties = ( + element: ExcalidrawLinearElement, +): Partial => { + if (isSharpArrow(element)) { + return { + points: element.points, + startArrowhead: element.startArrowhead, + endArrowhead: element.endArrowhead, + startBinding: element.startBinding, + endBinding: element.endBinding, + roundness: null, + }; + } + + return {}; +}; + +const getCurvedArrowProperties = ( + element: ExcalidrawLinearElement, +): Partial => { + if (isCurvedArrow(element)) { + return { + points: element.points, + startArrowhead: element.startArrowhead, + endArrowhead: element.endArrowhead, + startBinding: element.startBinding, + endBinding: element.endBinding, + roundness: element.roundness, + }; + } + + return {}; +}; + +const getElbowArrowProperties = ( + element: ExcalidrawLinearElement, +): Partial => { + if (isElbowArrow(element)) { + return { + points: element.points, + startArrowhead: element.startArrowhead, + endArrowhead: element.endArrowhead, + startBinding: element.startBinding, + endBinding: element.endBinding, + roundness: element.roundness, + fixedSegments: element.fixedSegments, + startIsSpecial: element.startIsSpecial, + endIsSpecial: element.endIsSpecial, + }; + } + + return {}; +}; + const getGenericSwitchableElements = (elements: ExcalidrawElement[]) => elements.filter((element) => isConvertibleGenericType(element.type)) as Array< | ExcalidrawRectangleElement