diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx index 018afaea12..be98161130 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx @@ -6,6 +6,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useMouseEvents } from 'features/regionalPrompts/hooks/mouseEventHooks'; import { $cursorPosition, + $tool, isRPLayer, rpLayerBboxChanged, rpLayerTranslated, @@ -35,6 +36,7 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem const height = useAppSelector((s) => s.generation.height); const state = useAppSelector((s) => s.regionalPrompts.present); const stage = useStore($stage); + const tool = useStore($tool); const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave } = useMouseEvents(); const cursorPosition = useStore($cursorPosition); const selectedLayerColor = useAppSelector(selectSelectedLayerColor); @@ -116,27 +118,28 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem }, [stage, width, height, wrapper]); useLayoutEffect(() => { + log.trace('Rendering brush preview'); if (!stage) { return; } - renderBrushPreview(stage, state.tool, selectedLayerColor, cursorPosition, state.brushSize); - }, [stage, state.tool, cursorPosition, state.brushSize, selectedLayerColor]); + renderBrushPreview(stage, tool, selectedLayerColor, cursorPosition, state.brushSize); + }, [stage, tool, cursorPosition, state.brushSize, selectedLayerColor]); useLayoutEffect(() => { log.trace('Rendering layers'); if (!stage) { return; } - renderLayers(stage, state.layers, state.selectedLayer, state.promptLayerOpacity, state.tool, onLayerPosChanged); - }, [onLayerPosChanged, stage, state.layers, state.promptLayerOpacity, state.tool, state.selectedLayer]); + renderLayers(stage, state.layers, state.selectedLayer, state.promptLayerOpacity, tool, onLayerPosChanged); + }, [onLayerPosChanged, stage, state.layers, state.promptLayerOpacity, tool, state.selectedLayer]); useLayoutEffect(() => { log.trace('Rendering bbox'); if (!stage) { return; } - renderBbox(stage, state.tool, state.selectedLayer, onBboxChanged); - }, [dispatch, stage, state.tool, state.selectedLayer, onBboxChanged]); + renderBbox(stage, tool, state.selectedLayer, onBboxChanged); + }, [dispatch, stage, tool, state.selectedLayer, onBboxChanged]); }; const $container = atom(null); diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx index b1e822b613..cb91a61385 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/ToolChooser.tsx @@ -1,21 +1,20 @@ import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { toolChanged } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { useStore } from '@nanostores/react'; +import { $tool } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { useCallback } from 'react'; import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold } from 'react-icons/pi'; export const ToolChooser: React.FC = () => { - const tool = useAppSelector((s) => s.regionalPrompts.present.tool); - const dispatch = useAppDispatch(); + const tool = useStore($tool); const setToolToBrush = useCallback(() => { - dispatch(toolChanged('brush')); - }, [dispatch]); + $tool.set('brush'); + }, []); const setToolToEraser = useCallback(() => { - dispatch(toolChanged('eraser')); - }, [dispatch]); + $tool.set('eraser'); + }, []); const setToolToMove = useCallback(() => { - dispatch(toolChanged('move')); - }, [dispatch]); + $tool.set('move'); + }, []); return ( diff --git a/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts index ad08f58e8d..f84deff274 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/hooks/mouseEventHooks.ts @@ -1,10 +1,11 @@ -import { getStore } from 'app/store/nanostores/store'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; import { $cursorPosition, $isMouseDown, $isMouseOver, + $tool, rpLayerLineAdded, rpLayerPointsAdded, } from 'features/regionalPrompts/store/regionalPromptsSlice'; @@ -12,8 +13,6 @@ import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import { useCallback } from 'react'; -const getTool = () => getStore().getState().regionalPrompts.present.tool; - const getIsFocused = (stage: Konva.Stage) => { return stage.container().contains(document.activeElement); }; @@ -29,6 +28,8 @@ const syncCursorPos = (stage: Konva.Stage) => { export const useMouseEvents = () => { const dispatch = useAppDispatch(); + const selectedLayer = useAppSelector((s) => s.regionalPrompts.present.selectedLayer); + const tool = useStore($tool); const onMouseDown = useCallback( (e: KonvaEventObject) => { @@ -41,12 +42,15 @@ export const useMouseEvents = () => { return; } $isMouseDown.set(true); - const tool = getTool(); + if (!selectedLayer) { + return; + } + // const tool = getTool(); if (tool === 'brush' || tool === 'eraser') { - dispatch(rpLayerLineAdded([pos.x, pos.y, pos.x, pos.y])); + dispatch(rpLayerLineAdded({ layerId: selectedLayer, points: [pos.x, pos.y, pos.x, pos.y], tool })); } }, - [dispatch] + [dispatch, selectedLayer, tool] ); const onMouseUp = useCallback( @@ -55,12 +59,12 @@ export const useMouseEvents = () => { if (!stage) { return; } - const tool = getTool(); + // const tool = getTool(); if ((tool === 'brush' || tool === 'eraser') && $isMouseDown.get()) { $isMouseDown.set(false); } }, - [] + [tool] ); const onMouseMove = useCallback( @@ -70,15 +74,15 @@ export const useMouseEvents = () => { return; } const pos = syncCursorPos(stage); - if (!pos) { + if (!pos || !selectedLayer) { return; } - const tool = getTool(); + // const tool = getTool(); if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) { - dispatch(rpLayerPointsAdded([pos.x, pos.y])); + dispatch(rpLayerPointsAdded({ layerId: selectedLayer, point: [pos.x, pos.y] })); } }, - [dispatch] + [dispatch, selectedLayer, tool] ); const onMouseLeave = useCallback((e: KonvaEventObject) => { @@ -109,13 +113,15 @@ export const useMouseEvents = () => { $isMouseDown.set(false); } else { $isMouseDown.set(true); - const tool = getTool(); + if (!selectedLayer) { + return; + } if (tool === 'brush' || tool === 'eraser') { - dispatch(rpLayerLineAdded([pos.x, pos.y, pos.x, pos.y])); + dispatch(rpLayerLineAdded({ layerId: selectedLayer, points: [pos.x, pos.y, pos.x, pos.y], tool })); } } }, - [dispatch] + [dispatch, selectedLayer, tool] ); return { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave }; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts index 6161957f9d..894178ec53 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts @@ -10,7 +10,9 @@ import type { UndoableOptions } from 'redux-undo'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; -export type Tool = 'brush' | 'eraser' | 'move'; +export type DrawingTool = 'brush' | 'eraser'; + +export type RPTool = DrawingTool | 'move'; type LayerObjectBase = { id: string; @@ -27,7 +29,7 @@ type ImageObject = LayerObjectBase & { type LineObject = LayerObjectBase & { kind: 'line'; - tool: Tool; + tool: DrawingTool; strokeWidth: number; points: number[]; }; @@ -63,7 +65,6 @@ export type Layer = RegionalPromptLayer; type RegionalPromptsState = { _version: 1; - tool: Tool; selectedLayer: string | null; layers: Layer[]; brushSize: number; @@ -73,7 +74,6 @@ type RegionalPromptsState = { export const initialRegionalPromptsState: RegionalPromptsState = { _version: 1, - tool: 'brush', selectedLayer: null, brushSize: 40, layers: [], @@ -197,34 +197,45 @@ export const regionalPromptsSlice = createSlice({ } }, rpLayerLineAdded: { - reducer: (state, action: PayloadAction<[number, number, number, number], string, { uuid: string }>) => { - const layer = state.layers.find((l) => l.id === state.selectedLayer); + reducer: ( + state, + action: PayloadAction< + { layerId: string; points: [number, number, number, number]; tool: DrawingTool }, + string, + { uuid: string } + > + ) => { + const { layerId, points, tool } = action.payload; + const layer = state.layers.find((l) => l.id === layerId); if (isRPLayer(layer)) { const lineId = getRPLayerLineId(layer.id, action.meta.uuid); layer.objects.push({ kind: 'line', - tool: state.tool, + tool: tool, id: lineId, - points: [ - action.payload[0] - layer.x, - action.payload[1] - layer.y, - action.payload[2] - layer.x, - action.payload[3] - layer.y, - ], + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener + points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], strokeWidth: state.brushSize, }); } }, - prepare: (payload: [number, number, number, number]) => ({ payload, meta: { uuid: uuidv4() } }), + prepare: (payload: { layerId: string; points: [number, number, number, number]; tool: DrawingTool }) => ({ + payload, + meta: { uuid: uuidv4() }, + }), }, - rpLayerPointsAdded: (state, action: PayloadAction<[number, number]>) => { - const layer = state.layers.find((l) => l.id === state.selectedLayer); + rpLayerPointsAdded: (state, action: PayloadAction<{ layerId: string; point: [number, number] }>) => { + const { layerId, point } = action.payload; + const layer = state.layers.find((l) => l.id === layerId); if (isRPLayer(layer)) { const lastLine = layer.objects.findLast(isLine); if (!lastLine) { return; } - lastLine.points.push(action.payload[0] - layer.x, action.payload[1] - layer.y); + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener + lastLine.points.push(point[0] - layer.x, point[1] - layer.y); } }, rpLayerAutoNegativeChanged: ( @@ -242,9 +253,6 @@ export const regionalPromptsSlice = createSlice({ brushSizeChanged: (state, action: PayloadAction) => { state.brushSize = action.payload; }, - toolChanged: (state, action: PayloadAction) => { - state.tool = action.payload; - }, promptLayerOpacityChanged: (state, action: PayloadAction) => { state.promptLayerOpacity = action.payload; }, @@ -304,7 +312,6 @@ export const { isEnabledChanged, brushSizeChanged, promptLayerOpacityChanged, - toolChanged, } = regionalPromptsSlice.actions; export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts; @@ -316,6 +323,7 @@ const migrateRegionalPromptsState = (state: any): any => { export const $isMouseDown = atom(false); export const $isMouseOver = atom(false); +export const $tool = atom('brush'); export const $cursorPosition = atom(null); // IDs for singleton layers and objects @@ -394,10 +402,6 @@ export const regionalPromptsUndoableConfig: UndoableOptions object.id; */ export const renderBrushPreview = ( stage: Konva.Stage, - tool: Tool, + tool: RPTool, color: RgbColor | null, cursorPos: Vector2d | null, brushSize: number @@ -130,7 +130,7 @@ const renderRPLayer = ( rpLayer: RegionalPromptLayer, rpLayerIndex: number, selectedLayerId: string | null, - tool: Tool, + tool: RPTool, layerOpacity: number, onLayerPosChanged?: (layerId: string, x: number, y: number) => void ) => { @@ -278,7 +278,7 @@ export const renderLayers = ( reduxLayers: Layer[], selectedLayerId: string | null, layerOpacity: number, - tool: Tool, + tool: RPTool, onLayerPosChanged?: (layerId: string, x: number, y: number) => void ) => { const reduxLayerIds = reduxLayers.map(mapId); @@ -312,7 +312,7 @@ const selectPromptLayerObjectGroup = (item: Node) => */ export const renderBbox = ( stage: Konva.Stage, - tool: Tool, + tool: RPTool, selectedLayerId: string | null, onBboxChanged: (layerId: string, bbox: IRect) => void ) => {