Toggle shapeSnap

This commit is contained in:
Mathias Krafft 2025-04-01 17:08:03 +02:00
parent 452373d769
commit 5ac50bdc88
No known key found for this signature in database
GPG key ID: D99E394FA2319429
8 changed files with 188 additions and 113 deletions

View file

@ -121,6 +121,8 @@ import {
ArrowheadCrowfootIcon, ArrowheadCrowfootIcon,
ArrowheadCrowfootOneIcon, ArrowheadCrowfootOneIcon,
ArrowheadCrowfootOneOrManyIcon, ArrowheadCrowfootOneOrManyIcon,
snapShapeEnabledIcon,
snapShapeDisabledIcon,
} from "../components/icons"; } from "../components/icons";
import { Fonts } from "../fonts"; 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 }) => (
<fieldset>
<legend>{t("labels.shapeSnap")}</legend>
<ButtonIconSelect
group="button"
options={[
{
value: false,
text: t("labels.shapeSnapDisable"),
icon: snapShapeDisabledIcon,
},
{
value: true,
text: t("labels.shapeSnapEnable"),
icon: snapShapeEnabledIcon,
},
]}
value={appState.isShapeSnapEnabled}
onChange={(value) => updateData(value)}
/>
</fieldset>
),
});

View file

@ -125,6 +125,7 @@ export type ActionName =
| "toggleLinearEditor" | "toggleLinearEditor"
| "toggleEraserTool" | "toggleEraserTool"
| "toggleHandTool" | "toggleHandTool"
| "toggleShapeSnap"
| "selectAllElementsInFrame" | "selectAllElementsInFrame"
| "removeAllElementsFromFrame" | "removeAllElementsFromFrame"
| "updateFrameRendering" | "updateFrameRendering"

View file

