diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 85225b5ea5..afa7e7abb6 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -456,11 +456,23 @@
},
"fitLayersToCanvas": {
"title": "Fit Layers to Canvas",
- "desc": "Scales and positions the view to fit all visible layers."
+ "desc": "Scale and position the view to fit all visible layers."
},
"setZoomTo100Percent": {
- "title": "Reset Zoom",
- "desc": "Resets the canvas zoom to 100%."
+ "title": "Zoom to 100%",
+ "desc": "Set the canvas zoom to 100%."
+ },
+ "setZoomTo200Percent": {
+ "title": "Zoom to 200%",
+ "desc": "Set the canvas zoom to 200%."
+ },
+ "setZoomTo400Percent": {
+ "title": "Zoom to 400%",
+ "desc": "Set the canvas zoom to 400%."
+ },
+ "setZoomTo800Percent": {
+ "title": "Zoom to 800%",
+ "desc": "Set the canvas zoom to 800%."
},
"quickSwitch": {
"title": "Layer Quick Switch",
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton.tsx
index 670b04fd4d..eea3c4e1a8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton.tsx
@@ -3,7 +3,7 @@ import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerP
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiArrowsOut } from 'react-icons/pi';
+import { PiResizeBold } from 'react-icons/pi';
export const CanvasToolbarFitBboxToLayersButton = memo(() => {
const { t } = useTranslation();
@@ -19,7 +19,7 @@ export const CanvasToolbarFitBboxToLayersButton = memo(() => {
variant="ghost"
aria-label={t('controlLayers.fitBboxToLayers')}
tooltip={t('controlLayers.fitBboxToLayers')}
- icon={}
+ icon={}
isDisabled={isBusy}
/>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx
index b96568c1e2..e56e35ab49 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx
@@ -1,63 +1,63 @@
-import { $alt, IconButton } from '@invoke-ai/ui-library';
+import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
-import { $canvasManager } from 'features/controlLayers/store/ephemeral';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
-import { memo, useCallback } from 'react';
+import { memo } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
+import { PiArrowsOutBold } from 'react-icons/pi';
export const CanvasToolbarResetViewButton = memo(() => {
const { t } = useTranslation();
- const canvasManager = useStore($canvasManager);
+ const canvasManager = useCanvasManager();
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
const imageViewer = useImageViewer();
- const resetZoom = useCallback(() => {
- if (!canvasManager) {
- return;
- }
- canvasManager.stage.setScale(1);
- }, [canvasManager]);
-
- const resetView = useCallback(() => {
- if (!canvasManager) {
- return;
- }
- canvasManager.stage.fitLayersToStage();
- }, [canvasManager]);
-
- const onReset = useCallback(() => {
- if ($alt.get()) {
- resetView();
- } else {
- resetZoom();
- }
- }, [resetView, resetZoom]);
-
useRegisteredHotkeys({
id: 'fitLayersToCanvas',
category: 'canvas',
- callback: resetView,
- options: { enabled: isCanvasActive && !imageViewer.isOpen },
+ callback: canvasManager.stage.fitLayersToStage,
+ options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
dependencies: [isCanvasActive, imageViewer.isOpen],
});
useRegisteredHotkeys({
id: 'setZoomTo100Percent',
category: 'canvas',
- callback: resetZoom,
- options: { enabled: isCanvasActive && !imageViewer.isOpen },
+ callback: () => canvasManager.stage.setScale(1),
+ options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
+ dependencies: [isCanvasActive, imageViewer.isOpen],
+ });
+ useRegisteredHotkeys({
+ id: 'setZoomTo200Percent',
+ category: 'canvas',
+ callback: () => canvasManager.stage.setScale(2),
+ options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
+ dependencies: [isCanvasActive, imageViewer.isOpen],
+ });
+ useRegisteredHotkeys({
+ id: 'setZoomTo400Percent',
+ category: 'canvas',
+ callback: () => canvasManager.stage.setScale(4),
+ options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
+ dependencies: [isCanvasActive, imageViewer.isOpen],
+ });
+ useRegisteredHotkeys({
+ id: 'setZoomTo800Percent',
+ category: 'canvas',
+ callback: () => canvasManager.stage.setScale(8),
+ options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
dependencies: [isCanvasActive, imageViewer.isOpen],
});
return (
}
+ tooltip={t('hotkeys.canvas.fitLayersToCanvas.title')}
+ aria-label={t('hotkeys.canvas.fitLayersToCanvas.title')}
+ onClick={canvasManager.stage.fitLayersToStage}
+ icon={}
variant="link"
+ alignSelf="stretch"
/>
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarScale.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarScale.tsx
index c51cea1443..8d4651e705 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarScale.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarScale.tsx
@@ -1,8 +1,7 @@
import {
$shift,
CompositeSlider,
- FormControl,
- FormLabel,
+ Flex,
IconButton,
NumberInput,
NumberInputField,
@@ -20,8 +19,7 @@ import { round } from 'lodash-es';
import { computed } from 'nanostores';
import type { KeyboardEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { PiCaretDownBold } from 'react-icons/pi';
+import { PiCaretDownBold, PiMagnifyingGlassMinusBold, PiMagnifyingGlassPlusBold } from 'react-icons/pi';
function formatPct(v: number | string) {
if (isNaN(Number(v))) {
@@ -71,7 +69,6 @@ const sliderDefaultValue = mapRawValueToSliderValue(100);
const snapCandidates = marks.slice(1, marks.length - 1);
export const CanvasToolbarScale = memo(() => {
- const { t } = useTranslation();
const canvasManager = useCanvasManager();
const scale = useStore(computed(canvasManager.stage.$stageAttrs, (attrs) => attrs.scale));
const [localScale, setLocalScale] = useState(scale * 100);
@@ -116,9 +113,9 @@ export const CanvasToolbarScale = memo(() => {
}, [scale]);
return (
-
-
- {t('controlLayers.zoom')}
+
+
+
{
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
});
CanvasToolbarScale.displayName = 'CanvasToolbarScale';
+
+const SCALE_SNAPS = [0.1, 0.15, 0.2, 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 5, 7.5, 10, 15, 20];
+
+const ZoomOutButton = () => {
+ const canvasManager = useCanvasManager();
+ const scale = useStore(computed(canvasManager.stage.$stageAttrs, (attrs) => attrs.scale));
+ const onClick = useCallback(() => {
+ const nextScale =
+ SCALE_SNAPS.slice()
+ .reverse()
+ .find((snap) => snap < scale) ?? canvasManager.stage.config.MIN_SCALE;
+ canvasManager.stage.setScale(Math.max(nextScale, canvasManager.stage.config.MIN_SCALE));
+ }, [canvasManager.stage, scale]);
+
+ return (
+ }
+ aria-label="Zoom out"
+ variant="link"
+ alignSelf="stretch"
+ isDisabled={scale <= canvasManager.stage.config.MIN_SCALE}
+ />
+ );
+};
+
+const ZoomInButton = () => {
+ const canvasManager = useCanvasManager();
+ const scale = useStore(computed(canvasManager.stage.$stageAttrs, (attrs) => attrs.scale));
+ const onClick = useCallback(() => {
+ const nextScale = SCALE_SNAPS.find((snap) => snap > scale) ?? canvasManager.stage.config.MAX_SCALE;
+ canvasManager.stage.setScale(Math.min(nextScale, canvasManager.stage.config.MAX_SCALE));
+ }, [canvasManager.stage, scale]);
+
+ return (
+ }
+ aria-label="Zoom out"
+ variant="link"
+ alignSelf="stretch"
+ isDisabled={scale >= canvasManager.stage.config.MAX_SCALE}
+ />
+ );
+};
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
index bc4d9eed70..c5e82f49d6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
@@ -441,7 +441,7 @@ export const getEmptyRect = (): Rect => {
return { x: 0, y: 0, width: 0, height: 0 };
};
-export function snapToNearest(value: number, candidateValues: number[], threshold: number): number {
+export function snapToNearest(value: number, candidateValues: number[], threshold: number = Infinity): number {
let closest = value;
let minDiff = Number.MAX_VALUE;
diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts
index cbbc2a2360..c86138de35 100644
--- a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts
+++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts
@@ -100,8 +100,11 @@ export const useHotkeyData = (): HotkeysData => {
addHotkey('canvas', 'selectViewTool', ['h']);
addHotkey('canvas', 'selectColorPickerTool', ['i']);
addHotkey('canvas', 'setFillToWhite', ['d']);
- addHotkey('canvas', 'fitLayersToCanvas', ['r']);
- addHotkey('canvas', 'setZoomTo100Percent', ['alt+r']);
+ addHotkey('canvas', 'fitLayersToCanvas', ['mod+0']);
+ addHotkey('canvas', 'setZoomTo100Percent', ['mod+1']);
+ addHotkey('canvas', 'setZoomTo200Percent', ['mod+2']);
+ addHotkey('canvas', 'setZoomTo400Percent', ['mod+3']);
+ addHotkey('canvas', 'setZoomTo800Percent', ['mod+4']);
addHotkey('canvas', 'quickSwitch', ['q']);
addHotkey('canvas', 'deleteSelected', ['delete', 'backspace']);
addHotkey('canvas', 'resetSelected', ['shift+c']);