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
invokeai/frontend/web/src/features
canvas/hooks
regionalPrompts
@ -10,6 +10,18 @@ import { clamp } from 'lodash-es';
|
||||
import type { MutableRefObject } 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 dispatch = useAppDispatch();
|
||||
const stageScale = useAppSelector((s) => s.canvas.stageScale);
|
||||
@ -36,15 +48,7 @@ const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
|
||||
}
|
||||
|
||||
if ($ctrl.get() || $meta.get()) {
|
||||
// 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);
|
||||
|
||||
dispatch(setBrushSize(newBrushSize));
|
||||
dispatch(setBrushSize(calculateNewBrushSize(brushSize, delta)));
|
||||
} else {
|
||||
const cursorPos = stageRef.current.getPointerPosition();
|
||||
let delta = e.evt.deltaY;
|
||||
|
@ -47,7 +47,7 @@ const useStageRenderer = (
|
||||
const height = useAppSelector((s) => s.generation.height);
|
||||
const state = useAppSelector((s) => s.regionalPrompts.present);
|
||||
const tool = useStore($tool);
|
||||
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave } = useMouseEvents();
|
||||
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel } = useMouseEvents();
|
||||
const cursorPosition = useStore($cursorPosition);
|
||||
const lastMouseDownPos = useStore($lastMouseDownPos);
|
||||
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
|
||||
@ -106,6 +106,7 @@ const useStageRenderer = (
|
||||
stageRef.current.on('mousemove', onMouseMove);
|
||||
stageRef.current.on('mouseenter', onMouseEnter);
|
||||
stageRef.current.on('mouseleave', onMouseLeave);
|
||||
stageRef.current.on('wheel', onMouseWheel);
|
||||
const stage = stageRef.current;
|
||||
|
||||
return () => {
|
||||
@ -115,8 +116,9 @@ const useStageRenderer = (
|
||||
stage.off('mousemove', onMouseMove);
|
||||
stage.off('mouseenter', onMouseEnter);
|
||||
stage.off('mouseleave', onMouseLeave);
|
||||
stage.off('wheel', onMouseWheel);
|
||||
};
|
||||
}, [stageRef, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave]);
|
||||
}, [stageRef, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
log.trace('Updating stage dimensions');
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $tool } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
$tool,
|
||||
layerAdded,
|
||||
selectedLayerDeleted,
|
||||
selectedLayerReset,
|
||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
||||
import { useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -9,6 +14,7 @@ import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBol
|
||||
|
||||
export const ToolChooser: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isDisabled = useAppSelector((s) => s.regionalPrompts.present.layers.length === 0);
|
||||
const tool = useStore($tool);
|
||||
|
||||
@ -29,6 +35,21 @@ export const ToolChooser: React.FC = () => {
|
||||
}, []);
|
||||
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 (
|
||||
<ButtonGroup isAttached>
|
||||
<IconButton
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { $ctrl, $meta } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom';
|
||||
import {
|
||||
$cursorPosition,
|
||||
$isMouseDown,
|
||||
$isMouseOver,
|
||||
$lastMouseDownPos,
|
||||
$tool,
|
||||
brushSizeChanged,
|
||||
maskLayerLineAdded,
|
||||
maskLayerPointsAdded,
|
||||
maskLayerRectAdded,
|
||||
@ -49,6 +52,8 @@ export const useMouseEvents = () => {
|
||||
const selectedLayerId = useAppSelector((s) => s.regionalPrompts.present.selectedLayerId);
|
||||
const tool = useStore($tool);
|
||||
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(
|
||||
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
||||
@ -175,5 +180,25 @@ export const useMouseEvents = () => {
|
||||
[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';
|
||||
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({
|
||||
name: 'regionalPrompts',
|
||||
@ -144,11 +151,7 @@ export const regionalPromptsSlice = createSlice({
|
||||
layerReset: (state, action: PayloadAction<string>) => {
|
||||
const layer = state.layers.find((l) => l.id === action.payload);
|
||||
if (layer) {
|
||||
layer.objects = [];
|
||||
layer.bbox = null;
|
||||
layer.isVisible = true;
|
||||
layer.needsPixelBbox = false;
|
||||
layer.bboxNeedsUpdate = false;
|
||||
resetLayer(layer);
|
||||
}
|
||||
},
|
||||
layerDeleted: (state, action: PayloadAction<string>) => {
|
||||
@ -177,6 +180,16 @@ export const regionalPromptsSlice = createSlice({
|
||||
state.layers = [];
|
||||
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
|
||||
|
||||
//#region Mask Layers
|
||||
@ -370,6 +383,8 @@ export const {
|
||||
layerBboxChanged,
|
||||
layerVisibilityToggled,
|
||||
allLayersDeleted,
|
||||
selectedLayerReset,
|
||||
selectedLayerDeleted,
|
||||
// Mask layer actions
|
||||
maskLayerLineAdded,
|
||||
maskLayerPointsAdded,
|
||||
|
Loading…
x
Reference in New Issue
Block a user