From 37e12ec201acc5c2f6d192f39245995d0e7ffbf4 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Wed, 23 Apr 2025 21:36:16 +1000 Subject: [PATCH] type safe element conversion --- packages/element/src/mutateElement.ts | 142 +++++++----------- .../excalidraw/components/ShapeSwitch.tsx | 52 +++++-- 2 files changed, 96 insertions(+), 98 deletions(-) diff --git a/packages/element/src/mutateElement.ts b/packages/element/src/mutateElement.ts index 61b908cd3..3a905bda3 100644 --- a/packages/element/src/mutateElement.ts +++ b/packages/element/src/mutateElement.ts @@ -27,15 +27,14 @@ import { isUsingAdaptiveRadius, } from "./typeChecks"; +import { newArrowElement, newElement, newLinearElement } from "./newElement"; + import type { ConvertibleGenericTypes, ConvertibleLinearTypes, - ExcalidrawArrowElement, ExcalidrawDiamondElement, - ExcalidrawElbowArrowElement, ExcalidrawElement, ExcalidrawEllipseElement, - ExcalidrawLinearElement, ExcalidrawRectangleElement, ExcalidrawSelectionElement, NonDeletedSceneElementsMap, @@ -241,17 +240,6 @@ export const CONVERTIBLE_GENERIC_TYPES: readonly ConvertibleGenericTypes[] = [ "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", @@ -313,106 +301,82 @@ export const convertElementType = < 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; + const nextElement = bumpVersion( + newElement({ + ...element, + type: newType, + roundness: + newType === "diamond" && element.roundness + ? { + type: isUsingAdaptiveRadius(newType) + ? ROUNDNESS.ADAPTIVE_RADIUS + : ROUNDNESS.PROPORTIONAL_RADIUS, + } + : element.roundness, + }), + ); - if (newType === "diamond" && element.roundness) { - (element as any).roundness = { - type: isUsingAdaptiveRadius(newType) - ? ROUNDNESS.ADAPTIVE_RADIUS - : ROUNDNESS.PROPORTIONAL_RADIUS, - }; - } - - update(); - - switch (element.type) { + switch (nextElement.type) { case "rectangle": - return element as ExcalidrawRectangleElement; + return nextElement as ExcalidrawRectangleElement; case "diamond": - return element as ExcalidrawDiamondElement; + return nextElement as ExcalidrawDiamondElement; case "ellipse": - return element as ExcalidrawEllipseElement; + return nextElement 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; - } - } + const nextElement = newLinearElement({ + ...element, + type: "line", + }); - (element as any).type = newType; + return bumpVersion(nextElement); } if (newType === "sharpArrow") { - if (startType === "elbowArrow") { - // drop elbow arrow specific properties - for (const key of ELBOW_ARROW_SPECIFIC_PROPERTIES) { - delete (element as any)[key]; - } - } + const nextElement = newArrowElement({ + ...element, + type: "arrow", + elbowed: false, + roundness: null, + startArrowhead: app.state.currentItemStartArrowhead, + endArrowhead: app.state.currentItemEndArrowhead, + }); - (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; + return bumpVersion(nextElement); } 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; + const nextElement = newArrowElement({ + ...element, + type: "arrow", + elbowed: false, + roundness: { + type: ROUNDNESS.PROPORTIONAL_RADIUS, + }, + startArrowhead: app.state.currentItemStartArrowhead, + endArrowhead: app.state.currentItemEndArrowhead, + }); + + return bumpVersion(nextElement); } 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; - } + const nextElement = newArrowElement({ + ...element, + type: "arrow", + elbowed: true, + fixedSegments: 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 bumpVersion(nextElement); } } diff --git a/packages/excalidraw/components/ShapeSwitch.tsx b/packages/excalidraw/components/ShapeSwitch.tsx index d0880104b..88525d30d 100644 --- a/packages/excalidraw/components/ShapeSwitch.tsx +++ b/packages/excalidraw/components/ShapeSwitch.tsx @@ -57,7 +57,6 @@ import type { } from "@excalidraw/element/types"; import { mutateElement, sceneCoordsToViewportCoords } from ".."; -import { getSelectedElements } from "../scene"; import { trackEvent } from "../analytics"; import { atom, editorJotaiStore, useAtom } from "../editor-jotai"; @@ -104,7 +103,7 @@ const ShapeSwitch = ({ app }: { app: App }) => { const [, setShapeSwitchLinear] = useAtom(shapeSwitchLinearAtom); const selectedElements = useMemo( - () => getSelectedElements(app.scene.getNonDeletedElementsMap(), app.state), + () => app.scene.getSelectedElements(app.state), [app.scene, app.state], ); const selectedElementsTypeRef = useRef<"generic" | "linear">(null); @@ -394,10 +393,7 @@ export const switchShapes = ( return false; } - const selectedElements = getSelectedElements( - app.scene.getNonDeletedElementsMap(), - app.state, - ); + const selectedElements = app.scene.getSelectedElements(app.state); const selectedElementIds = selectedElements.reduce( (acc, element) => ({ ...acc, [element.id]: true }), @@ -428,9 +424,31 @@ export const switchShapes = ( ]; if (nextType && isConvertibleGenericType(nextType)) { - for (const element of selectedGenericSwitchableElements) { - convertElementType(element, nextType, app, false); + const convertedElements: Record = {}; + for (const element of selectedGenericSwitchableElements) { + const convertedElement = convertElementType( + element, + nextType, + app, + false, + ); + convertedElements[convertedElement.id] = convertedElement; + } + + const nextElements = []; + + for (const element of app.scene.getElementsIncludingDeleted()) { + if (convertedElements[element.id]) { + nextElements.push(convertedElements[element.id]); + } else { + nextElements.push(element); + } + } + + app.scene.replaceAllElements(nextElements); + + for (const element of Object.values(convertedElements)) { const boundText = getBoundTextElement( element, app.scene.getNonDeletedElementsMap(), @@ -489,9 +507,25 @@ export const switchShapes = ( ]; if (nextType && isConvertibleLinearType(nextType)) { + const convertedElements: Record = {}; for (const element of selectedLinearSwitchableElements) { - convertElementType(element, nextType, app, false); + const converted = convertElementType(element, nextType, app, false); + convertedElements[converted.id] = converted; + } + const nextElements = []; + + for (const element of app.scene.getElementsIncludingDeleted()) { + if (convertedElements[element.id]) { + nextElements.push(convertedElements[element.id]); + } else { + nextElements.push(element); + } + } + + app.scene.replaceAllElements(nextElements); + + for (const element of Object.values(convertedElements)) { const cachedLinear = editorJotaiStore.get(shapeSwitchLinearAtom)?.[ element.id ];