diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx
index 96e03c677..3f6304047 100644
--- a/src/actions/actionCanvas.tsx
+++ b/src/actions/actionCanvas.tsx
@@ -4,13 +4,14 @@ import { getDefaultAppState } from "../appState";
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
-import { getNormalizedZoom } from "../scene";
+import { getNormalizedZoom, calculateScrollCenter } from "../scene";
import { KEYS } from "../keys";
import { getShortcutKey } from "../utils";
import useIsMobile from "../is-mobile";
import { register } from "./register";
import { newElementWith } from "../element/mutateElement";
-import { AppState } from "../types";
+import { AppState, FlooredNumber } from "../types";
+import { getCommonBounds } from "../element";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
@@ -73,6 +74,7 @@ const ZOOM_STEP = 0.1;
const KEY_CODES = {
MINUS: "Minus",
EQUAL: "Equal",
+ ONE: "Digit1",
ZERO: "Digit0",
NUM_SUBTRACT: "NumpadSubtract",
NUM_ADD: "NumpadAdd",
@@ -159,3 +161,64 @@ export const actionResetZoom = register({
(event.code === KEY_CODES.ZERO || event.code === KEY_CODES.NUM_ZERO) &&
(event[KEYS.CTRL_OR_CMD] || event.shiftKey),
});
+
+const calculateZoom = (
+ commonBounds: number[],
+ currentZoom: number,
+ {
+ scrollX,
+ scrollY,
+ }: {
+ scrollX: FlooredNumber;
+ scrollY: FlooredNumber;
+ },
+): number => {
+ const { innerWidth, innerHeight } = window;
+ const [x, y] = commonBounds;
+ const zoomX = -innerWidth / (2 * scrollX + 2 * x - innerWidth);
+ const zoomY = -innerHeight / (2 * scrollY + 2 * y - innerHeight);
+ const margin = 0.01;
+ let newZoom;
+
+ if (zoomX < zoomY) {
+ newZoom = zoomX - margin;
+ } else if (zoomY <= zoomX) {
+ newZoom = zoomY - margin;
+ } else {
+ newZoom = currentZoom;
+ }
+
+ if (newZoom <= 0.1) {
+ return 0.1;
+ }
+ if (newZoom >= 1) {
+ return 1;
+ }
+
+ return newZoom;
+};
+
+export const actionZoomToFit = register({
+ name: "zoomToFit",
+ perform: (elements, appState) => {
+ const nonDeletedElements = elements.filter((element) => !element.isDeleted);
+ const scrollCenter = calculateScrollCenter(nonDeletedElements);
+ const commonBounds = getCommonBounds(nonDeletedElements);
+ const zoom = calculateZoom(commonBounds, appState.zoom, scrollCenter);
+
+ return {
+ appState: {
+ ...appState,
+ scrollX: scrollCenter.scrollX,
+ scrollY: scrollCenter.scrollY,
+ zoom,
+ },
+ commitToHistory: false,
+ };
+ },
+ keyTest: (event) =>
+ event.code === KEY_CODES.ONE &&
+ event.shiftKey &&
+ !event.altKey &&
+ !event[KEYS.CTRL_OR_CMD],
+});
diff --git a/src/actions/index.ts b/src/actions/index.ts
index 4e186d147..885ef67e5 100644
--- a/src/actions/index.ts
+++ b/src/actions/index.ts
@@ -25,6 +25,7 @@ export {
actionZoomIn,
actionZoomOut,
actionResetZoom,
+ actionZoomToFit,
} from "./actionCanvas";
export { actionFinalize } from "./actionFinalize";
diff --git a/src/actions/types.ts b/src/actions/types.ts
index 85c4fc794..c3bd76bf5 100644
--- a/src/actions/types.ts
+++ b/src/actions/types.ts
@@ -48,6 +48,7 @@ export type ActionName =
| "zoomIn"
| "zoomOut"
| "resetZoom"
+ | "zoomToFit"
| "changeFontFamily"
| "changeTextAlign"
| "toggleFullScreen"
diff --git a/src/components/ShortcutsDialog.tsx b/src/components/ShortcutsDialog.tsx
index 1f1504f8a..94579a124 100644
--- a/src/components/ShortcutsDialog.tsx
+++ b/src/components/ShortcutsDialog.tsx
@@ -228,6 +228,10 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("buttons.resetZoom")}
shortcuts={[getShortcutKey("CtrlOrCmd+0")]}
/>
+