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.
This commit is contained in:
psychedelicious 2022-11-18 15:08:09 +11:00 committed by blessedcoolant
parent e28599cadb
commit aa96a457b6
11 changed files with 312 additions and 125 deletions

View File

@ -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;

View File

@ -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<StageType | null>;
export let canvasImageLayerRef: MutableRefObject<Konva.Layer | null>;
const IAICanvas = () => {
const {
isMaskEnabled,
@ -104,9 +103,18 @@ const IAICanvas = () => {
} = useAppSelector(selector);
useCanvasHotkeys();
// set the closure'd refs
stageRef = useRef<StageType>(null);
canvasImageLayerRef = useRef<Konva.Layer>(null);
const stageRef = useRef<Konva.Stage | null>(null);
const canvasBaseLayerRef = useRef<Konva.Layer | null>(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<Vector2d>({ x: 0, y: 0 });
@ -131,7 +139,7 @@ const IAICanvas = () => {
<div className="inpainting-canvas-wrapper">
<Stage
tabIndex={-1}
ref={stageRef}
ref={canvasStageRefCallback}
className={'inpainting-canvas-stage'}
style={{
...(stageCursor ? { cursor: stageCursor } : {}),
@ -160,7 +168,7 @@ const IAICanvas = () => {
<Layer
id={'base'}
ref={canvasImageLayerRef}
ref={canvasBaseLayerRefCallback}
listening={false}
imageSmoothingEnabled={false}
>

View File

@ -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 (
<IAIPopover
trigger="hover"
@ -42,7 +88,7 @@ const IAICanvasBrushButtonPopover = () => {
tooltip="Brush (B)"
icon={<FaPaintBrush />}
data-selected={tool === 'brush' && !isStaging}
onClick={() => dispatch(setTool('brush'))}
onClick={handleSelectBrushTool}
isDisabled={isStaging}
/>
}

View File

@ -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 (
<IAIPopover
trigger="hover"

View File

@ -16,6 +16,7 @@ import IAICheckbox from 'common/components/IAICheckbox';
import IAIColorPicker from 'common/components/IAIColorPicker';
import IAIButton from 'common/components/IAIButton';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { useHotkeys } from 'react-hotkeys-hook';
export const selector = createSelector(
[canvasSelector],
@ -41,6 +42,22 @@ const IAICanvasMaskButtonPopover = () => {
const { layer, maskColor, isMaskEnabled, shouldPreserveMaskedArea } =
useAppSelector(selector);
useHotkeys(
['q'],
() => {
handleToggleMaskLayer();
},
{
enabled: () => true,
preventDefault: true,
},
[layer]
);
const handleToggleMaskLayer = () => {
dispatch(setLayer(layer === 'mask' ? 'base' : 'mask'));
};
return (
<IAIPopover
trigger="hover"
@ -49,7 +66,7 @@ const IAICanvasMaskButtonPopover = () => {
aria-label="Select Mask Layer"
tooltip="Select Mask Layer"
data-alert={layer === 'mask'}
onClick={() => dispatch(setLayer(layer === 'mask' ? 'base' : 'mask'))}
onClick={handleToggleMaskLayer}
icon={<FaMask />}
/>
}

View File

@ -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 (
<div className="inpainting-settings">
@ -66,63 +198,33 @@ const IAICanvasOutpaintingControls = () => {
tooltip="Move (M)"
icon={<FaArrowsAlt />}
data-selected={tool === 'move' || isStaging}
onClick={() => dispatch(setTool('move'))}
onClick={handleSelectMoveTool}
/>
</ButtonGroup>
<ButtonGroup isAttached>
<IAIIconButton
aria-label="Merge Visible"
tooltip="Merge Visible"
aria-label="Merge Visible (Shift + M)"
tooltip="Merge Visible (Shift + M)"
icon={<FaLayerGroup />}
onClick={() => {
dispatch(
mergeAndUploadCanvas({
canvasImageLayerRef,
})
);
}}
onClick={handleMergeVisible}
/>
<IAIIconButton
aria-label="Save to Gallery"
tooltip="Save to Gallery"
aria-label="Save to Gallery (Shift + S)"
tooltip="Save to Gallery (Shift + S)"
icon={<FaSave />}
onClick={() => {
dispatch(
mergeAndUploadCanvas({
canvasImageLayerRef,
cropVisible: true,
saveToGallery: true,
})
);
}}
onClick={handleSaveToGallery}
/>
<IAIIconButton
aria-label="Copy Selection"
tooltip="Copy Selection"
aria-label="Copy to Clipboard (Cmd/Ctrl + C)"
tooltip="Copy to Clipboard (Cmd/Ctrl + C)"
icon={<FaCopy />}
onClick={() => {
dispatch(
mergeAndUploadCanvas({
canvasImageLayerRef,
cropVisible: true,
copyAfterSaving: true,
})
);
}}
onClick={handleCopyImageToClipboard}
/>
<IAIIconButton
aria-label="Download Selection"
tooltip="Download Selection"
aria-label="Download as Image (Shift + D)"
tooltip="Download as Image (Shift + D)"
icon={<FaDownload />}
onClick={() => {
dispatch(
mergeAndUploadCanvas({
canvasImageLayerRef,
cropVisible: true,
downloadAfterSaving: true,
})
);
}}
onClick={handleDownloadAsImage}
/>
</ButtonGroup>
<ButtonGroup isAttached>
@ -142,23 +244,13 @@ const IAICanvasOutpaintingControls = () => {
aria-label="Reset Canvas View"
tooltip="Reset Canvas View"
icon={<FaCrosshairs />}
onClick={() => {
if (!stageRef.current || !canvasImageLayerRef.current) return;
const clientRect = canvasImageLayerRef.current.getClientRect({
skipTransform: true,
});
dispatch(
resetCanvasView({
contentRect: clientRect,
})
);
}}
onClick={handleResetCanvasView}
/>
<IAIIconButton
aria-label="Reset Canvas"
tooltip="Reset Canvas"
icon={<FaTrash />}
onClick={() => dispatch(resetCanvas())}
onClick={handleResetCanvas}
/>
</ButtonGroup>
</div>

View File

@ -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<CanvasTool | null>(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;

View File

@ -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;

View File

@ -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<Konva.Layer | null>;
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
);

View File

@ -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;

View File

@ -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 = [