From 385e93443a360c7931d09fc8b18ea92a01b5800c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:11:40 +1000 Subject: [PATCH] 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) --- .../features/canvas/hooks/useCanvasZoom.ts | 22 ++++++++------- .../components/StageComponent.tsx | 6 +++-- .../components/ToolChooser.tsx | 25 +++++++++++++++-- .../regionalPrompts/hooks/mouseEventHooks.ts | 27 ++++++++++++++++++- .../store/regionalPromptsSlice.ts | 25 +++++++++++++---- 5 files changed, 86 insertions(+), 19 deletions(-) diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts index ef6a74ae9c..1434bc9afc 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts @@ -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) => { const dispatch = useAppDispatch(); const stageScale = useAppSelector((s) => s.canvas.stageScale); @@ -36,15 +48,7 @@ const useCanvasWheel = (stageRef: MutableRefObject) => { } 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; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx index 1f69b37bbe..dbdfa7c563 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx @@ -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'); diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx index 816f10f34d..a79c443a2f 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx @@ -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 ( { 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) => { @@ -175,5 +180,25 @@ export const useMouseEvents = () => { [dispatch, selectedLayerId, tool] ); - return { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave }; + const onMouseWheel = useCallback( + (e: KonvaEventObject) => { + 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 }; }; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts index 4ca70b488b..3013048470 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts @@ -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) => { 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) => { @@ -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,