diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index d507a83894..72be76bf1c 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -8,6 +8,7 @@ import { deepClone } from 'common/util/deepClone'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; +import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice'; import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; @@ -59,6 +60,7 @@ const allReducers = { [upscaleSlice.name]: upscaleSlice.reducer, [stylePresetSlice.name]: stylePresetSlice.reducer, [paramsSlice.name]: paramsSlice.reducer, + [toolSlice.name]: toolSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -101,6 +103,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [upscalePersistConfig.name]: upscalePersistConfig, [stylePresetPersistConfig.name]: stylePresetPersistConfig, [paramsPersistConfig.name]: paramsPersistConfig, + [toolPersistConfig.name]: toolPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx index 1c9a4f174c..e4a8abd1b0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx @@ -1,6 +1,6 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { invertScrollChanged } from 'features/controlLayers/store/toolSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsInvertScrollCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); + const invertScroll = useAppSelector((s) => s.tool.invertScroll); const onChange = useCallback( (e: ChangeEvent) => dispatch(invertScrollChanged(e.target.checked)), [dispatch] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx index 20a5115531..0b91faf3be 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolBrushWidth.tsx @@ -10,7 +10,7 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { brushWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { brushWidthChanged } from 'features/controlLayers/store/toolSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`; export const ToolBrushWidth = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const width = useAppSelector((s) => s.canvasV2.tool.brush.width); + const width = useAppSelector((s) => s.tool.brush.width); const onChange = useCallback( (v: number) => { dispatch(brushWidthChanged(Math.round(v))); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx index e227e0bedd..8f7eb2ad4f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolEraserWidth.tsx @@ -10,7 +10,7 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { eraserWidthChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { eraserWidthChanged } from 'features/controlLayers/store/toolSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,7 +20,7 @@ const formatPx = (v: number | string) => `${v} px`; export const ToolEraserWidth = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const width = useAppSelector((s) => s.canvasV2.tool.eraser.width); + const width = useAppSelector((s) => s.tool.eraser.width); const onChange = useCallback( (v: number) => { dispatch(eraserWidthChanged(Math.round(v))); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx index 477e23bc59..69f2afec91 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx @@ -2,14 +2,14 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@inv import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIColorPicker from 'common/components/IAIColorPicker'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import { fillChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { fillChanged } from 'features/controlLayers/store/toolSlice'; import type { RgbaColor } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const ToolFillColorPicker = memo(() => { const { t } = useTranslation(); - const fill = useAppSelector((s) => s.canvasV2.tool.fill); + const fill = useAppSelector((s) => s.tool.fill); const dispatch = useAppDispatch(); const onChange = useCallback( (color: RgbaColor) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 47f803fbf9..50ef8faa47 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -11,7 +11,6 @@ import type { CanvasEntityIdentifier, CanvasEraserLineState, CanvasRasterLayerState, - CanvasV2State, Coordinate, Rect, } from 'features/controlLayers/store/types'; @@ -83,11 +82,7 @@ export class CanvasLayerAdapter extends CanvasModuleBase { this.konva.layer.destroy(); }; - update = async (arg?: { - state: CanvasLayerAdapter['state']; - toolState: CanvasV2State['tool']; - isSelected: boolean; - }) => { + update = async (arg?: { state: CanvasLayerAdapter['state'] }) => { const state = get(arg, 'state', this.state); if (!this.isFirstRender && state === this.state) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index b836eca30a..072a7a5d12 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -112,7 +112,7 @@ export class CanvasManager extends CanvasModuleBase { // These atoms require the canvas manager to be set up before we can provide their initial values this.stateApi.$transformingEntity.set(null); this.stateApi.$toolState.set(this.stateApi.getToolState()); - this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getState().selectedEntityIdentifier); + this.stateApi.$selectedEntityIdentifier.set(this.stateApi.getCanvasState().selectedEntityIdentifier); this.stateApi.$currentFill.set(this.stateApi.getCurrentFill()); this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity()); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index a0aa213615..cd0f16d73c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -11,7 +11,6 @@ import type { CanvasEraserLineState, CanvasInpaintMaskState, CanvasRegionalGuidanceState, - CanvasV2State, Coordinate, Rect, } from 'features/controlLayers/store/types'; @@ -83,11 +82,7 @@ export class CanvasMaskAdapter extends CanvasModuleBase { this.konva.layer.destroy(); }; - update = async (arg?: { - state: CanvasMaskAdapter['state']; - toolState: CanvasV2State['tool']; - isSelected: boolean; - }) => { + update = async (arg?: { state: CanvasMaskAdapter['state'] }) => { const state = get(arg, 'state', this.state); if (!this.isFirstRender && state === this.state && state.fill === this.state.fill) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index 69236633bb..3c25a795a3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -28,7 +28,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { } render = async () => { - const state = this.manager.stateApi.getState(); + const state = this.manager.stateApi.getCanvasState(); if (!this.state) { this.log.trace('First render'); @@ -51,7 +51,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { await this.renderStagingArea(state, prevState); this.arrangeEntities(state, prevState); - this.manager.stateApi.$toolState.set(state.tool); + this.manager.stateApi.$toolState.set(this.manager.stateApi.getToolState()); this.manager.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier); this.manager.stateApi.$selectedEntity.set(this.manager.stateApi.getSelectedEntity()); this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentFill()); @@ -96,11 +96,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ - state: entityState, - toolState: state.tool, - isSelected: state.selectedEntityIdentifier?.id === entityState.id, - }); + await adapter.update({ state: entityState }); } } }; @@ -129,11 +125,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ - state: entityState, - toolState: state.tool, - isSelected: state.selectedEntityIdentifier?.id === entityState.id, - }); + await adapter.update({ state: entityState }); } } }; @@ -167,11 +159,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ - state: entityState, - toolState: state.tool, - isSelected: state.selectedEntityIdentifier?.id === entityState.id, - }); + await adapter.update({ state: entityState }); } } }; @@ -205,11 +193,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { adapterMap.set(adapter.id, adapter); this.manager.stage.addLayer(adapter.konva.layer); } - await adapter.update({ - state: entityState, - toolState: state.tool, - isSelected: state.selectedEntityIdentifier?.id === entityState.id, - }); + await adapter.update({ state: entityState }); } } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 60671a7f98..edb650f504 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -7,7 +7,6 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase' import { getPrefixedId } from 'features/controlLayers/konva/util'; import { bboxChanged, - brushWidthChanged, entityBrushLineAdded, entityEraserLineAdded, entityMoved, @@ -15,17 +14,20 @@ import { entityRectAdded, entityReset, entitySelected, - eraserWidthChanged, - fillChanged, } from 'features/controlLayers/store/canvasV2Slice'; import { selectAllRenderableEntities } from 'features/controlLayers/store/selectors'; +import { + brushWidthChanged, + eraserWidthChanged, + fillChanged, + type ToolState, +} from 'features/controlLayers/store/toolSlice'; import type { CanvasControlLayerState, CanvasEntityIdentifier, CanvasInpaintMaskState, CanvasRasterLayerState, CanvasRegionalGuidanceState, - CanvasV2State, Coordinate, EntityBrushLineAddedPayload, EntityEraserLineAddedPayload, @@ -97,7 +99,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { } // Reminder - use arrow functions to avoid binding issues - getState = () => { + getCanvasState = () => { return this.store.getState().canvasV2; }; resetEntity = (arg: EntityIdentifierPayload) => { @@ -141,36 +143,36 @@ export class CanvasStateApiModule extends CanvasModuleBase { ); }; getBbox = () => { - return this.getState().bbox; + return this.getCanvasState().bbox; }; getToolState = () => { - return this.getState().tool; + return this.store.getState().tool; }; getSettings = () => { - return this.getState().settings; + return this.getCanvasState().settings; }; getRegionsState = () => { - return this.getState().regions; + return this.getCanvasState().regions; }; getRasterLayersState = () => { - return this.getState().rasterLayers; + return this.getCanvasState().rasterLayers; }; getControlLayersState = () => { - return this.getState().controlLayers; + return this.getCanvasState().controlLayers; }; getInpaintMasksState = () => { - return this.getState().inpaintMasks; + return this.getCanvasState().inpaintMasks; }; getSession = () => { - return this.getState().session; + return this.getCanvasState().session; }; getIsSelected = (id: string) => { - return this.getState().selectedEntityIdentifier?.id === id; + return this.getCanvasState().selectedEntityIdentifier?.id === id; }; getEntity(identifier: CanvasEntityIdentifier): EntityStateAndAdapter | null { - const state = this.getState(); + const state = this.getCanvasState(); let entityState: EntityStateAndAdapter['state'] | null = null; let entityAdapter: EntityStateAndAdapter['adapter'] | null = null; @@ -202,7 +204,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { } getRenderedEntityCount = () => { - const renderableEntities = selectAllRenderableEntities(this.getState()); + const renderableEntities = selectAllRenderableEntities(this.getCanvasState()); let count = 0; for (const entity of renderableEntities) { if (entity.isEnabled) { @@ -213,7 +215,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { }; getSelectedEntity = () => { - const state = this.getState(); + const state = this.getCanvasState(); if (state.selectedEntityIdentifier) { return this.getEntity(state.selectedEntityIdentifier); } @@ -221,8 +223,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { }; getCurrentFill = () => { - const state = this.getState(); - let currentFill: RgbaColor = state.tool.fill; + let currentFill: RgbaColor = this.getToolState().fill; const selectedEntity = this.getSelectedEntity(); if (selectedEntity) { // These two entity types use a compositing rect for opacity. Their fill is always a solid color. @@ -239,15 +240,14 @@ export class CanvasStateApiModule extends CanvasModuleBase { // The brush should use the mask opacity for these enktity types return { ...selectedEntity.state.fill.color, a: 1 }; } else { - const state = this.getState(); - return state.tool.fill; + return this.getToolState().fill; } }; $transformingEntity = atom(null); $isProcessingTransform = atom(false); - $toolState: WritableAtom = atom(); + $toolState: WritableAtom = atom(); $currentFill: WritableAtom = atom(); $selectedEntity: WritableAtom = atom(); $selectedEntityIdentifier: WritableAtom = atom(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index fb765db6b9..5134b1bf50 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -15,7 +15,6 @@ import { regionsReducers } from 'features/controlLayers/store/regionsReducers'; import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors'; import { sessionReducers } from 'features/controlLayers/store/sessionReducers'; import { settingsReducers } from 'features/controlLayers/store/settingsReducers'; -import { toolReducers } from 'features/controlLayers/store/toolReducers'; import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; @@ -57,16 +56,6 @@ const initialState: CanvasV2State = { }, loras: [], ipAdapters: { entities: [] }, - tool: { - invertScroll: false, - fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500 - brush: { - width: 50, - }, - eraser: { - width: 50, - }, - }, bbox: { rect: { x: 0, y: 0, width: 512, height: 512 }, optimalDimension: 512, @@ -109,7 +98,6 @@ export const canvasV2Slice = createSlice({ // move out ...lorasReducers, ...settingsReducers, - ...toolReducers, ...sessionReducers, entitySelected: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; @@ -364,7 +352,6 @@ export const canvasV2Slice = createSlice({ const size = pick(state.bbox.rect, 'width', 'height'); state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, state.bbox.optimalDimension); state.session = deepClone(initialState.session); - state.tool = deepClone(initialState.tool); state.ipAdapters = deepClone(initialState.ipAdapters); state.rasterLayers = deepClone(initialState.rasterLayers); @@ -403,10 +390,6 @@ export const canvasV2Slice = createSlice({ }); export const { - brushWidthChanged, - eraserWidthChanged, - fillChanged, - invertScrollChanged, clipToBboxChanged, canvasReset, settingsDynamicGridToggled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts deleted file mode 100644 index 74916f783b..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/toolReducers.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State, RgbaColor } from 'features/controlLayers/store/types'; - -export const toolReducers = { - brushWidthChanged: (state, action: PayloadAction) => { - state.tool.brush.width = Math.round(action.payload); - }, - eraserWidthChanged: (state, action: PayloadAction) => { - state.tool.eraser.width = Math.round(action.payload); - }, - fillChanged: (state, action: PayloadAction) => { - state.tool.fill = action.payload; - }, - invertScrollChanged: (state, action: PayloadAction) => { - state.tool.invertScroll = action.payload; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts new file mode 100644 index 0000000000..bbe70d3bca --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/toolSlice.ts @@ -0,0 +1,54 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PersistConfig } from 'app/store/store'; +import type { RgbaColor } from 'features/controlLayers/store/types'; + +export type ToolState = { + invertScroll: boolean; + brush: { width: number }; + eraser: { width: number }; + fill: RgbaColor; +}; + +const initialState: ToolState = { + invertScroll: false, + fill: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500 + brush: { + width: 50, + }, + eraser: { + width: 50, + }, +}; + +export const toolSlice = createSlice({ + name: 'tool', + initialState, + reducers: { + brushWidthChanged: (state, action: PayloadAction) => { + state.brush.width = Math.round(action.payload); + }, + eraserWidthChanged: (state, action: PayloadAction) => { + state.eraser.width = Math.round(action.payload); + }, + fillChanged: (state, action: PayloadAction) => { + state.fill = action.payload; + }, + invertScrollChanged: (state, action: PayloadAction) => { + state.invertScroll = action.payload; + }, + }, +}); + +export const { brushWidthChanged, eraserWidthChanged, fillChanged, invertScrollChanged } = toolSlice.actions; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const toolPersistConfig: PersistConfig = { + name: toolSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 628bda4c80..766cdf8b3a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -715,12 +715,6 @@ export type CanvasV2State = { entities: CanvasIPAdapterState[]; }; loras: LoRA[]; - tool: { - invertScroll: boolean; - brush: { width: number }; - eraser: { width: number }; - fill: RgbaColor; - }; settings: { imageSmoothing: boolean; showHUD: boolean;