feat(ui): rp hotkeys

- Shift+C: Reset selected layer mask (same as canvas)
- Shift+D: Delete selected layer (cannot be Del, that deletes an image in gallery)
- Shift+A: Add layer (cannot be Ctrl+Shift+N, that opens a new window)
- Ctrl/Cmd+Wheel: Brush size (same as canvas)
This commit is contained in:
psychedelicious 2024-04-23 11:11:40 +10:00
parent 604217313a
commit 385e93443a
5 changed files with 86 additions and 19 deletions

View File

@ -10,6 +10,18 @@ import { clamp } from 'lodash-es';
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
export const calculateNewBrushSize = (brushSize: number, delta: number) => {
// This equation was derived by fitting a curve to the desired brush sizes and deltas
// see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565
const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize);
// This needs to be clamped to prevent the delta from getting too large
const finalDelta = clamp(targetDelta, -20, 20);
// The new brush size is also clamped to prevent it from getting too large or small
const newBrushSize = clamp(brushSize + finalDelta, 1, 500);
return newBrushSize;
};
const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => { const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const stageScale = useAppSelector((s) => s.canvas.stageScale); const stageScale = useAppSelector((s) => s.canvas.stageScale);
@ -36,15 +48,7 @@ const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
} }
if ($ctrl.get() || $meta.get()) { if ($ctrl.get() || $meta.get()) {
// This equation was derived by fitting a curve to the desired brush sizes and deltas dispatch(setBrushSize(calculateNewBrushSize(brushSize, delta)));
// see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565
const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize);
// This needs to be clamped to prevent the delta from getting too large
const finalDelta = clamp(targetDelta, -20, 20);
// The new brush size is also clamped to prevent it from getting too large or small
const newBrushSize = clamp(brushSize + finalDelta, 1, 500);
dispatch(setBrushSize(newBrushSize));
} else { } else {
const cursorPos = stageRef.current.getPointerPosition(); const cursorPos = stageRef.current.getPointerPosition();
let delta = e.evt.deltaY; let delta = e.evt.deltaY;

View File

@ -47,7 +47,7 @@ const useStageRenderer = (
const height = useAppSelector((s) => s.generation.height); const height = useAppSelector((s) => s.generation.height);
const state = useAppSelector((s) => s.regionalPrompts.present); const state = useAppSelector((s) => s.regionalPrompts.present);
const tool = useStore($tool); const tool = useStore($tool);
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave } = useMouseEvents(); const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel } = useMouseEvents();
const cursorPosition = useStore($cursorPosition); const cursorPosition = useStore($cursorPosition);
const lastMouseDownPos = useStore($lastMouseDownPos); const lastMouseDownPos = useStore($lastMouseDownPos);
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor); const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
@ -106,6 +106,7 @@ const useStageRenderer = (
stageRef.current.on('mousemove', onMouseMove); stageRef.current.on('mousemove', onMouseMove);
stageRef.current.on('mouseenter', onMouseEnter); stageRef.current.on('mouseenter', onMouseEnter);
stageRef.current.on('mouseleave', onMouseLeave); stageRef.current.on('mouseleave', onMouseLeave);
stageRef.current.on('wheel', onMouseWheel);
const stage = stageRef.current; const stage = stageRef.current;
return () => { return () => {
@ -115,8 +116,9 @@ const useStageRenderer = (
stage.off('mousemove', onMouseMove); stage.off('mousemove', onMouseMove);
stage.off('mouseenter', onMouseEnter); stage.off('mouseenter', onMouseEnter);
stage.off('mouseleave', onMouseLeave); stage.off('mouseleave', onMouseLeave);
stage.off('wheel', onMouseWheel);
}; };
}, [stageRef, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave]); }, [stageRef, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel]);
useLayoutEffect(() => { useLayoutEffect(() => {
log.trace('Updating stage dimensions'); log.trace('Updating stage dimensions');

View File

@ -1,7 +1,12 @@
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { $tool } from 'features/regionalPrompts/store/regionalPromptsSlice'; import {
$tool,
layerAdded,
selectedLayerDeleted,
selectedLayerReset,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -9,6 +14,7 @@ import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBol
export const ToolChooser: React.FC = () => { export const ToolChooser: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const isDisabled = useAppSelector((s) => s.regionalPrompts.present.layers.length === 0); const isDisabled = useAppSelector((s) => s.regionalPrompts.present.layers.length === 0);
const tool = useStore($tool); const tool = useStore($tool);
@ -29,6 +35,21 @@ export const ToolChooser: React.FC = () => {
}, []); }, []);
useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]); useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]);
const resetSelectedLayer = useCallback(() => {
dispatch(selectedLayerReset());
}, [dispatch]);
useHotkeys('shift+c', resetSelectedLayer);
const addLayer = useCallback(() => {
dispatch(layerAdded('vector_mask_layer'));
}, [dispatch]);
useHotkeys('shift+a', addLayer);
const deleteSelectedLayer = useCallback(() => {
dispatch(selectedLayerDeleted());
}, [dispatch]);
useHotkeys('shift+d', deleteSelectedLayer);
return ( return (
<ButtonGroup isAttached> <ButtonGroup isAttached>
<IconButton <IconButton

View File

@ -1,11 +1,14 @@
import { $ctrl, $meta } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom';
import { import {
$cursorPosition, $cursorPosition,
$isMouseDown, $isMouseDown,
$isMouseOver, $isMouseOver,
$lastMouseDownPos, $lastMouseDownPos,
$tool, $tool,
brushSizeChanged,
maskLayerLineAdded, maskLayerLineAdded,
maskLayerPointsAdded, maskLayerPointsAdded,
maskLayerRectAdded, maskLayerRectAdded,
@ -49,6 +52,8 @@ export const useMouseEvents = () => {
const selectedLayerId = useAppSelector((s) => s.regionalPrompts.present.selectedLayerId); const selectedLayerId = useAppSelector((s) => s.regionalPrompts.present.selectedLayerId);
const tool = useStore($tool); const tool = useStore($tool);
const lastCursorPosRef = useRef<[number, number] | null>(null); const lastCursorPosRef = useRef<[number, number] | null>(null);
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection);
const brushSize = useAppSelector((s) => s.regionalPrompts.present.brushSize);
const onMouseDown = useCallback( const onMouseDown = useCallback(
(e: KonvaEventObject<MouseEvent | TouchEvent>) => { (e: KonvaEventObject<MouseEvent | TouchEvent>) => {
@ -175,5 +180,25 @@ export const useMouseEvents = () => {
[dispatch, selectedLayerId, tool] [dispatch, selectedLayerId, tool]
); );
return { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave }; const onMouseWheel = useCallback(
(e: KonvaEventObject<WheelEvent>) => {
e.evt.preventDefault();
// checking for ctrl key is pressed or not,
// so that brush size can be controlled using ctrl + scroll up/down
// Invert the delta if the property is set to true
let delta = e.evt.deltaY;
if (shouldInvertBrushSizeScrollDirection) {
delta = -delta;
}
if ($ctrl.get() || $meta.get()) {
dispatch(brushSizeChanged(calculateNewBrushSize(brushSize, delta)));
}
},
[shouldInvertBrushSizeScrollDirection, brushSize, dispatch]
);
return { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel };
}; };

View File

@ -78,6 +78,13 @@ export const initialRegionalPromptsState: RegionalPromptsState = {
const isLine = (obj: VectorMaskLine | VectorMaskRect): obj is VectorMaskLine => obj.type === 'vector_mask_line'; const isLine = (obj: VectorMaskLine | VectorMaskRect): obj is VectorMaskLine => obj.type === 'vector_mask_line';
export const isVectorMaskLayer = (layer?: Layer): layer is VectorMaskLayer => layer?.type === 'vector_mask_layer'; export const isVectorMaskLayer = (layer?: Layer): layer is VectorMaskLayer => layer?.type === 'vector_mask_layer';
const resetLayer = (layer: VectorMaskLayer) => {
layer.objects = [];
layer.bbox = null;
layer.isVisible = true;
layer.needsPixelBbox = false;
layer.bboxNeedsUpdate = false;
};
export const regionalPromptsSlice = createSlice({ export const regionalPromptsSlice = createSlice({
name: 'regionalPrompts', name: 'regionalPrompts',
@ -144,11 +151,7 @@ export const regionalPromptsSlice = createSlice({
layerReset: (state, action: PayloadAction<string>) => { layerReset: (state, action: PayloadAction<string>) => {
const layer = state.layers.find((l) => l.id === action.payload); const layer = state.layers.find((l) => l.id === action.payload);
if (layer) { if (layer) {
layer.objects = []; resetLayer(layer);
layer.bbox = null;
layer.isVisible = true;
layer.needsPixelBbox = false;
layer.bboxNeedsUpdate = false;
} }
}, },
layerDeleted: (state, action: PayloadAction<string>) => { layerDeleted: (state, action: PayloadAction<string>) => {
@ -177,6 +180,16 @@ export const regionalPromptsSlice = createSlice({
state.layers = []; state.layers = [];
state.selectedLayerId = null; state.selectedLayerId = null;
}, },
selectedLayerReset: (state) => {
const layer = state.layers.find((l) => l.id === state.selectedLayerId);
if (layer) {
resetLayer(layer);
}
},
selectedLayerDeleted: (state) => {
state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId);
state.selectedLayerId = state.layers[0]?.id ?? null;
},
//#endregion //#endregion
//#region Mask Layers //#region Mask Layers
@ -370,6 +383,8 @@ export const {
layerBboxChanged, layerBboxChanged,
layerVisibilityToggled, layerVisibilityToggled,
allLayersDeleted, allLayersDeleted,
selectedLayerReset,
selectedLayerDeleted,
// Mask layer actions // Mask layer actions
maskLayerLineAdded, maskLayerLineAdded,
maskLayerPointsAdded, maskLayerPointsAdded,