diff --git a/invokeai/frontend/web/.storybook/preview.tsx b/invokeai/frontend/web/.storybook/preview.tsx index 791a48ab9e..5f568c5ce3 100644 --- a/invokeai/frontend/web/.storybook/preview.tsx +++ b/invokeai/frontend/web/.storybook/preview.tsx @@ -11,6 +11,7 @@ import { createStore } from '../src/app/store/store'; // @ts-ignore import translationEN from '../public/locales/en.json'; import { ReduxInit } from './ReduxInit'; +import { $store } from 'app/store/nanostores/store'; i18n.use(initReactI18next).init({ lng: 'en', @@ -25,6 +26,7 @@ i18n.use(initReactI18next).init({ }); const store = createStore(undefined, false); +$store.set(store) $baseUrl.set('http://localhost:9090'); const preview: Preview = { diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx index df50d07cda..eaecc7625d 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx @@ -24,6 +24,7 @@ export const LineComponent = ({ line, color }: Props) => { lineJoin="round" shadowForStrokeEnabled={false} listening={false} + globalCompositeOperation={line.tool === 'brush' ? 'source-over' : 'destination-out'} /> ); }; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx index fb81151d9f..53b0c7164a 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx @@ -5,6 +5,7 @@ import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButt import { BrushSize } from 'features/regionalPrompts/components/BrushSize'; import { LayerListItem } from 'features/regionalPrompts/components/LayerListItem'; import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage'; +import { ToolChooser } from 'features/regionalPrompts/components/ToolChooser'; import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice'; const selectLayerIdsReversed = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => @@ -18,6 +19,7 @@ export const RegionalPromptsEditor = () => { + {layerIdsReversed.map((id) => ( ))} diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx new file mode 100644 index 0000000000..4d178548ec --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx @@ -0,0 +1,33 @@ +import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { toolChanged } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { useCallback } from 'react'; +import { PiEraserBold, PiPaintBrushBold } from 'react-icons/pi'; + +export const ToolChooser: React.FC = () => { + const tool = useAppSelector((s) => s.regionalPrompts.tool); + const dispatch = useAppDispatch(); + const setToolToBrush = useCallback(() => { + dispatch(toolChanged('brush')); + }, [dispatch]); + const setToolToEraser = useCallback(() => { + dispatch(toolChanged('eraser')); + }, [dispatch]); + + return ( + + } + variant={tool === 'brush' ? 'solid' : 'outline'} + onClick={setToolToBrush} + /> + } + variant={tool === 'eraser' ? 'solid' : 'outline'} + onClick={setToolToEraser} + /> + + ); +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts index 3e3b8f0db5..084bb24294 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts @@ -1,10 +1,10 @@ +import { getStore } from 'app/store/nanostores/store'; import { useAppDispatch } from 'app/store/storeHooks'; import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; import { $cursorPosition, $isMouseDown, $isMouseOver, - $tool, lineAdded, pointsAdded, } from 'features/regionalPrompts/store/regionalPromptsSlice'; @@ -13,6 +13,8 @@ import type { KonvaEventObject } from 'konva/lib/Node'; import type { MutableRefObject } from 'react'; import { useCallback } from 'react'; +const getTool = () => getStore().getState().regionalPrompts.tool; + const getIsFocused = (stage: Konva.Stage) => { return stage.container().contains(document.activeElement); }; @@ -38,7 +40,8 @@ export const useMouseDown = (stageRef: MutableRefObject) => return; } $isMouseDown.set(true); - if ($tool.get() === 'brush') { + const tool = getTool(); + if (tool === 'brush' || tool === 'eraser') { dispatch(lineAdded([pos.x, pos.y])); } }, @@ -54,7 +57,8 @@ export const useMouseUp = (stageRef: MutableRefObject) => { if (!stageRef.current) { return; } - if ($tool.get() === 'brush' && $isMouseDown.get()) { + const tool = getTool(); + if ((tool === 'brush' || tool === 'eraser') && $isMouseDown.get()) { // Add another point to the last line. $isMouseDown.set(false); const pos = syncCursorPos(stageRef.current); @@ -80,7 +84,13 @@ export const useMouseMove = (stageRef: MutableRefObject) => if (!pos) { return; } - if (getIsFocused(stageRef.current) && $isMouseOver.get() && $isMouseDown.get() && $tool.get() === 'brush') { + const tool = getTool(); + if ( + getIsFocused(stageRef.current) && + $isMouseOver.get() && + $isMouseDown.get() && + (tool === 'brush' || tool === 'eraser') + ) { dispatch(pointsAdded([pos.x, pos.y])); } }, @@ -123,7 +133,8 @@ export const useMouseEnter = (stageRef: MutableRefObject) => $isMouseDown.set(false); } else { $isMouseDown.set(true); - if ($tool.get() === 'brush') { + const tool = getTool(); + if (tool === 'brush' || tool === 'eraser') { dispatch(lineAdded([pos.x, pos.y])); } } diff --git a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts index 83a87d67d6..0c644f8808 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts @@ -9,6 +9,8 @@ import type { RgbColor } from 'react-colorful'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; +export type Tool = 'brush' | 'eraser'; + type LayerObjectBase = { id: string; isSelected: boolean; @@ -25,6 +27,7 @@ type ImageObject = LayerObjectBase & { export type LineObject = LayerObjectBase & { kind: 'line'; + tool: Tool; strokeWidth: number; points: number[]; }; @@ -53,10 +56,9 @@ type PromptRegionLayer = LayerBase & { type Layer = PromptRegionLayer; -type Tool = 'brush'; - type RegionalPromptsState = { _version: 1; + tool: Tool; selectedLayer: string | null; layers: PromptRegionLayer[]; brushSize: number; @@ -64,6 +66,7 @@ type RegionalPromptsState = { const initialRegionalPromptsState: RegionalPromptsState = { _version: 1, + tool: 'brush', selectedLayer: null, brushSize: 40, layers: [], @@ -144,7 +147,7 @@ export const regionalPromptsSlice = createSlice({ if (!selectedLayer || selectedLayer.kind !== 'promptRegionLayer') { return; } - selectedLayer.objects.push(buildLine(action.meta.id, action.payload, state.brushSize)); + selectedLayer.objects.push(buildLine(action.meta.id, action.payload, state.brushSize, state.tool)); }, prepare: (payload: number[]) => ({ payload, meta: { id: uuidv4() } }), }, @@ -162,6 +165,9 @@ export const regionalPromptsSlice = createSlice({ brushSizeChanged: (state, action: PayloadAction) => { state.brushSize = action.payload; }, + toolChanged: (state, action: PayloadAction) => { + state.tool = action.payload; + }, }, }); @@ -190,9 +196,10 @@ const buildLayer = (id: string, kind: Layer['kind'], layerCount: number): Layer assert(false, `Unknown layer kind: ${kind}`); }; -const buildLine = (id: string, points: number[], brushSize: number): LineObject => ({ +const buildLine = (id: string, points: number[], brushSize: number, tool: Tool): LineObject => ({ isSelected: false, kind: 'line', + tool, id, points, strokeWidth: brushSize, @@ -213,6 +220,7 @@ export const { layerMovedToFront, layerMovedBackward, layerMovedToBack, + toolChanged, } = regionalPromptsSlice.actions; export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts; @@ -232,5 +240,4 @@ export const regionalPromptsPersistConfig: PersistConfig = export const $isMouseDown = atom(false); export const $isMouseOver = atom(false); export const $cursorPosition = atom(null); -export const $tool = atom('brush'); export const $stage = atom(null);