@ -141,113 +141,113 @@ const APP_STATE_STORAGE_CONF = (<
T extends Record<keyof AppState, Values>, T extends Record<keyof AppState, Values>,
>(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) => >(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
config)({ config)({
showWelcomeScreen: { browser: true, export: false, server: false }, showWelcomeScreen: { browser: true, export: false, server: false },
theme: { browser: true, export: false, server: false }, theme: { browser: true, export: false, server: false },
collaborators: { browser: false, export: false, server: false }, collaborators: { browser: false, export: false, server: false },
currentChartType: { browser: true, export: false, server: false }, currentChartType: { browser: true, export: false, server: false },
currentItemBackgroundColor: { browser: true, export: false, server: false }, currentItemBackgroundColor: { browser: true, export: false, server: false },
currentItemEndArrowhead: { browser: true, export: false, server: false }, currentItemEndArrowhead: { browser: true, export: false, server: false },
currentItemFillStyle: { browser: true, export: false, server: false }, currentItemFillStyle: { browser: true, export: false, server: false },
currentItemFontFamily: { browser: true, export: false, server: false }, currentItemFontFamily: { browser: true, export: false, server: false },
currentItemFontSize: { browser: true, export: false, server: false }, currentItemFontSize: { browser: true, export: false, server: false },
currentItemRoundness: { currentItemRoundness: {
browser: true, browser: true,
export: false, export: false,
server: false, server: false,
}, },
currentItemArrowType: { currentItemArrowType: {
browser: true, browser: true,
export: false, export: false,
server: false, server: false,
}, },
currentItemOpacity: { browser: true, export: false, server: false }, currentItemOpacity: { browser: true, export: false, server: false },
currentItemRoughness: { browser: true, export: false, server: false }, currentItemRoughness: { browser: true, export: false, server: false },
currentItemStartArrowhead: { browser: true, export: false, server: false }, currentItemStartArrowhead: { browser: true, export: false, server: false },
currentItemStrokeColor: { browser: true, export: false, server: false }, currentItemStrokeColor: { browser: true, export: false, server: false },
currentItemStrokeStyle: { browser: true, export: false, server: false }, currentItemStrokeStyle: { browser: true, export: false, server: false },
currentItemStrokeWidth: { browser: true, export: false, server: false }, currentItemStrokeWidth: { browser: true, export: false, server: false },
currentItemTextAlign: { browser: true, export: false, server: false }, currentItemTextAlign: { browser: true, export: false, server: false },
currentHoveredFontFamily: { browser: false, export: false, server: false }, currentHoveredFontFamily: { browser: false, export: false, server: false },
cursorButton: { browser: true, export: false, server: false }, cursorButton: { browser: true, export: false, server: false },
activeEmbeddable: { browser: false, export: false, server: false }, activeEmbeddable: { browser: false, export: false, server: false },
newElement: { browser: false, export: false, server: false }, newElement: { browser: false, export: false, server: false },
editingTextElement: { browser: false, export: false, server: false }, editingTextElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false }, editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false }, editingLinearElement: { browser: false, export: false, server: false },
activeTool: { browser: true, export: false, server: false }, activeTool: { browser: true, export: false, server: false },
penMode: { browser: true, export: false, server: false }, penMode: { browser: true, export: false, server: false },
penDetected: { browser: true, export: false, server: false }, penDetected: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false }, errorMessage: { browser: false, export: false, server: false },
exportBackground: { browser: true, export: false, server: false }, exportBackground: { browser: true, export: false, server: false },
exportEmbedScene: { browser: true, export: false, server: false }, exportEmbedScene: { browser: true, export: false, server: false },
exportScale: { browser: true, export: false, server: false }, exportScale: { browser: true, export: false, server: false },
exportWithDarkMode: { browser: true, export: false, server: false }, exportWithDarkMode: { browser: true, export: false, server: false },
fileHandle: { browser: false, export: false, server: false }, fileHandle: { browser: false, export: false, server: false },
gridSize: { browser: true, export: true, server: true }, gridSize: { browser: true, export: true, server: true },
gridStep: { browser: true, export: true, server: true }, gridStep: { browser: true, export: true, server: true },
gridModeEnabled: { browser: true, export: true, server: true }, gridModeEnabled: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false }, height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false }, isBindingEnabled: { browser: false, export: false, server: false },
isShapeSnapEnabled: { browser: false, export: false, server: false }, isShapeSnapEnabled: { browser: true, export: false, server: false },
defaultSidebarDockedPreference: { defaultSidebarDockedPreference: {
browser: true, browser: true,
export: false, export: false,
server: false, server: false,
}, },
isLoading: { browser: false, export: false, server: false }, isLoading: { browser: false, export: false, server: false },
isResizing: { browser: false, export: false, server: false }, isResizing: { browser: false, export: false, server: false },
isRotating: { browser: false, export: false, server: false }, isRotating: { browser: false, export: false, server: false },
lastPointerDownWith: { browser: true, export: false, server: false }, lastPointerDownWith: { browser: true, export: false, server: false },
multiElement: { browser: false, export: false, server: false }, multiElement: { browser: false, export: false, server: false },
name: { browser: true, export: false, server: false }, name: { browser: true, export: false, server: false },
offsetLeft: { browser: false, export: false, server: false }, offsetLeft: { browser: false, export: false, server: false },
offsetTop: { browser: false, export: false, server: false }, offsetTop: { browser: false, export: false, server: false },
contextMenu: { browser: false, export: false, server: false }, contextMenu: { browser: false, export: false, server: false },
openMenu: { browser: true, export: false, server: false }, openMenu: { browser: true, export: false, server: false },
openPopup: { browser: false, export: false, server: false }, openPopup: { browser: false, export: false, server: false },
openSidebar: { browser: true, export: false, server: false }, openSidebar: { browser: true, export: false, server: false },
openDialog: { browser: false, export: false, server: false }, openDialog: { browser: false, export: false, server: false },
pasteDialog: { browser: false, export: false, server: false }, pasteDialog: { browser: false, export: false, server: false },
previousSelectedElementIds: { browser: true, export: false, server: false }, previousSelectedElementIds: { browser: true, export: false, server: false },
resizingElement: { browser: false, export: false, server: false }, resizingElement: { browser: false, export: false, server: false },
scrolledOutside: { browser: true, export: false, server: false }, scrolledOutside: { browser: true, export: false, server: false },
scrollX: { browser: true, export: false, server: false }, scrollX: { browser: true, export: false, server: false },
scrollY: { browser: true, export: false, server: false }, scrollY: { browser: true, export: false, server: false },
selectedElementIds: { browser: true, export: false, server: false }, selectedElementIds: { browser: true, export: false, server: false },
hoveredElementIds: { browser: false, export: false, server: false }, hoveredElementIds: { browser: false, export: false, server: false },
selectedGroupIds: { browser: true, export: false, server: false }, selectedGroupIds: { browser: true, export: false, server: false },
selectedElementsAreBeingDragged: { selectedElementsAreBeingDragged: {
browser: false, browser: false,
export: false, export: false,
server: false, server: false,
}, },
selectionElement: { browser: false, export: false, server: false }, selectionElement: { browser: false, export: false, server: false },
shouldCacheIgnoreZoom: { browser: true, export: false, server: false }, shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
stats: { browser: true, export: false, server: false }, stats: { browser: true, export: false, server: false },
startBoundElement: { browser: false, export: false, server: false }, startBoundElement: { browser: false, export: false, server: false },
suggestedBindings: { browser: false, export: false, server: false }, suggestedBindings: { browser: false, export: false, server: false },
frameRendering: { browser: false, export: false, server: false }, frameRendering: { browser: false, export: false, server: false },
frameToHighlight: { browser: false, export: false, server: false }, frameToHighlight: { browser: false, export: false, server: false },
editingFrame: { browser: false, export: false, server: false }, editingFrame: { browser: false, export: false, server: false },
elementsToHighlight: { browser: false, export: false, server: false }, elementsToHighlight: { browser: false, export: false, server: false },
toast: { browser: false, export: false, server: false }, toast: { browser: false, export: false, server: false },
viewBackgroundColor: { browser: true, export: true, server: true }, viewBackgroundColor: { browser: true, export: true, server: true },
width: { browser: false, export: false, server: false }, width: { browser: false, export: false, server: false },
zenModeEnabled: { browser: true, export: false, server: false }, zenModeEnabled: { browser: true, export: false, server: false },
zoom: { browser: true, export: false, server: false }, zoom: { browser: true, export: false, server: false },
viewModeEnabled: { browser: false, export: false, server: false }, viewModeEnabled: { browser: false, export: false, server: false },
pendingImageElementId: { browser: false, export: false, server: false }, pendingImageElementId: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, export: false, server: false }, showHyperlinkPopup: { browser: false, export: false, server: false },
selectedLinearElement: { browser: true, export: false, server: false }, selectedLinearElement: { browser: true, export: false, server: false },
snapLines: { browser: false, export: false, server: false }, snapLines: { browser: false, export: false, server: false },
originSnapOffset: { browser: false, export: false, server: false }, originSnapOffset: { browser: false, export: false, server: false },
objectsSnapModeEnabled: { browser: true, export: false, server: false }, objectsSnapModeEnabled: { browser: true, export: false, server: false },
userToFollow: { browser: false, export: false, server: false }, userToFollow: { browser: false, export: false, server: false },
followedBy: { browser: false, export: false, server: false }, followedBy: { browser: false, export: false, server: false },
isCropping: { browser: false, export: false, server: false }, isCropping: { browser: false, export: false, server: false },
croppingElementId: { browser: false, export: false, server: false }, croppingElementId: { browser: false, export: false, server: false },
searchMatches: { browser: false, export: false, server: false }, searchMatches: { browser: false, export: false, server: false },
}); });
const _clearAppStateForStorage = < const _clearAppStateForStorage = <
ExportType extends "export" | "browser" | "server", ExportType extends "export" | "browser" | "server",
@ -257,8 +257,8 @@ const _clearAppStateForStorage = <
) => { ) => {
type ExportableKeys = { type ExportableKeys = {
[K in keyof typeof APP_STATE_STORAGE_CONF]: typeof APP_STATE_STORAGE_CONF[K][ExportType] extends true [K in keyof typeof APP_STATE_STORAGE_CONF]: typeof APP_STATE_STORAGE_CONF[K][ExportType] extends true
? K ? K
: never; : never;
}[keyof typeof APP_STATE_STORAGE_CONF]; }[keyof typeof APP_STATE_STORAGE_CONF];
const stateForExport = {} as { [K in ExportableKeys]?: typeof appState[K] }; const stateForExport = {} as { [K in ExportableKeys]?: typeof appState[K] };
for (const key of Object.keys(appState) as (keyof typeof appState)[]) { for (const key of Object.keys(appState) as (keyof typeof appState)[]) {

View file

@ -169,9 +169,12 @@ export const SelectedShapeActions = ({
renderAction("changeStrokeWidth")} renderAction("changeStrokeWidth")}
{(appState.activeTool.type === "freedraw" || {(appState.activeTool.type === "freedraw" ||
targetElements.some((element) => element.type === "freedraw")) && targetElements.some((element) => element.type === "freedraw")) && (
renderAction("changeStrokeShape")} <>
{renderAction("changeStrokeShape")}
{renderAction("toggleShapeSnap")}
</>
)}
{(hasStrokeStyle(appState.activeTool.type) || {(hasStrokeStyle(appState.activeTool.type) ||
targetElements.some((element) => hasStrokeStyle(element.type))) && ( targetElements.some((element) => hasStrokeStyle(element.type))) && (
<> <>

View file

@ -8959,7 +8959,19 @@ class App extends React.Component<AppProps, AppState> {
if (this.state.isShapeSnapEnabled) { if (this.state.isShapeSnapEnabled) {
const detectedElement = convertToShape(newElement); const detectedElement = convertToShape(newElement);
if (detectedElement !== 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.replaceAllElements([
...this.scene ...this.scene
.getElementsIncludingDeleted() .getElementsIncludingDeleted()

View file

@ -1887,6 +1887,23 @@ export const eyeClosedIcon = createIcon(
tablerIconProps, tablerIconProps,
); );
export const snapShapeEnabledIcon = createIcon(
<g stroke="currentColor" fill="none" strokeWidth="1.5">
<rect x="4" y="4" width="16" height="16" rx="2" />
<circle cx="16" cy="8" r="1.5" fill="currentColor" />
</g>,
tablerIconProps,
);
export const snapShapeDisabledIcon = createIcon(
<g stroke="currentColor" fill="none" strokeWidth="1.5">
<rect x="4" y="4" width="16" height="16" rx="2" />
<line x1="4" y1="4" x2="20" y2="20" />
<line x1="4" y1="20" x2="20" y2="4" />
</g>,
tablerIconProps,
);
export const brainIcon = createIcon( export const brainIcon = createIcon(
<g stroke="currentColor" fill="none"> <g stroke="currentColor" fill="none">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />

View file

@ -103,6 +103,9 @@
"loadingScene": "Loading scene…", "loadingScene": "Loading scene…",
"loadScene": "Load scene from file", "loadScene": "Load scene from file",
"align": "Align", "align": "Align",
"shapeSnap": "Snap to shapes",
"shapeSnapDisable": "Disable snap to shapes",
"shapeSnapEnable": "Enable snap to shapes",
"alignTop": "Align top", "alignTop": "Align top",
"alignBottom": "Align bottom", "alignBottom": "Align bottom",
"alignLeft": "Align left", "alignLeft": "Align left",

View file

@ -390,7 +390,6 @@ export const convertToShape = (
return newArrowElement({ return newArrowElement({
...freeDrawElement, ...freeDrawElement,
type: recognizedShape.type, type: recognizedShape.type,
endArrowhead: "arrow", // TODO: Get correct state
points: [ points: [
recognizedShape.simplified[0], recognizedShape.simplified[0],
recognizedShape.simplified[recognizedShape.simplified.length - 2] recognizedShape.simplified[recognizedShape.simplified.length - 2]