type safe element conversion

This commit is contained in:
Ryan Di 2025-04-23 21:36:16 +10:00
parent de6acc4bad
commit 37e12ec201
2 changed files with 96 additions and 98 deletions

View file

@ -27,15 +27,14 @@ import {
isUsingAdaptiveRadius, isUsingAdaptiveRadius,
} from "./typeChecks"; } from "./typeChecks";
import { newArrowElement, newElement, newLinearElement } from "./newElement";
import type { import type {
ConvertibleGenericTypes, ConvertibleGenericTypes,
ConvertibleLinearTypes, ConvertibleLinearTypes,
ExcalidrawArrowElement,
ExcalidrawDiamondElement, ExcalidrawDiamondElement,
ExcalidrawElbowArrowElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawEllipseElement, ExcalidrawEllipseElement,
ExcalidrawLinearElement,
ExcalidrawRectangleElement, ExcalidrawRectangleElement,
ExcalidrawSelectionElement, ExcalidrawSelectionElement,
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
@ -241,17 +240,6 @@ export const CONVERTIBLE_GENERIC_TYPES: readonly ConvertibleGenericTypes[] = [
"ellipse", "ellipse",
]; ];
const ELBOW_ARROW_SPECIFIC_PROPERTIES: Array<
keyof ExcalidrawElbowArrowElement
> = ["elbowed", "fixedSegments", "startIsSpecial", "endIsSpecial"];
const ARROW_TO_LINE_CLEAR_PROPERTIES: Array<keyof ExcalidrawArrowElement> = [
"startArrowhead",
"endArrowhead",
"startBinding",
"endBinding",
];
export const CONVERTIBLE_LINEAR_TYPES: readonly ConvertibleLinearTypes[] = [ export const CONVERTIBLE_LINEAR_TYPES: readonly ConvertibleLinearTypes[] = [
"line", "line",
"sharpArrow", "sharpArrow",
@ -313,106 +301,82 @@ export const convertElementType = <
ShapeCache.delete(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 ( if (
isConvertibleGenericType(startType) && isConvertibleGenericType(startType) &&
isConvertibleGenericType(newType) isConvertibleGenericType(newType)
) { ) {
(element as any).type = newType; const nextElement = bumpVersion(
newElement({
if (newType === "diamond" && element.roundness) { ...element,
(element as any).roundness = { type: newType,
roundness:
newType === "diamond" && element.roundness
? {
type: isUsingAdaptiveRadius(newType) type: isUsingAdaptiveRadius(newType)
? ROUNDNESS.ADAPTIVE_RADIUS ? ROUNDNESS.ADAPTIVE_RADIUS
: ROUNDNESS.PROPORTIONAL_RADIUS, : ROUNDNESS.PROPORTIONAL_RADIUS,
};
} }
: element.roundness,
}),
);
update(); switch (nextElement.type) {
switch (element.type) {
case "rectangle": case "rectangle":
return element as ExcalidrawRectangleElement; return nextElement as ExcalidrawRectangleElement;
case "diamond": case "diamond":
return element as ExcalidrawDiamondElement; return nextElement as ExcalidrawDiamondElement;
case "ellipse": case "ellipse":
return element as ExcalidrawEllipseElement; return nextElement as ExcalidrawEllipseElement;
} }
} }
if (isConvertibleLinearType(element.type)) { if (isConvertibleLinearType(element.type)) {
if (newType === "line") { if (newType === "line") {
for (const key of ELBOW_ARROW_SPECIFIC_PROPERTIES) { const nextElement = newLinearElement({
delete (element as any)[key]; ...element,
} type: "line",
for (const key of ARROW_TO_LINE_CLEAR_PROPERTIES) { });
if (key in element) {
(element as any)[key] = null;
}
}
(element as any).type = newType; return bumpVersion(nextElement);
} }
if (newType === "sharpArrow") { if (newType === "sharpArrow") {
if (startType === "elbowArrow") { const nextElement = newArrowElement({
// drop elbow arrow specific properties ...element,
for (const key of ELBOW_ARROW_SPECIFIC_PROPERTIES) { type: "arrow",
delete (element as any)[key]; elbowed: false,
} roundness: null,
} startArrowhead: app.state.currentItemStartArrowhead,
endArrowhead: app.state.currentItemEndArrowhead,
});
(element as any).type = "arrow"; return bumpVersion(nextElement);
(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 (newType === "curvedArrow") {
if (startType === "elbowArrow") { const nextElement = newArrowElement({
// drop elbow arrow specific properties ...element,
for (const key of ELBOW_ARROW_SPECIFIC_PROPERTIES) { type: "arrow",
delete (element as any)[key]; elbowed: false,
} roundness: {
}
(element as any).type = "arrow";
(element as any).elbowed = false;
(element as any).roundness = {
type: ROUNDNESS.PROPORTIONAL_RADIUS, type: ROUNDNESS.PROPORTIONAL_RADIUS,
}; },
(element as any).startArrowhead = app.state.currentItemStartArrowhead; startArrowhead: app.state.currentItemStartArrowhead,
(element as any).endArrowhead = app.state.currentItemEndArrowhead; endArrowhead: app.state.currentItemEndArrowhead,
});
return bumpVersion(nextElement);
} }
if (newType === "elbowArrow") { if (newType === "elbowArrow") {
(element as any).type = "arrow"; const nextElement = newArrowElement({
(element as any).elbowed = true; ...element,
(element as any).fixedSegments = null; type: "arrow",
(element as any).startIsSpecial = null; elbowed: true,
(element as any).endIsSpecial = null; fixedSegments: null,
} });
update(); return bumpVersion(nextElement);
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;
} }
} }

View file

@ -57,7 +57,6 @@ import type {
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import { mutateElement, sceneCoordsToViewportCoords } from ".."; import { mutateElement, sceneCoordsToViewportCoords } from "..";
import { getSelectedElements } from "../scene";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { atom, editorJotaiStore, useAtom } from "../editor-jotai"; import { atom, editorJotaiStore, useAtom } from "../editor-jotai";
@ -104,7 +103,7 @@ const ShapeSwitch = ({ app }: { app: App }) => {
const [, setShapeSwitchLinear] = useAtom(shapeSwitchLinearAtom); const [, setShapeSwitchLinear] = useAtom(shapeSwitchLinearAtom);
const selectedElements = useMemo( const selectedElements = useMemo(
() => getSelectedElements(app.scene.getNonDeletedElementsMap(), app.state), () => app.scene.getSelectedElements(app.state),
[app.scene, app.state], [app.scene, app.state],
); );
const selectedElementsTypeRef = useRef<"generic" | "linear">(null); const selectedElementsTypeRef = useRef<"generic" | "linear">(null);
@ -394,10 +393,7 @@ export const switchShapes = (
return false; return false;
} }
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(app.state);
app.scene.getNonDeletedElementsMap(),
app.state,
);
const selectedElementIds = selectedElements.reduce( const selectedElementIds = selectedElements.reduce(
(acc, element) => ({ ...acc, [element.id]: true }), (acc, element) => ({ ...acc, [element.id]: true }),
@ -428,9 +424,31 @@ export const switchShapes = (
]; ];
if (nextType && isConvertibleGenericType(nextType)) { if (nextType && isConvertibleGenericType(nextType)) {
for (const element of selectedGenericSwitchableElements) { const convertedElements: Record<string, ExcalidrawElement> = {};
convertElementType(element, nextType, app, false);
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( const boundText = getBoundTextElement(
element, element,
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
@ -489,9 +507,25 @@ export const switchShapes = (
]; ];
if (nextType && isConvertibleLinearType(nextType)) { if (nextType && isConvertibleLinearType(nextType)) {
const convertedElements: Record<string, ExcalidrawElement> = {};
for (const element of selectedLinearSwitchableElements) { 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)?.[ const cachedLinear = editorJotaiStore.get(shapeSwitchLinearAtom)?.[
element.id element.id
]; ];