diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 5a309b677..0ad1b518f 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -121,6 +121,8 @@ import { ArrowheadCrowfootIcon, ArrowheadCrowfootOneIcon, ArrowheadCrowfootOneOrManyIcon, + snapShapeEnabledIcon, + snapShapeDisabledIcon, } from "../components/icons"; import { Fonts } from "../fonts"; @@ -1818,3 +1820,41 @@ export const actionChangeArrowType = register({ ); }, }); + +export const actionToggleShapeSnap = register({ + name: "toggleShapeSnap", + label: "Toggle Snap to Shape", + trackEvent: false, + perform: (elements, appState) => { + return { + elements, + appState: { + ...appState, + isShapeSnapEnabled: !appState.isShapeSnapEnabled, + }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }; + }, + PanelComponent: ({ appState, updateData }) => ( +
+ {t("labels.shapeSnap")} + updateData(value)} + /> +
+ ), +}); diff --git a/packages/excalidraw/actions/types.ts b/packages/excalidraw/actions/types.ts index 152b9a0c7..5effd385b 100644 --- a/packages/excalidraw/actions/types.ts +++ b/packages/excalidraw/actions/types.ts @@ -125,6 +125,7 @@ export type ActionName = | "toggleLinearEditor" | "toggleEraserTool" | "toggleHandTool" + | "toggleShapeSnap" | "selectAllElementsInFrame" | "removeAllElementsFromFrame" | "updateFrameRendering" diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index c01af195d..c40af7bee 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -141,113 +141,113 @@ const APP_STATE_STORAGE_CONF = (< T extends Record, >(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) => config)({ - showWelcomeScreen: { browser: true, export: false, server: false }, - theme: { browser: true, export: false, server: false }, - collaborators: { browser: false, export: false, server: false }, - currentChartType: { browser: true, export: false, server: false }, - currentItemBackgroundColor: { browser: true, export: false, server: false }, - currentItemEndArrowhead: { browser: true, export: false, server: false }, - currentItemFillStyle: { browser: true, export: false, server: false }, - currentItemFontFamily: { browser: true, export: false, server: false }, - currentItemFontSize: { browser: true, export: false, server: false }, - currentItemRoundness: { - browser: true, - export: false, - server: false, - }, - currentItemArrowType: { - browser: true, - export: false, - server: false, - }, - currentItemOpacity: { browser: true, export: false, server: false }, - currentItemRoughness: { browser: true, export: false, server: false }, - currentItemStartArrowhead: { browser: true, export: false, server: false }, - currentItemStrokeColor: { browser: true, export: false, server: false }, - currentItemStrokeStyle: { browser: true, export: false, server: false }, - currentItemStrokeWidth: { browser: true, export: false, server: false }, - currentItemTextAlign: { browser: true, export: false, server: false }, - currentHoveredFontFamily: { browser: false, export: false, server: false }, - cursorButton: { browser: true, export: false, server: false }, - activeEmbeddable: { browser: false, export: false, server: false }, - newElement: { browser: false, export: false, server: false }, - editingTextElement: { browser: false, export: false, server: false }, - editingGroupId: { browser: true, export: false, server: false }, - editingLinearElement: { browser: false, export: false, server: false }, - activeTool: { browser: true, export: false, server: false }, - penMode: { browser: true, export: false, server: false }, - penDetected: { browser: true, export: false, server: false }, - errorMessage: { browser: false, export: false, server: false }, - exportBackground: { browser: true, export: false, server: false }, - exportEmbedScene: { browser: true, export: false, server: false }, - exportScale: { browser: true, export: false, server: false }, - exportWithDarkMode: { browser: true, export: false, server: false }, - fileHandle: { browser: false, export: false, server: false }, - gridSize: { browser: true, export: true, server: true }, - gridStep: { browser: true, export: true, server: true }, - gridModeEnabled: { browser: true, export: true, server: true }, - height: { browser: false, export: false, server: false }, - isBindingEnabled: { browser: false, export: false, server: false }, - isShapeSnapEnabled: { browser: false, export: false, server: false }, - defaultSidebarDockedPreference: { - browser: true, - export: false, - server: false, - }, - isLoading: { browser: false, export: false, server: false }, - isResizing: { browser: false, export: false, server: false }, - isRotating: { browser: false, export: false, server: false }, - lastPointerDownWith: { browser: true, export: false, server: false }, - multiElement: { browser: false, export: false, server: false }, - name: { browser: true, export: false, server: false }, - offsetLeft: { browser: false, export: false, server: false }, - offsetTop: { browser: false, export: false, server: false }, - contextMenu: { browser: false, export: false, server: false }, - openMenu: { browser: true, export: false, server: false }, - openPopup: { browser: false, export: false, server: false }, - openSidebar: { browser: true, export: false, server: false }, - openDialog: { browser: false, export: false, server: false }, - pasteDialog: { browser: false, export: false, server: false }, - previousSelectedElementIds: { browser: true, export: false, server: false }, - resizingElement: { browser: false, export: false, server: false }, - scrolledOutside: { browser: true, export: false, server: false }, - scrollX: { browser: true, export: false, server: false }, - scrollY: { browser: true, export: false, server: false }, - selectedElementIds: { browser: true, export: false, server: false }, - hoveredElementIds: { browser: false, export: false, server: false }, - selectedGroupIds: { browser: true, export: false, server: false }, - selectedElementsAreBeingDragged: { - browser: false, - export: false, - server: false, - }, - selectionElement: { browser: false, export: false, server: false }, - shouldCacheIgnoreZoom: { browser: true, export: false, server: false }, - stats: { browser: true, export: false, server: false }, - startBoundElement: { browser: false, export: false, server: false }, - suggestedBindings: { browser: false, export: false, server: false }, - frameRendering: { browser: false, export: false, server: false }, - frameToHighlight: { browser: false, export: false, server: false }, - editingFrame: { browser: false, export: false, server: false }, - elementsToHighlight: { browser: false, export: false, server: false }, - toast: { browser: false, export: false, server: false }, - viewBackgroundColor: { browser: true, export: true, server: true }, - width: { browser: false, export: false, server: false }, - zenModeEnabled: { browser: true, export: false, server: false }, - zoom: { browser: true, export: false, server: false }, - viewModeEnabled: { browser: false, export: false, server: false }, - pendingImageElementId: { browser: false, export: false, server: false }, - showHyperlinkPopup: { browser: false, export: false, server: false }, - selectedLinearElement: { browser: true, export: false, server: false }, - snapLines: { browser: false, export: false, server: false }, - originSnapOffset: { browser: false, export: false, server: false }, - objectsSnapModeEnabled: { browser: true, export: false, server: false }, - userToFollow: { browser: false, export: false, server: false }, - followedBy: { browser: false, export: false, server: false }, - isCropping: { browser: false, export: false, server: false }, - croppingElementId: { browser: false, export: false, server: false }, - searchMatches: { browser: false, export: false, server: false }, - }); + showWelcomeScreen: { browser: true, export: false, server: false }, + theme: { browser: true, export: false, server: false }, + collaborators: { browser: false, export: false, server: false }, + currentChartType: { browser: true, export: false, server: false }, + currentItemBackgroundColor: { browser: true, export: false, server: false }, + currentItemEndArrowhead: { browser: true, export: false, server: false }, + currentItemFillStyle: { browser: true, export: false, server: false }, + currentItemFontFamily: { browser: true, export: false, server: false }, + currentItemFontSize: { browser: true, export: false, server: false }, + currentItemRoundness: { + browser: true, + export: false, + server: false, + }, + currentItemArrowType: { + browser: true, + export: false, + server: false, + }, + currentItemOpacity: { browser: true, export: false, server: false }, + currentItemRoughness: { browser: true, export: false, server: false }, + currentItemStartArrowhead: { browser: true, export: false, server: false }, + currentItemStrokeColor: { browser: true, export: false, server: false }, + currentItemStrokeStyle: { browser: true, export: false, server: false }, + currentItemStrokeWidth: { browser: true, export: false, server: false }, + currentItemTextAlign: { browser: true, export: false, server: false }, + currentHoveredFontFamily: { browser: false, export: false, server: false }, + cursorButton: { browser: true, export: false, server: false }, + activeEmbeddable: { browser: false, export: false, server: false }, + newElement: { browser: false, export: false, server: false }, + editingTextElement: { browser: false, export: false, server: false }, + editingGroupId: { browser: true, export: false, server: false }, + editingLinearElement: { browser: false, export: false, server: false }, + activeTool: { browser: true, export: false, server: false }, + penMode: { browser: true, export: false, server: false }, + penDetected: { browser: true, export: false, server: false }, + errorMessage: { browser: false, export: false, server: false }, + exportBackground: { browser: true, export: false, server: false }, + exportEmbedScene: { browser: true, export: false, server: false }, + exportScale: { browser: true, export: false, server: false }, + exportWithDarkMode: { browser: true, export: false, server: false }, + fileHandle: { browser: false, export: false, server: false }, + gridSize: { browser: true, export: true, server: true }, + gridStep: { browser: true, export: true, server: true }, + gridModeEnabled: { browser: true, export: true, server: true }, + height: { browser: false, export: false, server: false }, + isBindingEnabled: { browser: false, export: false, server: false }, + isShapeSnapEnabled: { browser: true, export: false, server: false }, + defaultSidebarDockedPreference: { + browser: true, + export: false, + server: false, + }, + isLoading: { browser: false, export: false, server: false }, + isResizing: { browser: false, export: false, server: false }, + isRotating: { browser: false, export: false, server: false }, + lastPointerDownWith: { browser: true, export: false, server: false }, + multiElement: { browser: false, export: false, server: false }, + name: { browser: true, export: false, server: false }, + offsetLeft: { browser: false, export: false, server: false }, + offsetTop: { browser: false, export: false, server: false }, + contextMenu: { browser: false, export: false, server: false }, + openMenu: { browser: true, export: false, server: false }, + openPopup: { browser: false, export: false, server: false }, + openSidebar: { browser: true, export: false, server: false }, + openDialog: { browser: false, export: false, server: false }, + pasteDialog: { browser: false, export: false, server: false }, + previousSelectedElementIds: { browser: true, export: false, server: false }, + resizingElement: { browser: false, export: false, server: false }, + scrolledOutside: { browser: true, export: false, server: false }, + scrollX: { browser: true, export: false, server: false }, + scrollY: { browser: true, export: false, server: false }, + selectedElementIds: { browser: true, export: false, server: false }, + hoveredElementIds: { browser: false, export: false, server: false }, + selectedGroupIds: { browser: true, export: false, server: false }, + selectedElementsAreBeingDragged: { + browser: false, + export: false, + server: false, + }, + selectionElement: { browser: false, export: false, server: false }, + shouldCacheIgnoreZoom: { browser: true, export: false, server: false }, + stats: { browser: true, export: false, server: false }, + startBoundElement: { browser: false, export: false, server: false }, + suggestedBindings: { browser: false, export: false, server: false }, + frameRendering: { browser: false, export: false, server: false }, + frameToHighlight: { browser: false, export: false, server: false }, + editingFrame: { browser: false, export: false, server: false }, + elementsToHighlight: { browser: false, export: false, server: false }, + toast: { browser: false, export: false, server: false }, + viewBackgroundColor: { browser: true, export: true, server: true }, + width: { browser: false, export: false, server: false }, + zenModeEnabled: { browser: true, export: false, server: false }, + zoom: { browser: true, export: false, server: false }, + viewModeEnabled: { browser: false, export: false, server: false }, + pendingImageElementId: { browser: false, export: false, server: false }, + showHyperlinkPopup: { browser: false, export: false, server: false }, + selectedLinearElement: { browser: true, export: false, server: false }, + snapLines: { browser: false, export: false, server: false }, + originSnapOffset: { browser: false, export: false, server: false }, + objectsSnapModeEnabled: { browser: true, export: false, server: false }, + userToFollow: { browser: false, export: false, server: false }, + followedBy: { browser: false, export: false, server: false }, + isCropping: { browser: false, export: false, server: false }, + croppingElementId: { browser: false, export: false, server: false }, + searchMatches: { browser: false, export: false, server: false }, +}); const _clearAppStateForStorage = < ExportType extends "export" | "browser" | "server", @@ -257,8 +257,8 @@ const _clearAppStateForStorage = < ) => { type ExportableKeys = { [K in keyof typeof APP_STATE_STORAGE_CONF]: typeof APP_STATE_STORAGE_CONF[K][ExportType] extends true - ? K - : never; + ? K + : never; }[keyof typeof APP_STATE_STORAGE_CONF]; const stateForExport = {} as { [K in ExportableKeys]?: typeof appState[K] }; for (const key of Object.keys(appState) as (keyof typeof appState)[]) { diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index b69204867..dfea06fc3 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -169,9 +169,12 @@ export const SelectedShapeActions = ({ renderAction("changeStrokeWidth")} {(appState.activeTool.type === "freedraw" || - targetElements.some((element) => element.type === "freedraw")) && - renderAction("changeStrokeShape")} - + targetElements.some((element) => element.type === "freedraw")) && ( + <> + {renderAction("changeStrokeShape")} + {renderAction("toggleShapeSnap")} + + )} {(hasStrokeStyle(appState.activeTool.type) || targetElements.some((element) => hasStrokeStyle(element.type))) && ( <> diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 9046c393d..1b036821c 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -8959,7 +8959,19 @@ class App extends React.Component { if (this.state.isShapeSnapEnabled) { const detectedElement = convertToShape(newElement); + if (detectedElement !== newElement) { + if (detectedElement.type === "arrow") { + mutateElement( + detectedElement, + { + startArrowhead: this.state.currentItemStartArrowhead, + endArrowhead: this.state.currentItemEndArrowhead, + }, + // TODO: Make arrows bind to nearby elements if possible + ); + } + this.scene.replaceAllElements([ ...this.scene .getElementsIncludingDeleted() diff --git a/packages/excalidraw/components/icons.tsx b/packages/excalidraw/components/icons.tsx index c6d7f9473..bb7d1143a 100644 --- a/packages/excalidraw/components/icons.tsx +++ b/packages/excalidraw/components/icons.tsx @@ -1887,6 +1887,23 @@ export const eyeClosedIcon = createIcon( tablerIconProps, ); +export const snapShapeEnabledIcon = createIcon( + + + + , + tablerIconProps, +); + +export const snapShapeDisabledIcon = createIcon( + + + + + , + tablerIconProps, +); + export const brainIcon = createIcon( diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index f14b79705..7f1b7da47 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -103,6 +103,9 @@ "loadingScene": "Loading scene…", "loadScene": "Load scene from file", "align": "Align", + "shapeSnap": "Snap to shapes", + "shapeSnapDisable": "Disable snap to shapes", + "shapeSnapEnable": "Enable snap to shapes", "alignTop": "Align top", "alignBottom": "Align bottom", "alignLeft": "Align left", diff --git a/packages/utils/src/snapToShape.ts b/packages/utils/src/snapToShape.ts index 00b1b2c31..4707c4f4f 100644 --- a/packages/utils/src/snapToShape.ts +++ b/packages/utils/src/snapToShape.ts @@ -390,7 +390,6 @@ export const convertToShape = ( return newArrowElement({ ...freeDrawElement, type: recognizedShape.type, - endArrowhead: "arrow", // TODO: Get correct state points: [ recognizedShape.simplified[0], recognizedShape.simplified[recognizedShape.simplified.length - 2]