mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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:
parent
604217313a
commit
385e93443a
@ -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;
|
||||||
|
@ -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');
|
||||||
|
@ -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
|
||||||
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user