From aa96a457b622c99ca7bf09c1a1a5186737b1b7b3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:08:09 +1100 Subject: [PATCH] Adds hotkeys and refactors sharing of konva instances Adds hotkeys to canvas. As part of this change, the access to konva instance objects was refactored: Previously closure'd refs were used to indirectly get access to the konva instances outside of react components. Now, a getter and setter function are used to provide access directly to the konva objects. --- .../src/common/util/parameterTranslation.ts | 21 +- .../features/canvas/components/IAICanvas.tsx | 30 ++- .../IAICanvasBrushButtonPopover.tsx | 52 ++++- .../IAICanvasEraserButtonPopover.tsx | 42 +++- .../IAICanvasMaskButtonPopover.tsx | 19 +- .../IAICanvasToolbar/IAICanvasToolbar.tsx | 212 +++++++++++++----- .../features/canvas/hooks/useCanvasHotkeys.ts | 7 +- .../canvas/util/konvaInstanceProvider.ts | 16 ++ .../canvas/util/mergeAndUploadCanvas.ts | 19 +- .../src/features/gallery/ImageGallery.tsx | 14 -- .../system/HotkeysModal/HotkeysModal.tsx | 5 - 11 files changed, 312 insertions(+), 125 deletions(-) create mode 100644 frontend/src/features/canvas/util/konvaInstanceProvider.ts diff --git a/frontend/src/common/util/parameterTranslation.ts b/frontend/src/common/util/parameterTranslation.ts index 71300bc1b4..8d34337fbc 100644 --- a/frontend/src/common/util/parameterTranslation.ts +++ b/frontend/src/common/util/parameterTranslation.ts @@ -5,10 +5,13 @@ import { SystemState } from 'features/system/systemSlice'; import { stringToSeedWeightsArray } from './seedWeightPairs'; import randomInt from './randomInt'; import { InvokeTabName } from 'features/tabs/InvokeTabs'; -import { CanvasState, isCanvasMaskLine } from 'features/canvas/store/canvasTypes'; +import { + CanvasState, + isCanvasMaskLine, +} from 'features/canvas/store/canvasTypes'; import generateMask from 'features/canvas/util/generateMask'; -import { canvasImageLayerRef } from 'features/canvas/components/IAICanvas'; import openBase64ImageInTab from './openBase64ImageInTab'; +import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider'; export type FrontendToBackendParametersConfig = { generationMode: InvokeTabName; @@ -25,6 +28,8 @@ export type FrontendToBackendParametersConfig = { export const frontendToBackendParameters = ( config: FrontendToBackendParametersConfig ): { [key: string]: any } => { + const canvasBaseLayer = getCanvasBaseLayer(); + const { generationMode, optionsState, @@ -106,7 +111,7 @@ export const frontendToBackendParameters = ( } // inpainting exclusive parameters - if (generationMode === 'unifiedCanvas' && canvasImageLayerRef.current) { + if (generationMode === 'unifiedCanvas' && canvasBaseLayer) { const { layerState: { objects }, boundingBoxCoordinates, @@ -143,16 +148,16 @@ export const frontendToBackendParameters = ( generationParameters.bounding_box = boundingBox; - const tempScale = canvasImageLayerRef.current.scale(); + const tempScale = canvasBaseLayer.scale(); - canvasImageLayerRef.current.scale({ + canvasBaseLayer.scale({ x: 1 / stageScale, y: 1 / stageScale, }); - const absPos = canvasImageLayerRef.current.getAbsolutePosition(); + const absPos = canvasBaseLayer.getAbsolutePosition(); - const imageDataURL = canvasImageLayerRef.current.toDataURL({ + const imageDataURL = canvasBaseLayer.toDataURL({ x: boundingBox.x + absPos.x, y: boundingBox.y + absPos.y, width: boundingBox.width, @@ -166,7 +171,7 @@ export const frontendToBackendParameters = ( ]); } - canvasImageLayerRef.current.scale(tempScale); + canvasBaseLayer.scale(tempScale); generationParameters.init_img = imageDataURL; diff --git a/frontend/src/features/canvas/components/IAICanvas.tsx b/frontend/src/features/canvas/components/IAICanvas.tsx index a321267269..b5fb93d0e5 100644 --- a/frontend/src/features/canvas/components/IAICanvas.tsx +++ b/frontend/src/features/canvas/components/IAICanvas.tsx @@ -1,7 +1,6 @@ -import { MutableRefObject, useRef } from 'react'; +import { useCallback, useRef } from 'react'; import Konva from 'konva'; import { Layer, Stage } from 'react-konva'; -import { Stage as StageType } from 'konva/lib/Stage'; import { useAppSelector } from 'app/store'; import { canvasSelector, @@ -28,6 +27,10 @@ import IAICanvasIntermediateImage from './IAICanvasIntermediateImage'; import IAICanvasStatusText from './IAICanvasStatusText'; import IAICanvasStagingArea from './IAICanvasStagingArea'; import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar'; +import { + setCanvasBaseLayer, + setCanvasStage, +} from '../util/konvaInstanceProvider'; const selector = createSelector( [canvasSelector, isStagingSelector], @@ -84,10 +87,6 @@ const selector = createSelector( } ); -// Use a closure allow other components to use these things... not ideal... -export let stageRef: MutableRefObject; -export let canvasImageLayerRef: MutableRefObject; - const IAICanvas = () => { const { isMaskEnabled, @@ -104,9 +103,18 @@ const IAICanvas = () => { } = useAppSelector(selector); useCanvasHotkeys(); - // set the closure'd refs - stageRef = useRef(null); - canvasImageLayerRef = useRef(null); + const stageRef = useRef(null); + const canvasBaseLayerRef = useRef(null); + + const canvasStageRefCallback = useCallback((el: Konva.Stage) => { + setCanvasStage(el as Konva.Stage); + stageRef.current = el; + }, []); + + const canvasBaseLayerRefCallback = useCallback((el: Konva.Layer) => { + setCanvasBaseLayer(el as Konva.Layer); + canvasBaseLayerRef.current = el; + }, []); const lastCursorPositionRef = useRef({ x: 0, y: 0 }); @@ -131,7 +139,7 @@ const IAICanvas = () => {
{ diff --git a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasBrushButtonPopover.tsx b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasBrushButtonPopover.tsx index 7355bbc187..058283af72 100644 --- a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasBrushButtonPopover.tsx +++ b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasBrushButtonPopover.tsx @@ -1,5 +1,9 @@ import { createSelector } from '@reduxjs/toolkit'; -import { setBrushColor, setBrushSize, setTool } from 'features/canvas/store/canvasSlice'; +import { + setBrushColor, + setBrushSize, + setTool, +} from 'features/canvas/store/canvasSlice'; import { useAppDispatch, useAppSelector } from 'app/store'; import _ from 'lodash'; import IAIIconButton from 'common/components/IAIIconButton'; @@ -8,7 +12,11 @@ import IAIPopover from 'common/components/IAIPopover'; import IAIColorPicker from 'common/components/IAIColorPicker'; import IAISlider from 'common/components/IAISlider'; import { Flex } from '@chakra-ui/react'; -import { canvasSelector, isStagingSelector } from 'features/canvas/store/canvasSelectors'; +import { + canvasSelector, + isStagingSelector, +} from 'features/canvas/store/canvasSelectors'; +import { useHotkeys } from 'react-hotkeys-hook'; export const selector = createSelector( [canvasSelector, isStagingSelector], @@ -33,6 +41,44 @@ const IAICanvasBrushButtonPopover = () => { const dispatch = useAppDispatch(); const { tool, brushColor, brushSize, isStaging } = useAppSelector(selector); + useHotkeys( + ['b'], + () => { + handleSelectBrushTool(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [] + ); + + useHotkeys( + ['['], + () => { + dispatch(setBrushSize(Math.max(brushSize - 5, 5))); + }, + { + enabled: () => true, + preventDefault: true, + }, + [brushSize] + ); + + useHotkeys( + [']'], + () => { + dispatch(setBrushSize(Math.min(brushSize + 5, 500))); + }, + { + enabled: () => true, + preventDefault: true, + }, + [brushSize] + ); + + const handleSelectBrushTool = () => dispatch(setTool('brush')); + return ( { tooltip="Brush (B)" icon={} data-selected={tool === 'brush' && !isStaging} - onClick={() => dispatch(setTool('brush'))} + onClick={handleSelectBrushTool} isDisabled={isStaging} /> } diff --git a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasEraserButtonPopover.tsx b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasEraserButtonPopover.tsx index d9d029d14e..6ce48a2fb6 100644 --- a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasEraserButtonPopover.tsx +++ b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasEraserButtonPopover.tsx @@ -1,8 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; -import { - setEraserSize, - setTool, -} from 'features/canvas/store/canvasSlice'; +import { setEraserSize, setTool } from 'features/canvas/store/canvasSlice'; import { useAppDispatch, useAppSelector } from 'app/store'; import _ from 'lodash'; import IAIIconButton from 'common/components/IAIIconButton'; @@ -11,7 +8,10 @@ import IAIPopover from 'common/components/IAIPopover'; import IAISlider from 'common/components/IAISlider'; import { Flex } from '@chakra-ui/react'; import { useHotkeys } from 'react-hotkeys-hook'; -import { canvasSelector, isStagingSelector } from 'features/canvas/store/canvasSelectors'; +import { + canvasSelector, + isStagingSelector, +} from 'features/canvas/store/canvasSelectors'; export const selector = createSelector( [canvasSelector, isStagingSelector], @@ -37,17 +37,41 @@ const IAICanvasEraserButtonPopover = () => { const handleSelectEraserTool = () => dispatch(setTool('eraser')); useHotkeys( - 'e', - (e: KeyboardEvent) => { - e.preventDefault(); + ['e'], + () => { handleSelectEraserTool(); }, { - enabled: true, + enabled: () => true, + preventDefault: true, }, [tool] ); + useHotkeys( + ['['], + () => { + dispatch(setEraserSize(Math.max(eraserSize - 5, 5))); + }, + { + enabled: () => true, + preventDefault: true, + }, + [eraserSize] + ); + + useHotkeys( + [']'], + () => { + dispatch(setEraserSize(Math.min(eraserSize + 5, 500))); + }, + { + enabled: () => true, + preventDefault: true, + }, + [eraserSize] + ); + return ( { const { layer, maskColor, isMaskEnabled, shouldPreserveMaskedArea } = useAppSelector(selector); + useHotkeys( + ['q'], + () => { + handleToggleMaskLayer(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [layer] + ); + + const handleToggleMaskLayer = () => { + dispatch(setLayer(layer === 'mask' ? 'base' : 'mask')); + }; + return ( { aria-label="Select Mask Layer" tooltip="Select Mask Layer" data-alert={layer === 'mask'} - onClick={() => dispatch(setLayer(layer === 'mask' ? 'base' : 'mask'))} + onClick={handleToggleMaskLayer} icon={} /> } diff --git a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx index f175fed9f5..259cad2ed4 100644 --- a/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ b/frontend/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx @@ -1,15 +1,12 @@ import { ButtonGroup } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { - resizeAndScaleCanvas, resetCanvas, resetCanvasView, setTool, - fitBoundingBoxToStage, } from 'features/canvas/store/canvasSlice'; import { useAppDispatch, useAppSelector } from 'app/store'; import _ from 'lodash'; -import { canvasImageLayerRef, stageRef } from '../IAICanvas'; import IAIIconButton from 'common/components/IAIIconButton'; import { FaArrowsAlt, @@ -28,12 +25,15 @@ import IAICanvasEraserButtonPopover from './IAICanvasEraserButtonPopover'; import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover'; import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover'; import { mergeAndUploadCanvas } from 'features/canvas/util/mergeAndUploadCanvas'; -import IAICheckbox from 'common/components/IAICheckbox'; -import { ChangeEvent } from 'react'; import { canvasSelector, isStagingSelector, } from 'features/canvas/store/canvasSelectors'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { + getCanvasBaseLayer, + getCanvasStage, +} from 'features/canvas/util/konvaInstanceProvider'; export const selector = createSelector( [canvasSelector, isStagingSelector], @@ -54,6 +54,138 @@ export const selector = createSelector( const IAICanvasOutpaintingControls = () => { const dispatch = useAppDispatch(); const { tool, isStaging } = useAppSelector(selector); + const canvasBaseLayer = getCanvasBaseLayer(); + + useHotkeys( + ['m'], + () => { + handleSelectMoveTool(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [] + ); + + useHotkeys( + ['shift+r'], + () => { + handleResetCanvasView(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [canvasBaseLayer] + ); + + useHotkeys( + ['shift+c'], + () => { + handleResetCanvas(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [canvasBaseLayer] + ); + + useHotkeys( + ['shift+m'], + () => { + handleMergeVisible(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [canvasBaseLayer] + ); + + useHotkeys( + ['shift+s'], + () => { + handleSaveToGallery(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [canvasBaseLayer] + ); + + useHotkeys( + ['meta+c', 'ctrl+c'], + () => { + handleCopyImageToClipboard(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [canvasBaseLayer] + ); + + useHotkeys( + ['shift+d'], + () => { + handleDownloadAsImage(); + }, + { + enabled: () => true, + preventDefault: true, + }, + [canvasBaseLayer] + ); + + const handleSelectMoveTool = () => dispatch(setTool('move')); + + const handleResetCanvasView = () => { + if (!canvasBaseLayer) return; + const clientRect = canvasBaseLayer.getClientRect({ + skipTransform: true, + }); + dispatch( + resetCanvasView({ + contentRect: clientRect, + }) + ); + }; + + const handleResetCanvas = () => dispatch(resetCanvas()); + + const handleMergeVisible = () => { + dispatch(mergeAndUploadCanvas({})); + }; + + const handleSaveToGallery = () => { + dispatch( + mergeAndUploadCanvas({ + cropVisible: true, + saveToGallery: true, + }) + ); + }; + + const handleCopyImageToClipboard = () => { + dispatch( + mergeAndUploadCanvas({ + cropVisible: true, + copyAfterSaving: true, + }) + ); + }; + + const handleDownloadAsImage = () => { + dispatch( + mergeAndUploadCanvas({ + cropVisible: true, + downloadAfterSaving: true, + }) + ); + }; return (
@@ -66,63 +198,33 @@ const IAICanvasOutpaintingControls = () => { tooltip="Move (M)" icon={} data-selected={tool === 'move' || isStaging} - onClick={() => dispatch(setTool('move'))} + onClick={handleSelectMoveTool} /> } - onClick={() => { - dispatch( - mergeAndUploadCanvas({ - canvasImageLayerRef, - }) - ); - }} + onClick={handleMergeVisible} /> } - onClick={() => { - dispatch( - mergeAndUploadCanvas({ - canvasImageLayerRef, - cropVisible: true, - saveToGallery: true, - }) - ); - }} + onClick={handleSaveToGallery} /> } - onClick={() => { - dispatch( - mergeAndUploadCanvas({ - canvasImageLayerRef, - cropVisible: true, - copyAfterSaving: true, - }) - ); - }} + onClick={handleCopyImageToClipboard} /> } - onClick={() => { - dispatch( - mergeAndUploadCanvas({ - canvasImageLayerRef, - cropVisible: true, - downloadAfterSaving: true, - }) - ); - }} + onClick={handleDownloadAsImage} /> @@ -142,23 +244,13 @@ const IAICanvasOutpaintingControls = () => { aria-label="Reset Canvas View" tooltip="Reset Canvas View" icon={} - onClick={() => { - if (!stageRef.current || !canvasImageLayerRef.current) return; - const clientRect = canvasImageLayerRef.current.getClientRect({ - skipTransform: true, - }); - dispatch( - resetCanvasView({ - contentRect: clientRect, - }) - ); - }} + onClick={handleResetCanvasView} /> } - onClick={() => dispatch(resetCanvas())} + onClick={handleResetCanvas} />
diff --git a/frontend/src/features/canvas/hooks/useCanvasHotkeys.ts b/frontend/src/features/canvas/hooks/useCanvasHotkeys.ts index 1d4c2f28fd..7bed805e63 100644 --- a/frontend/src/features/canvas/hooks/useCanvasHotkeys.ts +++ b/frontend/src/features/canvas/hooks/useCanvasHotkeys.ts @@ -9,9 +9,9 @@ import { } from 'features/canvas/store/canvasSlice'; import { useAppDispatch, useAppSelector } from 'app/store'; import { useRef } from 'react'; -import { stageRef } from '../components/IAICanvas'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { CanvasTool } from '../store/canvasTypes'; +import { getCanvasStage } from '../util/konvaInstanceProvider'; const selector = createSelector( [canvasSelector, activeTabNameSelector], @@ -44,6 +44,9 @@ const useInpaintingCanvasHotkeys = () => { useAppSelector(selector); const previousToolRef = useRef(null); + + const canvasStage = getCanvasStage(); + // Toggle lock bounding box useHotkeys( 'shift+w', @@ -72,7 +75,7 @@ const useInpaintingCanvasHotkeys = () => { (e: KeyboardEvent) => { if (e.repeat) return; - stageRef.current?.container().focus(); + canvasStage?.container().focus(); if (tool !== 'move') { previousToolRef.current = tool; diff --git a/frontend/src/features/canvas/util/konvaInstanceProvider.ts b/frontend/src/features/canvas/util/konvaInstanceProvider.ts new file mode 100644 index 0000000000..4a36fb72ac --- /dev/null +++ b/frontend/src/features/canvas/util/konvaInstanceProvider.ts @@ -0,0 +1,16 @@ +import Konva from 'konva'; + +let canvasBaseLayer: Konva.Layer | null = null; +let canvasStage: Konva.Stage | null = null; + +export const setCanvasBaseLayer = (layer: Konva.Layer) => { + canvasBaseLayer = layer; +}; + +export const getCanvasBaseLayer = () => canvasBaseLayer; + +export const setCanvasStage = (stage: Konva.Stage) => { + canvasStage = stage; +}; + +export const getCanvasStage = () => canvasStage; diff --git a/frontend/src/features/canvas/util/mergeAndUploadCanvas.ts b/frontend/src/features/canvas/util/mergeAndUploadCanvas.ts index fa6cc1d27b..1dd1387b44 100644 --- a/frontend/src/features/canvas/util/mergeAndUploadCanvas.ts +++ b/frontend/src/features/canvas/util/mergeAndUploadCanvas.ts @@ -1,18 +1,16 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { RootState } from 'app/store'; -import Konva from 'konva'; -import { MutableRefObject } from 'react'; import * as InvokeAI from 'app/invokeai'; import { v4 as uuidv4 } from 'uuid'; import layerToDataURL from './layerToDataURL'; import downloadFile from './downloadFile'; import copyImage from './copyImage'; +import { getCanvasBaseLayer } from './konvaInstanceProvider'; export const mergeAndUploadCanvas = createAsyncThunk( 'canvas/mergeAndUploadCanvas', async ( args: { - canvasImageLayerRef: MutableRefObject; cropVisible?: boolean; saveToGallery?: boolean; downloadAfterSaving?: boolean; @@ -20,13 +18,8 @@ export const mergeAndUploadCanvas = createAsyncThunk( }, thunkAPI ) => { - const { - canvasImageLayerRef, - saveToGallery, - downloadAfterSaving, - cropVisible, - copyAfterSaving, - } = args; + const { saveToGallery, downloadAfterSaving, cropVisible, copyAfterSaving } = + args; const { getState } = thunkAPI; @@ -34,10 +27,12 @@ export const mergeAndUploadCanvas = createAsyncThunk( const stageScale = state.canvas.stageScale; - if (!canvasImageLayerRef.current) return; + const canvasBaseLayer = getCanvasBaseLayer(); + + if (!canvasBaseLayer) return; const { dataURL, boundingBox: originalBoundingBox } = layerToDataURL( - canvasImageLayerRef.current, + canvasBaseLayer, stageScale ); diff --git a/frontend/src/features/gallery/ImageGallery.tsx b/frontend/src/features/gallery/ImageGallery.tsx index cecb5d3aaf..99ba673af5 100644 --- a/frontend/src/features/gallery/ImageGallery.tsx +++ b/frontend/src/features/gallery/ImageGallery.tsx @@ -246,20 +246,6 @@ export default function ImageGallery() { [galleryImageMinimumWidth] ); - useHotkeys( - 'shift+r', - () => { - dispatch(setGalleryImageMinimumWidth(64)); - toast({ - title: `Reset Gallery Image Size`, - status: 'success', - duration: 2500, - isClosable: true, - }); - }, - [galleryImageMinimumWidth] - ); - // set gallery scroll position useEffect(() => { if (!galleryContainerRef.current) return; diff --git a/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx b/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx index f9fc98a1dd..6da8259b67 100644 --- a/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx +++ b/frontend/src/features/system/HotkeysModal/HotkeysModal.tsx @@ -135,11 +135,6 @@ export default function HotkeysModal({ children }: HotkeysModalProps) { desc: 'Decreases gallery thumbnails size', hotkey: 'Shift+Down', }, - { - title: 'Reset Gallery Image Size', - desc: 'Resets image gallery size', - hotkey: 'Shift+R', - }, ]; const unifiedCanvasHotkeys = [