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']);