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")]} /> +