mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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:
parent
e28599cadb
commit
aa96a457b6
@ -5,10 +5,13 @@ import { SystemState } from 'features/system/systemSlice';
|
|||||||
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
||||||
import randomInt from './randomInt';
|
import randomInt from './randomInt';
|
||||||
import { InvokeTabName } from 'features/tabs/InvokeTabs';
|
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 generateMask from 'features/canvas/util/generateMask';
|
||||||
import { canvasImageLayerRef } from 'features/canvas/components/IAICanvas';
|
|
||||||
import openBase64ImageInTab from './openBase64ImageInTab';
|
import openBase64ImageInTab from './openBase64ImageInTab';
|
||||||
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
|
|
||||||
export type FrontendToBackendParametersConfig = {
|
export type FrontendToBackendParametersConfig = {
|
||||||
generationMode: InvokeTabName;
|
generationMode: InvokeTabName;
|
||||||
@ -25,6 +28,8 @@ export type FrontendToBackendParametersConfig = {
|
|||||||
export const frontendToBackendParameters = (
|
export const frontendToBackendParameters = (
|
||||||
config: FrontendToBackendParametersConfig
|
config: FrontendToBackendParametersConfig
|
||||||
): { [key: string]: any } => {
|
): { [key: string]: any } => {
|
||||||
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
generationMode,
|
generationMode,
|
||||||
optionsState,
|
optionsState,
|
||||||
@ -106,7 +111,7 @@ export const frontendToBackendParameters = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// inpainting exclusive parameters
|
// inpainting exclusive parameters
|
||||||
if (generationMode === 'unifiedCanvas' && canvasImageLayerRef.current) {
|
if (generationMode === 'unifiedCanvas' && canvasBaseLayer) {
|
||||||
const {
|
const {
|
||||||
layerState: { objects },
|
layerState: { objects },
|
||||||
boundingBoxCoordinates,
|
boundingBoxCoordinates,
|
||||||
@ -143,16 +148,16 @@ export const frontendToBackendParameters = (
|
|||||||
|
|
||||||
generationParameters.bounding_box = boundingBox;
|
generationParameters.bounding_box = boundingBox;
|
||||||
|
|
||||||
const tempScale = canvasImageLayerRef.current.scale();
|
const tempScale = canvasBaseLayer.scale();
|
||||||
|
|
||||||
canvasImageLayerRef.current.scale({
|
canvasBaseLayer.scale({
|
||||||
x: 1 / stageScale,
|
x: 1 / stageScale,
|
||||||
y: 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,
|
x: boundingBox.x + absPos.x,
|
||||||
y: boundingBox.y + absPos.y,
|
y: boundingBox.y + absPos.y,
|
||||||
width: boundingBox.width,
|
width: boundingBox.width,
|
||||||
@ -166,7 +171,7 @@ export const frontendToBackendParameters = (
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvasImageLayerRef.current.scale(tempScale);
|
canvasBaseLayer.scale(tempScale);
|
||||||
|
|
||||||
generationParameters.init_img = imageDataURL;
|
generationParameters.init_img = imageDataURL;
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { MutableRefObject, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { Layer, Stage } from 'react-konva';
|
import { Layer, Stage } from 'react-konva';
|
||||||
import { Stage as StageType } from 'konva/lib/Stage';
|
|
||||||
import { useAppSelector } from 'app/store';
|
import { useAppSelector } from 'app/store';
|
||||||
import {
|
import {
|
||||||
canvasSelector,
|
canvasSelector,
|
||||||
@ -28,6 +27,10 @@ import IAICanvasIntermediateImage from './IAICanvasIntermediateImage';
|
|||||||
import IAICanvasStatusText from './IAICanvasStatusText';
|
import IAICanvasStatusText from './IAICanvasStatusText';
|
||||||
import IAICanvasStagingArea from './IAICanvasStagingArea';
|
import IAICanvasStagingArea from './IAICanvasStagingArea';
|
||||||
import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar';
|
import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar';
|
||||||
|
import {
|
||||||
|
setCanvasBaseLayer,
|
||||||
|
setCanvasStage,
|
||||||
|
} from '../util/konvaInstanceProvider';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[canvasSelector, isStagingSelector],
|
[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 IAICanvas = () => {
|
||||||
const {
|
const {
|
||||||
isMaskEnabled,
|
isMaskEnabled,
|
||||||
@ -104,9 +103,18 @@ const IAICanvas = () => {
|
|||||||
} = useAppSelector(selector);
|
} = useAppSelector(selector);
|
||||||
useCanvasHotkeys();
|
useCanvasHotkeys();
|
||||||
|
|
||||||
// set the closure'd refs
|
const stageRef = useRef<Konva.Stage | null>(null);
|
||||||
stageRef = useRef<StageType>(null);
|
const canvasBaseLayerRef = useRef<Konva.Layer | null>(null);
|
||||||
canvasImageLayerRef = useRef<Konva.Layer>(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 });
|
const lastCursorPositionRef = useRef<Vector2d>({ x: 0, y: 0 });
|
||||||
|
|
||||||
@ -131,7 +139,7 @@ const IAICanvas = () => {
|
|||||||
<div className="inpainting-canvas-wrapper">
|
<div className="inpainting-canvas-wrapper">
|
||||||
<Stage
|
<Stage
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
ref={stageRef}
|
ref={canvasStageRefCallback}
|
||||||
className={'inpainting-canvas-stage'}
|
className={'inpainting-canvas-stage'}
|
||||||
style={{
|
style={{
|
||||||
...(stageCursor ? { cursor: stageCursor } : {}),
|
...(stageCursor ? { cursor: stageCursor } : {}),
|
||||||
@ -160,7 +168,7 @@ const IAICanvas = () => {
|
|||||||
|
|
||||||
<Layer
|
<Layer
|
||||||
id={'base'}
|
id={'base'}
|
||||||
ref={canvasImageLayerRef}
|
ref={canvasBaseLayerRefCallback}
|
||||||
listening={false}
|
listening={false}
|
||||||
imageSmoothingEnabled={false}
|
imageSmoothingEnabled={false}
|
||||||
>
|
>
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
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 { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
@ -8,7 +12,11 @@ import IAIPopover from 'common/components/IAIPopover';
|
|||||||
import IAIColorPicker from 'common/components/IAIColorPicker';
|
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||||
import IAISlider from 'common/components/IAISlider';
|
import IAISlider from 'common/components/IAISlider';
|
||||||
import { Flex } from '@chakra-ui/react';
|
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(
|
export const selector = createSelector(
|
||||||
[canvasSelector, isStagingSelector],
|
[canvasSelector, isStagingSelector],
|
||||||
@ -33,6 +41,44 @@ const IAICanvasBrushButtonPopover = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { tool, brushColor, brushSize, isStaging } = useAppSelector(selector);
|
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 (
|
return (
|
||||||
<IAIPopover
|
<IAIPopover
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
@ -42,7 +88,7 @@ const IAICanvasBrushButtonPopover = () => {
|
|||||||
tooltip="Brush (B)"
|
tooltip="Brush (B)"
|
||||||
icon={<FaPaintBrush />}
|
icon={<FaPaintBrush />}
|
||||||
data-selected={tool === 'brush' && !isStaging}
|
data-selected={tool === 'brush' && !isStaging}
|
||||||
onClick={() => dispatch(setTool('brush'))}
|
onClick={handleSelectBrushTool}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import {
|
import { setEraserSize, setTool } from 'features/canvas/store/canvasSlice';
|
||||||
setEraserSize,
|
|
||||||
setTool,
|
|
||||||
} from 'features/canvas/store/canvasSlice';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store';
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
@ -11,7 +8,10 @@ import IAIPopover from 'common/components/IAIPopover';
|
|||||||
import IAISlider from 'common/components/IAISlider';
|
import IAISlider from 'common/components/IAISlider';
|
||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
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(
|
export const selector = createSelector(
|
||||||
[canvasSelector, isStagingSelector],
|
[canvasSelector, isStagingSelector],
|
||||||
@ -37,17 +37,41 @@ const IAICanvasEraserButtonPopover = () => {
|
|||||||
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'e',
|
['e'],
|
||||||
(e: KeyboardEvent) => {
|
() => {
|
||||||
e.preventDefault();
|
|
||||||
handleSelectEraserTool();
|
handleSelectEraserTool();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: true,
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
[tool]
|
[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 (
|
return (
|
||||||
<IAIPopover
|
<IAIPopover
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
|
@ -16,6 +16,7 @@ import IAICheckbox from 'common/components/IAICheckbox';
|
|||||||
import IAIColorPicker from 'common/components/IAIColorPicker';
|
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
export const selector = createSelector(
|
export const selector = createSelector(
|
||||||
[canvasSelector],
|
[canvasSelector],
|
||||||
@ -41,6 +42,22 @@ const IAICanvasMaskButtonPopover = () => {
|
|||||||
const { layer, maskColor, isMaskEnabled, shouldPreserveMaskedArea } =
|
const { layer, maskColor, isMaskEnabled, shouldPreserveMaskedArea } =
|
||||||
useAppSelector(selector);
|
useAppSelector(selector);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['q'],
|
||||||
|
() => {
|
||||||
|
handleToggleMaskLayer();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[layer]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleMaskLayer = () => {
|
||||||
|
dispatch(setLayer(layer === 'mask' ? 'base' : 'mask'));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IAIPopover
|
<IAIPopover
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
@ -49,7 +66,7 @@ const IAICanvasMaskButtonPopover = () => {
|
|||||||
aria-label="Select Mask Layer"
|
aria-label="Select Mask Layer"
|
||||||
tooltip="Select Mask Layer"
|
tooltip="Select Mask Layer"
|
||||||
data-alert={layer === 'mask'}
|
data-alert={layer === 'mask'}
|
||||||
onClick={() => dispatch(setLayer(layer === 'mask' ? 'base' : 'mask'))}
|
onClick={handleToggleMaskLayer}
|
||||||
icon={<FaMask />}
|
icon={<FaMask />}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import { ButtonGroup } from '@chakra-ui/react';
|
import { ButtonGroup } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import {
|
import {
|
||||||
resizeAndScaleCanvas,
|
|
||||||
resetCanvas,
|
resetCanvas,
|
||||||
resetCanvasView,
|
resetCanvasView,
|
||||||
setTool,
|
setTool,
|
||||||
fitBoundingBoxToStage,
|
|
||||||
} from 'features/canvas/store/canvasSlice';
|
} from 'features/canvas/store/canvasSlice';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store';
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { canvasImageLayerRef, stageRef } from '../IAICanvas';
|
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import {
|
import {
|
||||||
FaArrowsAlt,
|
FaArrowsAlt,
|
||||||
@ -28,12 +25,15 @@ import IAICanvasEraserButtonPopover from './IAICanvasEraserButtonPopover';
|
|||||||
import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover';
|
import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover';
|
||||||
import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover';
|
import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover';
|
||||||
import { mergeAndUploadCanvas } from 'features/canvas/util/mergeAndUploadCanvas';
|
import { mergeAndUploadCanvas } from 'features/canvas/util/mergeAndUploadCanvas';
|
||||||
import IAICheckbox from 'common/components/IAICheckbox';
|
|
||||||
import { ChangeEvent } from 'react';
|
|
||||||
import {
|
import {
|
||||||
canvasSelector,
|
canvasSelector,
|
||||||
isStagingSelector,
|
isStagingSelector,
|
||||||
} from 'features/canvas/store/canvasSelectors';
|
} from 'features/canvas/store/canvasSelectors';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import {
|
||||||
|
getCanvasBaseLayer,
|
||||||
|
getCanvasStage,
|
||||||
|
} from 'features/canvas/util/konvaInstanceProvider';
|
||||||
|
|
||||||
export const selector = createSelector(
|
export const selector = createSelector(
|
||||||
[canvasSelector, isStagingSelector],
|
[canvasSelector, isStagingSelector],
|
||||||
@ -54,6 +54,138 @@ export const selector = createSelector(
|
|||||||
const IAICanvasOutpaintingControls = () => {
|
const IAICanvasOutpaintingControls = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { tool, isStaging } = useAppSelector(selector);
|
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 (
|
return (
|
||||||
<div className="inpainting-settings">
|
<div className="inpainting-settings">
|
||||||
@ -66,63 +198,33 @@ const IAICanvasOutpaintingControls = () => {
|
|||||||
tooltip="Move (M)"
|
tooltip="Move (M)"
|
||||||
icon={<FaArrowsAlt />}
|
icon={<FaArrowsAlt />}
|
||||||
data-selected={tool === 'move' || isStaging}
|
data-selected={tool === 'move' || isStaging}
|
||||||
onClick={() => dispatch(setTool('move'))}
|
onClick={handleSelectMoveTool}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ButtonGroup isAttached>
|
<ButtonGroup isAttached>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Merge Visible"
|
aria-label="Merge Visible (Shift + M)"
|
||||||
tooltip="Merge Visible"
|
tooltip="Merge Visible (Shift + M)"
|
||||||
icon={<FaLayerGroup />}
|
icon={<FaLayerGroup />}
|
||||||
onClick={() => {
|
onClick={handleMergeVisible}
|
||||||
dispatch(
|
|
||||||
mergeAndUploadCanvas({
|
|
||||||
canvasImageLayerRef,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Save to Gallery"
|
aria-label="Save to Gallery (Shift + S)"
|
||||||
tooltip="Save to Gallery"
|
tooltip="Save to Gallery (Shift + S)"
|
||||||
icon={<FaSave />}
|
icon={<FaSave />}
|
||||||
onClick={() => {
|
onClick={handleSaveToGallery}
|
||||||
dispatch(
|
|
||||||
mergeAndUploadCanvas({
|
|
||||||
canvasImageLayerRef,
|
|
||||||
cropVisible: true,
|
|
||||||
saveToGallery: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Copy Selection"
|
aria-label="Copy to Clipboard (Cmd/Ctrl + C)"
|
||||||
tooltip="Copy Selection"
|
tooltip="Copy to Clipboard (Cmd/Ctrl + C)"
|
||||||
icon={<FaCopy />}
|
icon={<FaCopy />}
|
||||||
onClick={() => {
|
onClick={handleCopyImageToClipboard}
|
||||||
dispatch(
|
|
||||||
mergeAndUploadCanvas({
|
|
||||||
canvasImageLayerRef,
|
|
||||||
cropVisible: true,
|
|
||||||
copyAfterSaving: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Download Selection"
|
aria-label="Download as Image (Shift + D)"
|
||||||
tooltip="Download Selection"
|
tooltip="Download as Image (Shift + D)"
|
||||||
icon={<FaDownload />}
|
icon={<FaDownload />}
|
||||||
onClick={() => {
|
onClick={handleDownloadAsImage}
|
||||||
dispatch(
|
|
||||||
mergeAndUploadCanvas({
|
|
||||||
canvasImageLayerRef,
|
|
||||||
cropVisible: true,
|
|
||||||
downloadAfterSaving: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ButtonGroup isAttached>
|
<ButtonGroup isAttached>
|
||||||
@ -142,23 +244,13 @@ const IAICanvasOutpaintingControls = () => {
|
|||||||
aria-label="Reset Canvas View"
|
aria-label="Reset Canvas View"
|
||||||
tooltip="Reset Canvas View"
|
tooltip="Reset Canvas View"
|
||||||
icon={<FaCrosshairs />}
|
icon={<FaCrosshairs />}
|
||||||
onClick={() => {
|
onClick={handleResetCanvasView}
|
||||||
if (!stageRef.current || !canvasImageLayerRef.current) return;
|
|
||||||
const clientRect = canvasImageLayerRef.current.getClientRect({
|
|
||||||
skipTransform: true,
|
|
||||||
});
|
|
||||||
dispatch(
|
|
||||||
resetCanvasView({
|
|
||||||
contentRect: clientRect,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Reset Canvas"
|
aria-label="Reset Canvas"
|
||||||
tooltip="Reset Canvas"
|
tooltip="Reset Canvas"
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
onClick={() => dispatch(resetCanvas())}
|
onClick={handleResetCanvas}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,9 +9,9 @@ import {
|
|||||||
} from 'features/canvas/store/canvasSlice';
|
} from 'features/canvas/store/canvasSlice';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store';
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { stageRef } from '../components/IAICanvas';
|
|
||||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { CanvasTool } from '../store/canvasTypes';
|
import { CanvasTool } from '../store/canvasTypes';
|
||||||
|
import { getCanvasStage } from '../util/konvaInstanceProvider';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[canvasSelector, activeTabNameSelector],
|
[canvasSelector, activeTabNameSelector],
|
||||||
@ -44,6 +44,9 @@ const useInpaintingCanvasHotkeys = () => {
|
|||||||
useAppSelector(selector);
|
useAppSelector(selector);
|
||||||
|
|
||||||
const previousToolRef = useRef<CanvasTool | null>(null);
|
const previousToolRef = useRef<CanvasTool | null>(null);
|
||||||
|
|
||||||
|
const canvasStage = getCanvasStage();
|
||||||
|
|
||||||
// Toggle lock bounding box
|
// Toggle lock bounding box
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+w',
|
'shift+w',
|
||||||
@ -72,7 +75,7 @@ const useInpaintingCanvasHotkeys = () => {
|
|||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (e.repeat) return;
|
if (e.repeat) return;
|
||||||
|
|
||||||
stageRef.current?.container().focus();
|
canvasStage?.container().focus();
|
||||||
|
|
||||||
if (tool !== 'move') {
|
if (tool !== 'move') {
|
||||||
previousToolRef.current = tool;
|
previousToolRef.current = tool;
|
||||||
|
16
frontend/src/features/canvas/util/konvaInstanceProvider.ts
Normal file
16
frontend/src/features/canvas/util/konvaInstanceProvider.ts
Normal 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;
|
@ -1,18 +1,16 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { RootState } from 'app/store';
|
import { RootState } from 'app/store';
|
||||||
import Konva from 'konva';
|
|
||||||
import { MutableRefObject } from 'react';
|
|
||||||
import * as InvokeAI from 'app/invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import layerToDataURL from './layerToDataURL';
|
import layerToDataURL from './layerToDataURL';
|
||||||
import downloadFile from './downloadFile';
|
import downloadFile from './downloadFile';
|
||||||
import copyImage from './copyImage';
|
import copyImage from './copyImage';
|
||||||
|
import { getCanvasBaseLayer } from './konvaInstanceProvider';
|
||||||
|
|
||||||
export const mergeAndUploadCanvas = createAsyncThunk(
|
export const mergeAndUploadCanvas = createAsyncThunk(
|
||||||
'canvas/mergeAndUploadCanvas',
|
'canvas/mergeAndUploadCanvas',
|
||||||
async (
|
async (
|
||||||
args: {
|
args: {
|
||||||
canvasImageLayerRef: MutableRefObject<Konva.Layer | null>;
|
|
||||||
cropVisible?: boolean;
|
cropVisible?: boolean;
|
||||||
saveToGallery?: boolean;
|
saveToGallery?: boolean;
|
||||||
downloadAfterSaving?: boolean;
|
downloadAfterSaving?: boolean;
|
||||||
@ -20,13 +18,8 @@ export const mergeAndUploadCanvas = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
thunkAPI
|
thunkAPI
|
||||||
) => {
|
) => {
|
||||||
const {
|
const { saveToGallery, downloadAfterSaving, cropVisible, copyAfterSaving } =
|
||||||
canvasImageLayerRef,
|
args;
|
||||||
saveToGallery,
|
|
||||||
downloadAfterSaving,
|
|
||||||
cropVisible,
|
|
||||||
copyAfterSaving,
|
|
||||||
} = args;
|
|
||||||
|
|
||||||
const { getState } = thunkAPI;
|
const { getState } = thunkAPI;
|
||||||
|
|
||||||
@ -34,10 +27,12 @@ export const mergeAndUploadCanvas = createAsyncThunk(
|
|||||||
|
|
||||||
const stageScale = state.canvas.stageScale;
|
const stageScale = state.canvas.stageScale;
|
||||||
|
|
||||||
if (!canvasImageLayerRef.current) return;
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
|
||||||
|
if (!canvasBaseLayer) return;
|
||||||
|
|
||||||
const { dataURL, boundingBox: originalBoundingBox } = layerToDataURL(
|
const { dataURL, boundingBox: originalBoundingBox } = layerToDataURL(
|
||||||
canvasImageLayerRef.current,
|
canvasBaseLayer,
|
||||||
stageScale
|
stageScale
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -246,20 +246,6 @@ export default function ImageGallery() {
|
|||||||
[galleryImageMinimumWidth]
|
[galleryImageMinimumWidth]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'shift+r',
|
|
||||||
() => {
|
|
||||||
dispatch(setGalleryImageMinimumWidth(64));
|
|
||||||
toast({
|
|
||||||
title: `Reset Gallery Image Size`,
|
|
||||||
status: 'success',
|
|
||||||
duration: 2500,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[galleryImageMinimumWidth]
|
|
||||||
);
|
|
||||||
|
|
||||||
// set gallery scroll position
|
// set gallery scroll position
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!galleryContainerRef.current) return;
|
if (!galleryContainerRef.current) return;
|
||||||
|
@ -135,11 +135,6 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
|||||||
desc: 'Decreases gallery thumbnails size',
|
desc: 'Decreases gallery thumbnails size',
|
||||||
hotkey: 'Shift+Down',
|
hotkey: 'Shift+Down',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Reset Gallery Image Size',
|
|
||||||
desc: 'Resets image gallery size',
|
|
||||||
hotkey: 'Shift+R',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const unifiedCanvasHotkeys = [
|
const unifiedCanvasHotkeys = [
|
||||||
|
Loading…
Reference in New Issue
Block a user