support making transform handles optional
This commit is contained in:
parent
61699ff3c2
commit
3d0a1106ff
@ -223,6 +223,7 @@ import {
|
|||||||
withBatchedUpdatesThrottled,
|
withBatchedUpdatesThrottled,
|
||||||
updateObject,
|
updateObject,
|
||||||
setEraserCursor,
|
setEraserCursor,
|
||||||
|
getCustomElementConfig,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
||||||
import LayerUI from "./LayerUI";
|
import LayerUI from "./LayerUI";
|
||||||
@ -422,10 +423,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
coords: { x: number; y: number },
|
coords: { x: number; y: number },
|
||||||
name: string = "",
|
name: string = "",
|
||||||
) => {
|
) => {
|
||||||
const config = this.props.customElementsConfig!.find(
|
const config = getCustomElementConfig(
|
||||||
(config) => config.name === name,
|
this.props.customElementsConfig,
|
||||||
)!;
|
name,
|
||||||
|
);
|
||||||
|
if (!config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
coords.x,
|
coords.x,
|
||||||
coords.y,
|
coords.y,
|
||||||
@ -3393,6 +3397,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const elements = this.scene.getElements();
|
const elements = this.scene.getElements();
|
||||||
const selectedElements = getSelectedElements(elements, this.state);
|
const selectedElements = getSelectedElements(elements, this.state);
|
||||||
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
|
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
|
||||||
|
if (selectedElements[0].type === "custom") {
|
||||||
|
const config = getCustomElementConfig(
|
||||||
|
this.props.customElementsConfig,
|
||||||
|
selectedElements[0].name,
|
||||||
|
);
|
||||||
|
if (!config?.transformHandles) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
const elementWithTransformHandleType =
|
const elementWithTransformHandleType =
|
||||||
getElementWithTransformHandleType(
|
getElementWithTransformHandleType(
|
||||||
elements,
|
elements,
|
||||||
@ -4209,6 +4222,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const elementsWithinSelection = getElementsWithinSelection(
|
const elementsWithinSelection = getElementsWithinSelection(
|
||||||
elements,
|
elements,
|
||||||
draggingElement,
|
draggingElement,
|
||||||
|
this.props.customElementsConfig,
|
||||||
);
|
);
|
||||||
this.setState((prevState) =>
|
this.setState((prevState) =>
|
||||||
selectGroupsForSelectedElements(
|
selectGroupsForSelectedElements(
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import cssVariables from "./css/variables.module.scss";
|
import cssVariables from "./css/variables.module.scss";
|
||||||
import { AppProps } from "./types";
|
import { AppProps, CustomElementConfig } from "./types";
|
||||||
import { FontFamilyValues } from "./element/types";
|
import { FontFamilyValues } from "./element/types";
|
||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
@ -152,6 +152,14 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_CUSTOM_ELEMENT_CONFIG: Required<CustomElementConfig> = {
|
||||||
|
type: "custom",
|
||||||
|
name: "custom",
|
||||||
|
transformHandles: true,
|
||||||
|
svg: "",
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
};
|
||||||
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
||||||
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
||||||
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
||||||
|
@ -196,6 +196,7 @@ export default function App() {
|
|||||||
}, ${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
}, ${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
<path d="M256 32C114.6 32 .0272 125.1 .0272 240c0 47.63 19.91 91.25 52.91 126.2c-14.88 39.5-45.87 72.88-46.37 73.25c-6.625 7-8.375 17.25-4.625 26C5.818 474.2 14.38 480 24 480c61.5 0 109.1-25.75 139.1-46.25C191.1 442.8 223.3 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32zM256.1 400c-26.75 0-53.12-4.125-78.38-12.12l-22.75-7.125l-19.5 13.75c-14.25 10.12-33.88 21.38-57.5 29c7.375-12.12 14.37-25.75 19.88-40.25l10.62-28l-20.62-21.87C69.82 314.1 48.07 282.2 48.07 240c0-88.25 93.25-160 208-160s208 71.75 208 160S370.8 400 256.1 400z" />
|
<path d="M256 32C114.6 32 .0272 125.1 .0272 240c0 47.63 19.91 91.25 52.91 126.2c-14.88 39.5-45.87 72.88-46.37 73.25c-6.625 7-8.375 17.25-4.625 26C5.818 474.2 14.38 480 24 480c61.5 0 109.1-25.75 139.1-46.25C191.1 442.8 223.3 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32zM256.1 400c-26.75 0-53.12-4.125-78.38-12.12l-22.75-7.125l-19.5 13.75c-14.25 10.12-33.88 21.38-57.5 29c7.375-12.12 14.37-25.75 19.88-40.25l10.62-28l-20.62-21.87C69.82 314.1 48.07 282.2 48.07 240c0-88.25 93.25-160 208-160s208 71.75 208 160S370.8 400 256.1 400z" />
|
||||||
</svg>`)}`,
|
</svg>`)}`,
|
||||||
|
transformHandles: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,10 @@ import "../../css/styles.scss";
|
|||||||
|
|
||||||
import { AppProps, ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
import { AppProps, ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
||||||
import { defaultLang } from "../../i18n";
|
import { defaultLang } from "../../i18n";
|
||||||
import { DEFAULT_UI_OPTIONS } from "../../constants";
|
import {
|
||||||
|
DEFAULT_UI_OPTIONS,
|
||||||
|
DEFAULT_CUSTOM_ELEMENT_CONFIG,
|
||||||
|
} from "../../constants";
|
||||||
|
|
||||||
const Excalidraw = (props: ExcalidrawProps) => {
|
const Excalidraw = (props: ExcalidrawProps) => {
|
||||||
const {
|
const {
|
||||||
@ -37,7 +40,6 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
|||||||
generateIdForFile,
|
generateIdForFile,
|
||||||
onLinkOpen,
|
onLinkOpen,
|
||||||
renderCustomElementWidget,
|
renderCustomElementWidget,
|
||||||
customElementsConfig,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
@ -48,6 +50,11 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
|||||||
...canvasActions,
|
...canvasActions,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const customElementsConfig: AppProps["customElementsConfig"] =
|
||||||
|
props.customElementsConfig?.map((customElementConfig) => ({
|
||||||
|
...DEFAULT_CUSTOM_ELEMENT_CONFIG,
|
||||||
|
...customElementConfig,
|
||||||
|
}));
|
||||||
|
|
||||||
if (canvasActions?.export) {
|
if (canvasActions?.export) {
|
||||||
UIOptions.canvasActions.export.saveFileToDisk =
|
UIOptions.canvasActions.export.saveFileToDisk =
|
||||||
|
@ -25,7 +25,13 @@ import { RoughSVG } from "roughjs/bin/svg";
|
|||||||
import { RoughGenerator } from "roughjs/bin/generator";
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
import { RenderConfig } from "../scene/types";
|
import { RenderConfig } from "../scene/types";
|
||||||
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
|
import {
|
||||||
|
distance,
|
||||||
|
getFontString,
|
||||||
|
getFontFamilyString,
|
||||||
|
isRTL,
|
||||||
|
getCustomElementConfig,
|
||||||
|
} from "../utils";
|
||||||
import { isPathALoop } from "../math";
|
import { isPathALoop } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { AppState, BinaryFiles, Zoom } from "../types";
|
import { AppState, BinaryFiles, Zoom } from "../types";
|
||||||
@ -256,9 +262,13 @@ const drawElementOnCanvas = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "custom": {
|
case "custom": {
|
||||||
const config = renderConfig.customElementsConfig?.find(
|
const config = getCustomElementConfig(
|
||||||
(config) => config.name === element.name,
|
renderConfig.customElementsConfig,
|
||||||
)!;
|
element.name,
|
||||||
|
);
|
||||||
|
if (!config) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (!customElementImgCache[config.name]) {
|
if (!customElementImgCache[config.name]) {
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.id = config.name;
|
img.id = config.name;
|
||||||
|
@ -2,7 +2,7 @@ import { RoughCanvas } from "roughjs/bin/canvas";
|
|||||||
import { RoughSVG } from "roughjs/bin/svg";
|
import { RoughSVG } from "roughjs/bin/svg";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
|
|
||||||
import { AppState, BinaryFiles, Zoom } from "../types";
|
import { AppState, BinaryFiles, CustomElementConfig, Zoom } from "../types";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
@ -47,7 +47,11 @@ import {
|
|||||||
TransformHandles,
|
TransformHandles,
|
||||||
TransformHandleType,
|
TransformHandleType,
|
||||||
} from "../element/transformHandles";
|
} from "../element/transformHandles";
|
||||||
import { viewportCoordsToSceneCoords, supportsEmoji } from "../utils";
|
import {
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
|
supportsEmoji,
|
||||||
|
getCustomElementConfig,
|
||||||
|
} from "../utils";
|
||||||
import { UserIdleState } from "../types";
|
import { UserIdleState } from "../types";
|
||||||
import { THEME_FILTER } from "../constants";
|
import { THEME_FILTER } from "../constants";
|
||||||
import {
|
import {
|
||||||
@ -304,24 +308,35 @@ export const renderScene = (
|
|||||||
!appState.editingLinearElement
|
!appState.editingLinearElement
|
||||||
) {
|
) {
|
||||||
const selections = elements.reduce((acc, element) => {
|
const selections = elements.reduce((acc, element) => {
|
||||||
|
const isCustom = element.type === "custom";
|
||||||
|
let config: CustomElementConfig;
|
||||||
const selectionColors = [];
|
const selectionColors = [];
|
||||||
// local user
|
|
||||||
if (
|
if (element.type === "custom") {
|
||||||
appState.selectedElementIds[element.id] &&
|
config = getCustomElementConfig(
|
||||||
!isSelectedViaGroup(appState, element)
|
renderConfig.customElementsConfig,
|
||||||
) {
|
element.name,
|
||||||
selectionColors.push(oc.black);
|
)!;
|
||||||
}
|
}
|
||||||
// remote users
|
if (!isCustom || (isCustom && config!.transformHandles)) {
|
||||||
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
// local user
|
||||||
selectionColors.push(
|
if (
|
||||||
...renderConfig.remoteSelectedElementIds[element.id].map(
|
appState.selectedElementIds[element.id] &&
|
||||||
(socketId) => {
|
!isSelectedViaGroup(appState, element)
|
||||||
const { background } = getClientColors(socketId, appState);
|
) {
|
||||||
return background;
|
selectionColors.push(oc.black);
|
||||||
},
|
}
|
||||||
),
|
// remote users
|
||||||
);
|
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
||||||
|
selectionColors.push(
|
||||||
|
...renderConfig.remoteSelectedElementIds[element.id].map(
|
||||||
|
(socketId) => {
|
||||||
|
const { background } = getClientColors(socketId, appState);
|
||||||
|
return background;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (selectionColors.length) {
|
if (selectionColors.length) {
|
||||||
const [elementX1, elementY1, elementX2, elementY2] =
|
const [elementX1, elementY1, elementX2, elementY2] =
|
||||||
@ -351,7 +366,6 @@ export const renderScene = (
|
|||||||
selectionColors: [oc.black],
|
selectionColors: [oc.black],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const groupId of getSelectedGroupIds(appState)) {
|
for (const groupId of getSelectedGroupIds(appState)) {
|
||||||
// TODO: support multiplayer selected group IDs
|
// TODO: support multiplayer selected group IDs
|
||||||
addSelectionForGroupId(groupId);
|
addSelectionForGroupId(groupId);
|
||||||
@ -371,19 +385,32 @@ export const renderScene = (
|
|||||||
context.save();
|
context.save();
|
||||||
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
||||||
if (locallySelectedElements.length === 1) {
|
if (locallySelectedElements.length === 1) {
|
||||||
context.fillStyle = oc.white;
|
let showTransformHandles = true;
|
||||||
const transformHandles = getTransformHandles(
|
if (locallySelectedElements[0].type === "custom") {
|
||||||
locallySelectedElements[0],
|
const config = getCustomElementConfig(
|
||||||
renderConfig.zoom,
|
renderConfig.customElementsConfig,
|
||||||
"mouse", // when we render we don't know which pointer type so use mouse
|
locallySelectedElements[0].name,
|
||||||
);
|
|
||||||
if (!appState.viewModeEnabled) {
|
|
||||||
renderTransformHandles(
|
|
||||||
context,
|
|
||||||
renderConfig,
|
|
||||||
transformHandles,
|
|
||||||
locallySelectedElements[0].angle,
|
|
||||||
);
|
);
|
||||||
|
if (!config || !config.transformHandles) {
|
||||||
|
showTransformHandles = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showTransformHandles) {
|
||||||
|
context.fillStyle = oc.white;
|
||||||
|
const transformHandles = getTransformHandles(
|
||||||
|
locallySelectedElements[0],
|
||||||
|
renderConfig.zoom,
|
||||||
|
"mouse", // when we render we don't know which pointer type so use mouse
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!appState.viewModeEnabled) {
|
||||||
|
renderTransformHandles(
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
transformHandles,
|
||||||
|
locallySelectedElements[0].angle,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (locallySelectedElements.length > 1 && !appState.isRotating) {
|
} else if (locallySelectedElements.length > 1 && !appState.isRotating) {
|
||||||
const dashedLinePadding = 4 / renderConfig.zoom.value;
|
const dashedLinePadding = 4 / renderConfig.zoom.value;
|
||||||
@ -570,6 +597,7 @@ const renderTransformHandles = (
|
|||||||
renderConfig: RenderConfig,
|
renderConfig: RenderConfig,
|
||||||
transformHandles: TransformHandles,
|
transformHandles: TransformHandles,
|
||||||
angle: number,
|
angle: number,
|
||||||
|
name?: string,
|
||||||
): void => {
|
): void => {
|
||||||
Object.keys(transformHandles).forEach((key) => {
|
Object.keys(transformHandles).forEach((key) => {
|
||||||
const transformHandle = transformHandles[key as TransformHandleType];
|
const transformHandle = transformHandles[key as TransformHandleType];
|
||||||
|
@ -3,20 +3,27 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||||
import { AppState } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
|
import { getCustomElementConfig } from "../utils";
|
||||||
|
|
||||||
export const getElementsWithinSelection = (
|
export const getElementsWithinSelection = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
selection: NonDeletedExcalidrawElement,
|
selection: NonDeletedExcalidrawElement,
|
||||||
|
customElementConfig: ExcalidrawProps["customElementsConfig"],
|
||||||
) => {
|
) => {
|
||||||
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
||||||
getElementAbsoluteCoords(selection);
|
getElementAbsoluteCoords(selection);
|
||||||
return elements.filter((element) => {
|
return elements.filter((element) => {
|
||||||
const [elementX1, elementY1, elementX2, elementY2] =
|
const [elementX1, elementY1, elementX2, elementY2] =
|
||||||
getElementBounds(element);
|
getElementBounds(element);
|
||||||
|
const isCustom = element.type === "custom";
|
||||||
|
const allowSelection = isCustom
|
||||||
|
? getCustomElementConfig(customElementConfig, element.name)
|
||||||
|
?.transformHandles
|
||||||
|
: true;
|
||||||
return (
|
return (
|
||||||
|
allowSelection &&
|
||||||
element.type !== "selection" &&
|
element.type !== "selection" &&
|
||||||
!isBoundToContainer(element) &&
|
!isBoundToContainer(element) &&
|
||||||
selectionX1 <= elementX1 &&
|
selectionX1 <= elementX1 &&
|
||||||
|
@ -206,11 +206,10 @@ export type ExcalidrawAPIRefValue =
|
|||||||
ready?: false;
|
ready?: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CustomElementConfig = {
|
export type CustomElementConfig = {
|
||||||
type: "custom";
|
type: "custom";
|
||||||
name: string;
|
name: string;
|
||||||
resize?: boolean;
|
transformHandles?: boolean;
|
||||||
rotate?: boolean;
|
|
||||||
svg: string;
|
svg: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
@ -317,6 +316,7 @@ export type AppProps = ExcalidrawProps & {
|
|||||||
detectScroll: boolean;
|
detectScroll: boolean;
|
||||||
handleKeyboardGlobally: boolean;
|
handleKeyboardGlobally: boolean;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
|
customElementsConfig: Required<CustomElementConfig>[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A subset of App class properties that we need to use elsewhere
|
/** A subset of App class properties that we need to use elsewhere
|
||||||
|
12
src/utils.ts
12
src/utils.ts
@ -11,7 +11,7 @@ import {
|
|||||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { FontFamilyValues, FontString } from "./element/types";
|
import { FontFamilyValues, FontString } from "./element/types";
|
||||||
import { AppState, DataURL, Zoom } from "./types";
|
import { AppState, DataURL, ExcalidrawProps, Zoom } from "./types";
|
||||||
import { unstable_batchedUpdates } from "react-dom";
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
import { isDarwin } from "./keys";
|
import { isDarwin } from "./keys";
|
||||||
|
|
||||||
@ -612,3 +612,13 @@ export const updateObject = <T extends Record<string, any>>(
|
|||||||
...updates,
|
...updates,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCustomElementConfig = (
|
||||||
|
customElementConfig: ExcalidrawProps["customElementsConfig"],
|
||||||
|
name: string,
|
||||||
|
) => {
|
||||||
|
if (!customElementConfig) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return customElementConfig.find((config) => config.name === name);
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user