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,
} 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<keyof ExcalidrawArrowElement> = [
"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;
if (newType === "diamond" && element.roundness) {
(element as any).roundness = {
const nextElement = bumpVersion(
newElement({
...element,
type: newType,
roundness:
newType === "diamond" && element.roundness
? {
type: isUsingAdaptiveRadius(newType)
? ROUNDNESS.ADAPTIVE_RADIUS
: ROUNDNESS.PROPORTIONAL_RADIUS,
};
}
: element.roundness,
}),
);
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 = {
const nextElement = newArrowElement({
...element,
type: "arrow",
elbowed: false,
roundness: {
type: ROUNDNESS.PROPORTIONAL_RADIUS,
};
(element as any).startArrowhead = app.state.currentItemStartArrowhead;
(element as any).endArrowhead = app.state.currentItemEndArrowhead;
},
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);
}
}

View file

@ -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<string, ExcalidrawElement> = {};
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<string, ExcalidrawElement> = {};
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
];