This commit is contained in:
Xu Desheng 2025-04-07 07:58:48 +09:00 committed by GitHub
commit 53e89bd266
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1853 additions and 2063 deletions

View file

@ -412,3 +412,6 @@ export type NonDeletedSceneElementsMap = Map<
export type ElementsMapOrArray =
| readonly ExcalidrawElement[]
| Readonly<ElementsMap>;
/** Supported shape types for shape conversion */
export type ShapeType = "rectangle" | "diamond" | "ellipse";

View file

@ -0,0 +1,93 @@
import { newElementWith } from "@excalidraw/element/mutateElement";
import { isTextElement } from "@excalidraw/element/typeChecks";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type { ShapeType } from "@excalidraw/element/src/types";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
import type { AppState } from "../types";
import type { ActionResult } from "./types";
const SUPPORTED_TYPES: ShapeType[] = ["rectangle", "diamond", "ellipse"];
const isShapeTypeSupported = (
element: ExcalidrawElement,
): element is ExcalidrawElement & { type: ShapeType } => {
return SUPPORTED_TYPES.includes(element.type as ShapeType);
};
export const actionChangeShapeType = register({
name: "changeShapeType",
label: "Change shape type",
trackEvent: { category: "element" },
perform: (
elements: readonly ExcalidrawElement[],
appState: AppState,
targetType: ShapeType,
): ActionResult => {
// restrict the shape type to rectangle, diamond, ellipse
const selectedElements = elements.filter(
(el) => appState.selectedElementIds[el.id],
);
// If no elements are selected, return false
if (selectedElements.length === 0) {
return false;
}
// Filter out elements that can be converted to the target shape type
const convertibleElements = selectedElements.filter(isShapeTypeSupported);
// If no convertible elements, return false
if (convertibleElements.length === 0) {
return false;
}
// Create a map of element IDs that should be converted
const elementsToConvert = new Set(
convertibleElements
.filter((element) => element.type !== targetType) // Skip elements already of target type
.map((element) => element.id),
);
// If all elements are already the target type, return false
if (elementsToConvert.size === 0) {
return false;
}
// Map through all elements and convert the ones that need to be converted
const newElements = elements.map((el) => {
if (elementsToConvert.has(el.id)) {
// don't convert text to other shapes
if (isTextElement(el)) {
return el;
}
// Default case: just change the type
return newElementWith(el, {
type: targetType,
});
}
return el;
});
return {
elements: newElements,
appState: { ...appState },
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
keyTest: (event: KeyboardEvent | React.KeyboardEvent<Element>) => {
// Handle R/D/E with Ctrl/Cmd modifier
if (event.ctrlKey || event.metaKey) {
const key = event.key.toLowerCase();
return key === "r" || key === "d" || key === "e";
}
return false;
},
});

View file

@ -92,3 +92,5 @@ export { actionToggleLinearEditor } from "./actionLinearEditor";
export { actionToggleSearchMenu } from "./actionToggleSearchMenu";
export { actionToggleCropEditor } from "./actionCropEditor";
export { actionChangeShapeType } from "./actionChangeShapeType";

View file

@ -139,7 +139,8 @@ export type ActionName =
| "copyElementLink"
| "linkToElement"
| "cropEditor"
| "wrapSelectionInFrame";
| "wrapSelectionInFrame"
| "changeShapeType";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];

View file

@ -330,10 +330,13 @@ import type {
import type { ValueOf } from "@excalidraw/common/utility-types";
import type { ShapeType } from "@excalidraw/element/src/types";
import {
actionAddToLibrary,
actionBringForward,
actionBringToFront,
actionChangeShapeType,
actionCopy,
actionCopyAsPng,
actionCopyAsSvg,
@ -522,6 +525,7 @@ import type {
Offsets,
} from "../types";
import type { RoughCanvas } from "roughjs/bin/canvas";
import type { Action, ActionResult } from "../actions/types";
const AppContext = React.createContext<AppClassProperties>(null!);
@ -4133,6 +4137,31 @@ class App extends React.Component<AppProps, AppState> {
return;
}
// Handle shape type change shortcuts
if (event.ctrlKey || event.metaKey) {
const key = event.key.toLowerCase();
let targetType: ShapeType | null = null;
if (key === "r") {
targetType = "rectangle";
} else if (key === "d") {
targetType = "diamond";
} else if (key === "e") {
targetType = "ellipse";
}
if (targetType) {
event.preventDefault();
// Execute the action with the target shape type
this.actionManager.executeAction(
actionChangeShapeType,
"ui",
targetType,
);
return;
}
}
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElementsMap(),
this.state,

View file

@ -0,0 +1,2 @@
/** Supported shape types for shape conversion */
export type ShapeType = "rectangle" | "diamond" | "ellipse";

View file

@ -89,7 +89,7 @@
"jotai-scope": "0.7.2",
"lodash.throttle": "4.1.1",
"lodash.debounce": "4.0.8",
"nanoid": "3.3.3",
"nanoid": "5.1.5",
"open-color": "1.9.1",
"pako": "2.0.3",
"perfect-freehand": "1.2.0",

3782
yarn.lock

File diff suppressed because it is too large Load diff