feat: switch between basic shapes (#9270)
* feat: switch between basic shapes * add tab for testing * style tweaks * only show hint when a new node is created * fix panel state * refactor * combine captures into one * keep original font size * switch multi * switch different types altogether * use tab only * fix font size atom * do not switch from active tool change * prefer generic when mixed * provide an optional direction when shape switching * adjust panel bg & shadow * redraw to correctly position text * remove redundant code * only tab to switch if focusing on app container * limit which linear elements can be switched * add shape switch to command palette * remove hint * cache initial panel position * bend line to elbow if needed * remove debug logic * clean switch of arrows using app state * safe conversion between line, sharp, curved, and elbow * cache linear when panel shows up * type safe element conversion * rename type * respect initial type when switching between linears * fix elbow segment indexing * use latest linear * merge converted elbow points if too close * focus on panel after click * set roudness to null to fix drag points offset for elbows * remove Mutable * add arrowBoundToElement check * make it dependent on one signle state * unmount when not showing * simpler types, tidy up code * can change linear when it's linear + non-generic * fix popup component lifecycle * move constant to CLASSES * DRY out type detection * file & variable renaming * refactor * throw in not-prod instead * simplify * semi-fix bindings on `generic` type conversion --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
@@ -100,6 +100,7 @@ import {
|
||||
arrayToMap,
|
||||
type EXPORT_IMAGE_TYPES,
|
||||
randomInteger,
|
||||
CLASSES,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
@@ -431,7 +432,7 @@ import {
|
||||
} from "../components/hyperlink/Hyperlink";
|
||||
|
||||
import { Fonts } from "../fonts";
|
||||
import { editorJotaiStore } from "../editor-jotai";
|
||||
import { editorJotaiStore, type WritableAtom } from "../editor-jotai";
|
||||
import { ImageSceneDataError } from "../errors";
|
||||
import {
|
||||
getSnapLinesAtPointer,
|
||||
@@ -467,6 +468,12 @@ import { LassoTrail } from "../lasso";
|
||||
|
||||
import { EraserTrail } from "../eraser";
|
||||
|
||||
import ConvertElementTypePopup, {
|
||||
getConversionTypeFromElements,
|
||||
convertElementTypePopupAtom,
|
||||
convertElementTypes,
|
||||
} from "./ConvertElementTypePopup";
|
||||
|
||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
||||
@@ -498,7 +505,6 @@ import type { ExportedElements } from "../data";
|
||||
import type { ContextMenuItems } from "./ContextMenu";
|
||||
import type { FileSystemHandle } from "../data/filesystem";
|
||||
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
||||
|
||||
import type {
|
||||
AppClassProperties,
|
||||
AppProps,
|
||||
@@ -815,6 +821,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
}
|
||||
|
||||
updateEditorAtom = <Value, Args extends unknown[], Result>(
|
||||
atom: WritableAtom<Value, Args, Result>,
|
||||
...args: Args
|
||||
): Result => {
|
||||
const result = editorJotaiStore.set(atom, ...args);
|
||||
this.triggerRender();
|
||||
return result;
|
||||
};
|
||||
|
||||
private onWindowMessage(event: MessageEvent) {
|
||||
if (
|
||||
event.origin !== "https://player.vimeo.com" &&
|
||||
@@ -1583,6 +1598,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
const firstSelectedElement = selectedElements[0];
|
||||
|
||||
const showShapeSwitchPanel =
|
||||
editorJotaiStore.get(convertElementTypePopupAtom)?.type === "panel";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("excalidraw excalidraw-container", {
|
||||
@@ -1857,6 +1875,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
/>
|
||||
)}
|
||||
{this.renderFrameNames()}
|
||||
{showShapeSwitchPanel && (
|
||||
<ConvertElementTypePopup app={this} />
|
||||
)}
|
||||
</ExcalidrawActionManagerContext.Provider>
|
||||
{this.renderEmbeddables()}
|
||||
</ExcalidrawElementsContext.Provider>
|
||||
@@ -2138,7 +2159,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
|
||||
private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
|
||||
editorJotaiStore.set(activeEyeDropperAtom, {
|
||||
this.updateEditorAtom(activeEyeDropperAtom, {
|
||||
swapPreviewOnAlt: true,
|
||||
colorPickerType:
|
||||
type === "stroke" ? "elementStroke" : "elementBackground",
|
||||
@@ -4157,6 +4178,40 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Shape switching
|
||||
if (event.key === KEYS.ESCAPE) {
|
||||
this.updateEditorAtom(convertElementTypePopupAtom, null);
|
||||
} else if (
|
||||
event.key === KEYS.TAB &&
|
||||
(document.activeElement === this.excalidrawContainerRef?.current ||
|
||||
document.activeElement?.classList.contains(
|
||||
CLASSES.CONVERT_ELEMENT_TYPE_POPUP,
|
||||
))
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
const conversionType =
|
||||
getConversionTypeFromElements(selectedElements);
|
||||
|
||||
if (
|
||||
editorJotaiStore.get(convertElementTypePopupAtom)?.type === "panel"
|
||||
) {
|
||||
if (
|
||||
convertElementTypes(this, {
|
||||
conversionType,
|
||||
direction: event.shiftKey ? "left" : "right",
|
||||
})
|
||||
) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
}
|
||||
}
|
||||
if (conversionType) {
|
||||
this.updateEditorAtom(convertElementTypePopupAtom, {
|
||||
type: "panel",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
event.key === KEYS.ESCAPE &&
|
||||
this.flowChartCreator.isCreatingChart
|
||||
@@ -4615,7 +4670,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
||||
) {
|
||||
editorJotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
|
||||
this.updateEditorAtom(activeConfirmDialogAtom, "clearCanvas");
|
||||
}
|
||||
|
||||
// eye dropper
|
||||
@@ -6364,7 +6419,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
focus: false,
|
||||
})),
|
||||
}));
|
||||
editorJotaiStore.set(searchItemInFocusAtom, null);
|
||||
this.updateEditorAtom(searchItemInFocusAtom, null);
|
||||
}
|
||||
|
||||
if (editorJotaiStore.get(convertElementTypePopupAtom)) {
|
||||
this.updateEditorAtom(convertElementTypePopupAtom, null);
|
||||
}
|
||||
|
||||
// since contextMenu options are potentially evaluated on each render,
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
isWritableElement,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { actionToggleShapeSwitch } from "@excalidraw/excalidraw/actions/actionToggleShapeSwitch";
|
||||
|
||||
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
||||
|
||||
import {
|
||||
@@ -410,6 +412,14 @@ function CommandPaletteInner({
|
||||
actionManager.executeAction(actionToggleSearchMenu);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("labels.shapeSwitch"),
|
||||
category: DEFAULT_CATEGORIES.elements,
|
||||
icon: boltIcon,
|
||||
perform: () => {
|
||||
actionManager.executeAction(actionToggleShapeSwitch);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("labels.changeStroke"),
|
||||
keywords: ["color", "outline"],
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
@import "../css//variables.module.scss";
|
||||
|
||||
.excalidraw {
|
||||
.ConvertElementTypePopup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.2rem;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--island-bg-color);
|
||||
box-shadow: var(--shadow-island);
|
||||
padding: 0.5rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,10 @@ import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { angleIcon } from "../icons";
|
||||
|
||||
import { updateBindings } from "../../../element/src/binding";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
bindOrUnbindLinearElements,
|
||||
updateBoundElements,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
import {
|
||||
isFrameLikeElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
@@ -27,6 +22,8 @@ import type {
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { updateBindings } from "../../../element/src/binding";
|
||||
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
export type StatsInputProperty =
|
||||
@@ -194,19 +191,3 @@ export const getAtomicUnits = (
|
||||
});
|
||||
return _atomicUnits;
|
||||
};
|
||||
|
||||
export const updateBindings = (
|
||||
latestElement: ExcalidrawElement,
|
||||
scene: Scene,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
zoom?: AppState["zoom"];
|
||||
},
|
||||
) => {
|
||||
if (isLinearElement(latestElement)) {
|
||||
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
|
||||
} else {
|
||||
updateBoundElements(latestElement, scene, options);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user