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 }) => (
+
+ ),
+});
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]