diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 72be76bf1c..f7aee74dbc 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -6,6 +6,7 @@ import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; +import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice'; import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice'; @@ -61,6 +62,7 @@ const allReducers = { [stylePresetSlice.name]: stylePresetSlice.reducer, [paramsSlice.name]: paramsSlice.reducer, [toolSlice.name]: toolSlice.reducer, + [canvasSettingsSlice.name]: canvasSettingsSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -104,6 +106,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [stylePresetPersistConfig.name]: stylePresetPersistConfig, [paramsPersistConfig.name]: paramsPersistConfig, [toolPersistConfig.name]: toolPersistConfig, + [canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx index 0dfcae290a..ddfcfe8f3a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx @@ -1,13 +1,13 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { settingsAutoSaveToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { settingsAutoSaveToggled } from 'features/controlLayers/store/canvasSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const CanvasSettingsAutoSaveCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoSave = useAppSelector((s) => s.canvasV2.settings.autoSave); + const autoSave = useAppSelector((s) => s.canvasSettings.autoSave); const onChange = useCallback(() => dispatch(settingsAutoSaveToggled()), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx index 205c36070c..60b8cf4ff9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx @@ -1,6 +1,6 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { clipToBboxChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { clipToBboxChanged } from 'features/controlLayers/store/canvasSettingsSlice'; 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 CanvasSettingsClipToBboxCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const clipToBbox = useAppSelector((s) => s.canvasV2.settings.clipToBbox); + const clipToBbox = useAppSelector((s) => s.canvasSettings.clipToBbox); const onChange = useCallback( (e: ChangeEvent) => dispatch(clipToBboxChanged(e.target.checked)), [dispatch] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx index 71ffe1e729..f29b5ac16f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx @@ -1,13 +1,13 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { settingsDynamicGridToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { settingsDynamicGridToggled } from 'features/controlLayers/store/canvasSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const CanvasSettingsDynamicGridSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const dynamicGrid = useAppSelector((s) => s.canvasV2.settings.dynamicGrid); + const dynamicGrid = useAppSelector((s) => s.canvasSettings.dynamicGrid); const onChange = useCallback(() => { dispatch(settingsDynamicGridToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 81af6c00d4..f7679346c0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -52,7 +52,7 @@ type Props = { }; export const StageComponent = memo(({ asPreview = false }: Props) => { - const dynamicGrid = useAppSelector((s) => s.canvasV2.settings.dynamicGrid); + const dynamicGrid = useAppSelector((s) => s.canvasSettings.dynamicGrid); const [stage] = useState( () => diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts index 3c25a795a3..f62b275eb8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasRenderingModule.ts @@ -4,6 +4,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { CanvasSettingsState } from 'features/controlLayers/store/canvasSettingsSlice'; import type { CanvasV2State } from 'features/controlLayers/store/types'; import type { Logger } from 'roarr'; @@ -17,6 +18,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { subscriptions = new Set<() => void>(); state: CanvasV2State | null = null; + settings: CanvasSettingsState | null = null; constructor(manager: CanvasManager) { super(); @@ -29,20 +31,24 @@ export class CanvasRenderingModule extends CanvasModuleBase { render = async () => { const state = this.manager.stateApi.getCanvasState(); + const settings = this.manager.stateApi.getSettings(); - if (!this.state) { + if (!this.state || !this.settings) { this.log.trace('First render'); } const prevState = this.state; this.state = state; - if (prevState === state) { + const prevSettings = this.settings; + this.settings = settings; + + if (prevState === state && prevSettings === settings) { // No changes to state - no need to render return; } - this.renderBackground(state, prevState); + this.renderBackground(settings, prevSettings); await this.renderRasterLayers(state, prevState); await this.renderControlLayers(prevState, state); await this.renderRegionalGuidance(prevState, state); @@ -57,7 +63,7 @@ export class CanvasRenderingModule extends CanvasModuleBase { this.manager.stateApi.$currentFill.set(this.manager.stateApi.getCurrentFill()); // We have no prev state for the first render - if (!prevState) { + if (!prevState && !prevSettings) { this.manager.setCanvasManager(); } }; @@ -66,8 +72,8 @@ export class CanvasRenderingModule extends CanvasModuleBase { return { ...this.manager.getLoggingContext(), path: this.manager.path.join('.') }; }; - renderBackground = (state: CanvasV2State, prevState: CanvasV2State | null) => { - if (!prevState || state.settings.dynamicGrid !== prevState.settings.dynamicGrid) { + renderBackground = (settings: CanvasSettingsState, prevSettings: CanvasSettingsState | null) => { + if (!prevSettings || settings.dynamicGrid !== prevSettings.dynamicGrid) { this.manager.background.render(); } }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index edb650f504..1d5d28ef8c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -150,7 +150,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { return this.store.getState().tool; }; getSettings = () => { - return this.getCanvasState().settings; + return this.store.getState().canvasSettings; }; getRegionsState = () => { return this.getCanvasState().regions; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts new file mode 100644 index 0000000000..18e8340d33 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -0,0 +1,53 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PersistConfig } from 'app/store/store'; + +export type CanvasSettingsState = { + imageSmoothing: boolean; + showHUD: boolean; + autoSave: boolean; + preserveMaskedArea: boolean; + cropToBboxOnSave: boolean; + clipToBbox: boolean; + dynamicGrid: boolean; +}; + +const initialState: CanvasSettingsState = { + // TODO(psyche): These are copied from old canvas state, need to be implemented + autoSave: false, + imageSmoothing: true, + preserveMaskedArea: false, + showHUD: true, + clipToBbox: false, + cropToBboxOnSave: false, + dynamicGrid: false, +}; + +export const canvasSettingsSlice = createSlice({ + name: 'canvasSettings', + initialState, + reducers: { + clipToBboxChanged: (state, action: PayloadAction) => { + state.clipToBbox = action.payload; + }, + settingsDynamicGridToggled: (state) => { + state.dynamicGrid = !state.dynamicGrid; + }, + settingsAutoSaveToggled: (state) => { + state.autoSave = !state.autoSave; + }, + }, +}); + +export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled } = canvasSettingsSlice.actions; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const canvasSettingsPersistConfig: PersistConfig = { + name: canvasSettingsSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 5134b1bf50..b7667b58eb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -14,7 +14,6 @@ import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersR 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 { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify'; import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; @@ -66,16 +65,6 @@ const initialState: CanvasV2State = { height: 512, }, }, - settings: { - // TODO(psyche): These are copied from old canvas state, need to be implemented - autoSave: false, - imageSmoothing: true, - preserveMaskedArea: false, - showHUD: true, - clipToBbox: false, - cropToBboxOnSave: false, - dynamicGrid: false, - }, session: { mode: 'generate', isStaging: false, @@ -97,7 +86,6 @@ export const canvasV2Slice = createSlice({ ...bboxReducers, // move out ...lorasReducers, - ...settingsReducers, ...sessionReducers, entitySelected: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; @@ -390,10 +378,7 @@ export const canvasV2Slice = createSlice({ }); export const { - clipToBboxChanged, canvasReset, - settingsDynamicGridToggled, - settingsAutoSaveToggled, // All entities entitySelected, entityNameChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts deleted file mode 100644 index 324077f8eb..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; - -export const settingsReducers = { - clipToBboxChanged: (state, action: PayloadAction) => { - state.settings.clipToBbox = action.payload; - }, - settingsDynamicGridToggled: (state) => { - state.settings.dynamicGrid = !state.settings.dynamicGrid; - }, - settingsAutoSaveToggled: (state) => { - state.settings.autoSave = !state.settings.autoSave; - }, -} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 766cdf8b3a..7f20ee06f3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -715,15 +715,6 @@ export type CanvasV2State = { entities: CanvasIPAdapterState[]; }; loras: LoRA[]; - settings: { - imageSmoothing: boolean; - showHUD: boolean; - autoSave: boolean; - preserveMaskedArea: boolean; - cropToBboxOnSave: boolean; - clipToBbox: boolean; - dynamicGrid: boolean; - }; bbox: { rect: { x: number; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index 0eaa86c9e7..cf724b227e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -31,8 +31,8 @@ export const buildSD1Graph = async ( const generationMode = manager.compositor.getGenerationMode(); log.debug({ generationMode }, 'Building SD1/SD2 graph'); - const { canvasV2, params } = state; - const { bbox, session, settings } = canvasV2; + const { canvasV2, params, canvasSettings } = state; + const { bbox, session } = canvasV2; const { model, @@ -274,7 +274,7 @@ export const buildSD1Graph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = session.mode === 'generate' || settings.autoSave; + const shouldSaveToGallery = session.mode === 'generate' || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'), diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 35b7e40173..976e59527c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -31,8 +31,8 @@ export const buildSDXLGraph = async ( const generationMode = manager.compositor.getGenerationMode(); log.debug({ generationMode }, 'Building SDXL graph'); - const { params, canvasV2 } = state; - const { bbox, session, settings } = canvasV2; + const { params, canvasV2, canvasSettings } = state; + const { bbox, session } = canvasV2; const { model, @@ -277,7 +277,7 @@ export const buildSDXLGraph = async ( canvasOutput = addWatermarker(g, canvasOutput); } - const shouldSaveToGallery = session.mode === 'generate' || settings.autoSave; + const shouldSaveToGallery = session.mode === 'generate' || canvasSettings.autoSave; g.updateNode(canvasOutput, { id: getPrefixedId('canvas_output'),