switch multi

This commit is contained in:
Ryan Di 2025-03-24 11:45:02 +11:00
parent 230a339c7b
commit 41a4dadaaf
5 changed files with 279 additions and 190 deletions

View file

@ -392,6 +392,7 @@ import {
} from "../element/textMeasurements"; } from "../element/textMeasurements";
import ShapeSwitch, { import ShapeSwitch, {
adjustBoundTextSize,
GENERIC_SWITCHABLE_SHAPES, GENERIC_SWITCHABLE_SHAPES,
LINEAR_SWITCHABLE_SHAPES, LINEAR_SWITCHABLE_SHAPES,
shapeSwitchAtom, shapeSwitchAtom,
@ -446,6 +447,8 @@ import type {
MagicGenerationData, MagicGenerationData,
ExcalidrawNonSelectionElement, ExcalidrawNonSelectionElement,
ExcalidrawArrowElement, ExcalidrawArrowElement,
GenericSwitchableToolType,
LinearSwitchableToolType,
} from "../element/types"; } from "../element/types";
import type { import type {
RenderInteractiveSceneCallback, RenderInteractiveSceneCallback,
@ -573,7 +576,6 @@ const gesture: Gesture = {
initialDistance: null, initialDistance: null,
initialScale: null, initialScale: null,
}; };
let textWysiwygSubmitHandler: (() => void) | null = null;
class App extends React.Component<AppProps, AppState> { class App extends React.Component<AppProps, AppState> {
canvas: AppClassProperties["canvas"]; canvas: AppClassProperties["canvas"];
@ -4108,28 +4110,24 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
const firstElement = selectedElements[0]; const genericSwitchable = isGenericSwitchableElement(selectedElements);
const isGenericSwitchable = const linearSwitchable = isLinearSwitchableElement(selectedElements);
firstElement && isGenericSwitchableElement(firstElement);
const isLinearSwitchable = if (genericSwitchable || linearSwitchable) {
firstElement && isLinearSwitchableElement(firstElement); const firstElement = selectedElements[0];
if (
selectedElements.length === 1 &&
(isGenericSwitchable || isLinearSwitchable)
) {
if (event.key === KEYS.ESCAPE) { if (event.key === KEYS.ESCAPE) {
editorJotaiStore.set(shapeSwitchAtom, null); editorJotaiStore.set(shapeSwitchAtom, null);
} else if (event.key === KEYS.SLASH || event.key === KEYS.TAB) { } else if (event.key === KEYS.SLASH || event.key === KEYS.TAB) {
event.preventDefault(); event.preventDefault();
if (editorJotaiStore.get(shapeSwitchAtom)?.type === "panel") { if (editorJotaiStore.get(shapeSwitchAtom)?.type === "panel") {
const index = isGenericSwitchable const index = genericSwitchable
? GENERIC_SWITCHABLE_SHAPES.indexOf(selectedElements[0].type) ? GENERIC_SWITCHABLE_SHAPES.indexOf(selectedElements[0].type)
: LINEAR_SWITCHABLE_SHAPES.indexOf(selectedElements[0].type); : LINEAR_SWITCHABLE_SHAPES.indexOf(selectedElements[0].type);
const nextType = ( const nextType = (
isGenericSwitchable genericSwitchable
? GENERIC_SWITCHABLE_SHAPES[ ? GENERIC_SWITCHABLE_SHAPES[
(index + 1) % GENERIC_SWITCHABLE_SHAPES.length (index + 1) % GENERIC_SWITCHABLE_SHAPES.length
] ]
@ -4145,16 +4143,20 @@ class App extends React.Component<AppProps, AppState> {
type: "panel", type: "panel",
}); });
if (!editorJotaiStore.get(shapeSwitchFontSizeAtom)) { if (!editorJotaiStore.get(shapeSwitchFontSizeAtom)) {
const boundText = getBoundTextElement( selectedElements.forEach((element) => {
firstElement, const boundText = getBoundTextElement(
this.scene.getNonDeletedElementsMap(), element,
); this.scene.getNonDeletedElementsMap(),
if (boundText && isGenericSwitchable) { );
editorJotaiStore.set(shapeSwitchFontSizeAtom, { if (boundText && genericSwitchable && firstElement) {
fontSize: boundText.fontSize, editorJotaiStore.set(shapeSwitchFontSizeAtom, {
elementType: firstElement.type, [element.id]: {
}); fontSize: boundText.fontSize,
} elementType: element.type as GenericSwitchableToolType,
},
});
}
});
} }
} }
} }
@ -4819,90 +4821,98 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state, this.state,
); );
const firstElement = selectedElements[0]; const selectedElementIds = selectedElements.reduce(
(acc, element) => ({ ...acc, [element.id]: true }),
{},
);
if ( if (
firstElement && isGenericSwitchableElement(selectedElements) &&
selectedElements.length === 1 &&
isGenericSwitchableElement(firstElement) &&
isGenericSwitchableToolType(tool.type) isGenericSwitchableToolType(tool.type)
) { ) {
ShapeCache.delete(firstElement); selectedElements.forEach((element) => {
ShapeCache.delete(element);
mutateElement(firstElement, { mutateElement(
type: tool.type, element,
roundness: {
tool.type === "diamond" && firstElement.roundness type: tool.type as GenericSwitchableToolType,
? { roundness:
type: isUsingAdaptiveRadius(tool.type) tool.type === "diamond" && element.roundness
? ROUNDNESS.ADAPTIVE_RADIUS ? {
: ROUNDNESS.PROPORTIONAL_RADIUS, type: isUsingAdaptiveRadius(tool.type)
value: ROUNDNESS.PROPORTIONAL_RADIUS, ? ROUNDNESS.ADAPTIVE_RADIUS
} : ROUNDNESS.PROPORTIONAL_RADIUS,
: firstElement.roundness, value: ROUNDNESS.PROPORTIONAL_RADIUS,
}); }
: element.roundness,
},
false,
);
const boundText = getBoundTextElement( const boundText = getBoundTextElement(
firstElement, element,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
); );
if (boundText) { if (boundText) {
if ( if (
editorJotaiStore.get(shapeSwitchFontSizeAtom)?.elementType === editorJotaiStore.get(shapeSwitchFontSizeAtom)?.[element.id]
tool.type ?.elementType === tool.type
) { ) {
mutateElement( mutateElement(
boundText,
{
fontSize:
editorJotaiStore.get(shapeSwitchFontSizeAtom)?.[element.id]
?.fontSize ?? boundText.fontSize,
},
false,
);
}
adjustBoundTextSize(
element,
boundText, boundText,
{ this.scene.getNonDeletedElementsMap(),
fontSize:
editorJotaiStore.get(shapeSwitchFontSizeAtom)?.fontSize ??
boundText.fontSize,
},
false,
); );
} }
});
this.startTextEditing({ this.setState((prevState) => {
sceneX: firstElement.x + firstElement.width / 2, return {
sceneY: firstElement.y + firstElement.height / 2, selectedElementIds,
container: firstElement as ExcalidrawTextContainer, activeTool: updateActiveTool(prevState, { type: "selection" }),
keepContainerDimensions: true, };
}); });
}
this.setState((prevState) => ({
selectedElementIds: {
[firstElement.id]: true,
},
activeTool: updateActiveTool(prevState, { type: "selection" }),
}));
textWysiwygSubmitHandler?.();
this.store.shouldCaptureIncrement(); this.store.shouldCaptureIncrement();
} }
if ( if (
firstElement && isLinearSwitchableElement(selectedElements) &&
selectedElements.length === 1 &&
isLinearSwitchableElement(firstElement) &&
isLinearSwitchableToolType(tool.type) isLinearSwitchableToolType(tool.type)
) { ) {
ShapeCache.delete(firstElement); selectedElements.forEach((element) => {
ShapeCache.delete(element);
mutateElement(firstElement as ExcalidrawLinearElement, { mutateElement(
type: tool.type, element as ExcalidrawLinearElement,
startArrowhead: null, {
endArrowhead: tool.type === "arrow" ? "arrow" : null, type: tool.type as LinearSwitchableToolType,
startArrowhead: null,
endArrowhead: tool.type === "arrow" ? "arrow" : null,
},
false,
);
}); });
const firstElement = selectedElements[0];
this.setState((prevState) => ({ this.setState((prevState) => ({
selectedElementIds: { selectedElementIds,
[firstElement.id]: true, selectedLinearElement:
}, selectedElements.length === 1
selectedLinearElement: new LinearElementEditor( ? new LinearElementEditor(firstElement as ExcalidrawLinearElement)
firstElement as ExcalidrawLinearElement, : null,
),
activeTool: updateActiveTool(prevState, { type: "selection" }), activeTool: updateActiveTool(prevState, { type: "selection" }),
})); }));
} }
@ -5037,7 +5047,7 @@ class App extends React.Component<AppProps, AppState> {
]); ]);
}; };
textWysiwygSubmitHandler = textWysiwyg({ textWysiwyg({
id: element.id, id: element.id,
canvas: this.canvas, canvas: this.canvas,
getViewportCoords: (x, y) => { getViewportCoords: (x, y) => {
@ -5060,7 +5070,6 @@ class App extends React.Component<AppProps, AppState> {
} }
}), }),
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => { onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
textWysiwygSubmitHandler = null;
const isDeleted = !nextOriginalText.trim(); const isDeleted = !nextOriginalText.trim();
updateElement(nextOriginalText, isDeleted); updateElement(nextOriginalText, isDeleted);
// select the created text element only if submitting via keyboard // select the created text element only if submitting via keyboard

View file

@ -6,15 +6,24 @@ import { pointFrom, pointRotateRads } from "@excalidraw/math";
import { atom, useAtom } from "../editor-jotai"; import { atom, useAtom } from "../editor-jotai";
import { getElementAbsoluteCoords } from "../element"; import { getElementAbsoluteCoords, refreshTextDimensions } from "../element";
import { sceneCoordsToViewportCoords } from "../utils"; import { getFontString, sceneCoordsToViewportCoords } from "../utils";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { isArrowElement, isLinearElement } from "../element/typeChecks"; import { isArrowElement, isLinearElement } from "../element/typeChecks";
import { t } from "../i18n"; import { t } from "../i18n";
import "./ShapeSwitch.scss"; import {
computeBoundTextPosition,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
} from "../element/textElement";
import { wrapText } from "../element/textWrapping";
import { measureText } from "../element/textMeasurements";
import { mutateElement } from "../element/mutateElement";
import { getCommonBoundingBox } from "../element/bounds";
import { ToolButton } from "./ToolButton";
import { import {
ArrowIcon, ArrowIcon,
DiamondIcon, DiamondIcon,
@ -22,10 +31,17 @@ import {
LineIcon, LineIcon,
RectangleIcon, RectangleIcon,
} from "./icons"; } from "./icons";
import { ToolButton } from "./ToolButton";
import "./ShapeSwitch.scss";
import type App from "./App"; import type App from "./App";
import type { ExcalidrawElement } from "../element/types"; import type {
ElementsMap,
ExcalidrawElement,
ExcalidrawTextContainer,
ExcalidrawTextElementWithContainer,
GenericSwitchableToolType,
} from "../element/types";
import type { ToolType } from "../types"; import type { ToolType } from "../types";
const GAP_HORIZONTAL = 8; const GAP_HORIZONTAL = 8;
@ -46,8 +62,10 @@ export const shapeSwitchAtom = atom<
| null | null
>(null); >(null);
export const shapeSwitchFontSizeAtom = atom<{ export const shapeSwitchFontSizeAtom = atom<{
fontSize: number; [id: string]: {
elementType: "rectangle" | "diamond" | "ellipse"; fontSize: number;
elementType: GenericSwitchableToolType;
};
} | null>(null); } | null>(null);
const ShapeSwitch = ({ app }: { app: App }) => { const ShapeSwitch = ({ app }: { app: App }) => {
@ -64,22 +82,22 @@ const ShapeSwitch = ({ app }: { app: App }) => {
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
app.state, app.state,
); );
const firstElement = selectedElements[0];
const isSingleSelected = firstElement && selectedElements.length === 1;
// clear if hint target no longer matches // clear if hint target no longer matches
if (shapeSwitch.type === "hint" && firstElement?.id !== shapeSwitch.id) { if (
shapeSwitch.type === "hint" &&
selectedElements?.[0]?.id !== shapeSwitch.id
) {
setShapeSwitch(null); setShapeSwitch(null);
return null; return null;
} }
if (!isSingleSelected) { if (selectedElements.length === 0) {
setShapeSwitch(null); setShapeSwitch(null);
return null; return null;
} }
const props = { app, element: firstElement }; const props = { app, elements: selectedElements };
switch (shapeSwitch.type) { switch (shapeSwitch.type) {
case "hint": case "hint":
@ -91,7 +109,13 @@ const ShapeSwitch = ({ app }: { app: App }) => {
} }
}; };
const Hint = ({ app, element }: { app: App; element: ExcalidrawElement }) => { const Hint = ({
app,
elements,
}: {
app: App;
elements: ExcalidrawElement[];
}) => {
const [, setShapeSwitch] = useAtom(shapeSwitchAtom); const [, setShapeSwitch] = useAtom(shapeSwitchAtom);
const hintRef = useRef<HTMLDivElement>(null); const hintRef = useRef<HTMLDivElement>(null);
@ -114,14 +138,14 @@ const Hint = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
}, [setShapeSwitch]); }, [setShapeSwitch]);
const [x1, y1, , , cx, cy] = getElementAbsoluteCoords( const [x1, y1, , , cx, cy] = getElementAbsoluteCoords(
element, elements[0],
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
); );
const rotatedTopLeft = pointRotateRads( const rotatedTopLeft = pointRotateRads(
pointFrom(x1, y1), pointFrom(x1, y1),
pointFrom(cx, cy), pointFrom(cx, cy),
element.angle, elements[0].angle,
); );
const { x, y } = sceneCoordsToViewportCoords( const { x, y } = sceneCoordsToViewportCoords(
@ -135,7 +159,7 @@ const Hint = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
return ( return (
<div <div
ref={hintRef} ref={hintRef}
key={element.id} // key={element.id}
style={{ style={{
position: "absolute", position: "absolute",
bottom: `${ bottom: `${
@ -158,17 +182,35 @@ const Hint = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
); );
}; };
const Panel = ({ app, element }: { app: App; element: ExcalidrawElement }) => { const Panel = ({
const [x1, , , y2, cx, cy] = getElementAbsoluteCoords( app,
element, elements,
app.scene.getNonDeletedElementsMap(), }: {
); app: App;
elements: ExcalidrawElement[];
}) => {
let [x1, y2, cx, cy] = [0, 0, 0, 0];
let rotatedBottomLeft = [0, 0];
const rotatedBottomLeft = pointRotateRads( if (elements.length === 1) {
pointFrom(x1, y2), [x1, , , y2, cx, cy] = getElementAbsoluteCoords(
pointFrom(cx, cy), elements[0],
element.angle, app.scene.getNonDeletedElementsMap(),
); );
rotatedBottomLeft = pointRotateRads(
pointFrom(x1, y2),
pointFrom(cx, cy),
elements[0].angle,
);
} else {
const { minX, maxY, midX, midY } = getCommonBoundingBox(elements);
x1 = minX;
y2 = maxY;
cx = midX;
cy = midY;
rotatedBottomLeft = pointFrom(x1, y2);
}
const { x, y } = sceneCoordsToViewportCoords( const { x, y } = sceneCoordsToViewportCoords(
{ {
@ -178,7 +220,7 @@ const Panel = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
app.state, app.state,
); );
const SHAPES: [string, string, ReactNode][] = isLinearElement(element) const SHAPES: [string, string, ReactNode][] = isLinearElement(elements[0])
? [ ? [
["arrow", "5", ArrowIcon], ["arrow", "5", ArrowIcon],
["line", "6", LineIcon], ["line", "6", LineIcon],
@ -203,18 +245,22 @@ const Panel = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
> >
{SHAPES.map(([type, shortcut, icon]) => { {SHAPES.map(([type, shortcut, icon]) => {
const isSelected = const isSelected =
type === element.type || type === elements[0].type ||
(isArrowElement(element) && element.elbowed && type === "elbow") || (isArrowElement(elements[0]) &&
(isArrowElement(element) && element.roundness && type === "curve") || elements[0].elbowed &&
(isArrowElement(element) && type === "elbow") ||
!element.elbowed && (isArrowElement(elements[0]) &&
!element.roundness && elements[0].roundness &&
type === "curve") ||
(isArrowElement(elements[0]) &&
!elements[0].elbowed &&
!elements[0].roundness &&
type === "straight"); type === "straight");
return ( return (
<ToolButton <ToolButton
className="Shape" className="Shape"
key={`${element.version}_${type}`} key={`${elements[0].version}_${type}`}
type="radio" type="radio"
icon={icon} icon={icon}
checked={isSelected} checked={isSelected}
@ -241,4 +287,77 @@ const Panel = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
); );
}; };
export const adjustBoundTextSize = (
container: ExcalidrawTextContainer,
boundText: ExcalidrawTextElementWithContainer,
elementsMap: ElementsMap,
) => {
const maxWidth = getBoundTextMaxWidth(container, boundText);
const maxHeight = getBoundTextMaxHeight(container, boundText);
const wrappedText = wrapText(
boundText.text,
getFontString(boundText),
maxWidth,
);
let metrics = measureText(
wrappedText,
getFontString(boundText),
boundText.lineHeight,
);
let nextFontSize = boundText.fontSize;
while (
(metrics.width > maxWidth || metrics.height > maxHeight) &&
nextFontSize > 0
) {
nextFontSize -= 1;
const _updatedTextElement = {
...boundText,
fontSize: nextFontSize,
};
metrics = measureText(
boundText.text,
getFontString(_updatedTextElement),
boundText.lineHeight,
);
}
mutateElement(
boundText,
{
fontSize: nextFontSize,
width: metrics.width,
height: metrics.height,
},
false,
);
const { x, y } = computeBoundTextPosition(container, boundText, elementsMap);
mutateElement(
boundText,
{
x,
y,
},
false,
);
mutateElement(
boundText,
{
...refreshTextDimensions(
boundText,
container,
elementsMap,
boundText.originalText,
),
containerId: container.id,
},
false,
);
};
export default ShapeSwitch; export default ShapeSwitch;

View file

@ -10,7 +10,6 @@ import {
import { parseClipboard } from "../clipboard"; import { parseClipboard } from "../clipboard";
import { CLASSES, POINTER_BUTTON } from "../constants"; import { CLASSES, POINTER_BUTTON } from "../constants";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import Scene from "../scene/Scene";
import { import {
isWritableElement, isWritableElement,
getFontString, getFontString,
@ -35,7 +34,7 @@ import {
computeBoundTextPosition, computeBoundTextPosition,
getBoundTextElement, getBoundTextElement,
} from "./textElement"; } from "./textElement";
import { getTextWidth, measureText } from "./textMeasurements"; import { getTextWidth } from "./textMeasurements";
import { normalizeText } from "./textMeasurements"; import { normalizeText } from "./textMeasurements";
import { wrapText } from "./textWrapping"; import { wrapText } from "./textWrapping";
import { import {
@ -86,7 +85,6 @@ export const textWysiwyg = ({
excalidrawContainer, excalidrawContainer,
app, app,
autoSelect = true, autoSelect = true,
keepContainerDimensions = false,
}: { }: {
id: ExcalidrawElement["id"]; id: ExcalidrawElement["id"];
/** /**
@ -127,8 +125,7 @@ export const textWysiwyg = ({
const updateWysiwygStyle = () => { const updateWysiwygStyle = () => {
const appState = app.state; const appState = app.state;
const updatedTextElement = const updatedTextElement = app.scene.getElement<ExcalidrawTextElement>(id);
Scene.getScene(element)?.getElement<ExcalidrawTextElement>(id);
if (!updatedTextElement) { if (!updatedTextElement) {
return; return;
@ -190,48 +187,6 @@ export const textWysiwyg = ({
updatedTextElement as ExcalidrawTextElementWithContainer, updatedTextElement as ExcalidrawTextElementWithContainer,
); );
if (keepContainerDimensions) {
const wrappedText = wrapText(
updatedTextElement.text,
getFontString(updatedTextElement),
maxWidth,
);
let metrics = measureText(
wrappedText,
getFontString(updatedTextElement),
updatedTextElement.lineHeight,
);
if (width > maxWidth || height > maxHeight) {
let nextFontSize = updatedTextElement.fontSize;
while (
(metrics.width > maxWidth || metrics.height > maxHeight) &&
nextFontSize > 0
) {
nextFontSize -= 1;
const _updatedTextElement = {
...updatedTextElement,
fontSize: nextFontSize,
};
metrics = measureText(
updatedTextElement.text,
getFontString(_updatedTextElement),
updatedTextElement.lineHeight,
);
}
mutateElement(
updatedTextElement,
{ fontSize: nextFontSize },
false,
);
}
width = metrics.width;
height = metrics.height;
}
// autogrow container height if text exceeds // autogrow container height if text exceeds
if (!isArrowElement(container) && height > maxHeight) { if (!isArrowElement(container) && height > maxHeight) {
const targetContainerHeight = computeContainerDimensionForBoundText( const targetContainerHeight = computeContainerDimensionForBoundText(
@ -578,7 +533,7 @@ export const textWysiwyg = ({
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the // it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
// wysiwyg on update // wysiwyg on update
cleanup(); cleanup();
const updateElement = Scene.getScene(element)?.getElement( const updateElement = app.scene.getElement(
element.id, element.id,
) as ExcalidrawTextElement; ) as ExcalidrawTextElement;
if (!updateElement) { if (!updateElement) {

View file

@ -30,6 +30,8 @@ import type {
ExcalidrawRectangleElement, ExcalidrawRectangleElement,
ExcalidrawEllipseElement, ExcalidrawEllipseElement,
ExcalidrawDiamondElement, ExcalidrawDiamondElement,
GenericSwitchableToolType,
LinearSwitchableToolType,
} from "./types"; } from "./types";
export const isInitializedImageElement = ( export const isInitializedImageElement = (
@ -341,22 +343,25 @@ export const isBounds = (box: unknown): box is Bounds =>
typeof box[2] === "number" && typeof box[2] === "number" &&
typeof box[3] === "number"; typeof box[3] === "number";
type NonEmptyArray<T> = [T, ...T[]];
type ExcalidrawGenericSwitchableElement = type ExcalidrawGenericSwitchableElement =
| ExcalidrawRectangleElement | ExcalidrawRectangleElement
| ExcalidrawEllipseElement | ExcalidrawEllipseElement
| ExcalidrawDiamondElement; | ExcalidrawDiamondElement;
type GenericSwitchableToolType = "rectangle" | "ellipse" | "diamond";
type LinearSwitchableToolType = "arrow" | "line";
export const isGenericSwitchableElement = ( export const isGenericSwitchableElement = (
element: ExcalidrawElement, elements: ExcalidrawElement[],
): element is ExcalidrawGenericSwitchableElement => { ): elements is NonEmptyArray<ExcalidrawGenericSwitchableElement> => {
if (elements.length === 0) {
return false;
}
const firstType = elements[0].type;
return ( return (
element.type === "rectangle" || (firstType === "rectangle" ||
element.type === "ellipse" || firstType === "ellipse" ||
element.type === "diamond" firstType === "diamond") &&
elements.every((element) => element.type === firstType)
); );
}; };
@ -367,18 +372,16 @@ export const isGenericSwitchableToolType = (
}; };
export const isLinearSwitchableElement = ( export const isLinearSwitchableElement = (
element: ExcalidrawElement, elements: ExcalidrawElement[],
): element is ExcalidrawLinearElement => { ): elements is NonEmptyArray<ExcalidrawLinearElement> => {
if (element.type === "arrow" || element.type === "line") { if (elements.length === 0) {
if ( return false;
(!element.boundElements || element.boundElements.length === 0) &&
!element.startBinding &&
!element.endBinding
) {
return true;
}
} }
return false; const firstType = elements[0].type;
return (
(firstType === "arrow" || firstType === "line") &&
elements.every((element) => element.type === firstType)
);
}; };
export const isLinearSwitchableToolType = ( export const isLinearSwitchableToolType = (

View file

@ -411,3 +411,6 @@ export type NonDeletedSceneElementsMap = Map<
export type ElementsMapOrArray = export type ElementsMapOrArray =
| readonly ExcalidrawElement[] | readonly ExcalidrawElement[]
| Readonly<ElementsMap>; | Readonly<ElementsMap>;
export type GenericSwitchableToolType = "rectangle" | "ellipse" | "diamond";
export type LinearSwitchableToolType = "line" | "arrow";