diff --git a/invokeai/frontend/web/.storybook/ReduxInit.tsx b/invokeai/frontend/web/.storybook/ReduxInit.tsx index b4989c75564..b284c6dff9c 100644 --- a/invokeai/frontend/web/.storybook/ReduxInit.tsx +++ b/invokeai/frontend/web/.storybook/ReduxInit.tsx @@ -3,7 +3,7 @@ import type { PropsWithChildren } from 'react'; import { memo, useEffect } from 'react'; import { useAppDispatch } from '../src/app/store/storeHooks'; -import { modelChanged } from '../src/features/controlLayers/store/paramsSlice'; +import { modelChanged } from 'features/controlLayers/store/actions'; /** * Initializes some state for storybook. Must be in a different component * so that it is run inside the redux context. @@ -13,7 +13,9 @@ export const ReduxInit = memo(({ children }: PropsWithChildren) => { useGlobalModifiersInit(); useEffect(() => { dispatch( - modelChanged({ model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' } }) + modelChanged({ + model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' }, + }) ); }, [dispatch]); diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index e7a533e2ae8..e3192c0e72a 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -48,7 +48,7 @@ "@invoke-ai/ui-library": "^0.0.47", "@nanostores/react": "^1.0.0", "@observ33r/object-equals": "^1.1.5", - "@reduxjs/toolkit": "2.8.2", + "@reduxjs/toolkit": "2.9.0", "@roarr/browser-log-writer": "^1.3.0", "@xyflow/react": "^12.8.2", "ag-psd": "^28.2.2", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index e80b7011165..99e0eeca83a 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^1.1.5 version: 1.1.5 '@reduxjs/toolkit': - specifier: 2.8.2 - version: 2.8.2(react-redux@9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + specifier: 2.9.0 + version: 2.9.0(react-redux@9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1))(react@18.3.1) '@roarr/browser-log-writer': specifier: ^1.3.0 version: 1.3.0 @@ -1249,8 +1249,8 @@ packages: resolution: {integrity: sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==} engines: {node: '>=18.17.0', npm: '>=9.5.0'} - '@reduxjs/toolkit@2.8.2': - resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==} + '@reduxjs/toolkit@2.9.0': + resolution: {integrity: sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 || ^19 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -5683,7 +5683,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + '@reduxjs/toolkit@2.9.0(react-redux@9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': dependencies: '@standard-schema/spec': 1.0.0 '@standard-schema/utils': 0.3.0 diff --git a/invokeai/frontend/web/src/app/store/middleware/actionContextMiddleware.ts b/invokeai/frontend/web/src/app/store/middleware/actionContextMiddleware.ts new file mode 100644 index 00000000000..6bad65e5893 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/actionContextMiddleware.ts @@ -0,0 +1,23 @@ +import type { Middleware, UnknownAction } from '@reduxjs/toolkit'; +import { injectTabActionContext } from 'app/store/util'; +import { isCanvasInstanceAction } from 'features/controlLayers/store/canvasSlice'; +import { selectActiveCanvasId, selectActiveTab } from 'features/controlLayers/store/selectors'; +import { isTabInstanceParamsAction } from 'features/controlLayers/store/tabSlice'; + +export const actionContextMiddleware: Middleware = (store) => (next) => (action) => { + const currentAction = action as UnknownAction; + + if (isTabActionContextRequired(currentAction)) { + const state = store.getState(); + const tab = selectActiveTab(state); + const canvasId = tab === 'canvas' ? selectActiveCanvasId(state) : undefined; + + injectTabActionContext(currentAction, tab, canvasId); + } + + return next(action); +}; + +const isTabActionContextRequired = (action: UnknownAction) => { + return isTabInstanceParamsAction(action) || isCanvasInstanceAction(action); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts index 4c8f139779a..2c59068a56f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts @@ -1,14 +1,15 @@ import type { AppStartListening } from 'app/store/store'; -import { setInfillMethod } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams, setInfillMethod } from 'features/controlLayers/store/paramsSlice'; import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged } from 'features/system/store/systemSlice'; import { appInfoApi } from 'services/api/endpoints/appInfo'; export const addAppConfigReceivedListener = (startAppListening: AppStartListening) => { startAppListening({ matcher: appInfoApi.endpoints.getAppConfig.matchFulfilled, - effect: (action, { getState, dispatch }) => { + effect: (action, api) => { + const { getState, dispatch } = api; const { infill_methods = [], nsfw_methods = [], watermarking_methods = [] } = action.payload; - const infillMethod = getState().params.infillMethod; + const infillMethod = selectActiveTabParams(getState()).infillMethod; if (!infill_methods.includes(infillMethod)) { // If the selected infill method does not exist, prefer 'lama' if it's in the list, otherwise 'tile'. diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index d185a03f220..b96b8dbb2af 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -1,6 +1,6 @@ import type { AppStartListening } from 'app/store/store'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectCanvases } from 'features/controlLayers/store/selectors'; import { getImageUsage } from 'features/deleteImageModal/store/state'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { selectNodesSlice } from 'features/nodes/store/selectors'; @@ -19,12 +19,12 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS const state = getState(); const nodes = selectNodesSlice(state); - const canvas = selectCanvasSlice(state); + const canvases = selectCanvases(state); const upscale = selectUpscaleSlice(state); const refImages = selectRefImagesSlice(state); deleted_images.forEach((image_name) => { - const imageUsage = getImageUsage(nodes, canvas, upscale, refImages, image_name); + const imageUsage = getImageUsage(nodes, canvases, upscale, refImages, image_name); if (imageUsage.isNodesImage && !wasNodeEditorReset) { dispatch(nodeEditorReset()); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index 41b2eb509e5..ee3ac026ae9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,14 +1,18 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/store'; +import { modelChanged } from 'features/controlLayers/store/actions'; import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice'; -import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; -import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice'; +import { + buildSelectIsStagingBySessionId, + selectActiveCanvasStagingAreaSessionId, +} from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { loraIsEnabledChanged, selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; +import { selectActiveTabParams, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice'; import { + selectActiveCanvas, selectAllEntitiesOfType, selectBboxModelBase, - selectCanvasSlice, } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { modelSelected } from 'features/parameters/store/actions'; @@ -31,7 +35,8 @@ const log = logger('models'); export const addModelSelectedListener = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: modelSelected, - effect: (action, { getState, dispatch }) => { + effect: (action, api) => { + const { getState, dispatch } = api; const state = getState(); const result = zParameterModel.safeParse(action.payload); @@ -42,14 +47,15 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = const newModel = result.data; const newBase = newModel.base; - const didBaseModelChange = state.params.model?.base !== newBase; + const params = selectActiveTabParams(state); + const didBaseModelChange = params.model?.base !== newBase; if (didBaseModelChange) { // we may need to reset some incompatible submodels let modelsUpdatedDisabledOrCleared = 0; // handle incompatible loras - state.loras.loras.forEach((lora) => { + selectAddedLoRAs(state).forEach((lora) => { if (lora.model.base !== newBase) { dispatch(loraIsEnabledChanged({ id: lora.id, isEnabled: false })); modelsUpdatedDisabledOrCleared += 1; @@ -57,7 +63,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = }); // handle incompatible vae - const { vae } = state.params; + const { vae } = params; if (vae && vae.base !== newBase) { dispatch(vaeSelected(null)); modelsUpdatedDisabledOrCleared += 1; @@ -118,7 +124,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = const newRegionalRefImageModel = selectRegionalRefImageModels(state)[0] ?? null; // All regional guidance entities are updated to use the same new model. - const canvasState = selectCanvasSlice(state); + const canvasState = selectActiveCanvas(state); const canvasRegionalGuidanceEntities = selectAllEntitiesOfType(canvasState, 'regional_guidance'); for (const entity of canvasRegionalGuidanceEntities) { for (const refImage of entity.referenceImages) { @@ -152,14 +158,16 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } } - dispatch(modelChanged({ model: newModel, previousModel: state.params.model })); + dispatch(modelChanged({ model: newModel, previousModel: params.model })); const modelBase = selectBboxModelBase(state); - if (modelBase !== state.params.model?.base) { + if (modelBase !== params.model?.base) { // Sync generate tab settings whenever the model base changes dispatch(syncedToOptimalDimension()); - const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state); + const sessionId = selectActiveCanvasStagingAreaSessionId(state); + const selectIsStaging = buildSelectIsStagingBySessionId(sessionId); + const isStaging = selectIsStaging(state); if (!isStaging) { // Canvas tab only syncs if not staging dispatch(bboxSyncedToOptimalDimension()); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 62f398b5ed8..b20a8255a08 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -1,17 +1,18 @@ import { logger } from 'app/logging/logger'; import type { AppDispatch, AppStartListening, RootState } from 'app/store/store'; +import { modelChanged } from 'features/controlLayers/store/actions'; import { controlLayerModelChanged, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice'; -import { loraDeleted } from 'features/controlLayers/store/lorasSlice'; +import { loraDeleted, selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; import { clipEmbedModelSelected, fluxVAESelected, - modelChanged, refinerModelChanged, + selectActiveTabParams, t5EncoderModelSelected, vaeSelected, } from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier, isFLUXReduxConfig, @@ -103,7 +104,7 @@ type ModelHandler = ( ) => undefined; const handleMainModels: ModelHandler = (models, state, dispatch, log) => { - const selectedMainModel = state.params.model; + const selectedMainModel = selectActiveTabParams(state).model; const allMainModels = models.filter(isNonRefinerMainModelConfig).sort((a) => (a.base === 'sdxl' ? -1 : 1)); const firstModel = allMainModels[0]; @@ -144,7 +145,7 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { }; const handleRefinerModels: ModelHandler = (models, state, dispatch, log) => { - const selectedRefinerModel = state.params.refinerModel; + const selectedRefinerModel = selectActiveTabParams(state).refinerModel; // `null` is a valid refiner model - no need to do anything. if (selectedRefinerModel === null) { @@ -168,7 +169,7 @@ const handleRefinerModels: ModelHandler = (models, state, dispatch, log) => { }; const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { - const selectedVAEModel = state.params.vae; + const selectedVAEModel = selectActiveTabParams(state).vae; // `null` is a valid VAE - it means "use the VAE baked into the currently-selected main model" if (selectedVAEModel === null) { @@ -193,7 +194,7 @@ const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { const handleLoRAModels: ModelHandler = (models, state, dispatch, log) => { const loraModels = models.filter(isLoRAModelConfig); - state.loras.loras.forEach((lora) => { + selectAddedLoRAs(state).forEach((lora) => { const isLoRAAvailable = loraModels.some((m) => m.key === lora.model.key); if (isLoRAAvailable) { return; @@ -221,7 +222,7 @@ const handleVideoModels: ModelHandler = (models, state, dispatch, log) => { const handleControlAdapterModels: ModelHandler = (models, state, dispatch, log) => { const caModels = models.filter(isControlLayerModelConfig); - selectCanvasSlice(state).controlLayers.entities.forEach((entity) => { + selectActiveCanvas(state).controlLayers.entities.forEach((entity) => { const selectedControlAdapterModel = entity.controlAdapter.model; // `null` is a valid control adapter model - no need to do anything. if (!selectedControlAdapterModel) { @@ -256,7 +257,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => { dispatch(refImageModelChanged({ id: entity.id, modelConfig: null })); }); - selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => { + selectActiveCanvas(state).regionalGuidance.entities.forEach((entity) => { entity.referenceImages.forEach(({ id: referenceImageId, config }) => { if (!isRegionalGuidanceIPAdapterConfig(config)) { return; @@ -299,7 +300,7 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => { dispatch(refImageModelChanged({ id: entity.id, modelConfig: null })); }); - selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => { + selectActiveCanvas(state).regionalGuidance.entities.forEach((entity) => { entity.referenceImages.forEach(({ id: referenceImageId, config }) => { if (!isRegionalGuidanceFLUXReduxConfig(config)) { return; @@ -417,7 +418,7 @@ const handleTileControlNetModel: ModelHandler = (models, state, dispatch, log) = }; const handleT5EncoderModels: ModelHandler = (models, state, dispatch, log) => { - const selectedT5EncoderModel = state.params.t5EncoderModel; + const selectedT5EncoderModel = selectActiveTabParams(state).t5EncoderModel; const t5EncoderModels = models.filter((m) => isT5EncoderModelConfig(m)); // If the currently selected model is available, we don't need to do anything @@ -445,7 +446,7 @@ const handleT5EncoderModels: ModelHandler = (models, state, dispatch, log) => { }; const handleCLIPEmbedModels: ModelHandler = (models, state, dispatch, log) => { - const selectedCLIPEmbedModel = state.params.clipEmbedModel; + const selectedCLIPEmbedModel = selectActiveTabParams(state).clipEmbedModel; const CLIPEmbedModels = models.filter((m) => isCLIPEmbedModelConfig(m)); // If the currently selected model is available, we don't need to do anything @@ -473,7 +474,7 @@ const handleCLIPEmbedModels: ModelHandler = (models, state, dispatch, log) => { }; const handleFLUXVAEModels: ModelHandler = (models, state, dispatch, log) => { - const selectedFLUXVAEModel = state.params.fluxVAE; + const selectedFLUXVAEModel = selectActiveTabParams(state).fluxVAE; const fluxVAEModels = models.filter((m) => isFluxVAEModelConfig(m)); // If the currently selected model is available, we don't need to do anything diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index f568bfe10c4..8842b51a34d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -1,9 +1,13 @@ import type { AppStartListening } from 'app/store/store'; import { isNil } from 'es-toolkit'; import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; -import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { + buildSelectIsStagingBySessionId, + selectActiveCanvasStagingAreaSessionId, +} from 'features/controlLayers/store/canvasStagingAreaSlice'; import { heightChanged, + selectActiveTabParams, setCfgRescaleMultiplier, setCfgScale, setGuidance, @@ -13,6 +17,7 @@ import { vaeSelected, widthChanged, } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { setDefaultSettings } from 'features/parameters/store/actions'; import { isParameterCFGRescaleMultiplier, @@ -26,7 +31,6 @@ import { zParameterVAEModel, } from 'features/parameters/types/parameterSchemas'; import { toast } from 'features/toast/toast'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { t } from 'i18next'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import { isNonRefinerMainModelConfig } from 'services/api/types'; @@ -34,10 +38,11 @@ import { isNonRefinerMainModelConfig } from 'services/api/types'; export const addSetDefaultSettingsListener = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: setDefaultSettings, - effect: async (action, { dispatch, getState }) => { + effect: async (action, api) => { + const { dispatch, getState } = api; const state = getState(); - const currentModel = state.params.model; + const currentModel = selectActiveTabParams(state).model; if (!currentModel) { return; @@ -115,7 +120,9 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni } const setSizeOptions = { updateAspectRatio: true, clamp: true }; - const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state); + const sessionId = selectActiveCanvasStagingAreaSessionId(state); + const selectIsStaging = buildSelectIsStagingBySessionId(sessionId); + const isStaging = selectIsStaging(state); const activeTab = selectActiveTab(getState()); if (activeTab === 'generate') { diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 12fcfa5a406..492a0565afa 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -17,20 +17,13 @@ import { addModelSelectedListener } from 'app/store/middleware/listenerMiddlewar import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded'; import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings'; import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected'; -import { deepClone } from 'common/util/deepClone'; -import { merge } from 'es-toolkit'; -import { omit, pick } from 'es-toolkit/compat'; import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/slice'; -import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice'; import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice'; -import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; -import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice'; -import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice'; +import { tabSliceConfig } from 'features/controlLayers/store/tabSlice'; import { dynamicPromptsSliceConfig } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { gallerySliceConfig } from 'features/gallery/store/gallerySlice'; import { modelManagerSliceConfig } from 'features/modelManagerV2/store/modelManagerV2Slice'; -import { nodesSliceConfig } from 'features/nodes/store/nodesSlice'; +import { nodesSliceConfig, undoableNodesSliceReducer } from 'features/nodes/store/nodesSlice'; import { workflowLibrarySliceConfig } from 'features/nodes/store/workflowLibrarySlice'; import { workflowSettingsSliceConfig } from 'features/nodes/store/workflowSettingsSlice'; import { upscaleSliceConfig } from 'features/parameters/store/upscaleSlice'; @@ -44,13 +37,13 @@ import { diff } from 'jsondiffpatch'; import dynamicMiddlewares from 'redux-dynamic-middlewares'; import type { SerializeFunction, UnserializeFunction } from 'redux-remember'; import { REMEMBER_REHYDRATED, rememberEnhancer, rememberReducer } from 'redux-remember'; -import undoable, { newHistory } from 'redux-undo'; import { serializeError } from 'serialize-error'; import { api } from 'services/api'; import { authToastMiddleware } from 'services/api/authToastMiddleware'; import type { JsonObject } from 'type-fest'; import { reduxRememberDriver } from './enhancers/reduxRemember/driver'; +import { actionContextMiddleware } from './middleware/actionContextMiddleware'; import { actionSanitizer } from './middleware/devtools/actionSanitizer'; import { actionsDenylist } from './middleware/devtools/actionsDenylist'; import { stateSanitizer } from './middleware/devtools/stateSanitizer'; @@ -63,19 +56,15 @@ const log = logger('system'); // When adding a slice, add the config to the SLICE_CONFIGS object below, then add the reducer to ALL_REDUCERS. const SLICE_CONFIGS = { - [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig, - [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig, [canvasSliceConfig.slice.reducerPath]: canvasSliceConfig, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig, [configSliceConfig.slice.reducerPath]: configSliceConfig, [dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig, [gallerySliceConfig.slice.reducerPath]: gallerySliceConfig, - [lorasSliceConfig.slice.reducerPath]: lorasSliceConfig, + [tabSliceConfig.slice.reducerPath]: tabSliceConfig, [modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig, [nodesSliceConfig.slice.reducerPath]: nodesSliceConfig, - [paramsSliceConfig.slice.reducerPath]: paramsSliceConfig, [queueSliceConfig.slice.reducerPath]: queueSliceConfig, - [refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig, [stylePresetSliceConfig.slice.reducerPath]: stylePresetSliceConfig, [systemSliceConfig.slice.reducerPath]: systemSliceConfig, [uiSliceConfig.slice.reducerPath]: uiSliceConfig, @@ -89,27 +78,15 @@ const SLICE_CONFIGS = { // Remember to wrap undoable reducers in `undoable()`! const ALL_REDUCERS = { [api.reducerPath]: api.reducer, - [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer, - [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer, - // Undoable! - [canvasSliceConfig.slice.reducerPath]: undoable( - canvasSliceConfig.slice.reducer, - canvasSliceConfig.undoableConfig?.reduxUndoOptions - ), + [canvasSliceConfig.slice.reducerPath]: canvasSliceConfig.slice.reducer, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer, [configSliceConfig.slice.reducerPath]: configSliceConfig.slice.reducer, [dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer, [gallerySliceConfig.slice.reducerPath]: gallerySliceConfig.slice.reducer, - [lorasSliceConfig.slice.reducerPath]: lorasSliceConfig.slice.reducer, + [tabSliceConfig.slice.reducerPath]: tabSliceConfig.slice.reducer, [modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig.slice.reducer, - // Undoable! - [nodesSliceConfig.slice.reducerPath]: undoable( - nodesSliceConfig.slice.reducer, - nodesSliceConfig.undoableConfig?.reduxUndoOptions - ), - [paramsSliceConfig.slice.reducerPath]: paramsSliceConfig.slice.reducer, + [nodesSliceConfig.slice.reducerPath]: undoableNodesSliceReducer, [queueSliceConfig.slice.reducerPath]: queueSliceConfig.slice.reducer, - [refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig.slice.reducer, [stylePresetSliceConfig.slice.reducerPath]: stylePresetSliceConfig.slice.reducer, [systemSliceConfig.slice.reducerPath]: systemSliceConfig.slice.reducer, [uiSliceConfig.slice.reducerPath]: uiSliceConfig.slice.reducer, @@ -128,24 +105,17 @@ const unserialize: UnserializeFunction = (data, key) => { if (!sliceConfig?.persistConfig) { throw new Error(`No persist config for slice "${key}"`); } - const { getInitialState, persistConfig, undoableConfig } = sliceConfig; + const { getInitialState, persistConfig } = sliceConfig; let state; try { - const initialState = getInitialState(); - const parsed = JSON.parse(data); - - // We need to inject non-persisted values from initial state into the rehydrated state. These values always are - // required to be in the state, but won't be in the persisted data. Build an object that consists of only these - // values, then merge it with the rehydrated state. - const nonPersistedSubsetOfState = pick(initialState, persistConfig.persistDenylist ?? []); - const stateToMigrate = merge(deepClone(parsed), nonPersistedSubsetOfState); + const parsedState = JSON.parse(data); // Run migrations to bring old state up to date with the current version. - const migrated = persistConfig.migrate(stateToMigrate); + const migrated = persistConfig.migrate(parsedState); log.debug( { - persistedData: parsed as JsonObject, + persistedData: parsedState as JsonObject, rehydratedData: migrated as JsonObject, diff: diff(data, migrated) as JsonObject, }, @@ -160,12 +130,7 @@ const unserialize: UnserializeFunction = (data, key) => { state = getInitialState(); } - // Undoable slices must be wrapped in a history! - if (undoableConfig) { - return newHistory([], state, []); - } else { - return state; - } + return persistConfig.deserialize ? persistConfig.deserialize(state) : state; }; const serialize: SerializeFunction = (data, key) => { @@ -174,12 +139,9 @@ const serialize: SerializeFunction = (data, key) => { throw new Error(`No persist config for slice "${key}"`); } - const result = omit( - sliceConfig.undoableConfig ? data.present : data, - sliceConfig.persistConfig.persistDenylist ?? [] - ); + const state = sliceConfig.persistConfig.serialize ? sliceConfig.persistConfig.serialize(data) : data; - return JSON.stringify(result); + return JSON.stringify(state); }; const PERSISTED_KEYS = Object.values(SLICE_CONFIGS) @@ -199,6 +161,7 @@ export const createStore = (options?: { persist?: boolean; persistDebounce?: num .concat(api.middleware) .concat(dynamicMiddlewares) .concat(authToastMiddleware) + .concat(actionContextMiddleware) // .concat(getDebugLoggerMiddleware({ withDiff: true, withNextState: true })) .prepend(listenerMiddleware.middleware), enhancers: (getDefaultEnhancers) => { diff --git a/invokeai/frontend/web/src/app/store/types.ts b/invokeai/frontend/web/src/app/store/types.ts index 28b28e1889b..adf2d7480f7 100644 --- a/invokeai/frontend/web/src/app/store/types.ts +++ b/invokeai/frontend/web/src/app/store/types.ts @@ -1,10 +1,10 @@ import type { Slice } from '@reduxjs/toolkit'; -import type { UndoableOptions } from 'redux-undo'; import type { ZodType } from 'zod'; type StateFromSlice = T extends Slice ? U : never; +export type SerializedStateFromDenyList = Omit; -export type SliceConfig = { +export type SliceConfig, TSerializedState = StateFromSlice> = { /** * The redux slice (return of createSlice). */ @@ -16,7 +16,7 @@ export type SliceConfig = { /** * A function that returns the initial state of the slice. */ - getInitialState: () => StateFromSlice; + getInitialState: () => TSerializedState; /** * The optional persist configuration for this slice. If omitted, the slice will not be persisted. */ @@ -28,19 +28,20 @@ export type SliceConfig = { * @param state The rehydrated state. * @returns A correctly-shaped state. */ - migrate: (state: unknown) => StateFromSlice; + migrate: (state: unknown) => TSerializedState; /** - * Keys to omit from the persisted state. + * Serializes the state + * + * @param state The internal state + * @returns The serialized state */ - persistDenylist?: (keyof StateFromSlice)[]; - }; - /** - * The optional undoable configuration for this slice. If omitted, the slice will not be undoable. - */ - undoableConfig?: { + serialize?: (state: TInternalState) => TSerializedState; /** - * The options to be passed into redux-undo. + * Deserializes the state + * + * @param state The serialized state + * @returns The internal state */ - reduxUndoOptions: UndoableOptions>; + deserialize?: (state: unknown) => TInternalState; }; }; diff --git a/invokeai/frontend/web/src/app/store/util.ts b/invokeai/frontend/web/src/app/store/util.ts new file mode 100644 index 00000000000..8c7df447672 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/util.ts @@ -0,0 +1,29 @@ +import type { UnknownAction } from '@reduxjs/toolkit'; +import type { TabName } from 'features/controlLayers/store/types'; + +const TAB_KEY = Symbol('tab'); +const CANVAS_ID_KEY = Symbol('canvasId'); + +type TabActionContext = { + [TAB_KEY]: TabName; + [CANVAS_ID_KEY]?: string; +}; + +export const injectTabActionContext = (action: UnknownAction, tab: TabName, canvasId?: string) => { + const context: TabActionContext = canvasId ? { [TAB_KEY]: tab, [CANVAS_ID_KEY]: canvasId } : { [TAB_KEY]: tab }; + Object.assign(action, { meta: context }); +}; + +export const extractTabActionContext = (action: UnknownAction & { meta?: Partial }) => { + const tab = action.meta?.[TAB_KEY]; + const canvasId = action.meta?.[CANVAS_ID_KEY]; + + if (!tab || (tab === 'canvas' && !canvasId)) { + return undefined; + } + + return { + tab, + canvasId, + }; +}; diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index b24f83a1b15..45e00d70b35 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -1,6 +1,6 @@ import { zFilterType } from 'features/controlLayers/store/filters'; +import { zTabName } from 'features/controlLayers/store/types'; import { zParameterPrecision, zParameterScheduler } from 'features/parameters/types/parameterSchemas'; -import { zTabName } from 'features/ui/store/uiTypes'; import type { PartialDeep } from 'type-fest'; import z from 'zod'; diff --git a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx index 0018a78622c..20f4fa6bc10 100644 --- a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx +++ b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx @@ -1,9 +1,9 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { allEntitiesDeleted, inpaintMaskAdded } from 'features/controlLayers/store/canvasSlice'; -import { $canvasManager } from 'features/controlLayers/store/ephemeral'; import { paramsReset } from 'features/controlLayers/store/paramsSlice'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsCounterClockwiseBold } from 'react-icons/pi'; @@ -12,12 +12,13 @@ export const SessionMenuItems = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const tab = useAppSelector(selectActiveTab); + const canvasManager = useCanvasManagerSafe(); const resetCanvasLayers = useCallback(() => { dispatch(allEntitiesDeleted()); dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true })); - $canvasManager.get()?.stage.fitBboxToStage(); - }, [dispatch]); + canvasManager?.stage.fitBboxToStage(); + }, [dispatch, canvasManager]); const resetGenerationSettings = useCallback(() => { dispatch(paramsReset()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index a7f8c812af2..560765f9c91 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import type { GroupBase } from 'chakra-react-select'; import { groupBy, reduce } from 'es-toolkit/compat'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import type { ModelIdentifierField } from 'features/nodes/types/common'; import { selectSystemShouldEnableModelDescriptions } from 'features/system/store/systemSlice'; import { useCallback, useMemo } from 'react'; @@ -31,7 +31,7 @@ const groupByBaseFunc = (model: T) => model.base.toUpp const groupByBaseAndTypeFunc = (model: T) => `${model.base.toUpperCase()} / ${model.type.replaceAll('_', ' ').toUpperCase()}`; -const selectBaseWithSDXLFallback = createSelector(selectParamsSlice, (params) => params.model?.base ?? 'sdxl'); +const selectBaseWithSDXLFallback = createSelector(selectActiveTabParams, (params) => params.model?.base ?? 'sdxl'); export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask.tsx index 7178c3d123b..b0c77b02860 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasAlertsPreserveMask = memo(() => { const { t } = useTranslation(); - const preserveMask = useAppSelector(selectPreserveMask); + const preserveMask = useAppSelector((state) => selectPreserveMask(state)); if (!preserveMask) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx index 5a4da84bfe1..6112e02a397 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasAlertsSaveAllImagesToGallery = memo(() => { const { t } = useTranslation(); - const saveAllImagesToGallery = useAppSelector(selectSaveAllImagesToGallery); + const saveAllImagesToGallery = useAppSelector((state) => selectSaveAllImagesToGallery(state)); if (!saveAllImagesToGallery) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx index c7ec2151a3b..761d245b6cf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx @@ -9,7 +9,7 @@ import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle'; import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden'; import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; import { - selectCanvasSlice, + selectActiveCanvas, selectEntityOrThrow, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; @@ -32,13 +32,13 @@ type AlertData = { const buildSelectIsEnabled = (entityIdentifier: CanvasEntityIdentifier) => createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isEnabled ); const buildSelectIsLocked = (entityIdentifier: CanvasEntityIdentifier) => createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isLocked ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAutoProcessSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAutoProcessSwitch.tsx index 7137fb3b6de..dc96a3fb22e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAutoProcessSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAutoProcessSwitch.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasAutoProcessSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoProcess = useAppSelector(selectAutoProcess); + const autoProcess = useAppSelector((state) => selectAutoProcess(state)); const onChange = useCallback(() => { dispatch(settingsAutoProcessToggled()); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx index 7e38cdd9fda..d446ecd0997 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx @@ -20,7 +20,7 @@ import { clamp, round } from 'es-toolkit/compat'; import { snapToNearest } from 'features/controlLayers/konva/util'; import { entityOpacityChanged } from 'features/controlLayers/store/canvasSlice'; import { - selectCanvasSlice, + selectActiveCanvas, selectEntity, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; @@ -61,7 +61,7 @@ const sliderDefaultValue = mapRawValueToSliderValue(1); const snapCandidates = marks.slice(1, marks.length - 1); -const selectOpacity = createSelector(selectCanvasSlice, (canvas) => { +const selectOpacity = createSelector(selectActiveCanvas, (canvas) => { const selectedEntityIdentifier = canvas.selectedEntityIdentifier; if (!selectedEntityIdentifier) { return 1; // fallback to 100% opacity diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch.tsx index 13a15363486..bc160b0c234 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasOperationIsolatedLayerPreviewSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isolatedLayerPreview = useAppSelector(selectIsolatedLayerPreview); + const isolatedLayerPreview = useAppSelector((state) => selectIsolatedLayerPreview(state)); const onChangeIsolatedPreview = useCallback(() => { dispatch(settingsIsolatedLayerPreviewToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx index 1f295d4ef40..e44b4b33a23 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx @@ -3,14 +3,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const buildSelectWithTransparencyEffect = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerBadgesContent').withTransparencyEffect ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx index 953638fad4c..804a991b7ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx @@ -19,7 +19,7 @@ import { } from 'features/controlLayers/store/canvasSlice'; import { getFilterForModel } from 'features/controlLayers/store/filters'; import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier, ControlModeV2 } from 'features/controlLayers/store/types'; import { replaceCanvasEntityObjectsWithImage } from 'features/imageActions/actions'; import { memo, useCallback, useMemo } from 'react'; @@ -33,7 +33,7 @@ import type { } from 'services/api/types'; const buildSelectControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { const layer = selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerControlAdapter'); return layer.controlAdapter; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx index a353ee59f19..ea62bfd454b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx @@ -3,11 +3,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList'; import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer'; -import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { memo } from 'react'; -const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => { +const selectEntityIdentifiers = createMemoizedSelector(selectActiveCanvas, (canvas) => { return canvas.controlLayers.entities.map(getEntityIdentifier).toReversed(); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx index 9ae7bec53e0..2e382cc063e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx @@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked'; import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,7 +12,7 @@ import { PiDropHalfBold } from 'react-icons/pi'; const buildSelectWithTransparencyEffect = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerMenuItemsTransparencyEffect').withTransparencyEffect ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx index e440ea75098..ab9d235b8a5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx @@ -35,7 +35,7 @@ const FilterContentAdvanced = memo( const config = useStore(adapter.filterer.$filterConfig); const isProcessing = useStore(adapter.filterer.$isProcessing); const hasImageState = useStore(adapter.filterer.$hasImageState); - const autoProcess = useAppSelector(selectAutoProcess); + const autoProcess = useAppSelector((state) => selectAutoProcess(state)); const onChangeFilterConfig = useCallback( (filterConfig: FilterConfig) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDenoiseLimitSlider.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDenoiseLimitSlider.tsx index 7f1e154c378..c2ccf38afc6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDenoiseLimitSlider.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskDenoiseLimitSlider.tsx @@ -7,7 +7,7 @@ import { inpaintMaskDenoiseLimitChanged, inpaintMaskDenoiseLimitDeleted, } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -19,7 +19,7 @@ export const InpaintMaskDenoiseLimitSlider = memo(() => { const selectDenoiseLimit = useMemo( () => createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskDenoiseLimitSlider').denoiseLimit ), [entityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx index 8bbb49a9865..97a720de5db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx @@ -3,11 +3,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList'; import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask'; -import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { memo } from 'react'; -const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => { +const selectEntityIdentifiers = createMemoizedSelector(selectActiveCanvas, (canvas) => { return canvas.inpaintMasks.entities.map(getEntityIdentifier).toReversed(); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx index 47ec27f6b2b..9a65f0294eb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider.tsx @@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InpaintMaskDeleteModifierButton } from 'features/controlLayers/components/InpaintMask/InpaintMaskDeleteModifierButton'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { inpaintMaskNoiseChanged, inpaintMaskNoiseDeleted } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -16,7 +16,7 @@ export const InpaintMaskNoiseSlider = memo(() => { const selectNoiseLevel = useMemo( () => createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskNoiseSlider').noiseLevel ), [entityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx index 861ed0b6e74..6d55fee2327 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskSettings.tsx @@ -4,18 +4,18 @@ import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/c import { InpaintMaskDenoiseLimitSlider } from 'features/controlLayers/components/InpaintMask/InpaintMaskDenoiseLimitSlider'; import { InpaintMaskNoiseSlider } from 'features/controlLayers/components/InpaintMask/InpaintMaskNoiseSlider'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; const buildSelectHasDenoiseLimit = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings'); return entity.denoiseLimit !== undefined; }); const buildSelectHasNoiseLevel = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings'); return entity.noiseLevel !== undefined; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx index 871b9e055f1..3a44edfe372 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx @@ -1,9 +1,12 @@ import { Box } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; import { useInvokeCanvas } from 'features/controlLayers/hooks/useInvokeCanvas'; +import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; export const InvokeCanvasComponent = memo(() => { - const ref = useInvokeCanvas(); + const canvasId = useAppSelector(selectActiveCanvasId); + const ref = useInvokeCanvas(canvasId); return ( { const canvasManager = useCanvasManager(); const selectHasAdjustments = useMemo(() => { - return createSelector(selectCanvasSlice, (canvas) => Boolean(selectEntity(canvas, entityIdentifier)?.adjustments)); + return createSelector(selectActiveCanvas, (canvas) => Boolean(selectEntity(canvas, entityIdentifier)?.adjustments)); }, [entityIdentifier]); const hasAdjustments = useAppSelector(selectHasAdjustments); const selectMode = useMemo(() => { return createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.mode ?? 'simple' ); }, [entityIdentifier]); @@ -40,7 +40,7 @@ export const RasterLayerAdjustmentsPanel = memo(() => { const selectEnabled = useMemo(() => { return createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled ?? false ); }, [entityIdentifier]); @@ -48,7 +48,7 @@ export const RasterLayerAdjustmentsPanel = memo(() => { const selectCollapsed = useMemo(() => { return createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.collapsed ?? false ); }, [entityIdentifier]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCurvesAdjustmentsEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCurvesAdjustmentsEditor.tsx index 9610927e016..a0151b4100c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCurvesAdjustmentsEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerCurvesAdjustmentsEditor.tsx @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityAdapterContext } from 'features/controlLayers/contexts/EntityAdapterContext'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rasterLayerAdjustmentsCurvesUpdated } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntity } from 'features/controlLayers/store/selectors'; import type { ChannelName, ChannelPoints, CurvesAdjustmentsConfig } from 'features/controlLayers/store/types'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -72,7 +72,7 @@ export const RasterLayerCurvesAdjustmentsEditor = memo(() => { const { t } = useTranslation(); const selectCurves = useMemo(() => { return createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.curves ?? DEFAULT_CURVES ); }, [entityIdentifier]); @@ -80,7 +80,7 @@ export const RasterLayerCurvesAdjustmentsEditor = memo(() => { const selectIsDisabled = useMemo(() => { return createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled !== true ); }, [entityIdentifier]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx index c585a49cc3e..b0200b889c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx @@ -3,11 +3,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList'; import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; -import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { memo } from 'react'; -const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => { +const selectEntityIdentifiers = createMemoizedSelector(selectActiveCanvas, (canvas) => { return canvas.rasterLayers.entities.map(getEntityIdentifier).toReversed(); }); const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsAdjustments.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsAdjustments.tsx index 86fac78cb3e..21fdae49e35 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsAdjustments.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsAdjustments.tsx @@ -1,10 +1,12 @@ import { MenuItem } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rasterLayerAdjustmentsCancel, rasterLayerAdjustmentsSet } from 'features/controlLayers/store/canvasSlice'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { makeDefaultRasterLayerAdjustments } from 'features/controlLayers/store/util'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiSlidersHorizontalBold } from 'react-icons/pi'; @@ -12,9 +14,12 @@ export const RasterLayerMenuItemsAdjustments = memo(() => { const dispatch = useAppDispatch(); const entityIdentifier = useEntityIdentifierContext<'raster_layer'>(); const { t } = useTranslation(); - const layer = useAppSelector((s) => - s.canvas.present.rasterLayers.entities.find((e: CanvasRasterLayerState) => e.id === entityIdentifier.id) - ); + const selectRasterLayer = useMemo(() => { + return createSelector(selectActiveCanvas, (canvas) => + canvas.rasterLayers.entities.find((e: CanvasRasterLayerState) => e.id === entityIdentifier.id) + ); + }, [entityIdentifier]); + const layer = useAppSelector(selectRasterLayer); const hasAdjustments = Boolean(layer?.adjustments); const onToggleAdjustmentsPresence = useCallback(() => { if (hasAdjustments) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerSimpleAdjustmentsEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerSimpleAdjustmentsEditor.tsx index 42c45e1c36d..5bea74a4bdc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerSimpleAdjustmentsEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerSimpleAdjustmentsEditor.tsx @@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rasterLayerAdjustmentsSimpleUpdated } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntity } from 'features/controlLayers/store/selectors'; import type { SimpleAdjustmentsConfig } from 'features/controlLayers/store/types'; import React, { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -21,7 +21,7 @@ const AdjustmentSliderRow = ({ label, name, onChange, min = -1, max = 1, step = const entityIdentifier = useEntityIdentifierContext<'raster_layer'>(); const selectValue = useMemo(() => { return createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.simple?.[name] ?? DEFAULT_SIMPLE_ADJUSTMENTS[name] ); @@ -54,7 +54,7 @@ export const RasterLayerSimpleAdjustmentsEditor = memo(() => { const { t } = useTranslation(); const selectIsDisabled = useMemo(() => { return createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled !== true ); }, [entityIdentifier]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx index a754f0e4da4..b65facc276f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx @@ -2,11 +2,12 @@ import { Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { objectEquals } from '@observ33r/object-equals'; import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks'; import { UploadImageIconButton } from 'common/hooks/useImageUploadButton'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasSlice'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import type { CroppableImageWithDims } from 'features/controlLayers/store/types'; import { imageDTOToCroppableImage, imageDTOToImageWithDims } from 'features/controlLayers/store/util'; import { Editor } from 'features/cropper/lib/editor'; @@ -15,7 +16,6 @@ import type { setGlobalReferenceImageDndTarget, setRegionalGuidanceReferenceImag import { DndDropTarget } from 'features/dnd/DndDropTarget'; import { DndImage } from 'features/dnd/DndImage'; import { DndImageIcon } from 'features/dnd/DndImageIcon'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiCropBold, PiRulerBold } from 'react-icons/pi'; @@ -39,6 +39,7 @@ export const RefImageImage = memo( }: Props) => { const { t } = useTranslation(); const store = useAppStore(); + const dispatch = useAppDispatch(); const isConnected = useStore($isConnected); const tab = useAppSelector(selectActiveTab); const isStaging = useCanvasIsStaging(); @@ -77,10 +78,10 @@ export const RefImageImage = memo( store.dispatch(bboxSizeRecalled({ width, height })); store.dispatch(bboxSizeOptimized()); } else if (tab === 'generate') { - store.dispatch(sizeRecalled({ width, height })); - store.dispatch(sizeOptimized()); + dispatch(sizeRecalled({ width, height })); + dispatch(sizeOptimized()); } - }, [imageDTO, isStaging, store, tab]); + }, [dispatch, imageDTO, isStaging, store, tab]); const edit = useCallback(() => { if (!originalImageDTO) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx index 6683e247b05..64fd77cb6fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx @@ -6,17 +6,17 @@ import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/Canva import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext'; import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks'; import { useNewGlobalReferenceImageFromBbox } from 'features/controlLayers/hooks/saveCanvasHooks'; -import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { refImageAdded, selectIsRefImagePanelOpen, selectRefImageEntityIds, selectSelectedRefEntityId, } from 'features/controlLayers/store/refImagesSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { imageDTOToCroppableImage } from 'features/controlLayers/store/util'; import { addGlobalReferenceImageDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiBoundingBoxBold, PiUploadBold } from 'react-icons/pi'; @@ -132,7 +132,7 @@ AddRefImageDropTargetAndButton.displayName = 'AddRefImageDropTargetAndButton'; const BboxButton = memo(() => { const { t } = useTranslation(); - const isBusy = useCanvasIsBusySafe(); + const isBusy = useCanvasIsBusy(); const newGlobalReferenceImageFromBbox = useNewGlobalReferenceImageFromBbox(); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx index 0c9153f16fd..50e0df1591e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx @@ -28,6 +28,7 @@ import { selectRefImageEntityOrThrow, selectRefImagesSlice, } from 'features/controlLayers/store/refImagesSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import type { CLIPVisionModelV2, CroppableImageWithDims, @@ -37,7 +38,6 @@ import type { import { isFLUXReduxConfig, isIPAdapterConfig } from 'features/controlLayers/store/types'; import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd'; import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback, useMemo } from 'react'; import type { ChatGPT4oModelConfig, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx index 810a1c0f48b..d3108215e5a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx @@ -2,7 +2,7 @@ import { Badge } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,7 +12,7 @@ export const RegionalGuidanceBadges = memo(() => { const selectAutoNegative = useMemo( () => createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceBadges').autoNegative ), [entityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx index 75224b7689a..6dee9de316c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx @@ -3,11 +3,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList'; import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidance'; -import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { memo } from 'react'; -const selectEntityIdentifiers = createMemoizedSelector(selectCanvasSlice, (canvas) => { +const selectEntityIdentifiers = createMemoizedSelector(selectActiveCanvas, (canvas) => { return canvas.regionalGuidance.entities.map(getEntityIdentifier).toReversed(); }); const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx index 402487bd39d..18491e9a3fa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx @@ -21,7 +21,7 @@ import { rgRefImageIPAdapterWeightChanged, rgRefImageModelChanged, } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier, CLIPVisionModelV2, @@ -51,7 +51,7 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro }, [dispatch, entityIdentifier, referenceImageId]); const selectConfig = useMemo( () => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); assert(referenceImage, `Regional Guidance IP Adapter with id ${referenceImageId} not found`); return referenceImage.config; @@ -190,7 +190,7 @@ const buildSelectIPAdapterHasImage = ( entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, referenceImageId: string ) => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); return !!referenceImage && referenceImage.config.image !== null; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx index cf2e858341d..e96ab3df743 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx @@ -4,7 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceIPAdapterSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { Fragment, memo, useMemo } from 'react'; export const RegionalGuidanceIPAdapters = memo(() => { @@ -12,7 +12,7 @@ export const RegionalGuidanceIPAdapters = memo(() => { const selectIPAdapterIds = useMemo( () => - createMemoizedSelector(selectCanvasSlice, (canvas) => { + createMemoizedSelector(selectActiveCanvas, (canvas) => { const ipAdapterIds = selectEntityOrThrow( canvas, entityIdentifier, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx index 85400f55739..2c01fe11e7a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx @@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgAutoNegativeToggled } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiSelectionInverseBold } from 'react-icons/pi'; @@ -15,7 +15,7 @@ export const RegionalGuidanceMenuItemsAutoNegative = memo(() => { const selectAutoNegative = useMemo( () => createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceMenuItemsAutoNegative').autoNegative ), [entityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx index 05348223753..c61bea50cda 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; @@ -21,7 +21,7 @@ export const RegionalGuidanceNegativePrompt = memo(() => { const selectPrompt = useMemo( () => createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceNegativePrompt').negativePrompt ?? '' ), [entityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx index b54f261a67a..028d3e6b6f0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; @@ -21,7 +21,7 @@ export const RegionalGuidancePositivePrompt = memo(() => { const selectPrompt = useMemo( () => createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidancePositivePrompt').positivePrompt ?? '' ), [entityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx index 85285dd4ef3..b56ac232415 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx @@ -1,17 +1,17 @@ import { Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks'; import { UploadImageIconButton } from 'common/hooks/useImageUploadButton'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasSlice'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import type { ImageWithDims } from 'features/controlLayers/store/types'; import type { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; import { DndImage } from 'features/dnd/DndImage'; import { DndImageIcon } from 'features/dnd/DndImageIcon'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; @@ -29,6 +29,7 @@ type Props = { export const RegionalGuidanceRefImageImage = memo(({ image, onChangeImage, dndTarget, dndTargetData }: Props) => { const { t } = useTranslation(); const store = useAppStore(); + const dispatch = useAppDispatch(); const isConnected = useStore($isConnected); const tab = useAppSelector(selectActiveTab); const isStaging = useCanvasIsStaging(); @@ -59,10 +60,10 @@ export const RegionalGuidanceRefImageImage = memo(({ image, onChangeImage, dndTa store.dispatch(bboxSizeRecalled({ width, height })); store.dispatch(bboxSizeOptimized()); } else if (tab === 'generate') { - store.dispatch(sizeRecalled({ width, height })); - store.dispatch(sizeOptimized()); + dispatch(sizeRecalled({ width, height })); + dispatch(sizeOptimized()); } - }, [imageDTO, isStaging, store, tab]); + }, [dispatch, imageDTO, isStaging, store, tab]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx index 078480c3234..2aa24fca65f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx @@ -4,7 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { RegionalGuidanceAddPromptsIPAdapterButtons } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; @@ -13,7 +13,7 @@ import { RegionalGuidanceNegativePrompt } from './RegionalGuidanceNegativePrompt import { RegionalGuidancePositivePrompt } from './RegionalGuidancePositivePrompt'; const buildSelectFlags = (entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>) => - createMemoizedSelector(selectCanvasSlice, (canvas) => { + createMemoizedSelector(selectActiveCanvas, (canvas) => { const entity = selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceSettings'); return { hasPositivePrompt: entity.positivePrompt !== null, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SelectObject/SelectObjectActionButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SelectObject/SelectObjectActionButtons.tsx index 1cce8f0e34a..6314ba71e62 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SelectObject/SelectObjectActionButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SelectObject/SelectObjectActionButtons.tsx @@ -18,7 +18,7 @@ export const SelectObjectActionButtons = memo(({ adapter }: SelectObjectActionBu const isProcessing = useStore(adapter.segmentAnything.$isProcessing); const hasInput = useStore(adapter.segmentAnything.$hasInputData); const hasImageState = useStore(adapter.segmentAnything.$hasImageState); - const autoProcess = useAppSelector(selectAutoProcess); + const autoProcess = useAppSelector((state) => selectAutoProcess(state)); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsBboxOverlaySwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsBboxOverlaySwitch.tsx index c74d37222aa..50e306ddc9c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsBboxOverlaySwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsBboxOverlaySwitch.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsBboxOverlaySwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const bboxOverlay = useAppSelector(selectBboxOverlay); + const bboxOverlay = useAppSelector((state) => selectBboxOverlay(state)); const onChange = useCallback(() => { dispatch(settingsBboxOverlayToggled()); }, [dispatch]); 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 65986e398cb..54a23d8859f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx @@ -1,17 +1,14 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSettingsSlice, settingsClipToBboxChanged } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectClipToBbox, settingsClipToBboxChanged } from 'features/controlLayers/store/canvasSettingsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -const selectClipToBbox = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.clipToBbox); - export const CanvasSettingsClipToBboxCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const clipToBbox = useAppSelector(selectClipToBbox); + const clipToBbox = useAppSelector((state) => selectClipToBbox(state)); const onChange = useCallback( (e: ChangeEvent) => dispatch(settingsClipToBboxChanged(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 cc26e2a7aaa..eae900afb5d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsDynamicGridSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const dynamicGrid = useAppSelector(selectDynamicGrid); + const dynamicGrid = useAppSelector((state) => selectDynamicGrid(state)); const onChange = useCallback(() => { dispatch(settingsDynamicGridToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsGridSize.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsGridSize.tsx index 91cebd4bd6f..a3758502e49 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsGridSize.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsGridSize.tsx @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsSnapToGridCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const snapToGrid = useAppSelector(selectSnapToGrid); + const snapToGrid = useAppSelector((state) => selectSnapToGrid(state)); const onChange = useCallback>(() => { dispatch(settingsSnapToGridToggled()); }, [dispatch]); 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 7d1dbba3f56..8f474e801c9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx @@ -1,23 +1,17 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { - selectCanvasSettingsSlice, + selectInvertScrollForToolWidth, settingsInvertScrollForToolWidthChanged, } from 'features/controlLayers/store/canvasSettingsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -const selectInvertScrollForToolWidth = createSelector( - selectCanvasSettingsSlice, - (settings) => settings.invertScrollForToolWidth -); - export const CanvasSettingsInvertScrollCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const invertScrollForToolWidth = useAppSelector(selectInvertScrollForToolWidth); + const invertScrollForToolWidth = useAppSelector((state) => selectInvertScrollForToolWidth(state)); const onChange = useCallback( (e: ChangeEvent) => { dispatch(settingsInvertScrollForToolWidthChanged(e.target.checked)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsIsolatedLayerPreviewSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsIsolatedLayerPreviewSwitch.tsx index d83b6762f20..9066e7f1cf4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsIsolatedLayerPreviewSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsIsolatedLayerPreviewSwitch.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsIsolatedLayerPreviewSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isolatedLayerPreview = useAppSelector(selectIsolatedLayerPreview); + const isolatedLayerPreview = useAppSelector((state) => selectIsolatedLayerPreview(state)); const onChange = useCallback(() => { dispatch(settingsIsolatedLayerPreviewToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsIsolatedStagingPreviewSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsIsolatedStagingPreviewSwitch.tsx index cb8a759b81d..73620f5ae1b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsIsolatedStagingPreviewSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsIsolatedStagingPreviewSwitch.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsIsolatedStagingPreviewSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isolatedStagingPreview = useAppSelector(selectIsolatedStagingPreview); + const isolatedStagingPreview = useAppSelector((state) => selectIsolatedStagingPreview(state)); const onChange = useCallback(() => { dispatch(settingsIsolatedStagingPreviewToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsOutputOnlyMaskedRegionsCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsOutputOnlyMaskedRegionsCheckbox.tsx index f9b2e5d8216..270c396b7a3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsOutputOnlyMaskedRegionsCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsOutputOnlyMaskedRegionsCheckbox.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsOutputOnlyMaskedRegionsCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const outputOnlyMaskedRegions = useAppSelector(selectOutputOnlyMaskedRegions); + const outputOnlyMaskedRegions = useAppSelector((state) => selectOutputOnlyMaskedRegions(state)); const onChange = useCallback(() => { dispatch(settingsOutputOnlyMaskedRegionsToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPreserveMaskCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPreserveMaskCheckbox.tsx index 08bd50a2fa0..1c850491b51 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPreserveMaskCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPreserveMaskCheckbox.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsPreserveMaskCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const preserveMask = useAppSelector(selectPreserveMask); + const preserveMask = useAppSelector((state) => selectPreserveMask(state)); const onChange = useCallback(() => dispatch(settingsPreserveMaskToggled()), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPressureSensitivity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPressureSensitivity.tsx index 1f59b30abf7..754d5990ea0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPressureSensitivity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsPressureSensitivity.tsx @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsPressureSensitivityCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const pressureSensitivity = useAppSelector(selectPressureSensitivity); + const pressureSensitivity = useAppSelector((state) => selectPressureSensitivity(state)); const onChange = useCallback>(() => { dispatch(settingsPressureSensitivityToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRuleOfThirdsGuideSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRuleOfThirdsGuideSwitch.tsx index d23f566c9e5..058e4654636 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRuleOfThirdsGuideSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsRuleOfThirdsGuideSwitch.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsRuleOfThirdsSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const ruleOfThirds = useAppSelector(selectRuleOfThirds); + const ruleOfThirds = useAppSelector((state) => selectRuleOfThirds(state)); const onChange = useCallback(() => { dispatch(settingsRuleOfThirdsToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsSaveAllImagesToGalleryCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsSaveAllImagesToGalleryCheckbox.tsx index c1c0d72d02d..eaa60c55bbf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsSaveAllImagesToGalleryCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsSaveAllImagesToGalleryCheckbox.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsSaveAllImagesToGalleryCheckbox = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const saveAllImagesToGallery = useAppSelector(selectSaveAllImagesToGallery); + const saveAllImagesToGallery = useAppSelector((state) => selectSaveAllImagesToGallery(state)); const onChange = useCallback(() => { dispatch(settingsSaveAllImagesToGalleryToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsShowHUDSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsShowHUDSwitch.tsx index e570e0019e5..9629c7b2a70 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsShowHUDSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsShowHUDSwitch.tsx @@ -1,16 +1,13 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSettingsSlice, settingsShowHUDToggled } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectShowHUD, settingsShowHUDToggled } from 'features/controlLayers/store/canvasSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD); - export const CanvasSettingsShowHUDSwitch = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const showHUD = useAppSelector(selectShowHUD); + const showHUD = useAppSelector((state) => selectShowHUD(state)); const onChange = useCallback(() => { dispatch(settingsShowHUDToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsShowProgressOnCanvasSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsShowProgressOnCanvasSwitch.tsx index 1912a384426..2ddd23b20dc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsShowProgressOnCanvasSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsShowProgressOnCanvasSwitch.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; export const CanvasSettingsShowProgressOnCanvas = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const showProgressOnCanvas = useAppSelector(selectShowProgressOnCanvas); + const showProgressOnCanvas = useAppSelector((state) => selectShowProgressOnCanvas(state)); const onChange = useCallback(() => { dispatch(settingsShowProgressOnCanvasToggled()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemPreviewMini.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemPreviewMini.tsx index 0d874605923..b5e19f52fc4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemPreviewMini.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemPreviewMini.tsx @@ -47,7 +47,7 @@ export const QueueItemPreviewMini = memo(({ item, index }: Props) => { const $isSelected = useMemo(() => ctx.buildIsSelectedComputed(item.item_id), [ctx, item.item_id]); const isSelected = useStore($isSelected); const imageDTO = useOutputImageDTO(item.item_id); - const autoSwitch = useAppSelector(selectStagingAreaAutoSwitch); + const autoSwitch = useAppSelector((state) => selectStagingAreaAutoSwitch(state)); const onClick = useCallback(() => { ctx.select(item.item_id); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx index f9fc483eea5..be985b83fd7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx @@ -12,8 +12,7 @@ import { PiCaretLineRightBold, PiCaretRightBold, PiMoonBold } from 'react-icons/ export const StagingAreaAutoSwitchButtons = memo(() => { const canvasManager = useCanvasManager(); const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage); - - const autoSwitch = useAppSelector(selectStagingAreaAutoSwitch); + const autoSwitch = useAppSelector((state) => selectStagingAreaAutoSwitch(state)); const dispatch = useAppDispatch(); const onClickOff = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/__mocks__/mockStagingAreaApp.ts b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/__mocks__/mockStagingAreaApp.ts index 15a50ceb115..6bb4631adec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/__mocks__/mockStagingAreaApp.ts +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/__mocks__/mockStagingAreaApp.ts @@ -1,6 +1,6 @@ import { merge } from 'es-toolkit'; import type { StagingAreaAppApi } from 'features/controlLayers/components/StagingArea/state'; -import type { AutoSwitchMode } from 'features/controlLayers/store/canvasSettingsSlice'; +import type { AutoSwitchMode } from 'features/controlLayers/store/types'; import type { ImageDTO, S } from 'services/api/types'; import type { PartialDeep } from 'type-fest'; import { vi } from 'vitest'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx index 6b8da8dc4da..7bd8bf04df4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx @@ -1,14 +1,15 @@ import { useStore } from '@nanostores/react'; -import { useAppStore } from 'app/store/storeHooks'; +import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { selectStagingAreaAutoSwitch, settingsStagingAreaAutoSwitchChanged, } from 'features/controlLayers/store/canvasSettingsSlice'; import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice'; import { - buildSelectCanvasQueueItems, + buildSelectCanvasQueueItemsBySessionId, canvasQueueItemDiscarded, canvasSessionReset, + selectActiveCanvasStagingAreaSessionId, } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; @@ -26,14 +27,17 @@ import { getInitialProgressData, StagingAreaApi } from './state'; const StagingAreaContext = createContext(null); -export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWithChildren<{ sessionId: string }>) => { +export const StagingAreaContextProvider = memo(({ children }: PropsWithChildren) => { const store = useAppStore(); const socket = useStore($socket); - const stagingAreaAppApi = useMemo(() => { - const selectQueueItems = buildSelectCanvasQueueItems(sessionId); + const sessionId = useAppSelector(selectActiveCanvasStagingAreaSessionId); + const selectQueueItems = useMemo(() => buildSelectCanvasQueueItemsBySessionId(sessionId), [sessionId]); + const stagingAreaAppApi = useMemo(() => { const _stagingAreaAppApi: StagingAreaAppApi = { - getAutoSwitch: () => selectStagingAreaAutoSwitch(store.getState()), + getAutoSwitch: () => { + return selectStagingAreaAutoSwitch(store.getState()); + }, getImageDTO: (imageName: string) => getImageDTOSafe(imageName), onInvocationProgress: (handler) => { socket?.on('invocation_progress', handler); @@ -65,9 +69,11 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi }, onDiscardAll: () => { store.dispatch(canvasSessionReset()); - store.dispatch( - queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false }) - ); + if (sessionId) { + store.dispatch( + queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false }) + ); + } }, onAccept: (item, imageDTO) => { const bboxRect = selectBboxRect(store.getState()); @@ -81,9 +87,11 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi store.dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' })); store.dispatch(canvasSessionReset()); - store.dispatch( - queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false }) - ); + if (sessionId) { + store.dispatch( + queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false }) + ); + } }, onAutoSwitchChange: (mode) => { store.dispatch(settingsStagingAreaAutoSwitchChanged(mode)); @@ -91,11 +99,17 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi }; return _stagingAreaAppApi; - }, [sessionId, socket, store]); + }, [sessionId, selectQueueItems, socket, store]); const [stagingAreaApi] = useState(() => new StagingAreaApi()); useEffect(() => { + if (!sessionId) { + return () => { + stagingAreaApi.cleanup(); + }; + } + stagingAreaApi.connectToApp(sessionId, stagingAreaAppApi); // We need to subscribe to the queue items query manually to ensure the staging area actually gets the items diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/state.ts b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/state.ts index b6412b048ec..6546be3928b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/state.ts +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/state.ts @@ -1,5 +1,5 @@ import { clamp } from 'es-toolkit'; -import type { AutoSwitchMode } from 'features/controlLayers/store/canvasSettingsSlice'; +import type { AutoSwitchMode } from 'features/controlLayers/store/types'; import type { ProgressImage } from 'features/nodes/types/common'; import type { MapStore } from 'nanostores'; import { atom, computed, map } from 'nanostores'; 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 c192687e2e9..cb383975df2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx @@ -9,12 +9,13 @@ import { Portal, Tooltip, } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbaColorPicker from 'common/components/ColorPicker/RgbaColorPicker'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { - selectCanvasSettingsSlice, + selectActiveColor, + selectBgColor, + selectFgColor, settingsActiveColorToggled, settingsBgColorChanged, settingsColorsSetToDefault, @@ -25,15 +26,11 @@ import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/us import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -const selectActiveColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.activeColor); -const selectBgColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.bgColor); -const selectFgColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.fgColor); - export const ToolFillColorPicker = memo(() => { const { t } = useTranslation(); - const activeColorType = useAppSelector(selectActiveColor); - const bgColor = useAppSelector(selectBgColor); - const fgColor = useAppSelector(selectFgColor); + const activeColorType = useAppSelector((state) => selectActiveColor(state)); + const bgColor = useAppSelector((state) => selectBgColor(state)); + const fgColor = useAppSelector((state) => selectFgColor(state)); const { activeColor, tooltip, bgColorzIndex, fgColorzIndex } = useMemo(() => { if (activeColorType === 'bgColor') { return { activeColor: bgColor, tooltip: t('controlLayers.fill.bgFillColor'), bgColorzIndex: 2, fgColorzIndex: 1 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolWidthPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolWidthPicker.tsx index 3fa270893a3..2e39cb78986 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolWidthPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolWidthPicker.tsx @@ -14,11 +14,11 @@ import { PopoverTrigger, Portal, } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { clamp } from 'es-toolkit/compat'; import { - selectCanvasSettingsSlice, + selectBrushWidth, + selectEraserWidth, settingsBrushWidthChanged, settingsEraserWidthChanged, } from 'features/controlLayers/store/canvasSettingsSlice'; @@ -180,9 +180,6 @@ const SliderToolWidthPickerComponent = memo( ); SliderToolWidthPickerComponent.displayName = 'SliderToolWidthPickerComponent'; -const selectBrushWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.brushWidth); -const selectEraserWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.eraserWidth); - export const ToolWidthPicker = memo(() => { const ref = useRef(null); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx index 78c768b6693..7d9d0f07f2c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx @@ -6,7 +6,7 @@ import { upperFirst } from 'es-toolkit/compat'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled'; import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; -import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityOrThrow } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { getControlLayerWarnings, @@ -23,7 +23,7 @@ import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; const buildSelectWarnings = (entityIdentifier: CanvasEntityIdentifier, t: TFunction) => { - return createSelector(selectCanvasSlice, selectMainModelConfig, (canvas, model) => { + return createSelector(selectActiveCanvas, selectMainModelConfig, (canvas, model) => { // This component is used within a so we can safely assume that the entity exists. // Should never throw. const entity = selectEntityOrThrow(canvas, entityIdentifier, 'CanvasEntityHeaderWarnings'); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx index f9f625c1df8..f81d22981d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx @@ -9,14 +9,14 @@ import { entityArrangedToBack, entityArrangedToFront, } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; -import type { CanvasEntityIdentifier, CanvasState } from 'features/controlLayers/store/types'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; +import type { CanvasEntity, CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowDownBold, PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold } from 'react-icons/pi'; const getIndexAndCount = ( - canvas: CanvasState, + canvas: CanvasEntity, { id, type }: CanvasEntityIdentifier ): { index: number; count: number } => { if (type === 'raster_layer') { @@ -54,7 +54,7 @@ export const CanvasEntityMenuItemsArrange = memo(() => { const isBusy = useCanvasIsBusy(); const selectValidActions = useMemo( () => - createMemoizedSelector(selectCanvasSlice, (canvas) => { + createMemoizedSelector(selectActiveCanvas, (canvas) => { const { index, count } = getIndexAndCount(canvas, entityIdentifier); return { canMoveForwardOne: index < count - 1, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx index d8a9bb1c71f..f5d90e398c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx @@ -6,7 +6,7 @@ import { debounce } from 'es-toolkit/compat'; import { useEntityAdapter } from 'features/controlLayers/contexts/EntityAdapterContext'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { TRANSPARENCY_CHECKERBOARD_PATTERN_DARK_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern'; -import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntity } from 'features/controlLayers/store/selectors'; import React, { memo, useEffect, useMemo, useRef } from 'react'; import { useSelector } from 'react-redux'; @@ -20,7 +20,7 @@ export const CanvasEntityPreviewImage = memo(() => { const adapter = useEntityAdapter(entityIdentifier); const selectMaskColor = useMemo( () => - createSelector(selectCanvasSlice, (state) => { + createSelector(selectActiveCanvas, (state) => { const entity = selectEntity(state, entityIdentifier); if (!entity) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx index ca3528c7a0e..a264ed784ba 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx @@ -1,31 +1,34 @@ import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { $canvasManager } from 'features/controlLayers/store/ephemeral'; +import { $canvasManagers } from 'features/controlLayers/store/ephemeral'; +import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; import type { PropsWithChildren } from 'react'; -import { createContext, memo, useContext } from 'react'; +import { createContext, memo } from 'react'; import { assert } from 'tsafe'; -const CanvasManagerContext = createContext(null); +const CanvasManagerContext = createContext<{ [canvasId: string]: CanvasManager } | null>(null); export const CanvasManagerProviderGate = memo(({ children }: PropsWithChildren) => { - const canvasManager = useStore($canvasManager); + const canvasManagers = useStore($canvasManagers); + const selectedCanvasId = useAppSelector(selectActiveCanvasId); - if (!canvasManager) { + if (Object.keys(canvasManagers).length === 0 || !canvasManagers[selectedCanvasId]) { return null; } - return {children}; + return {children}; }); CanvasManagerProviderGate.displayName = 'CanvasManagerProviderGate'; /** - * Consumes the CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise - * it will throw an error. + * Consumes the CanvasManager from the context. If the CanvasManager is not available, it will throw an error. */ export const useCanvasManager = (): CanvasManager => { - const canvasManager = useContext(CanvasManagerContext); - assert(canvasManager, 'useCanvasManagerContext must be used within a CanvasManagerProviderGate'); + const canvasManager = useCanvasManagerSafe(); + assert(canvasManager, 'Canvas manager does not exist'); + return canvasManager; }; @@ -33,6 +36,8 @@ export const useCanvasManager = (): CanvasManager => { * Consumes the CanvasManager from the context. If the CanvasManager is not available, it will return null. */ export const useCanvasManagerSafe = (): CanvasManager | null => { - const canvasManager = useStore($canvasManager); - return canvasManager; + const canvasManagers = useStore($canvasManagers); + const canvasId = useAppSelector(selectActiveCanvasId); + + return canvasManagers[canvasId] ?? null; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 062937edcd0..f209a89dcec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -17,7 +17,7 @@ import { } from 'features/controlLayers/store/canvasSlice'; import { selectBase, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { - selectCanvasSlice, + selectActiveCanvas, selectEntity, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; @@ -270,7 +270,7 @@ export const useAddInpaintMaskDenoiseLimit = (entityIdentifier: CanvasEntityIden export const buildSelectValidRegionalGuidanceActions = ( entityIdentifier: CanvasEntityIdentifier<'regional_guidance'> ) => { - return createMemoizedSelector(selectCanvasSlice, (canvas) => { + return createMemoizedSelector(selectActiveCanvas, (canvas) => { const entity = selectEntity(canvas, entityIdentifier); return { canAddPositivePrompt: entity?.positivePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts index 02eec59b702..ee806515165 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts @@ -1,22 +1,6 @@ import { useStore } from '@nanostores/react'; import { $false } from 'app/store/nanostores/util'; -import { useCanvasManager, useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; - -/** - * Returns a boolena indicating whether the canvas is busy: - * - While staging - * - While an entity is transforming - * - While an entity is filtering - * - While the canvas is doing some other long-running operation, like rasterizing a layer - * - * This hook will throw an error if the canvas manager is not initialized. - */ -export const useCanvasIsBusy = () => { - const canvasManager = useCanvasManager(); - const isBusy = useStore(canvasManager.$isBusy); - - return isBusy; -}; +import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; /** * Returns a boolena indicating whether the canvas is busy: @@ -27,7 +11,7 @@ export const useCanvasIsBusy = () => { * * This hook will fall back to false if the canvas manager is not initialized. */ -export const useCanvasIsBusySafe = () => { +export const useCanvasIsBusy = () => { const canvasManager = useCanvasManagerSafe(); const isBusy = useStore(canvasManager?.$isBusy ?? $false); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsStaging.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsStaging.ts new file mode 100644 index 00000000000..ca84d1f4ee6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsStaging.ts @@ -0,0 +1,13 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { + buildSelectIsStagingBySessionId, + selectActiveCanvasStagingAreaSessionId, +} from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useMemo } from 'react'; + +export const useCanvasIsStaging = () => { + const sessionId = useAppSelector(selectActiveCanvasStagingAreaSessionId); + const selectIsStagingBySessionIdSelector = useMemo(() => buildSelectIsStagingBySessionId(sessionId), [sessionId]); + + return useAppSelector(selectIsStagingBySessionIdSelector); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts index 5c497bf6c35..409b699ccc3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts @@ -1,13 +1,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityIsBookmarkedForQuickSwitch = (entityIdentifier: CanvasEntityIdentifier) => { const selectIsBookmarkedForQuickSwitch = useMemo( () => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { return canvas.bookmarkedEntityIdentifier?.id === entityIdentifier.id; }), [entityIdentifier] diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts index 938364d91d7..1f05f751163 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts @@ -1,13 +1,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityIsEnabled = (entityIdentifier: CanvasEntityIdentifier) => { const selectIsEnabled = useMemo( () => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return false; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsLocked.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsLocked.ts index 48c9489335b..8e49ab4c516 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsLocked.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsLocked.ts @@ -1,13 +1,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityIsLocked = (entityIdentifier: CanvasEntityIdentifier | null) => { const selectIsLocked = useMemo( () => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { if (!entityIdentifier) { return false; } diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts index fde57b6a781..d905a5ff9b7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts @@ -1,13 +1,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntity } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; const createSelectName = (entityIdentifier: CanvasEntityIdentifier) => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts index cf70719c910..2d76ca2dbcb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts @@ -1,13 +1,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityTypeCount = (type: CanvasEntityIdentifier['type']): number => { const selectEntityCount = useMemo( () => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { switch (type) { case 'control_layer': return canvas.controlLayers.entities.length; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts index 04bc110fccd..00cc300a4ad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts @@ -1,13 +1,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; export const useEntityTypeIsHidden = (type: CanvasEntityIdentifier['type']): boolean => { const selectIsHidden = useMemo( () => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { switch (type) { case 'control_layer': return canvas.controlLayers.isHidden; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts index 76d997c705b..32be4f9dfc0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts @@ -3,7 +3,7 @@ import { logger } from 'app/logging/logger'; import { useAppStore } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { $canvasManager } from 'features/controlLayers/store/ephemeral'; +import { $canvasManagers } from 'features/controlLayers/store/ephemeral'; import Konva from 'konva'; import { useLayoutEffect, useState } from 'react'; import { $socket } from 'services/events/stores'; @@ -14,8 +14,8 @@ const log = logger('canvas'); // This will log warnings when layers > 5 Konva.showWarnings = import.meta.env.MODE === 'development'; -const useKonvaPixelRatioWatcher = () => { - useAssertSingleton('useKonvaPixelRatioWatcher'); +const useKonvaPixelRatioWatcher = (canvasId: string) => { + useAssertSingleton(`useKonvaPixelRatioWatcher-${canvasId}`); const dpr = useDevicePixelRatio({ round: false }); @@ -24,12 +24,13 @@ const useKonvaPixelRatioWatcher = () => { }, [dpr]); }; -export const useInvokeCanvas = (): ((el: HTMLDivElement | null) => void) => { - useAssertSingleton('useInvokeCanvas'); - useKonvaPixelRatioWatcher(); +export const useInvokeCanvas = (canvasId: string): ((el: HTMLDivElement | null) => void) => { + useAssertSingleton(`useInvokeCanvas-${canvasId}`); + useKonvaPixelRatioWatcher(canvasId); const store = useAppStore(); const socket = useStore($socket); const [container, containerRef] = useState(null); + const currentManager = $canvasManagers.get()[canvasId]; useLayoutEffect(() => { log.debug('Initializing renderer'); @@ -44,20 +45,18 @@ export const useInvokeCanvas = (): ((el: HTMLDivElement | null) => void) => { return () => {}; } - const currentManager = $canvasManager.get(); if (currentManager) { currentManager.stage.setContainer(container); return; } - const manager = new CanvasManager(container, store, socket); + const manager = new CanvasManager(canvasId, container, store, socket); manager.initialize(); return () => { manager.destroy(); - $canvasManager.set(null); }; - }, [container, socket, store]); + }, [canvasId, container, socket, store, currentManager]); return containerRef; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useNextPrevEntity.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useNextPrevEntity.ts index 000ecc53725..208834c3f57 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useNextPrevEntity.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useNextPrevEntity.ts @@ -2,14 +2,14 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { entitySelected } from 'features/controlLayers/store/canvasSlice'; -import { selectAllEntities, selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectAllEntities } from 'features/controlLayers/store/selectors'; import type { CanvasEntityState } from 'features/controlLayers/store/types'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; -const selectNextEntityIdentifier = createMemoizedSelector(selectCanvasSlice, (canvas) => { +const selectNextEntityIdentifier = createMemoizedSelector(selectActiveCanvas, (canvas) => { const selectedEntityIdentifier = canvas.selectedEntityIdentifier; const allEntities = selectAllEntities(canvas); let nextEntity: CanvasEntityState | null = null; @@ -25,7 +25,7 @@ const selectNextEntityIdentifier = createMemoizedSelector(selectCanvasSlice, (ca return getEntityIdentifier(nextEntity); }); -const selectPrevEntityIdentifier = createMemoizedSelector(selectCanvasSlice, (canvas) => { +const selectPrevEntityIdentifier = createMemoizedSelector(selectActiveCanvas, (canvas) => { const selectedEntityIdentifier = canvas.selectedEntityIdentifier; const allEntities = selectAllEntities(canvas); let prevEntity: CanvasEntityState | null = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts index 6552f4f2e52..6d46745dd04 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useNextRenderableEntityIdentifier.ts @@ -1,6 +1,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice, selectEntityIdentifierBelowThisOne } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntityIdentifierBelowThisOne } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; @@ -8,7 +8,7 @@ import { useMemo } from 'react'; export const useEntityIdentifierBelowThisOne = (entityIdentifier: T): T | null => { const selector = useMemo( () => - createMemoizedSelector(selectCanvasSlice, (canvas) => { + createMemoizedSelector(selectActiveCanvas, (canvas) => { const nextEntity = selectEntityIdentifierBelowThisOne(canvas, entityIdentifier); if (!nextEntity) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts index 2b45f61b291..92e3cca6ddb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts @@ -19,8 +19,8 @@ import { import { buildSelectIsSelected, getSelectIsTypeHidden, + selectActiveCanvas, selectBboxRect, - selectCanvasSlice, selectEntity, } from 'features/controlLayers/store/selectors'; import type { @@ -316,7 +316,7 @@ export abstract class CanvasEntityAdapterBase selectEntity(canvas, this.entityIdentifier) as T | undefined ); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts index f417f8ba890..02f25a4406c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts @@ -2,7 +2,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { - selectCanvasSlice, + selectActiveCanvas, selectControlLayerEntities, selectInpaintMaskEntities, selectRasterLayerEntities, @@ -10,10 +10,10 @@ import { } from 'features/controlLayers/store/selectors'; import type { CanvasControlLayerState, + CanvasEntity, CanvasInpaintMaskState, CanvasRasterLayerState, CanvasRegionalGuidanceState, - CanvasState, } from 'features/controlLayers/store/types'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import type { Logger } from 'roarr'; @@ -54,7 +54,7 @@ export class CanvasEntityRendererModule extends CanvasModuleBase { this.manager.stateApi.createStoreSubscription(selectRegionalGuidanceEntities, this.createNewRegionalGuidance) ); - this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasSlice, this.arrangeEntities)); + this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectActiveCanvas, this.arrangeEntities)); } initialize = () => { @@ -63,7 +63,7 @@ export class CanvasEntityRendererModule extends CanvasModuleBase { this.createNewControlLayers(this.manager.stateApi.runSelector(selectControlLayerEntities)); this.createNewRegionalGuidance(this.manager.stateApi.runSelector(selectRegionalGuidanceEntities)); this.createNewInpaintMasks(this.manager.stateApi.runSelector(selectInpaintMaskEntities)); - this.arrangeEntities(this.manager.stateApi.runSelector(selectCanvasSlice), null); + this.arrangeEntities(this.manager.stateApi.runSelector(selectActiveCanvas), null); }; createNewRasterLayers = (entities: CanvasRasterLayerState[]) => { @@ -102,7 +102,7 @@ export class CanvasEntityRendererModule extends CanvasModuleBase { } }; - arrangeEntities = (state: CanvasState, prevState: CanvasState | null) => { + arrangeEntities = (state: CanvasEntity, prevState: CanvasEntity | null) => { if ( !prevState || state.rasterLayers.entities !== prevState.rasterLayers.entities || diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 38248136625..d3148d9310f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -15,7 +15,7 @@ import { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStag import { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/CanvasToolModule'; import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { $canvasManager } from 'features/controlLayers/store/ephemeral'; +import { $canvasManagers } from 'features/controlLayers/store/ephemeral'; import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types'; import { isControlLayerEntityIdentifier, @@ -38,6 +38,7 @@ import { CanvasStateApiModule } from './CanvasStateApiModule'; export class CanvasManager extends CanvasModuleBase { readonly type = 'manager'; readonly id: string; + readonly canvasId: string; readonly path: string[]; readonly manager: CanvasManager; readonly parent: CanvasManager; @@ -75,9 +76,10 @@ export class CanvasManager extends CanvasModuleBase { */ $isBusy: Atom; - constructor(container: HTMLDivElement, store: AppStore, socket: AppSocket) { + constructor(canvasId: string, container: HTMLDivElement, store: AppStore, socket: AppSocket) { super(); this.id = getPrefixedId(this.type); + this.canvasId = canvasId; this.path = [this.id]; this.manager = this; this.parent = this; @@ -251,7 +253,10 @@ export class CanvasManager extends CanvasModuleBase { canvasModule.initialize?.(); } - $canvasManager.set(this); + $canvasManagers.set({ + ...$canvasManagers.get(), + [this.canvasId]: this, + }); }; destroy = () => { @@ -265,7 +270,9 @@ export class CanvasManager extends CanvasModuleBase { canvasModule.destroy(); } - $canvasManager.set(null); + const managers = { ...$canvasManagers.get() }; + delete managers[this.canvasId]; + $canvasManagers.set(managers); }; repr = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 57027aaa8f9..2f3abeed1f2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -9,7 +9,7 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase' import type { SubscriptionHandler } from 'features/controlLayers/konva/util'; import { createReduxSubscription, getPrefixedId } from 'features/controlLayers/konva/util'; import { - selectCanvasSettingsSlice, + selectCanvasSettingsByCanvasId, settingsBgColorChanged, settingsBrushWidthChanged, settingsEraserWidthChanged, @@ -29,15 +29,11 @@ import { rasterLayerAdded, rgAdded, } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { - selectAllRenderableEntities, - selectBbox, - selectCanvasSlice, - selectGridSize, -} from 'features/controlLayers/store/selectors'; +import { selectCanvasStagingAreaByCanvasId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectGridSize } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveCanvas, selectAllRenderableEntities, selectBbox } from 'features/controlLayers/store/selectors'; import type { - CanvasState, + CanvasEntity, EntityBrushLineAddedPayload, EntityEraserLineAddedPayload, EntityIdentifierPayload, @@ -127,8 +123,8 @@ export class CanvasStateApiModule extends CanvasModuleBase { * * The state is stored in redux. */ - getCanvasState = (): CanvasState => { - return this.runSelector(selectCanvasSlice); + getCanvasState = (): CanvasEntity => { + return this.runSelector(selectActiveCanvas); }; /** @@ -312,7 +308,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { * Gets the canvas settings from redux. */ getSettings = () => { - return this.runSelector(selectCanvasSettingsSlice); + return this.runSelector((state) => selectCanvasSettingsByCanvasId(state, this.manager.canvasId)); }; /** @@ -371,7 +367,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { * Gets the canvas staging area state from redux. */ getStagingArea = () => { - return this.runSelector(selectCanvasSessionSlice); + return this.runSelector((state) => selectCanvasStagingAreaByCanvasId(state, this.manager.canvasId)); }; /** diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts index b9b8adae9b8..7b2193b0bd3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts @@ -12,8 +12,8 @@ import { getIsPrimaryMouseDown, getPrefixedId, } from 'features/controlLayers/konva/util'; -import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectCanvasByCanvasId } from 'features/controlLayers/store/selectors'; import type { CanvasControlLayerState, CanvasInpaintMaskState, @@ -135,8 +135,18 @@ export class CanvasToolModule extends CanvasModuleBase { this.subscriptions.add(this.manager.stage.$stageAttrs.listen(this.render)); this.subscriptions.add(this.manager.$isBusy.listen(this.render)); - this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasSettingsSlice, this.render)); - this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasSlice, this.render)); + this.subscriptions.add( + this.manager.stateApi.createStoreSubscription( + (state) => selectCanvasByCanvasId(state, this.manager.canvasId), + this.render + ) + ); + this.subscriptions.add( + this.manager.stateApi.createStoreSubscription( + (state) => selectCanvasSettingsByCanvasId(state, this.manager.canvasId), + this.render + ) + ); this.subscriptions.add( this.$tool.listen(() => { // On tool switch, reset mouse state diff --git a/invokeai/frontend/web/src/features/controlLayers/store/actions.ts b/invokeai/frontend/web/src/features/controlLayers/store/actions.ts index 9e1d9734cda..2198a9cf4c9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/actions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/actions.ts @@ -1,4 +1,10 @@ import { createAction } from '@reduxjs/toolkit'; +import type { ParameterModel } from 'features/parameters/types/parameterSchemas'; // Needed to split this from canvasSlice.ts to avoid circular dependencies export const canvasReset = createAction('canvas/canvasReset'); + +// Needed to split this from paramsSlice.ts to avoid circular dependencies +export const modelChanged = createAction<{ model: ParameterModel | null; previousModel?: ParameterModel | null }>( + 'params/modelChanged' +); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index bbeac05a1d2..24103b7ca26 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -1,102 +1,11 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; -import { createSelector, createSlice } from '@reduxjs/toolkit'; +import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { SliceConfig } from 'app/store/types'; -import type { RgbaColor } from 'features/controlLayers/store/types'; -import { RGBA_BLACK, RGBA_WHITE, zRgbaColor } from 'features/controlLayers/store/types'; -import { z } from 'zod'; +import type { CanvasSettingsState, RgbaColor } from 'features/controlLayers/store/types'; +import { RGBA_BLACK, RGBA_WHITE } from 'features/controlLayers/store/types'; +import { assert } from 'tsafe'; -const zAutoSwitchMode = z.enum(['off', 'switch_on_start', 'switch_on_finish']); -export type AutoSwitchMode = z.infer; - -const zCanvasSettingsState = z.object({ - /** - * Whether to show HUD (Heads-Up Display) on the canvas. - */ - showHUD: z.boolean(), - /** - * Whether to clip lines and shapes to the generation bounding box. If disabled, lines and shapes will be clipped to - * the canvas bounds. - */ - clipToBbox: z.boolean(), - /** - * Whether to show a dynamic grid on the canvas. If disabled, a checkerboard pattern will be shown instead. - */ - dynamicGrid: z.boolean(), - /** - * Whether to invert the scroll direction when adjusting the brush or eraser width with the scroll wheel. - */ - invertScrollForToolWidth: z.boolean(), - /** - * The width of the brush tool. - */ - brushWidth: z.int().gt(0), - /** - * The width of the eraser tool. - */ - eraserWidth: z.int().gt(0), - /** - * The colors to use when drawing lines or filling shapes. - */ - activeColor: z.enum(['bgColor', 'fgColor']), - bgColor: zRgbaColor, - fgColor: zRgbaColor, - /** - * Whether to composite inpainted/outpainted regions back onto the source image when saving canvas generations. - * - * If disabled, inpainted/outpainted regions will be saved with a transparent background. - * - * When `sendToCanvas` is disabled, this setting is ignored, masked regions will always be composited. - */ - outputOnlyMaskedRegions: z.boolean(), - /** - * Whether to automatically process the operations like filtering and auto-masking. - */ - autoProcess: z.boolean(), - /** - * The snap-to-grid setting for the canvas. - */ - snapToGrid: z.boolean(), - /** - * Whether to show progress on the canvas when generating images. - */ - showProgressOnCanvas: z.boolean(), - /** - * Whether to show the bounding box overlay on the canvas. - */ - bboxOverlay: z.boolean(), - /** - * Whether to preserve the masked region instead of inpainting it. - */ - preserveMask: z.boolean(), - /** - * Whether to show only raster layers while staging. - */ - isolatedStagingPreview: z.boolean(), - /** - * Whether to show only the selected layer while filtering, transforming, or doing other operations. - */ - isolatedLayerPreview: z.boolean(), - /** - * Whether to use pressure sensitivity for the brush and eraser tool when a pen device is used. - */ - pressureSensitivity: z.boolean(), - /** - * Whether to show the rule of thirds composition guide overlay on the canvas. - */ - ruleOfThirds: z.boolean(), - /** - * Whether to save all staging images to the gallery instead of keeping them as intermediate images. - */ - saveAllImagesToGallery: z.boolean(), - /** - * The auto-switch mode for the canvas staging area. - */ - stagingAreaAutoSwitch: zAutoSwitchMode, -}); - -type CanvasSettingsState = z.infer; -const getInitialState = (): CanvasSettingsState => ({ +export const getInitialCanvasSettings = (): CanvasSettingsState => ({ showHUD: true, clipToBbox: false, dynamicGrid: false, @@ -120,9 +29,9 @@ const getInitialState = (): CanvasSettingsState => ({ stagingAreaAutoSwitch: 'switch_on_start', }); -const slice = createSlice({ +export const canvasSettingsState = createSlice({ name: 'canvasSettings', - initialState: getInitialState(), + initialState: {} as CanvasSettingsState, reducers: { settingsClipToBboxChanged: (state, action: PayloadAction) => { state.clipToBbox = action.payload; @@ -200,6 +109,8 @@ const slice = createSlice({ }, }); +export const isCanvasSettingsStateAction = isAnyOf(...Object.values(canvasSettingsState.actions)); + export const { settingsClipToBboxChanged, settingsDynamicGridToggled, @@ -223,36 +134,44 @@ export const { settingsRuleOfThirdsToggled, settingsSaveAllImagesToGalleryToggled, settingsStagingAreaAutoSwitchChanged, -} = slice.actions; +} = canvasSettingsState.actions; -export const canvasSettingsSliceConfig: SliceConfig = { - slice, - schema: zCanvasSettingsState, - getInitialState, - persistConfig: { - migrate: (state) => zCanvasSettingsState.parse(state), - }, +export const selectCanvasSettingsByCanvasId = (state: RootState, canvasId: string) => { + const instance = state.canvas.canvases[canvasId]; + assert(instance, 'Canvas does not exist'); + return instance.settings; +}; +const selectActiveCanvasSettings = (state: RootState) => { + return state.canvas.canvases[state.canvas.activeCanvasId]!.settings; }; -export const selectCanvasSettingsSlice = (s: RootState) => s.canvasSettings; -const createCanvasSettingsSelector = (selector: Selector) => - createSelector(selectCanvasSettingsSlice, selector); +const buildActiveCanvasSettingsSelector = + (selector: Selector) => + (state: RootState) => + selector(selectActiveCanvasSettings(state)); -export const selectPreserveMask = createCanvasSettingsSelector((settings) => settings.preserveMask); -export const selectOutputOnlyMaskedRegions = createCanvasSettingsSelector( - (settings) => settings.outputOnlyMaskedRegions +export const selectPreserveMask = buildActiveCanvasSettingsSelector((state) => state.preserveMask); +export const selectOutputOnlyMaskedRegions = buildActiveCanvasSettingsSelector( + (state) => state.outputOnlyMaskedRegions ); -export const selectDynamicGrid = createCanvasSettingsSelector((settings) => settings.dynamicGrid); -export const selectBboxOverlay = createCanvasSettingsSelector((settings) => settings.bboxOverlay); -export const selectShowHUD = createCanvasSettingsSelector((settings) => settings.showHUD); -export const selectAutoProcess = createCanvasSettingsSelector((settings) => settings.autoProcess); -export const selectSnapToGrid = createCanvasSettingsSelector((settings) => settings.snapToGrid); -export const selectShowProgressOnCanvas = createCanvasSettingsSelector( - (canvasSettings) => canvasSettings.showProgressOnCanvas +export const selectDynamicGrid = buildActiveCanvasSettingsSelector((state) => state.dynamicGrid); +export const selectInvertScrollForToolWidth = buildActiveCanvasSettingsSelector( + (state) => state.invertScrollForToolWidth ); -export const selectIsolatedStagingPreview = createCanvasSettingsSelector((settings) => settings.isolatedStagingPreview); -export const selectIsolatedLayerPreview = createCanvasSettingsSelector((settings) => settings.isolatedLayerPreview); -export const selectPressureSensitivity = createCanvasSettingsSelector((settings) => settings.pressureSensitivity); -export const selectRuleOfThirds = createCanvasSettingsSelector((settings) => settings.ruleOfThirds); -export const selectSaveAllImagesToGallery = createCanvasSettingsSelector((settings) => settings.saveAllImagesToGallery); -export const selectStagingAreaAutoSwitch = createCanvasSettingsSelector((settings) => settings.stagingAreaAutoSwitch); +export const selectBboxOverlay = buildActiveCanvasSettingsSelector((state) => state.bboxOverlay); +export const selectShowHUD = buildActiveCanvasSettingsSelector((state) => state.showHUD); +export const selectClipToBbox = buildActiveCanvasSettingsSelector((state) => state.clipToBbox); +export const selectAutoProcess = buildActiveCanvasSettingsSelector((state) => state.autoProcess); +export const selectSnapToGrid = buildActiveCanvasSettingsSelector((state) => state.snapToGrid); +export const selectShowProgressOnCanvas = buildActiveCanvasSettingsSelector((state) => state.showProgressOnCanvas); +export const selectIsolatedStagingPreview = buildActiveCanvasSettingsSelector((state) => state.isolatedStagingPreview); +export const selectIsolatedLayerPreview = buildActiveCanvasSettingsSelector((state) => state.isolatedLayerPreview); +export const selectPressureSensitivity = buildActiveCanvasSettingsSelector((state) => state.pressureSensitivity); +export const selectRuleOfThirds = buildActiveCanvasSettingsSelector((state) => state.ruleOfThirds); +export const selectSaveAllImagesToGallery = buildActiveCanvasSettingsSelector((state) => state.saveAllImagesToGallery); +export const selectStagingAreaAutoSwitch = buildActiveCanvasSettingsSelector((state) => state.stagingAreaAutoSwitch); +export const selectActiveColor = buildActiveCanvasSettingsSelector((state) => state.activeColor); +export const selectBgColor = buildActiveCanvasSettingsSelector((state) => state.bgColor); +export const selectFgColor = buildActiveCanvasSettingsSelector((state) => state.fgColor); +export const selectBrushWidth = buildActiveCanvasSettingsSelector((state) => state.brushWidth); +export const selectEraserWidth = buildActiveCanvasSettingsSelector((state) => state.eraserWidth); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index ee1a9c6ba44..6fb10768d8a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -1,13 +1,14 @@ import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { SliceConfig } from 'app/store/types'; +import { extractTabActionContext } from 'app/store/util'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; +import { isPlainObject } from 'es-toolkit'; import { merge } from 'es-toolkit/compat'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { canvasReset } from 'features/controlLayers/store/actions'; -import { modelChanged } from 'features/controlLayers/store/paramsSlice'; +import { canvasReset, modelChanged } from 'features/controlLayers/store/actions'; import { selectAllEntities, selectAllEntitiesOfType, @@ -15,10 +16,15 @@ import { selectRegionalGuidanceReferenceImage, } from 'features/controlLayers/store/selectors'; import type { + CanvasEntity, CanvasEntityStateFromType, CanvasEntityType, CanvasInpaintMaskState, + CanvasInstanceStateBase, + CanvasInstanceStateWithHistory, CanvasMetadata, + CanvasState, + CanvasStateWithHistory, ChannelName, ChannelPoints, ControlLoRAConfig, @@ -40,6 +46,7 @@ import { API_BASE_MODELS } from 'features/parameters/types/constants'; import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect } from 'konva/lib/types'; import type { UndoableOptions } from 'redux-undo'; +import undoable, { newHistory } from 'redux-undo'; import { type ControlLoRAModelConfig, type ControlNetModelConfig, @@ -50,15 +57,23 @@ import { isIPAdapterModelConfig, type T2IAdapterModelConfig, } from 'services/api/types'; +import { assert } from 'tsafe'; +import { canvasSettingsState, getInitialCanvasSettings, isCanvasSettingsStateAction } from './canvasSettingsSlice'; +import { + canvasStagingAreaState, + getInitialCanvasStagingAreaState, + isCanvasStagingAreaStateAction, +} from './canvasStagingAreaSlice'; +import { getInitialTabInstanceParamsState, isTabInstanceParamsAction, tabInstanceParamsSlice } from './tabSlice'; import type { AspectRatioID, BoundingBoxScaleMethod, CanvasControlLayerState, CanvasEntityIdentifier, + CanvasInstanceState, CanvasRasterLayerState, CanvasRegionalGuidanceState, - CanvasState, CLIPVisionModelV2, ControlModeV2, ControlNetConfig, @@ -78,7 +93,6 @@ import { FLUX_KONTEXT_ASPECT_RATIOS, GEMINI_2_5_ASPECT_RATIOS, getEntityIdentifier, - getInitialCanvasState, IMAGEN_ASPECT_RATIOS, isChatGPT4oAspectRatioID, isFluxKontextAspectRatioID, @@ -86,7 +100,8 @@ import { isImagenAspectRatioID, isRegionalGuidanceFLUXReduxConfig, isRegionalGuidanceIPAdapterConfig, - zCanvasState, + zCanvasStateWithHistory, + zCanvasStateWithoutHistory, } from './types'; import { converters, @@ -104,11 +119,178 @@ import { makeDefaultRasterLayerAdjustments, } from './util'; -const slice = createSlice({ +const getInitialCanvasEntity = (): CanvasEntity => ({ + selectedEntityIdentifier: null, + bookmarkedEntityIdentifier: null, + inpaintMasks: { isHidden: false, entities: [] }, + rasterLayers: { isHidden: false, entities: [] }, + controlLayers: { isHidden: false, entities: [] }, + regionalGuidance: { isHidden: false, entities: [] }, + bbox: { + rect: { x: 0, y: 0, width: 512, height: 512 }, + aspectRatio: deepClone(DEFAULT_ASPECT_RATIO_CONFIG), + scaleMethod: 'auto', + scaledSize: { width: 512, height: 512 }, + modelBase: 'sd-1', + }, +}); + +const getInitialCanvasInstanceState = (id: string, name: string): CanvasInstanceState => ({ + id, + name, + canvas: getInitialCanvasEntity(), + params: getInitialTabInstanceParamsState(), + settings: getInitialCanvasSettings(), + staging: getInitialCanvasStagingAreaState(), +}); + +const getInitialCanvasInstanceHistoryState = (id: string, name: string): CanvasInstanceStateWithHistory => { + const instance = getInitialCanvasInstanceState(id, name); + + return { + ...instance, + canvas: newHistory([], instance.canvas, []), + }; +}; + +const getInitialCanvasState = (): CanvasState => { + const canvasId = getPrefixedId('canvas'); + const canvasName = getNextCanvasName([]); + const canvas = getInitialCanvasInstanceState(canvasId, canvasName); + + return { + _version: 4, + activeCanvasId: canvasId, + canvases: { [canvasId]: canvas }, + }; +}; + +const getInitialCanvasHistoryState = (): CanvasStateWithHistory => { + const state = getInitialCanvasState(); + + return { + ...state, + canvases: Object.fromEntries( + Object.entries(state.canvases).map(([canvasId, instance]) => [ + canvasId, + { ...instance, canvas: newHistory([], instance.canvas, []) }, + ]) + ), + }; +}; + +const getNextCanvasName = (canvases: CanvasInstanceStateBase[]): string => { + for (let i = 1; ; i++) { + const name = `Canvas-${i}`; + if (!canvases.some((c) => c.name === name)) { + return name; + } + } +}; + +const canvasSlice = createSlice({ name: 'canvas', - initialState: getInitialCanvasState(), + initialState: getInitialCanvasHistoryState(), + reducers: { + canvasAdded: { + reducer: (state, action: PayloadAction<{ canvasId: string; isSelected?: boolean }>) => { + const { canvasId, isSelected } = action.payload; + + const name = getNextCanvasName(Object.values(state.canvases)); + const canvas = getInitialCanvasInstanceHistoryState(canvasId, name); + + state.canvases[canvasId] = canvas; + + if (isSelected) { + state.activeCanvasId = canvasId; + } + }, + prepare: (payload: { isSelected?: boolean }) => { + return { + payload: { ...payload, canvasId: getPrefixedId('canvas') }, + }; + }, + }, + canvasActivated: (state, action: PayloadAction<{ canvasId: string }>) => { + const { canvasId } = action.payload; + + const canvas = state.canvases[canvasId]; + if (!canvas) { + return; + } + + state.activeCanvasId = canvas.id; + }, + canvasDeleted: (state, action: PayloadAction<{ canvasId: string }>) => { + const { canvasId } = action.payload; + const canvasIds = Object.keys(state.canvases); + + const canvas = state.canvases[canvasId]; + if (!canvas) { + return; + } + + if (canvasIds.length === 1) { + throw new Error('Last canvas cannot be deleted'); + } + + const index = canvasIds.indexOf(canvas.id); + const nextIndex = index > 0 ? index - 1 : index + 1; + + state.activeCanvasId = canvasIds[nextIndex]!; + delete state.canvases[canvas.id]; + }, + }, + extraReducers(builder) { + builder.addMatcher(isCanvasInstanceAction, (state, action) => { + const context = extractTabActionContext(action); + + if (!context || context.tab !== 'canvas' || !context.canvasId) { + return; + } + + const canvasInstance = state.canvases[context.canvasId]; + if (!canvasInstance) { + return; + } + + state.canvases[context.canvasId] = canvasInstanceState.reducer(canvasInstance, action); + }); + }, +}); + +const canvasInstanceState = createSlice({ + name: 'canvasInstance', + initialState: {} as CanvasInstanceStateWithHistory, + reducers: { + canvasNameChanged: (state, action: PayloadAction<{ name: string }>) => { + const { name } = action.payload; + + state.name = name; + }, + }, + extraReducers(builder) { + builder.addDefaultCase((state, action) => { + if (isCanvasEntityStateAction(action)) { + state.canvas = undoableCanvasEntityReducer(state.canvas, action); + } + if (isTabInstanceParamsAction(action)) { + state.params = tabInstanceParamsSlice.reducer(state.params, action); + } + if (isCanvasSettingsStateAction(action)) { + state.settings = canvasSettingsState.reducer(state.settings, action); + } + if (isCanvasStagingAreaStateAction(action)) { + state.staging = canvasStagingAreaState.reducer(state.staging, action); + } + }); + }, +}); + +const canvasEntityState = createSlice({ + name: 'canvasEntity', + initialState: {} as CanvasEntity, reducers: { - // undoable canvas state //#region Raster layers rasterLayerAdjustmentsSet: ( state, @@ -1521,7 +1703,7 @@ const slice = createSlice({ entityDeleted: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - let selectedEntityIdentifier: CanvasState['selectedEntityIdentifier'] = null; + let selectedEntityIdentifier: CanvasEntity['selectedEntityIdentifier'] = null; const allEntities = selectAllEntities(state); const index = allEntities.findIndex((entity) => entity.id === entityIdentifier.id); const nextIndex = allEntities.length > 1 ? (index + 1) % allEntities.length : -1; @@ -1584,7 +1766,7 @@ const slice = createSlice({ moveToStart(selectAllEntitiesOfType(state, entity.type), entity); }, entitiesReordered: ( - state: CanvasState, + state: CanvasEntity, action: PayloadAction<{ type: T; entityIdentifiers: CanvasEntityIdentifier[] }> ) => { const { type, entityIdentifiers } = action.payload; @@ -1655,7 +1837,7 @@ const slice = createSlice({ }, allEntitiesDeleted: (state) => { // Deleting all entities is equivalent to resetting the state for each entity type - const initialState = getInitialCanvasState(); + const initialState = getInitialCanvasEntity(); state.rasterLayers = initialState.rasterLayers; state.controlLayers = initialState.controlLayers; state.inpaintMasks = initialState.inpaintMasks; @@ -1675,7 +1857,7 @@ const slice = createSlice({ }, extraReducers(builder) { builder.addCase(canvasReset, (state) => { - return resetState(state); + return resetCanvasState(state); }); builder.addCase(modelChanged, (state, action) => { const { model } = action.payload; @@ -1714,8 +1896,8 @@ const slice = createSlice({ }, }); -const resetState = (state: CanvasState) => { - const newState = getInitialCanvasState(); +const resetCanvasState = (state: CanvasEntity) => { + const newState = getInitialCanvasEntity(); // We need to retain the optimal dimension across resets, as it is changed only when the model changes. Copy it // from the old state, then recalculate the bbox size & scaled size. @@ -1728,11 +1910,50 @@ const resetState = (state: CanvasState) => { ); newState.bbox.rect.width = rect.width; newState.bbox.rect.height = rect.height; + syncScaledSize(newState); +}; - return newState; +const syncScaledSize = (state: CanvasEntity) => { + if (API_BASE_MODELS.includes(state.bbox.modelBase)) { + // Imagen3 has fixed sizes. Scaled bbox is not supported. + return; + } + if (state.bbox.scaleMethod === 'auto') { + // Sync both aspect ratio and size + const { width, height } = state.bbox.rect; + state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.modelBase); + } else if (state.bbox.scaleMethod === 'manual' && state.bbox.aspectRatio.isLocked) { + // Only sync the aspect ratio if manual & locked + state.bbox.scaledSize = calculateNewSize( + state.bbox.aspectRatio.value, + state.bbox.scaledSize.width * state.bbox.scaledSize.height, + state.bbox.modelBase + ); + } }; +export const isCanvasInstanceAction = (action: UnknownAction) => + isCanvasInstanceStateAction(action) || + isCanvasEntityStateAction(action) || + isTabInstanceParamsAction(action) || + isCanvasSettingsStateAction(action) || + isCanvasStagingAreaStateAction(action); +const isCanvasInstanceStateAction = isAnyOf(...Object.values(canvasInstanceState.actions)); +const isCanvasEntityStateAction = isAnyOf(...Object.values(canvasEntityState.actions), modelChanged, canvasReset); + +export const { + // Canvas + canvasAdded, + canvasDeleted, + canvasActivated, +} = canvasSlice.actions; + +export const { + canvasNameChanged, + // inpaintMaskRecalled, +} = canvasInstanceState.actions; + export const { canvasMetadataRecalled, canvasUndo, @@ -1829,37 +2050,18 @@ export const { inpaintMaskDenoiseLimitChanged, inpaintMaskDenoiseLimitDeleted, // inpaintMaskRecalled, -} = slice.actions; - -const syncScaledSize = (state: CanvasState) => { - if (API_BASE_MODELS.includes(state.bbox.modelBase)) { - // Imagen3 has fixed sizes. Scaled bbox is not supported. - return; - } - if (state.bbox.scaleMethod === 'auto') { - // Sync both aspect ratio and size - const { width, height } = state.bbox.rect; - state.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, state.bbox.modelBase); - } else if (state.bbox.scaleMethod === 'manual' && state.bbox.aspectRatio.isLocked) { - // Only sync the aspect ratio if manual & locked - state.bbox.scaledSize = calculateNewSize( - state.bbox.aspectRatio.value, - state.bbox.scaledSize.width * state.bbox.scaledSize.height, - state.bbox.modelBase - ); - } -}; +} = canvasEntityState.actions; let filter = true; -const canvasUndoableConfig: UndoableOptions = { +const canvasEntityUndoableConfig: UndoableOptions = { limit: 64, undoType: canvasUndo.type, redoType: canvasRedo.type, clearHistoryType: canvasClearHistory.type, filter: (action, _state, _history) => { - // Ignore all actions from other slices - if (!action.type.startsWith(slice.name)) { + // Ignore both all actions from other slices and canvas management actions + if (!action.type.startsWith(canvasEntityState.name)) { return false; } // Throttle rapid actions of the same type @@ -1870,15 +2072,70 @@ const canvasUndoableConfig: UndoableOptions = { // debug: import.meta.env.MODE === 'development', }; -export const canvasSliceConfig: SliceConfig = { - slice, +const undoableCanvasEntityReducer = undoable(canvasEntityState.reducer, canvasEntityUndoableConfig); + +export const canvasSliceConfig: SliceConfig = { + slice: canvasSlice, getInitialState: getInitialCanvasState, - schema: zCanvasState, + schema: zCanvasStateWithHistory, persistConfig: { - migrate: (state) => zCanvasState.parse(state), - }, - undoableConfig: { - reduxUndoOptions: canvasUndoableConfig, + migrate: (state) => { + assert(isPlainObject(state)); + if (state._version === 3) { + // Migrate from v3 to v4: slice represented a canvas instance -> slice represents multiple canvas instances + const canvasId = getPrefixedId('canvas'); + const canvasName = getNextCanvasName([]); + + const canvas = { + id: canvasId, + name: canvasName, + canvas: { ...state }, + settings: getInitialCanvasSettings(), + } as CanvasInstanceState; + + state = { + _version: 4, + activeCanvasId: canvas.id, + canvases: { [canvasId]: canvas }, + migration: { + isMultiCanvasMigrationPending: true, + }, + }; + } + return zCanvasStateWithoutHistory.parse(state); + }, + serialize: (state) => { + return { + _version: state._version, + activeCanvasId: state.activeCanvasId, + canvases: Object.fromEntries( + Object.entries(state.canvases).map(([canvasId, instance]) => [ + canvasId, + { + ...instance, + canvas: instance.canvas.present, + }, + ]) + ), + }; + }, + deserialize: (state) => { + const canvasState = state as CanvasState; + + return { + _version: canvasState._version, + activeCanvasId: canvasState.activeCanvasId, + canvases: Object.fromEntries( + Object.entries(canvasState.canvases).map(([canvasId, instance]) => [ + canvasId, + { + ...instance, + canvas: newHistory([], instance.canvas, []), + }, + ]) + ), + }; + }, }, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 694abcda1c6..f7843003b51 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -1,34 +1,27 @@ -import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSelector, createSlice, isAnyOf } from '@reduxjs/toolkit'; import { EMPTY_ARRAY } from 'app/store/constants'; import type { RootState } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; -import type { SliceConfig } from 'app/store/types'; -import { isPlainObject } from 'es-toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { useMemo } from 'react'; import { queueApi } from 'services/api/endpoints/queue'; import { assert } from 'tsafe'; -import z from 'zod'; -const zCanvasStagingAreaState = z.object({ - _version: z.literal(1), - canvasSessionId: z.string(), - canvasDiscardedQueueItems: z.array(z.number().int()), -}); -type CanvasStagingAreaState = z.infer; +import { selectActiveCanvasId } from './selectors'; +import type { CanvasStagingAreaState } from './types'; -const getInitialState = (): CanvasStagingAreaState => ({ +export const getInitialCanvasStagingAreaState = (): CanvasStagingAreaState => ({ _version: 1, canvasSessionId: getPrefixedId('canvas'), canvasDiscardedQueueItems: [], }); -const slice = createSlice({ +export const canvasStagingAreaState = createSlice({ name: 'canvasSession', - initialState: getInitialState(), + initialState: {} as CanvasStagingAreaState, reducers: { canvasQueueItemDiscarded: (state, action: PayloadAction<{ itemId: number }>) => { const { itemId } = action.payload; + if (!state.canvasDiscardedQueueItems.includes(itemId)) { state.canvasDiscardedQueueItems.push(itemId); } @@ -36,6 +29,7 @@ const slice = createSlice({ canvasSessionReset: { reducer: (state, action: PayloadAction<{ canvasSessionId: string }>) => { const { canvasSessionId } = action.payload; + state.canvasSessionId = canvasSessionId; state.canvasDiscardedQueueItems = []; }, @@ -50,52 +44,52 @@ const slice = createSlice({ }, }); -export const { canvasSessionReset, canvasQueueItemDiscarded } = slice.actions; +export const isCanvasStagingAreaStateAction = isAnyOf(...Object.values(canvasStagingAreaState.actions)); -export const canvasSessionSliceConfig: SliceConfig = { - slice, - schema: zCanvasStagingAreaState, - getInitialState, - persistConfig: { - migrate: (state) => { - assert(isPlainObject(state)); - if (!('_version' in state)) { - state._version = 1; - state.canvasSessionId = state.canvasSessionId ?? getPrefixedId('canvas'); - } +export const { canvasSessionReset, canvasQueueItemDiscarded } = canvasStagingAreaState.actions; - return zCanvasStagingAreaState.parse(state); - }, - }, +const findCanvasStagingAreaByCanvasId = (state: RootState, canvasId: string) => { + const instance = state.canvas.canvases[canvasId]; + assert(instance, 'Canvas does not exist'); + return instance.staging; }; - -export const selectCanvasSessionSlice = (s: RootState) => s[slice.name]; -export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ canvasSessionId }) => canvasSessionId); - -const selectDiscardedItems = createSelector( - selectCanvasSessionSlice, - ({ canvasDiscardedQueueItems }) => canvasDiscardedQueueItems -); - -export const buildSelectCanvasQueueItems = (sessionId: string) => +export const selectCanvasStagingAreaByCanvasId = (state: RootState, canvasId: string) => + findCanvasStagingAreaByCanvasId(state, canvasId); +const selectActiveCanvasStagingArea = (state: RootState) => { + const canvasId = selectActiveCanvasId(state); + return findCanvasStagingAreaByCanvasId(state, canvasId); +}; +const selectCanvasStagingAreaBySessionId = (state: RootState, sessionId: string) => { + const instance = Object.values(state.canvas.canvases).find((canvas) => canvas.staging.canvasSessionId === sessionId); + assert(instance, 'Canvas does not exist'); + return instance.staging; +}; +export const selectCanvasStagingAreaSessionId = (state: RootState, canvasId: string) => { + const session = selectCanvasStagingAreaByCanvasId(state, canvasId); + return session.canvasSessionId; +}; +export const selectActiveCanvasStagingAreaSessionId = (state: RootState) => { + const session = selectActiveCanvasStagingArea(state); + return session.canvasSessionId; +}; +const selectCanvasStagingAreaDiscardedItemsBySessionId = (state: RootState, sessionId: string) => { + const session = selectCanvasStagingAreaBySessionId(state, sessionId); + return session.canvasDiscardedQueueItems; +}; +export const buildSelectCanvasQueueItemsBySessionId = (sessionId: string) => createSelector( - [queueApi.endpoints.listAllQueueItems.select({ destination: sessionId }), selectDiscardedItems], + queueApi.endpoints.listAllQueueItems.select({ destination: sessionId }), + (state: RootState) => selectCanvasStagingAreaDiscardedItemsBySessionId(state, sessionId), ({ data }, discardedItems) => { if (!data) { return EMPTY_ARRAY; } return data.filter( - ({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems.includes(item_id) + ({ status, item_id }) => status !== 'canceled' && status !== 'failed' && !discardedItems?.includes(item_id) ); } ); - -export const buildSelectIsStaging = (sessionId: string) => - createSelector([buildSelectCanvasQueueItems(sessionId)], (queueItems) => { +export const buildSelectIsStagingBySessionId = (sessionId: string) => + createSelector(buildSelectCanvasQueueItemsBySessionId(sessionId), (queueItems) => { return queueItems.length > 0; }); -export const useCanvasIsStaging = () => { - const sessionId = useAppSelector(selectCanvasSessionId); - const selector = useMemo(() => buildSelectIsStaging(sessionId), [sessionId]); - return useAppSelector(selector); -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ephemeral.ts b/invokeai/frontend/web/src/features/controlLayers/store/ephemeral.ts index 5b449ca92a1..7a679071b72 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/ephemeral.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/ephemeral.ts @@ -4,6 +4,6 @@ import { atom } from 'nanostores'; // Ephemeral state for canvas - not persisted across sessions. /** - * The global canvas manager instance. + * The global canvas manager instances. */ -export const $canvasManager = atom(null); +export const $canvasManagers = atom<{ [canvasId: string]: CanvasManager }>({}); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts index dfde382fcab..e6f0f11ab87 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts @@ -1,13 +1,13 @@ -import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import { createSlice, isAnyOf, type PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { SliceConfig } from 'app/store/types'; import type { NumericalParameterConfig } from 'app/types/invokeai'; import { paramsReset } from 'features/controlLayers/store/paramsSlice'; -import { type LoRA, zLoRA } from 'features/controlLayers/store/types'; +import type { LoRA, LoRAsState } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { LoRAModelConfig } from 'services/api/types'; import { v4 as uuidv4 } from 'uuid'; -import z from 'zod'; + +import { selectActiveCanvasId, selectActiveTab } from './selectors'; export const DEFAULT_LORA_WEIGHT_CONFIG: NumericalParameterConfig = { initial: 0.75, @@ -19,20 +19,13 @@ export const DEFAULT_LORA_WEIGHT_CONFIG: NumericalParameterConfig = { coarseStep: 0.05, }; -const zLoRAsState = z.object({ - loras: z.array(zLoRA), -}); -type LoRAsState = z.infer; - -const getInitialState = (): LoRAsState => ({ +export const getInitialLoRAsState = (): LoRAsState => ({ loras: [], }); -const selectLoRA = (state: LoRAsState, id: string) => state.loras.find((lora) => lora.id === id); - -const slice = createSlice({ +export const lorasSlice = createSlice({ name: 'loras', - initialState: getInitialState(), + initialState: {} as LoRAsState, reducers: { loraAdded: { reducer: (state, action: PayloadAction<{ model: LoRAModelConfig; id: string }>) => { @@ -57,7 +50,7 @@ const slice = createSlice({ }, loraWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { const { id, weight } = action.payload; - const lora = selectLoRA(state, id); + const lora = findLoRA(state, id); if (!lora) { return; } @@ -65,7 +58,7 @@ const slice = createSlice({ }, loraIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { const { id, isEnabled } = action.payload; - const lora = selectLoRA(state, id); + const lora = findLoRA(state, id); if (!lora) { return; } @@ -78,26 +71,42 @@ const slice = createSlice({ extraReducers(builder) { builder.addCase(paramsReset, () => { // When a new session is requested, clear all LoRAs - return getInitialState(); + return getInitialLoRAsState(); }); }, }); +export const isLoRAsStateAction = isAnyOf(...Object.values(lorasSlice.actions), paramsReset); + export const { loraAdded, loraRecalled, loraDeleted, loraWeightChanged, loraIsEnabledChanged, loraAllDeleted } = - slice.actions; + lorasSlice.actions; -export const lorasSliceConfig: SliceConfig = { - slice, - schema: zLoRAsState, - getInitialState, - persistConfig: { - migrate: (state) => zLoRAsState.parse(state), - }, +const initialLoRAsState = getInitialLoRAsState(); + +const selectActiveTabLoRAs = (state: RootState) => { + const tab = selectActiveTab(state); + const canvasId = selectActiveCanvasId(state); + + switch (tab) { + case 'generate': + return state.tab.generate.loras; + case 'canvas': + return state.canvas.canvases[canvasId]!.params.loras; + case 'upscaling': + return state.tab.upscaling.loras; + case 'video': + return state.tab.video.loras; + default: + // Fallback for global controls in other tabs + return initialLoRAsState; + } }; -export const selectLoRAsSlice = (state: RootState) => state.loras; -export const selectAddedLoRAs = createSelector(selectLoRAsSlice, (loras) => loras.loras); -export const buildSelectLoRA = (id: string) => - createSelector([selectLoRAsSlice], (loras) => { - return selectLoRA(loras, id); - }); +const findLoRA = (state: LoRAsState, id: string) => state.loras.find((lora) => lora.id === id); + +export const selectLoRAsSlice = (state: RootState) => selectActiveTabLoRAs(state); +export const selectAddedLoRAs = (state: RootState) => selectActiveTabLoRAs(state).loras; +export const selectLoRA = (state: RootState, id: string) => { + const loras = selectLoRAsSlice(state); + return findLoRA(loras, id); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index 3f148c5efbb..f1d528f1f83 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -1,10 +1,8 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; -import { createSelector, createSlice } from '@reduxjs/toolkit'; +import { createSelector, createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { SliceConfig } from 'app/store/types'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; -import { isPlainObject } from 'es-toolkit'; import { clamp } from 'es-toolkit/compat'; import type { AspectRatioID, ParamsState, RgbaColor } from 'features/controlLayers/store/types'; import { @@ -13,7 +11,6 @@ import { DEFAULT_ASPECT_RATIO_CONFIG, FLUX_KONTEXT_ASPECT_RATIOS, GEMINI_2_5_ASPECT_RATIOS, - getInitialParamsState, IMAGEN_ASPECT_RATIOS, isChatGPT4oAspectRatioID, isFluxKontextAspectRatioID, @@ -54,11 +51,65 @@ import type { import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models'; import { isNonRefinerMainModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; -const slice = createSlice({ +import { modelChanged } from './actions'; +import { selectActiveCanvasId, selectActiveTab } from './selectors'; + +export const getInitialParamsState = (): ParamsState => ({ + maskBlur: 16, + maskBlurMethod: 'box', + canvasCoherenceMode: 'Gaussian Blur', + canvasCoherenceMinDenoise: 0, + canvasCoherenceEdgeSize: 16, + infillMethod: 'lama', + infillTileSize: 32, + infillPatchmatchDownscaleSize: 1, + infillColorValue: { r: 0, g: 0, b: 0, a: 1 }, + cfgScale: 7.5, + cfgRescaleMultiplier: 0, + guidance: 4, + img2imgStrength: 0.75, + optimizedDenoisingEnabled: true, + iterations: 1, + scheduler: 'dpmpp_3m_k', + upscaleScheduler: 'kdpm_2', + upscaleCfgScale: 2, + seed: 0, + shouldRandomizeSeed: true, + steps: 30, + model: null, + vae: null, + vaePrecision: 'fp32', + fluxVAE: null, + seamlessXAxis: false, + seamlessYAxis: false, + clipSkip: 0, + shouldUseCpuNoise: true, + positivePrompt: '', + positivePromptHistory: [], + negativePrompt: null, + refinerModel: null, + refinerSteps: 20, + refinerCFGScale: 7.5, + refinerScheduler: 'euler', + refinerPositiveAestheticScore: 6, + refinerNegativeAestheticScore: 2.5, + refinerStart: 0.8, + t5EncoderModel: null, + clipEmbedModel: null, + clipLEmbedModel: null, + clipGEmbedModel: null, + controlLora: null, + dimensions: { + width: 512, + height: 512, + aspectRatio: deepClone(DEFAULT_ASPECT_RATIO_CONFIG), + }, +}); + +export const paramsState = createSlice({ name: 'params', - initialState: getInitialParamsState(), + initialState: {} as ParamsState, reducers: { setIterations: (state, action: PayloadAction) => { state.iterations = action.payload; @@ -104,33 +155,6 @@ const slice = createSlice({ setShouldRandomizeSeed: (state, action: PayloadAction) => { state.shouldRandomizeSeed = action.payload; }, - modelChanged: ( - state, - action: PayloadAction<{ model: ParameterModel | null; previousModel?: ParameterModel | null }> - ) => { - const { previousModel } = action.payload; - const result = zParamsState.shape.model.safeParse(action.payload.model); - if (!result.success) { - return; - } - const model = result.data; - state.model = model; - - // If the model base changes (e.g. SD1.5 -> SDXL), we need to change a few things - if (model === null || previousModel?.base === model.base) { - return; - } - - if (API_BASE_MODELS.includes(model.base)) { - state.dimensions.aspectRatio.isLocked = true; - state.dimensions.aspectRatio.value = 1; - state.dimensions.aspectRatio.id = '1:1'; - state.dimensions.width = 1024; - state.dimensions.height = 1024; - } - - applyClipSkip(state, model, state.clipSkip); - }, vaeSelected: (state, action: PayloadAction) => { // null is a valid VAE! const result = zParamsState.shape.vae.safeParse(action.payload); @@ -403,6 +427,32 @@ const slice = createSlice({ }, paramsReset: (state) => resetState(state), }, + extraReducers(builder) { + builder.addCase(modelChanged, (state, action) => { + const { previousModel } = action.payload; + const result = zParamsState.shape.model.safeParse(action.payload.model); + if (!result.success) { + return; + } + const model = result.data; + state.model = model; + + // If the model base changes (e.g. SD1.5 -> SDXL), we need to change a few things + if (model === null || previousModel?.base === model.base) { + return; + } + + if (API_BASE_MODELS.includes(model.base)) { + state.dimensions.aspectRatio.isLocked = true; + state.dimensions.aspectRatio.value = 1; + state.dimensions.aspectRatio.id = '1:1'; + state.dimensions.width = 1024; + state.dimensions.height = 1024; + } + + applyClipSkip(state, model, state.clipSkip); + }); + }, }); const applyClipSkip = (state: { clipSkip: number }, model: ParameterModel | null, clipSkip: number) => { @@ -448,6 +498,8 @@ const resetState = (state: ParamsState): ParamsState => { return newState; }; +export const isTabParamsStateAction = isAnyOf(...Object.values(paramsState.actions), modelChanged); + export const { setInfillMethod, setInfillTileSize, @@ -492,7 +544,6 @@ export const { setRefinerPositiveAestheticScore, setRefinerNegativeAestheticScore, setRefinerStart, - modelChanged, // Dimensions sizeRecalled, @@ -505,45 +556,42 @@ export const { syncedToOptimalDimension, paramsReset, -} = slice.actions; - -export const paramsSliceConfig: SliceConfig = { - slice, - schema: zParamsState, - getInitialState: getInitialParamsState, - persistConfig: { - migrate: (state) => { - assert(isPlainObject(state)); - - if (!('_version' in state)) { - // v0 -> v1, add _version and remove x/y from dimensions, lifting width/height to top level - state._version = 1; - state.dimensions.width = state.dimensions.rect.width; - state.dimensions.height = state.dimensions.rect.height; - } - - if (state._version === 1) { - // v1 -> v2, add positive prompt history - state._version = 2; - state.positivePromptHistory = []; - } - - return zParamsState.parse(state); - }, - }, +} = paramsState.actions; + +const initialTabParamsState = getInitialParamsState(); + +export const selectActiveTabParams = (state: RootState) => { + const tab = selectActiveTab(state); + const canvasId = selectActiveCanvasId(state); + + switch (tab) { + case 'generate': + return state.tab.generate.params; + case 'canvas': + return state.canvas.canvases[canvasId]!.params.params; + case 'upscaling': + return state.tab.upscaling.params; + case 'video': + return state.tab.video.params; + default: + // Fallback for global controls in other tabs + return initialTabParamsState; + } }; -export const selectParamsSlice = (state: RootState) => state.params; -const createParamsSelector = (selector: Selector) => createSelector(selectParamsSlice, selector); - -export const selectBase = createParamsSelector((params) => params.model?.base); -export const selectIsSDXL = createParamsSelector((params) => params.model?.base === 'sdxl'); -export const selectIsFLUX = createParamsSelector((params) => params.model?.base === 'flux'); -export const selectIsSD3 = createParamsSelector((params) => params.model?.base === 'sd-3'); -export const selectIsCogView4 = createParamsSelector((params) => params.model?.base === 'cogview4'); -export const selectIsImagen3 = createParamsSelector((params) => params.model?.base === 'imagen3'); -export const selectIsImagen4 = createParamsSelector((params) => params.model?.base === 'imagen4'); -export const selectIsFluxKontext = createParamsSelector((params) => { +const buildActiveTabParamsSelector = + (selector: Selector) => + (state: RootState) => + selector(selectActiveTabParams(state)); + +export const selectBase = buildActiveTabParamsSelector((params) => params.model?.base); +export const selectIsSDXL = buildActiveTabParamsSelector((params) => params.model?.base === 'sdxl'); +export const selectIsFLUX = buildActiveTabParamsSelector((params) => params.model?.base === 'flux'); +export const selectIsSD3 = buildActiveTabParamsSelector((params) => params.model?.base === 'sd-3'); +export const selectIsCogView4 = buildActiveTabParamsSelector((params) => params.model?.base === 'cogview4'); +export const selectIsImagen3 = buildActiveTabParamsSelector((params) => params.model?.base === 'imagen3'); +export const selectIsImagen4 = buildActiveTabParamsSelector((params) => params.model?.base === 'imagen4'); +export const selectIsFluxKontext = buildActiveTabParamsSelector((params) => { if (params.model?.base === 'flux-kontext') { return true; } @@ -552,42 +600,46 @@ export const selectIsFluxKontext = createParamsSelector((params) => { } return false; }); -export const selectIsChatGPT4o = createParamsSelector((params) => params.model?.base === 'chatgpt-4o'); -export const selectIsGemini2_5 = createParamsSelector((params) => params.model?.base === 'gemini-2.5'); - -export const selectModel = createParamsSelector((params) => params.model); -export const selectModelKey = createParamsSelector((params) => params.model?.key); -export const selectVAE = createParamsSelector((params) => params.vae); -export const selectFLUXVAE = createParamsSelector((params) => params.fluxVAE); -export const selectVAEKey = createParamsSelector((params) => params.vae?.key); -export const selectT5EncoderModel = createParamsSelector((params) => params.t5EncoderModel); -export const selectCLIPEmbedModel = createParamsSelector((params) => params.clipEmbedModel); -export const selectCLIPLEmbedModel = createParamsSelector((params) => params.clipLEmbedModel); - -export const selectCLIPGEmbedModel = createParamsSelector((params) => params.clipGEmbedModel); - -export const selectCFGScale = createParamsSelector((params) => params.cfgScale); -export const selectGuidance = createParamsSelector((params) => params.guidance); -export const selectSteps = createParamsSelector((params) => params.steps); -export const selectCFGRescaleMultiplier = createParamsSelector((params) => params.cfgRescaleMultiplier); -export const selectCLIPSkip = createParamsSelector((params) => params.clipSkip); -export const selectHasModelCLIPSkip = createParamsSelector((params) => hasModelClipSkip(params.model)); -export const selectCanvasCoherenceEdgeSize = createParamsSelector((params) => params.canvasCoherenceEdgeSize); -export const selectCanvasCoherenceMinDenoise = createParamsSelector((params) => params.canvasCoherenceMinDenoise); -export const selectCanvasCoherenceMode = createParamsSelector((params) => params.canvasCoherenceMode); -export const selectMaskBlur = createParamsSelector((params) => params.maskBlur); -export const selectInfillMethod = createParamsSelector((params) => params.infillMethod); -export const selectInfillTileSize = createParamsSelector((params) => params.infillTileSize); -export const selectInfillPatchmatchDownscaleSize = createParamsSelector( +export const selectIsChatGPT4o = buildActiveTabParamsSelector((params) => params.model?.base === 'chatgpt-4o'); +export const selectIsGemini2_5 = buildActiveTabParamsSelector((params) => params.model?.base === 'gemini-2.5'); + +export const selectModel = buildActiveTabParamsSelector((params) => params.model); +export const selectModelKey = buildActiveTabParamsSelector((params) => params.model?.key); +export const selectVAE = buildActiveTabParamsSelector((params) => params.vae); +export const selectFLUXVAE = buildActiveTabParamsSelector((params) => params.fluxVAE); +export const selectVAEKey = buildActiveTabParamsSelector((params) => params.vae?.key); +export const selectT5EncoderModel = buildActiveTabParamsSelector((params) => params.t5EncoderModel); +export const selectCLIPEmbedModel = buildActiveTabParamsSelector((params) => params.clipEmbedModel); +export const selectCLIPLEmbedModel = buildActiveTabParamsSelector((params) => params.clipLEmbedModel); + +export const selectCLIPGEmbedModel = buildActiveTabParamsSelector((params) => params.clipGEmbedModel); + +export const selectCFGScale = buildActiveTabParamsSelector((params) => params.cfgScale); +export const selectGuidance = buildActiveTabParamsSelector((params) => params.guidance); +export const selectSteps = buildActiveTabParamsSelector((params) => params.steps); +export const selectCFGRescaleMultiplier = buildActiveTabParamsSelector((params) => params.cfgRescaleMultiplier); +export const selectCLIPSkip = buildActiveTabParamsSelector((params) => params.clipSkip); +export const selectHasModelCLIPSkip = buildActiveTabParamsSelector((params) => hasModelClipSkip(params.model)); +export const selectCanvasCoherenceEdgeSize = buildActiveTabParamsSelector((params) => params.canvasCoherenceEdgeSize); +export const selectCanvasCoherenceMinDenoise = buildActiveTabParamsSelector( + (params) => params.canvasCoherenceMinDenoise +); +export const selectCanvasCoherenceMode = buildActiveTabParamsSelector((params) => params.canvasCoherenceMode); +export const selectMaskBlur = buildActiveTabParamsSelector((params) => params.maskBlur); +export const selectInfillMethod = buildActiveTabParamsSelector((params) => params.infillMethod); +export const selectInfillTileSize = buildActiveTabParamsSelector((params) => params.infillTileSize); +export const selectInfillPatchmatchDownscaleSize = buildActiveTabParamsSelector( (params) => params.infillPatchmatchDownscaleSize ); -export const selectInfillColorValue = createParamsSelector((params) => params.infillColorValue); -export const selectImg2imgStrength = createParamsSelector((params) => params.img2imgStrength); -export const selectOptimizedDenoisingEnabled = createParamsSelector((params) => params.optimizedDenoisingEnabled); -export const selectPositivePrompt = createParamsSelector((params) => params.positivePrompt); -export const selectNegativePrompt = createParamsSelector((params) => params.negativePrompt); -export const selectNegativePromptWithFallback = createParamsSelector((params) => params.negativePrompt ?? ''); -export const selectHasNegativePrompt = createParamsSelector((params) => params.negativePrompt !== null); +export const selectInfillColorValue = buildActiveTabParamsSelector((params) => params.infillColorValue); +export const selectImg2imgStrength = buildActiveTabParamsSelector((params) => params.img2imgStrength); +export const selectOptimizedDenoisingEnabled = buildActiveTabParamsSelector( + (params) => params.optimizedDenoisingEnabled +); +export const selectPositivePrompt = buildActiveTabParamsSelector((params) => params.positivePrompt); +export const selectNegativePrompt = buildActiveTabParamsSelector((params) => params.negativePrompt); +export const selectNegativePromptWithFallback = buildActiveTabParamsSelector((params) => params.negativePrompt ?? ''); +export const selectHasNegativePrompt = buildActiveTabParamsSelector((params) => params.negativePrompt !== null); export const selectModelSupportsNegativePrompt = createSelector( selectModel, (model) => !!model && SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS.includes(model.base) @@ -616,41 +668,47 @@ export const selectModelSupportsOptimizedDenoising = createSelector( selectModel, (model) => !!model && SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS.includes(model.base) ); -export const selectScheduler = createParamsSelector((params) => params.scheduler); -export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis); -export const selectSeamlessYAxis = createParamsSelector((params) => params.seamlessYAxis); -export const selectSeed = createParamsSelector((params) => params.seed); -export const selectShouldRandomizeSeed = createParamsSelector((params) => params.shouldRandomizeSeed); -export const selectVAEPrecision = createParamsSelector((params) => params.vaePrecision); -export const selectIterations = createParamsSelector((params) => params.iterations); -export const selectShouldUseCPUNoise = createParamsSelector((params) => params.shouldUseCpuNoise); - -export const selectUpscaleScheduler = createParamsSelector((params) => params.upscaleScheduler); -export const selectUpscaleCfgScale = createParamsSelector((params) => params.upscaleCfgScale); - -export const selectPositivePromptHistory = createParamsSelector((params) => params.positivePromptHistory); -export const selectRefinerCFGScale = createParamsSelector((params) => params.refinerCFGScale); -export const selectRefinerModel = createParamsSelector((params) => params.refinerModel); -export const selectIsRefinerModelSelected = createParamsSelector((params) => Boolean(params.refinerModel)); -export const selectRefinerPositiveAestheticScore = createParamsSelector( +export const selectScheduler = buildActiveTabParamsSelector((params) => params.scheduler); +export const selectSeamlessXAxis = buildActiveTabParamsSelector((params) => params.seamlessXAxis); +export const selectSeamlessYAxis = buildActiveTabParamsSelector((params) => params.seamlessYAxis); +export const selectSeed = buildActiveTabParamsSelector((params) => params.seed); +export const selectShouldRandomizeSeed = buildActiveTabParamsSelector((params) => params.shouldRandomizeSeed); +export const selectVAEPrecision = buildActiveTabParamsSelector((params) => params.vaePrecision); +export const selectIterations = buildActiveTabParamsSelector((params) => params.iterations); +export const selectShouldUseCPUNoise = buildActiveTabParamsSelector((params) => params.shouldUseCpuNoise); + +export const selectUpscaleScheduler = buildActiveTabParamsSelector((params) => params.upscaleScheduler); +export const selectUpscaleCfgScale = buildActiveTabParamsSelector((params) => params.upscaleCfgScale); + +export const selectPositivePromptHistory = buildActiveTabParamsSelector((params) => params.positivePromptHistory); +export const selectRefinerCFGScale = buildActiveTabParamsSelector((params) => params.refinerCFGScale); +export const selectRefinerModel = buildActiveTabParamsSelector((params) => params.refinerModel); +export const selectIsRefinerModelSelected = buildActiveTabParamsSelector((params) => Boolean(params.refinerModel)); +export const selectRefinerPositiveAestheticScore = buildActiveTabParamsSelector( (params) => params.refinerPositiveAestheticScore ); -export const selectRefinerNegativeAestheticScore = createParamsSelector( +export const selectRefinerNegativeAestheticScore = buildActiveTabParamsSelector( (params) => params.refinerNegativeAestheticScore ); -export const selectRefinerScheduler = createParamsSelector((params) => params.refinerScheduler); -export const selectRefinerStart = createParamsSelector((params) => params.refinerStart); -export const selectRefinerSteps = createParamsSelector((params) => params.refinerSteps); - -export const selectWidth = createParamsSelector((params) => params.dimensions.width); -export const selectHeight = createParamsSelector((params) => params.dimensions.height); -export const selectAspectRatioID = createParamsSelector((params) => params.dimensions.aspectRatio.id); -export const selectAspectRatioValue = createParamsSelector((params) => params.dimensions.aspectRatio.value); -export const selectAspectRatioIsLocked = createParamsSelector((params) => params.dimensions.aspectRatio.isLocked); +export const selectRefinerScheduler = buildActiveTabParamsSelector((params) => params.refinerScheduler); +export const selectRefinerStart = buildActiveTabParamsSelector((params) => params.refinerStart); +export const selectRefinerSteps = buildActiveTabParamsSelector((params) => params.refinerSteps); + +export const selectWidth = buildActiveTabParamsSelector((params) => params.dimensions.width); +export const selectHeight = buildActiveTabParamsSelector((params) => params.dimensions.height); +export const selectAspectRatioID = buildActiveTabParamsSelector((params) => params.dimensions.aspectRatio.id); +export const selectAspectRatioValue = buildActiveTabParamsSelector((params) => params.dimensions.aspectRatio.value); +export const selectAspectRatioIsLocked = buildActiveTabParamsSelector( + (params) => params.dimensions.aspectRatio.isLocked +); +export const selectOptimalDimension = buildActiveTabParamsSelector((params) => + getOptimalDimension(params.model?.base ?? null) +); +export const selectGridSize = buildActiveTabParamsSelector((params) => getGridSize(params.model?.base ?? null)); export const selectMainModelConfig = createSelector( selectModelConfigsQuery, - selectParamsSlice, + selectActiveTabParams, (modelConfigs, { model }) => { if (!modelConfigs.data) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts index e787d08fca0..8282cb11a98 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts @@ -1,11 +1,11 @@ import { objectEquals } from '@observ33r/object-equals'; import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSelector, createSlice } from '@reduxjs/toolkit'; +import { createSelector, createSlice, isAnyOf } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import type { RootState } from 'app/store/store'; -import type { SliceConfig } from 'app/store/types'; import { clamp } from 'es-toolkit/compat'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import type { CroppableImageWithDims, FLUXReduxImageInfluence, @@ -21,8 +21,9 @@ import type { import { assert } from 'tsafe'; import type { PartialDeep } from 'type-fest'; +import { selectActiveCanvasId } from './selectors'; import type { CLIPVisionModelV2, IPMethodV2, RefImageState } from './types'; -import { getInitialRefImagesState, isFLUXReduxConfig, isIPAdapterConfig, zRefImagesState } from './types'; +import { isFLUXReduxConfig, isIPAdapterConfig } from './types'; import { getReferenceImageState, initialChatGPT4oReferenceImage, @@ -32,6 +33,12 @@ import { initialIPAdapter, } from './util'; +export const getInitialRefImagesState = (): RefImagesState => ({ + selectedEntityId: null, + isPanelOpen: false, + entities: [], +}); + type PayloadActionWithId = T extends void ? PayloadAction<{ id: string }> : PayloadAction< @@ -40,7 +47,7 @@ type PayloadActionWithId = T extends void } & T >; -const slice = createSlice({ +export const refImagesSlice = createSlice({ name: 'refImages', initialState: getInitialRefImagesState(), reducers: { @@ -264,6 +271,8 @@ const slice = createSlice({ }, }); +export const isRefImagesStateAction = isAnyOf(...Object.values(refImagesSlice.actions)); + export const { refImageSelected, refImageAdded, @@ -277,19 +286,32 @@ export const { refImageFLUXReduxImageInfluenceChanged, refImageIsEnabledToggled, refImagesRecalled, -} = slice.actions; +} = refImagesSlice.actions; -export const refImagesSliceConfig: SliceConfig = { - slice, - schema: zRefImagesState, - getInitialState: getInitialRefImagesState, - persistConfig: { - migrate: (state) => zRefImagesState.parse(state), - persistDenylist: ['selectedEntityId', 'isPanelOpen'], - }, +export const refImagesDenyList = ['selectedEntityId', 'isPanelOpen'] as const; + +const initialRefImages = getInitialRefImagesState(); + +const selectActiveTabRefImages = (state: RootState) => { + const tab = selectActiveTab(state); + const canvasId = selectActiveCanvasId(state); + + switch (tab) { + case 'generate': + return state.tab.generate.refImages; + case 'canvas': + return state.canvas.canvases[canvasId]!.params.refImages; + case 'upscaling': + return state.tab.upscaling.refImages; + case 'video': + return state.tab.video.refImages; + default: + // Fallback for global controls in other tabs + return initialRefImages; + } }; -export const selectRefImagesSlice = (state: RootState) => state.refImages; +export const selectRefImagesSlice = (state: RootState) => selectActiveTabRefImages(state); export const selectReferenceImageEntities = createSelector(selectRefImagesSlice, (state) => state.entities); export const selectSelectedRefEntityId = createSelector(selectRefImagesSlice, (state) => state.selectedEntityId); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 5c0abfdb892..12c000de424 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -1,9 +1,8 @@ -import type { Selector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import type { CanvasControlLayerState, + CanvasEntity, CanvasEntityIdentifier, CanvasEntityState, CanvasEntityType, @@ -11,18 +10,49 @@ import type { CanvasMetadata, CanvasRasterLayerState, CanvasRegionalGuidanceState, - CanvasState, } from 'features/controlLayers/store/types'; -import { getGridSize, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; +/** + * Selects the active tab + */ +export const selectActiveTab = (state: RootState) => state.tab.activeTab; + /** * Selects the canvas slice from the root state */ -export const selectCanvasSlice = (state: RootState) => state.canvas.present; +const selectCanvasSlice = (state: RootState) => state.canvas; + +/** + * Selects the canvases + */ +export const selectCanvases = createSelector(selectCanvasSlice, (state) => + Object.values(state.canvases).map((instance) => ({ + id: instance.id, + name: instance.name, + ...instance.canvas.present, + isActive: instance.id === state.activeCanvasId, + canDelete: Object.keys(state.canvases).length > 1, + })) +); + +/** + * Selects the active canvas with history + */ +const selectActiveCanvasWithHistory = createSelector( + selectCanvasSlice, + (state) => state.canvases[state.activeCanvasId]!.canvas +); + +export const selectActiveCanvas = createSelector(selectActiveCanvasWithHistory, (canvas) => canvas.present); +export const selectActiveCanvasId = createSelector(selectCanvasSlice, (state) => state.activeCanvasId); -const createCanvasSelector = (selector: Selector) => createSelector(selectCanvasSlice, selector); +export const selectCanvasByCanvasId = (state: RootState, canvasId: string) => { + const instance = selectCanvasSlice(state).canvases[canvasId]; + assert(instance, 'Canvas does not exist'); + return instance.canvas.present; +}; /** * Selects the total canvas entity count: @@ -34,7 +64,7 @@ const createCanvasSelector = (selector: Selector) => createSe * * All entities are counted, regardless of their state. */ -const selectEntityCountAll = createCanvasSelector((canvas) => { +const selectEntityCountAll = createSelector(selectActiveCanvas, (canvas) => { return ( canvas.regionalGuidance.entities.length + canvas.rasterLayers.entities.length + @@ -45,22 +75,25 @@ const selectEntityCountAll = createCanvasSelector((canvas) => { const isVisibleEntity = (entity: CanvasEntityState) => entity.isEnabled && entity.objects.length > 0; -export const selectRasterLayerEntities = createCanvasSelector((canvas) => canvas.rasterLayers.entities); +export const selectRasterLayerEntities = createSelector(selectActiveCanvas, (canvas) => canvas.rasterLayers.entities); export const selectActiveRasterLayerEntities = createSelector(selectRasterLayerEntities, (entities) => entities.filter(isVisibleEntity) ); -export const selectControlLayerEntities = createCanvasSelector((canvas) => canvas.controlLayers.entities); +export const selectControlLayerEntities = createSelector(selectActiveCanvas, (canvas) => canvas.controlLayers.entities); export const selectActiveControlLayerEntities = createSelector(selectControlLayerEntities, (entities) => entities.filter(isVisibleEntity) ); -export const selectInpaintMaskEntities = createCanvasSelector((canvas) => canvas.inpaintMasks.entities); +export const selectInpaintMaskEntities = createSelector(selectActiveCanvas, (canvas) => canvas.inpaintMasks.entities); export const selectActiveInpaintMaskEntities = createSelector(selectInpaintMaskEntities, (entities) => entities.filter(isVisibleEntity) ); -export const selectRegionalGuidanceEntities = createCanvasSelector((canvas) => canvas.regionalGuidance.entities); +export const selectRegionalGuidanceEntities = createSelector( + selectActiveCanvas, + (canvas) => canvas.regionalGuidance.entities +); export const selectActiveRegionalGuidanceEntities = createSelector(selectRegionalGuidanceEntities, (entities) => entities.filter(isVisibleEntity) ); @@ -70,28 +103,12 @@ export const selectActiveRegionalGuidanceEntities = createSelector(selectRegiona */ export const selectHasEntities = createSelector(selectEntityCountAll, (count) => count > 0); -/** - * Selects the optimal dimension for the canvas based on the currently-selected model - */ -export const selectOptimalDimension = createSelector(selectParamsSlice, (params): number => { - const modelBase = params.model?.base; - return getOptimalDimension(modelBase ?? null); -}); - -/** - * Selects the grid size for the canvas based on the currently-selected model - */ -export const selectGridSize = createSelector(selectParamsSlice, (params): number => { - const modelBase = params.model?.base; - return getGridSize(modelBase ?? null); -}); - /** * Selects a single entity from the canvas slice. If the entity identifier is narrowed to a specific type, the * return type will be narrowed as well. */ export function selectEntity( - state: CanvasState, + state: CanvasEntity, entityIdentifier: T ): Extract | undefined { const { id, type } = entityIdentifier; @@ -121,7 +138,7 @@ export function selectEntity( * Selects the entity identifier for the entity that is below the given entity in terms of draw order. */ export function selectEntityIdentifierBelowThisOne( - state: CanvasState, + state: CanvasEntity, entityIdentifier: T ): Extract | undefined { const { id, type } = entityIdentifier; @@ -164,7 +181,7 @@ export function selectEntityIdentifierBelowThisOne( - state: CanvasState, + state: CanvasEntity, entityIdentifier: T, caller: string ): Extract { @@ -174,14 +191,14 @@ export function selectEntityOrThrow( } export const selectEntityExists = (entityIdentifier: T) => { - return createCanvasSelector((canvas) => Boolean(selectEntity(canvas, entityIdentifier))); + return createSelector(selectActiveCanvas, (canvas) => Boolean(selectEntity(canvas, entityIdentifier))); }; /** * Selects all entities of the given type. */ export function selectAllEntitiesOfType( - state: CanvasState, + state: CanvasEntity, type: T ): Extract[] { let entities: CanvasEntityState[] = []; @@ -208,7 +225,7 @@ export function selectAllEntitiesOfType( /** * Selects all entities, in the order they are displayed in the list. */ -export function selectAllEntities(state: CanvasState): CanvasEntityState[] { +export function selectAllEntities(state: CanvasEntity): CanvasEntityState[] { // These are in the same order as they are displayed in the list! return [ ...state.inpaintMasks.entities.toReversed(), @@ -226,7 +243,7 @@ export function selectAllEntities(state: CanvasState): CanvasEntityState[] { * - Regional guidance */ export function selectAllRenderableEntities( - state: CanvasState + state: CanvasEntity ): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] { return [ ...state.rasterLayers.entities, @@ -240,7 +257,7 @@ export function selectAllRenderableEntities( * Selects the IP adapter for the specific Regional Guidance layer. */ export function selectRegionalGuidanceReferenceImage( - state: CanvasState, + state: CanvasEntity, entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, referenceImageId: string ) { @@ -251,22 +268,22 @@ export function selectRegionalGuidanceReferenceImage( return entity.referenceImages.find(({ id }) => id === referenceImageId); } -export const selectBbox = createCanvasSelector((canvas) => canvas.bbox); +export const selectBbox = createSelector(selectActiveCanvas, (canvas) => canvas.bbox); export const selectSelectedEntityIdentifier = createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => canvas.selectedEntityIdentifier ); export const selectBookmarkedEntityIdentifier = createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas) => canvas.bookmarkedEntityIdentifier ); -export const selectCanvasMayUndo = (state: RootState) => state.canvas.past.length > 0; -export const selectCanvasMayRedo = (state: RootState) => state.canvas.future.length > 0; +export const selectCanvasMayUndo = createSelector(selectActiveCanvasWithHistory, (canvas) => canvas.past.length > 0); +export const selectCanvasMayRedo = createSelector(selectActiveCanvasWithHistory, (canvas) => canvas.future.length > 0); export const selectSelectedEntityFill = createSelector( - selectCanvasSlice, + selectActiveCanvas, selectSelectedEntityIdentifier, (canvas, selectedEntityIdentifier) => { if (!selectedEntityIdentifier) { @@ -283,10 +300,10 @@ export const selectSelectedEntityFill = createSelector( } ); -const selectRasterLayersIsHidden = createCanvasSelector((canvas) => canvas.rasterLayers.isHidden); -const selectControlLayersIsHidden = createCanvasSelector((canvas) => canvas.controlLayers.isHidden); -const selectInpaintMasksIsHidden = createCanvasSelector((canvas) => canvas.inpaintMasks.isHidden); -const selectRegionalGuidanceIsHidden = createCanvasSelector((canvas) => canvas.regionalGuidance.isHidden); +const selectRasterLayersIsHidden = createSelector(selectActiveCanvas, (canvas) => canvas.rasterLayers.isHidden); +const selectControlLayersIsHidden = createSelector(selectActiveCanvas, (canvas) => canvas.controlLayers.isHidden); +const selectInpaintMasksIsHidden = createSelector(selectActiveCanvas, (canvas) => canvas.inpaintMasks.isHidden); +const selectRegionalGuidanceIsHidden = createSelector(selectActiveCanvas, (canvas) => canvas.regionalGuidance.isHidden); /** * Returns the hidden selector for the given entity type. @@ -324,7 +341,7 @@ export const buildSelectIsSelected = (entityIdentifier: CanvasEntityIdentifier) * Other entities are considered empty if they have no objects. */ export const buildSelectHasObjects = (entityIdentifier: CanvasEntityIdentifier) => { - return createCanvasSelector((canvas) => { + return createSelector(selectActiveCanvas, (canvas) => { const entity = selectEntity(canvas, entityIdentifier); if (!entity) { @@ -334,17 +351,17 @@ export const buildSelectHasObjects = (entityIdentifier: CanvasEntityIdentifier) }); }; -export const selectWidth = createCanvasSelector((canvas) => canvas.bbox.rect.width); -export const selectHeight = createCanvasSelector((canvas) => canvas.bbox.rect.height); -export const selectAspectRatioID = createCanvasSelector((canvas) => canvas.bbox.aspectRatio.id); -export const selectAspectRatioValue = createCanvasSelector((canvas) => canvas.bbox.aspectRatio.value); +export const selectWidth = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.rect.width); +export const selectHeight = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.rect.height); +export const selectAspectRatioID = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.aspectRatio.id); +export const selectAspectRatioValue = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.aspectRatio.value); export const selectScaledSize = createSelector(selectBbox, (bbox) => bbox.scaledSize); export const selectScaleMethod = createSelector(selectBbox, (bbox) => bbox.scaleMethod); export const selectBboxRect = createSelector(selectBbox, (bbox) => bbox.rect); export const selectBboxModelBase = createSelector(selectBbox, (bbox) => bbox.modelBase); export const selectCanvasMetadata = createSelector( - selectCanvasSlice, + selectActiveCanvas, (canvas): { canvas_v2_metadata: CanvasMetadata } => { const canvas_v2_metadata: CanvasMetadata = { controlLayers: selectAllEntitiesOfType(canvas, 'control_layer'), @@ -360,6 +377,6 @@ export const selectCanvasMetadata = createSelector( * Selects whether all non-raster layer categories (control layers, inpaint masks, regional guidance) are hidden. * This is used to determine the state of the toggle button that shows/hides all non-raster layers. */ -export const selectNonRasterLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => { +export const selectNonRasterLayersIsHidden = createSelector(selectActiveCanvas, (canvas) => { return canvas.controlLayers.isHidden && canvas.inpaintMasks.isHidden && canvas.regionalGuidance.isHidden; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/tabSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/tabSlice.ts new file mode 100644 index 00000000000..de6701fc176 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/tabSlice.ts @@ -0,0 +1,123 @@ +import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { SerializedStateFromDenyList, SliceConfig } from 'app/store/types'; +import { extractTabActionContext } from 'app/store/util'; +import { isPlainObject } from 'es-toolkit'; +import { merge, omit } from 'es-toolkit/compat'; +import { assert } from 'tsafe'; + +import { getInitialLoRAsState, isLoRAsStateAction, lorasSlice } from './lorasSlice'; +import { getInitialParamsState as getInitialParamsState, isTabParamsStateAction, paramsState } from './paramsSlice'; +import { getInitialRefImagesState, isRefImagesStateAction, refImagesDenyList, refImagesSlice } from './refImagesSlice'; +import type { RefImagesState, TabInstanceState as TabInstanceParamsState, TabName, TabState } from './types'; +import { zTabState } from './types'; + +export const getInitialTabInstanceParamsState = (): TabInstanceParamsState => ({ + loras: getInitialLoRAsState(), + params: getInitialParamsState(), + refImages: getInitialRefImagesState(), +}); + +const getInitialTabState = (): TabState => ({ + activeTab: 'generate' as const, + generate: getInitialTabInstanceParamsState(), + upscaling: getInitialTabInstanceParamsState(), + video: getInitialTabInstanceParamsState(), +}); + +const tabSlice = createSlice({ + name: 'tab', + initialState: getInitialTabState(), + reducers: { + setActiveTab: (state, action: PayloadAction) => { + state.activeTab = action.payload; + }, + }, + extraReducers(builder) { + builder.addMatcher(isTabInstanceParamsAction, (state, action) => { + const context = extractTabActionContext(action); + + if (!context) { + return; + } + + switch (context.tab) { + case 'generate': + state.generate = tabInstanceParamsSlice.reducer(state.generate, action); + break; + case 'upscaling': + state.upscaling = tabInstanceParamsSlice.reducer(state.upscaling, action); + break; + case 'video': + state.video = tabInstanceParamsSlice.reducer(state.video, action); + break; + } + }); + }, +}); + +export const tabInstanceParamsSlice = createSlice({ + name: 'tabInstance', + initialState: {} as TabInstanceParamsState, + reducers: {}, + extraReducers(builder) { + builder.addDefaultCase((state, action) => { + if (isLoRAsStateAction(action)) { + state.loras = lorasSlice.reducer(state.loras, action); + } + if (isTabParamsStateAction(action)) { + state.params = paramsState.reducer(state.params, action); + } + if (isRefImagesStateAction(action)) { + state.refImages = refImagesSlice.reducer(state.refImages, action); + } + }); + }, +}); + +export const { setActiveTab } = tabSlice.actions; + +export const isTabInstanceParamsAction = (action: UnknownAction) => + isLoRAsStateAction(action) || isTabParamsStateAction(action) || isRefImagesStateAction(action); + +type SerializedRefImagesState = SerializedStateFromDenyList; +type SerializedTabInstanceParamsState = Omit & { + refImages: SerializedRefImagesState; +}; +type SerializedTabState = Omit & { + generate: SerializedTabInstanceParamsState; + upscaling: SerializedTabInstanceParamsState; + video: SerializedTabInstanceParamsState; +}; + +export const tabSliceConfig: SliceConfig = { + slice: tabSlice, + getInitialState: getInitialTabState, + schema: zTabState, + persistConfig: { + migrate: (state) => { + assert(isPlainObject(state)); + return zTabState.parse(state); + }, + serialize: (state) => ({ + ...state, + generate: { + ...state.generate, + refImages: omit(state.generate.refImages, refImagesDenyList), + }, + upscaling: { + ...state.upscaling, + refImages: omit(state.upscaling.refImages, refImagesDenyList), + }, + video: { + ...state.video, + refImages: omit(state.video.refImages, refImagesDenyList), + }, + }), + deserialize: (state) => { + const tabInstanceParamsState = state as SerializedTabState; + + return merge(tabInstanceParamsState, getInitialTabState()); + }, + }, +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 3163bd85b2a..106ff7c6e95 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -116,7 +116,7 @@ const zRgbColor = z.object({ b: z.number().int().min(0).max(255), }); export type RgbColor = z.infer; -export const zRgbaColor = zRgbColor.extend({ +const zRgbaColor = zRgbColor.extend({ a: z.number().min(0).max(1), }); export type RgbaColor = z.infer; @@ -532,13 +532,17 @@ export const zCanvasEntityIdentifer = z.object({ }); export type CanvasEntityIdentifier = { id: string; type: T }; -export const zLoRA = z.object({ +const zLoRA = z.object({ id: z.string(), isEnabled: z.boolean(), model: zModelIdentifierField, weight: z.number().gte(-10).lte(10), }); export type LoRA = z.infer; +const zLoRAsState = z.object({ + loras: z.array(zLoRA), +}); +export type LoRAsState = z.infer; export const zAspectRatioID = z.enum(['Free', '21:9', '16:9', '3:2', '4:3', '1:1', '3:4', '2:3', '9:16', '9:21']); export type AspectRatioID = z.infer; @@ -683,8 +687,17 @@ const zPositivePromptHistory = z .array(zParameterPositivePrompt) .transform((arr) => arr.slice(0, MAX_POSITIVE_PROMPT_HISTORY)); +const zRefImagesState = z.object({ + selectedEntityId: z.string().nullable(), + isPanelOpen: z.boolean(), + entities: z.array(zRefImageState), +}); +export type RefImagesState = z.infer; + +export const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue', 'video']); +export type TabName = z.infer; + export const zParamsState = z.object({ - _version: z.literal(2), maskBlur: z.number(), maskBlurMethod: zParameterMaskBlurMethod, canvasCoherenceMode: zParameterCanvasCoherenceMode, @@ -732,58 +745,20 @@ export const zParamsState = z.object({ dimensions: zDimensionsState, }); export type ParamsState = z.infer; -export const getInitialParamsState = (): ParamsState => ({ - _version: 2, - maskBlur: 16, - maskBlurMethod: 'box', - canvasCoherenceMode: 'Gaussian Blur', - canvasCoherenceMinDenoise: 0, - canvasCoherenceEdgeSize: 16, - infillMethod: 'lama', - infillTileSize: 32, - infillPatchmatchDownscaleSize: 1, - infillColorValue: { r: 0, g: 0, b: 0, a: 1 }, - cfgScale: 7.5, - cfgRescaleMultiplier: 0, - guidance: 4, - img2imgStrength: 0.75, - optimizedDenoisingEnabled: true, - iterations: 1, - scheduler: 'dpmpp_3m_k', - upscaleScheduler: 'kdpm_2', - upscaleCfgScale: 2, - seed: 0, - shouldRandomizeSeed: true, - steps: 30, - model: null, - vae: null, - vaePrecision: 'fp32', - fluxVAE: null, - seamlessXAxis: false, - seamlessYAxis: false, - clipSkip: 0, - shouldUseCpuNoise: true, - positivePrompt: '', - positivePromptHistory: [], - negativePrompt: null, - refinerModel: null, - refinerSteps: 20, - refinerCFGScale: 7.5, - refinerScheduler: 'euler', - refinerPositiveAestheticScore: 6, - refinerNegativeAestheticScore: 2.5, - refinerStart: 0.8, - t5EncoderModel: null, - clipEmbedModel: null, - clipLEmbedModel: null, - clipGEmbedModel: null, - controlLora: null, - dimensions: { - width: 512, - height: 512, - aspectRatio: deepClone(DEFAULT_ASPECT_RATIO_CONFIG), - }, + +const zTabInstanceParamsState = z.object({ + loras: zLoRAsState, + params: zParamsState, + refImages: zRefImagesState, +}); +export type TabInstanceState = z.infer; +export const zTabState = z.object({ + activeTab: zTabName, + generate: zTabInstanceParamsState, + upscaling: zTabInstanceParamsState, + video: zTabInstanceParamsState, }); +export type TabState = z.infer; const zInpaintMasks = z.object({ isHidden: z.boolean(), @@ -801,8 +776,115 @@ const zRegionalGuidance = z.object({ isHidden: z.boolean(), entities: z.array(zCanvasRegionalGuidanceState), }); -export const zCanvasState = z.object({ - _version: z.literal(3), + +const zAutoSwitchMode = z.enum(['off', 'switch_on_start', 'switch_on_finish']); +export type AutoSwitchMode = z.infer; + +const zCanvasSettingsState = z.object({ + /** + * Whether to show HUD (Heads-Up Display) on the canvas. + */ + showHUD: z.boolean(), + /** + * Whether to clip lines and shapes to the generation bounding box. If disabled, lines and shapes will be clipped to + * the canvas bounds. + */ + clipToBbox: z.boolean(), + /** + * Whether to show a dynamic grid on the canvas. If disabled, a checkerboard pattern will be shown instead. + */ + dynamicGrid: z.boolean(), + /** + * Whether to invert the scroll direction when adjusting the brush or eraser width with the scroll wheel. + */ + invertScrollForToolWidth: z.boolean(), + /** + * The width of the brush tool. + */ + brushWidth: z.int().gt(0), + /** + * The width of the eraser tool. + */ + eraserWidth: z.int().gt(0), + /** + * The colors to use when drawing lines or filling shapes. + */ + activeColor: z.enum(['bgColor', 'fgColor']), + bgColor: zRgbaColor, + fgColor: zRgbaColor, + /** + * Whether to composite inpainted/outpainted regions back onto the source image when saving canvas generations. + * + * If disabled, inpainted/outpainted regions will be saved with a transparent background. + * + * When `sendToCanvas` is disabled, this setting is ignored, masked regions will always be composited. + */ + outputOnlyMaskedRegions: z.boolean(), + /** + * Whether to automatically process the operations like filtering and auto-masking. + */ + autoProcess: z.boolean(), + /** + * The snap-to-grid setting for the canvas. + */ + snapToGrid: z.boolean(), + /** + * Whether to show progress on the canvas when generating images. + */ + showProgressOnCanvas: z.boolean(), + /** + * Whether to show the bounding box overlay on the canvas. + */ + bboxOverlay: z.boolean(), + /** + * Whether to preserve the masked region instead of inpainting it. + */ + preserveMask: z.boolean(), + /** + * Whether to show only raster layers while staging. + */ + isolatedStagingPreview: z.boolean(), + /** + * Whether to show only the selected layer while filtering, transforming, or doing other operations. + */ + isolatedLayerPreview: z.boolean(), + /** + * Whether to use pressure sensitivity for the brush and eraser tool when a pen device is used. + */ + pressureSensitivity: z.boolean(), + /** + * Whether to show the rule of thirds composition guide overlay on the canvas. + */ + ruleOfThirds: z.boolean(), + /** + * Whether to save all staging images to the gallery instead of keeping them as intermediate images. + */ + saveAllImagesToGallery: z.boolean(), + /** + * The auto-switch mode for the canvas staging area. + */ + stagingAreaAutoSwitch: zAutoSwitchMode, +}); +export type CanvasSettingsState = z.infer; + +const zCanvasStagingAreaState = z.object({ + _version: z.literal(1), + canvasSessionId: z.string(), + canvasDiscardedQueueItems: z.array(z.number().int()), +}); +export type CanvasStagingAreaState = z.infer; + +const zStateWithHistory = (stateSchema: T) => + z.object({ + past: z.array(stateSchema), + present: stateSchema, + future: z.array(stateSchema), + _latestUnfiltered: stateSchema.optional(), + group: z.string().optional(), + index: z.number().optional(), + limit: z.number().optional(), + }); +const zCanvasEntity = z.object({ selectedEntityIdentifier: zCanvasEntityIdentifer.nullable(), bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable(), inpaintMasks: zInpaintMasks, @@ -811,35 +893,33 @@ export const zCanvasState = z.object({ regionalGuidance: zRegionalGuidance, bbox: zBboxState, }); -export type CanvasState = z.infer; -export const getInitialCanvasState = (): CanvasState => ({ - _version: 3, - selectedEntityIdentifier: null, - bookmarkedEntityIdentifier: null, - inpaintMasks: { isHidden: false, entities: [] }, - rasterLayers: { isHidden: false, entities: [] }, - controlLayers: { isHidden: false, entities: [] }, - regionalGuidance: { isHidden: false, entities: [] }, - bbox: { - rect: { x: 0, y: 0, width: 512, height: 512 }, - aspectRatio: deepClone(DEFAULT_ASPECT_RATIO_CONFIG), - scaleMethod: 'auto', - scaledSize: { width: 512, height: 512 }, - modelBase: 'sd-1', - }, -}); - -export const zRefImagesState = z.object({ - selectedEntityId: z.string().nullable(), - isPanelOpen: z.boolean(), - entities: z.array(zRefImageState), -}); -export type RefImagesState = z.infer; -export const getInitialRefImagesState = (): RefImagesState => ({ - selectedEntityId: null, - isPanelOpen: false, - entities: [], +export type CanvasEntity = z.infer; +const zCanvasInstanceStateBase = z.object({ + id: zId, + name: z.string().min(1), }); +const zCanvasInstanceState = (canvasEntitySchema: T) => + zCanvasInstanceStateBase.extend({ + canvas: canvasEntitySchema, + params: zTabInstanceParamsState, + settings: zCanvasSettingsState, + staging: zCanvasStagingAreaState, + }); +const zCanvasInstanceStateWithoutHistory = zCanvasInstanceState(zCanvasEntity); +const zCanvasInstanceStateWithHistory = zCanvasInstanceState(zStateWithHistory(zCanvasEntity)); +export type CanvasInstanceStateBase = z.infer; +export type CanvasInstanceState = z.infer; +export type CanvasInstanceStateWithHistory = z.infer; +const zCanvasState = (canvasInstanceSchema: T) => + z.object({ + _version: z.literal(4), + activeCanvasId: zId, + canvases: z.record(zId, canvasInstanceSchema), + }); +export const zCanvasStateWithoutHistory = zCanvasState(zCanvasInstanceStateWithoutHistory); +export const zCanvasStateWithHistory = zCanvasState(zCanvasInstanceStateWithHistory); +export type CanvasState = z.infer; +export type CanvasStateWithHistory = z.infer; export const zCanvasReferenceImageState_OLD = zCanvasEntityBase.extend({ type: z.literal('reference_image'), diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts index 38aa8b039f3..1fd137136d4 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts @@ -8,8 +8,8 @@ import { selectReferenceImageEntities, selectRefImagesSlice, } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; -import type { CanvasState, RefImagesState } from 'features/controlLayers/store/types'; +import { selectCanvases } from 'features/controlLayers/store/selectors'; +import type { CanvasEntity, RefImagesState } from 'features/controlLayers/store/types'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; import { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors'; import { itemSelected } from 'features/gallery/store/gallerySlice'; @@ -156,11 +156,11 @@ const getImageUsageFromImageNames = (image_names: string[], state: RootState): I } const nodes = selectNodesSlice(state); - const canvas = selectCanvasSlice(state); + const canvases = selectCanvases(state); const upscale = selectUpscaleSlice(state); const refImages = selectRefImagesSlice(state); - return image_names.map((image_name) => getImageUsage(nodes, canvas, upscale, refImages, image_name)); + return image_names.map((image_name) => getImageUsage(nodes, canvases, upscale, refImages, image_name)); }; const getImageUsageSummary = (imageUsage: ImageUsage[]): ImageUsage => ({ @@ -220,17 +220,20 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, image_name: }; const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image_name: string) => { - selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => { - let shouldDelete = false; - for (const obj of objects) { - if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) { - shouldDelete = true; - break; + const canvases = selectCanvases(state); + canvases.forEach((canvas) => { + canvas.controlLayers.entities.forEach(({ id, objects }) => { + let shouldDelete = false; + for (const obj of objects) { + if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) { + shouldDelete = true; + break; + } } - } - if (shouldDelete) { - dispatch(entityDeleted({ entityIdentifier: { id, type: 'control_layer' } })); - } + if (shouldDelete) { + dispatch(entityDeleted({ entityIdentifier: { id, type: 'control_layer' } })); + } + }); }); }; @@ -246,23 +249,26 @@ const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, image_na }; const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, image_name: string) => { - selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => { - let shouldDelete = false; - for (const obj of objects) { - if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) { - shouldDelete = true; - break; + const canvases = selectCanvases(state); + canvases.forEach((canvas) => { + canvas.rasterLayers.entities.forEach(({ id, objects }) => { + let shouldDelete = false; + for (const obj of objects) { + if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) { + shouldDelete = true; + break; + } } - } - if (shouldDelete) { - dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } })); - } + if (shouldDelete) { + dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } })); + } + }); }); }; export const getImageUsage = ( nodes: NodesState, - canvas: CanvasState, + canvases: CanvasEntity[], upscale: UpscaleState, refImages: RefImagesState, image_name: string @@ -292,20 +298,28 @@ export const getImageUsage = ( config.image?.original.image.image_name === image_name || config.image?.crop?.image.image_name === image_name ); - const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) => - objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) + const isRasterLayerImage = canvases.some((canvas) => + canvas.rasterLayers.entities.some(({ objects }) => + objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) + ) ); - const isControlLayerImage = canvas.controlLayers.entities.some(({ objects }) => - objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) + const isControlLayerImage = canvases.some((canvas) => + canvas.controlLayers.entities.some(({ objects }) => + objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) + ) ); - const isInpaintMaskImage = canvas.inpaintMasks.entities.some(({ objects }) => - objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) + const isInpaintMaskImage = canvases.some((canvas) => + canvas.inpaintMasks.entities.some(({ objects }) => + objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name) + ) ); - const isRegionalGuidanceImage = canvas.regionalGuidance.entities.some(({ referenceImages }) => - referenceImages.some(({ config }) => config.image?.image_name === image_name) + const isRegionalGuidanceImage = canvases.some((canvas) => + canvas.regionalGuidance.entities.some(({ referenceImages }) => + referenceImages.some(({ config }) => config.image?.image_name === image_name) + ) ); const imageUsage: ImageUsage = { diff --git a/invokeai/frontend/web/src/features/dnd/FullscreenDropzone.tsx b/invokeai/frontend/web/src/features/dnd/FullscreenDropzone.tsx index f10b5b9d598..1dde287ae82 100644 --- a/invokeai/frontend/web/src/features/dnd/FullscreenDropzone.tsx +++ b/invokeai/frontend/web/src/features/dnd/FullscreenDropzone.tsx @@ -9,12 +9,12 @@ import { useAppSelector } from 'app/store/storeHooks'; import { getFocusedRegion } from 'common/hooks/focus'; import { useClientSideUpload } from 'common/hooks/useClientSideUpload'; import { setFileToPaste } from 'features/controlLayers/components/CanvasPasteModal'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { DndDropOverlay } from 'features/dnd/DndDropOverlay'; import type { DndTargetState } from 'features/dnd/types'; import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; import { selectIsClientSideUploadEnabled } from 'features/system/store/configSlice'; import { toast } from 'features/toast/toast'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { uploadImages } from 'services/api/endpoints/images'; diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts b/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts index 8e071261224..02f0bfabe44 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts @@ -1,9 +1,10 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { SliceConfig } from 'app/store/types'; +import type { SerializedStateFromDenyList, SliceConfig } from 'app/store/types'; import { buildZodTypeGuard } from 'common/util/zodUtils'; import { isPlainObject } from 'es-toolkit'; +import { merge, omit } from 'es-toolkit/compat'; import { assert } from 'tsafe'; import { z } from 'zod'; @@ -69,21 +70,30 @@ export const { seedBehaviourChanged, } = slice.actions; -export const dynamicPromptsSliceConfig: SliceConfig = { - slice, - schema: zDynamicPromptsState, - getInitialState, - persistConfig: { - migrate: (state) => { - assert(isPlainObject(state)); - if (!('_version' in state)) { - state._version = 1; - } - return zDynamicPromptsState.parse(state); +const denyList = ['prompts', 'parsingError', 'isError', 'isLoading'] as const; +type SerializedDynamicPromptsState = SerializedStateFromDenyList; + +export const dynamicPromptsSliceConfig: SliceConfig = + { + slice, + schema: zDynamicPromptsState, + getInitialState, + persistConfig: { + migrate: (state) => { + assert(isPlainObject(state)); + if (!('_version' in state)) { + state._version = 1; + } + return zDynamicPromptsState.parse(state); + }, + serialize: (state) => omit(state, denyList), + deserialize: (state) => { + const dynamicPromptsState = state as SerializedDynamicPromptsState; + + return merge(dynamicPromptsState, getInitialState()); + }, }, - persistDenylist: ['prompts', 'parsingError', 'isError', 'isLoading'], - }, -}; + }; export const selectDynamicPromptsSlice = (state: RootState) => state.dynamicPrompts; const createDynamicPromptsSelector = (selector: Selector) => diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 47d59540f1d..586e9ca2ba4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -17,7 +17,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { some } from 'es-toolkit/compat'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectCanvases } from 'features/controlLayers/store/selectors'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import { getImageUsage } from 'features/deleteImageModal/store/state'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; @@ -56,10 +56,10 @@ const DeleteBoardModal = () => { const selectImageUsageSummary = useMemo( () => createMemoizedSelector( - [selectNodesSlice, selectCanvasSlice, selectUpscaleSlice, selectRefImagesSlice], - (nodes, canvas, upscale, refImages) => { + [selectNodesSlice, selectCanvases, selectUpscaleSlice, selectRefImagesSlice], + (nodes, canvases, upscale, refImages) => { const allImageUsage = (boardImageNames ?? []).map((imageName) => - getImageUsage(nodes, canvas, upscale, refImages, imageName) + getImageUsage(nodes, canvases, upscale, refImages, imageName) ); const imageUsageSummary: ImageUsage = { diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemLocateInGalery.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemLocateInGalery.tsx index 8055e592cfe..50b8a3233c6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemLocateInGalery.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemLocateInGalery.tsx @@ -1,11 +1,11 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext'; import { boardIdSelected } from 'features/gallery/store/gallerySlice'; import { IMAGE_CATEGORIES } from 'features/gallery/store/types'; import { navigationApi } from 'features/ui/layouts/navigation-api'; import { useGalleryPanel } from 'features/ui/layouts/use-gallery-panel'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback, useMemo } from 'react'; import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemNewCanvasFromImageSubMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemNewCanvasFromImageSubMenu.tsx index b40525ae474..8667b497b8c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemNewCanvasFromImageSubMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemNewCanvasFromImageSubMenu.tsx @@ -1,8 +1,6 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppStore } from 'app/store/storeHooks'; import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu'; -import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { useItemDTOContextImageOnly } from 'features/gallery/contexts/ItemDTOContext'; import { newCanvasFromImage } from 'features/imageActions/actions'; import { toast } from 'features/toast/toast'; @@ -17,8 +15,6 @@ export const ContextMenuItemNewCanvasFromImageSubMenu = memo(() => { const subMenu = useSubMenu(); const store = useAppStore(); const imageDTO = useItemDTOContextImageOnly(); - const isBusy = useCanvasIsBusySafe(); - const isStaging = useCanvasIsStaging(); const onClickNewCanvasWithRasterLayerFromImage = useCallback(async () => { const { dispatch, getState } = store; @@ -99,32 +95,16 @@ export const ContextMenuItemNewCanvasFromImageSubMenu = memo(() => { - } - onClickCapture={onClickNewCanvasWithRasterLayerFromImage} - isDisabled={isStaging || isBusy} - > + } onClickCapture={onClickNewCanvasWithRasterLayerFromImage}> {t('controlLayers.asRasterLayer')} - } - onClickCapture={onClickNewCanvasWithRasterLayerFromImageWithResize} - isDisabled={isStaging || isBusy} - > + } onClickCapture={onClickNewCanvasWithRasterLayerFromImageWithResize}> {t('controlLayers.asRasterLayerResize')} - } - onClickCapture={onClickNewCanvasWithControlLayerFromImage} - isDisabled={isStaging || isBusy} - > + } onClickCapture={onClickNewCanvasWithControlLayerFromImage}> {t('controlLayers.asControlLayer')} - } - onClickCapture={onClickNewCanvasWithControlLayerFromImageWithResize} - isDisabled={isStaging || isBusy} - > + } onClickCapture={onClickNewCanvasWithControlLayerFromImageWithResize}> {t('controlLayers.asControlLayerResize')} diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemNewLayerFromImageSubMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemNewLayerFromImageSubMenu.tsx index 710a381d937..4705c4d338a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemNewLayerFromImageSubMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/MenuItems/ContextMenuItemNewLayerFromImageSubMenu.tsx @@ -2,7 +2,7 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppStore } from 'app/store/storeHooks'; import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu'; import { NewLayerIcon } from 'features/controlLayers/components/common/icons'; -import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; import { useItemDTOContextImageOnly } from 'features/gallery/contexts/ItemDTOContext'; import { sentImageToCanvas } from 'features/gallery/store/actions'; import { createNewCanvasEntityFromImage } from 'features/imageActions/actions'; @@ -18,7 +18,7 @@ export const ContextMenuItemNewLayerFromImageSubMenu = memo(() => { const subMenu = useSubMenu(); const store = useAppStore(); const imageDTO = useItemDTOContextImageOnly(); - const isBusy = useCanvasIsBusySafe(); + const isBusy = useCanvasIsBusy(); const onClickNewRasterLayerFromImage = useCallback(async () => { const { dispatch, getState } = store; diff --git a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionMenuItems.tsx index 3556e1a9d48..a3034f4cbe3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ContextMenu/SingleSelectionMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { IconMenuItemGroup } from 'common/components/IconMenuItem'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { ContextMenuItemChangeBoard } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemChangeBoard'; import { ContextMenuItemCopy } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemCopy'; import { ContextMenuItemDownload } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemDownload'; @@ -20,7 +21,6 @@ import { ContextMenuItemUseAsRefImage } from 'features/gallery/components/Contex import { ContextMenuItemUseForPromptGeneration } from 'features/gallery/components/ContextMenu/MenuItems/ContextMenuItemUseForPromptGeneration'; import { ItemDTOContextProvider } from 'features/gallery/contexts/ItemDTOContext'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import type { ImageDTO } from 'services/api/types'; import { ContextMenuItemDeleteImage } from './MenuItems/ContextMenuItemDeleteImage'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index 7994b231d69..33e978b8572 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -1,5 +1,6 @@ import { Button, Divider, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; import SingleSelectionMenuItems from 'features/gallery/components/ContextMenu/SingleSelectionMenuItems'; import { useDeleteImage } from 'features/gallery/hooks/useDeleteImage'; @@ -16,7 +17,6 @@ import { PostProcessingPopover } from 'features/parameters/components/PostProces import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { navigationApi } from 'features/ui/layouts/navigation-api'; import { useGalleryPanel } from 'features/ui/layouts/use-gallery-panel'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback, useMemo } from 'react'; import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx index 28e91a1e5f0..7d133a8a305 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentVideoButtons.tsx @@ -1,12 +1,12 @@ import { Button, Divider, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { useDeleteVideo } from 'features/deleteImageModal/hooks/use-delete-video'; import { DeleteVideoButton } from 'features/deleteVideoModal/components/DeleteVideoButton'; import SingleSelectionVideoMenuItems from 'features/gallery/components/ContextMenu/SingleSelectionVideoMenuItems'; import { boardIdSelected } from 'features/gallery/store/gallerySlice'; import { navigationApi } from 'features/ui/layouts/navigation-api'; import { useGalleryPanel } from 'features/ui/layouts/use-gallery-panel'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useVideoViewerContext } from 'features/video/context/VideoViewerContext'; import { useCaptureVideoFrame } from 'features/video/hooks/useCaptureVideoFrame'; import { memo, useCallback, useState } from 'react'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/NoContentForViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/NoContentForViewer.tsx index 62248a1883c..306aeddbbc3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/NoContentForViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/NoContentForViewer.tsx @@ -3,12 +3,12 @@ import { Alert, AlertDescription, AlertIcon, Button, Divider, Flex, Link, Spinne import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { InvokeLogoIcon } from 'common/components/InvokeLogoIcon'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { LOADING_SYMBOL, useHasImages } from 'features/gallery/hooks/useHasImages'; import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { selectIsLocal } from 'features/system/store/configSlice'; import { navigationApi } from 'features/ui/layouts/navigation-api'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import type { PropsWithChildren } from 'react'; import { memo, useCallback, useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useEditImage.ts b/invokeai/frontend/web/src/features/gallery/hooks/useEditImage.ts index cb0cc9bb27a..cc41c1a592a 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useEditImage.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useEditImage.ts @@ -1,6 +1,6 @@ import { useAppStore } from 'app/store/storeHooks'; import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { newCanvasFromImage } from 'features/imageActions/actions'; import { toast } from 'features/toast/toast'; import { navigationApi } from 'features/ui/layouts/navigation-api'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallAllImageMetadata.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallAllImageMetadata.ts index 9baa96bf2d6..b163b63d5df 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallAllImageMetadata.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallAllImageMetadata.ts @@ -1,7 +1,7 @@ import { useAppSelector, useAppStore } from 'app/store/storeHooks'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useCallback, useMemo } from 'react'; import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; import type { ImageDTO } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallCLIPSkip.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallCLIPSkip.ts index f6513bb0738..4eb5dd9072c 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallCLIPSkip.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallCLIPSkip.ts @@ -1,8 +1,8 @@ import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { selectHasModelCLIPSkip } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; +import type { TabName } from 'features/controlLayers/store/types'; import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import type { TabName } from 'features/ui/store/uiTypes'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; import type { ImageDTO } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts index 3feb074a35e..6081f5e228c 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts @@ -1,7 +1,7 @@ import { useAppSelector, useAppStore } from 'app/store/storeHooks'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { MetadataUtils } from 'features/metadata/parsing'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useCallback, useMemo } from 'react'; import type { ImageDTO } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallPrompts.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallPrompts.ts index 57fe0e5360c..c47d1fb6029 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallPrompts.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallPrompts.ts @@ -1,7 +1,7 @@ import { useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; +import type { TabName } from 'features/controlLayers/store/types'; import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import type { TabName } from 'features/ui/store/uiTypes'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; import type { ImageDTO } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts index 8f7ea9db6d7..34c081802ad 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts @@ -1,7 +1,7 @@ import { useAppSelector, useAppStore } from 'app/store/storeHooks'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useCallback, useMemo } from 'react'; import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; import type { ImageDTO } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallSeed.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallSeed.ts index 4a874118d48..f988201fe48 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallSeed.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallSeed.ts @@ -1,7 +1,7 @@ import { useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; +import type { TabName } from 'features/controlLayers/store/types'; import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import type { TabName } from 'features/ui/store/uiTypes'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; import type { ImageDTO } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 1f99fc7a6d3..4da9e5fd7e7 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,8 +1,9 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { SliceConfig } from 'app/store/types'; +import type { SerializedStateFromDenyList, SliceConfig } from 'app/store/types'; import { isPlainObject } from 'es-toolkit'; +import { merge, omit } from 'es-toolkit/compat'; import type { BoardRecordOrderBy } from 'services/api/types'; import { assert } from 'tsafe'; @@ -176,7 +177,10 @@ export const { export const selectGallerySlice = (state: RootState) => state.gallery; -export const gallerySliceConfig: SliceConfig = { +const denyList = ['selection', 'selectedBoardId', 'galleryView', 'imageToCompare'] as const; +type SerializedGalleryState = SerializedStateFromDenyList; + +export const gallerySliceConfig: SliceConfig = { slice, schema: zGalleryState, getInitialState, @@ -188,6 +192,14 @@ export const gallerySliceConfig: SliceConfig = { } return zGalleryState.parse(state); }, - persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'imageToCompare'], + serialize: (state) => { + const a = omit(state, denyList); + return a; + }, + deserialize: (state) => { + const galleryState = state as SerializedGalleryState; + + return merge(galleryState, getInitialState()); + }, }, }; diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts index 14d27e900c1..1181080d67c 100644 --- a/invokeai/frontend/web/src/features/imageActions/actions.ts +++ b/invokeai/frontend/web/src/features/imageActions/actions.ts @@ -3,9 +3,9 @@ import { deepClone } from 'common/util/deepClone'; import { getDefaultRegionalGuidanceRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks'; import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { canvasReset } from 'features/controlLayers/store/actions'; import { bboxChangedFromCanvas, + canvasAdded, canvasClearHistory, controlLayerAdded, entityRasterized, @@ -155,7 +155,6 @@ export const createNewCanvasEntityFromImage = async (arg: { /** * Creates a new canvas with the given image as the only layer: - * - Reset the canvas * - Resize the bbox to the image's aspect ratio at the optimal size for the selected model * - Add the image as a layer of the given type * - If `withResize`: Resizes the layer to fit the bbox using the 'fill' strategy @@ -214,7 +213,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasReset()); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(rasterLayerAdded({ overrides, isSelected: true })); @@ -231,7 +230,7 @@ export const newCanvasFromImage = async (arg: { controlAdapter: deepClone(initialControlNet), } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasReset()); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(controlLayerAdded({ overrides, isSelected: true })); @@ -247,7 +246,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasReset()); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(inpaintMaskAdded({ overrides, isSelected: true })); @@ -263,7 +262,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasReset()); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(rgAdded({ overrides, isSelected: true })); @@ -277,7 +276,7 @@ export const newCanvasFromImage = async (arg: { const config = getDefaultRegionalGuidanceRefImageConfig(getState); config.image = imageDTOToImageWithDims(imageDTO); const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), config }]; - dispatch(canvasReset()); + dispatch(canvasAdded({ isSelected: true })); dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true })); if (withInpaintMask) { dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true })); diff --git a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx index 89cbc37353c..5d8ed73ae1b 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx @@ -12,22 +12,21 @@ import { import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { - buildSelectLoRA, DEFAULT_LORA_WEIGHT_CONFIG, loraDeleted, loraIsEnabledChanged, loraWeightChanged, + selectLoRA, } from 'features/controlLayers/store/lorasSlice'; import type { LoRA } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; import { useGetModelConfigQuery } from 'services/api/endpoints/models'; const MARKS = [-1, 0, 1, 2]; export const LoRACard = memo((props: { id: string }) => { - const selectLoRA = useMemo(() => buildSelectLoRA(props.id), [props.id]); - const lora = useAppSelector(selectLoRA); + const lora = useAppSelector((state) => selectLoRA(state, props.id)); if (!lora) { return null; diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index 10cd0e32f7f..3c66ef9f061 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -33,6 +33,7 @@ import { widthChanged, } from 'features/controlLayers/store/paramsSlice'; import { refImagesRecalled } from 'features/controlLayers/store/refImagesSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import type { CanvasMetadata, LoRA, @@ -101,7 +102,6 @@ import { zParameterStrength, } from 'features/parameters/types/parameterSchemas'; import { toast } from 'features/toast/toast'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { t } from 'i18next'; import type { ComponentType } from 'react'; import { useCallback, useEffect, useState } from 'react'; @@ -296,7 +296,7 @@ const NegativePrompt: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(negativePromptChanged(value || null)); + store.dispatch(negativePromptChanged(value)); }, i18nKey: 'metadata.negativePrompt', LabelComponent: MetadataLabel, diff --git a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts index 7f20f21c22a..45608fa3ba1 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts +++ b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts @@ -1,8 +1,9 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { SliceConfig } from 'app/store/types'; +import type { SerializedStateFromDenyList, SliceConfig } from 'app/store/types'; import { isPlainObject } from 'es-toolkit'; +import { merge, omit } from 'es-toolkit/compat'; import { zModelType } from 'features/nodes/types/common'; import { assert } from 'tsafe'; import z from 'zod'; @@ -67,7 +68,10 @@ export const { shouldInstallInPlaceChanged, } = slice.actions; -export const modelManagerSliceConfig: SliceConfig = { +const denyList = ['selectedModelKey', 'selectedModelMode', 'filteredModelType', 'searchTerm'] as const; +type SerializedModelManagerState = SerializedStateFromDenyList; + +export const modelManagerSliceConfig: SliceConfig = { slice, schema: zModelManagerState, getInitialState, @@ -79,7 +83,12 @@ export const modelManagerSliceConfig: SliceConfig = { } return zModelManagerState.parse(state); }, - persistDenylist: ['selectedModelKey', 'selectedModelMode', 'filteredModelType', 'searchTerm'], + serialize: (state) => omit(state, denyList), + deserialize: (state) => { + const modelManagerState = state as SerializedModelManagerState; + + return merge(modelManagerState, getInitialState()); + }, }, }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx index 55c683029fd..2505674fd74 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx @@ -18,6 +18,7 @@ import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { memoize } from 'es-toolkit/compat'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked'; import { @@ -38,7 +39,6 @@ import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; import { toast } from 'features/toast/toast'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { computed } from 'nanostores'; import type { ChangeEvent } from 'react'; import { memo, useCallback, useMemo, useRef, useState } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 20c27d2cd6e..2f17d88c912 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -85,7 +85,8 @@ import { } from 'features/nodes/types/workflow'; import { atom, computed } from 'nanostores'; import type { MouseEvent } from 'react'; -import type { UndoableOptions } from 'redux-undo'; +import type { StateWithHistory, UndoableOptions } from 'redux-undo'; +import undoable, { newHistory } from 'redux-undo'; import { assert } from 'tsafe'; import type { z } from 'zod'; @@ -804,7 +805,9 @@ const reduxUndoOptions: UndoableOptions = { }, }; -export const nodesSliceConfig: SliceConfig = { +export const undoableNodesSliceReducer = undoable(slice.reducer, reduxUndoOptions); + +export const nodesSliceConfig: SliceConfig, NodesState> = { slice, schema: zNodesState, getInitialState, @@ -816,9 +819,12 @@ export const nodesSliceConfig: SliceConfig = { } return zNodesState.parse(state); }, - }, - undoableConfig: { - reduxUndoOptions, + serialize: (state) => state.present, + deserialize: (state) => { + const nodesState = state as NodesState; + + return newHistory([], nodesState, []); + }, }, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index 1b393d3a672..d90c14a2f28 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -1,6 +1,7 @@ import type { RootState } from 'app/store/store'; import { generateSeeds } from 'common/util/generateSeeds'; import { range } from 'es-toolkit/compat'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import type { SeedBehaviour } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { API_BASE_MODELS, VIDEO_BASE_MODELS } from 'features/parameters/types/constants'; @@ -34,7 +35,7 @@ export const prepareLinearUIBatch = (arg: { destination: string; }): EnqueueBatchArg => { const { state, g, base, prepend, positivePromptNode, seedNode, origin, destination } = arg; - const { iterations, shouldRandomizeSeed, seed } = state.params; + const { iterations, shouldRandomizeSeed, seed } = selectActiveTabParams(state); const { prompts, seedBehaviour } = state.dynamicPrompts; const data: Batch['data'] = []; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts index 26db510599c..b62ca6adc67 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildMultidiffusionUpscaleGraph.ts @@ -1,5 +1,6 @@ import type { RootState } from 'app/store/store'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addSDXLLoRAs } from 'features/nodes/util/graph/generation/addSDXLLoRAs'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; @@ -11,7 +12,14 @@ import { getBoardField, selectPresetModifiedPrompts } from './graphBuilderUtils' import type { GraphBuilderReturn } from './types'; export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise => { - const { model, upscaleCfgScale: cfg_scale, upscaleScheduler: scheduler, steps, vaePrecision, vae } = state.params; + const { + model, + upscaleCfgScale: cfg_scale, + upscaleScheduler: scheduler, + steps, + vaePrecision, + vae, + } = selectActiveTabParams(state); const { upscaleModel, upscaleInitialImage, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXFill.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXFill.ts index 175e3aeb88d..ae7d425eed3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXFill.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXFill.ts @@ -2,8 +2,8 @@ import { objectEquals } from '@observ33r/object-equals'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getDenoisingStartAndEnd, @@ -35,8 +35,8 @@ export const addFLUXFill = async ({ denoise.width = scaledSize.width; denoise.height = scaledSize.height; - const params = selectParamsSlice(state); - const canvasSettings = selectCanvasSettingsSlice(state); + const params = selectActiveTabParams(state); + const canvasSettings = selectCanvasSettingsByCanvasId(state, manager.canvasId); const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer'); const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, rect, { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXLoRAs.ts index a24adc07b7e..67709857726 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXLoRAs.ts @@ -1,5 +1,6 @@ import type { RootState } from 'app/store/store'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation, S } from 'services/api/types'; @@ -11,7 +12,7 @@ export const addFLUXLoRAs = ( modelLoader: Invocation<'flux_model_loader'>, fluxTextEncoder: Invocation<'flux_text_encoder'> ): void => { - const enabledLoRAs = state.loras.loras.filter((l) => l.isEnabled && l.model.base === 'flux'); + const enabledLoRAs = selectAddedLoRAs(state).filter((l) => l.isEnabled && l.model.base === 'flux'); const loraCount = enabledLoRAs.length; if (loraCount === 0) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index ac71ec4b0cb..ebc539b6b33 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -2,8 +2,8 @@ import { objectEquals } from '@observ33r/object-equals'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getDenoisingStartAndEnd, @@ -48,8 +48,8 @@ export const addInpaint = async ({ denoise.denoising_start = denoising_start; denoise.denoising_end = denoising_end; - const params = selectParamsSlice(state); - const canvasSettings = selectCanvasSettingsSlice(state); + const params = selectActiveTabParams(state); + const canvasSettings = selectCanvasSettingsByCanvasId(state, manager.canvasId); const { originalSize, scaledSize, rect } = getOriginalAndScaledSizesForOtherModes(state); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts index 79a8521efba..2bea0e07350 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addLoRAs.ts @@ -1,5 +1,6 @@ import type { RootState } from 'app/store/store'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation, S } from 'services/api/types'; @@ -14,7 +15,7 @@ export const addLoRAs = ( posCond: Invocation<'compel'>, negCond: Invocation<'compel'> ): void => { - const enabledLoRAs = state.loras.loras.filter( + const enabledLoRAs = selectAddedLoRAs(state).filter( (l) => l.isEnabled && (l.model.base === 'sd-1' || l.model.base === 'sd-2') ); const loraCount = enabledLoRAs.length; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 3a74c4c0edc..ef56b2456ec 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -2,8 +2,8 @@ import { objectEquals } from '@observ33r/object-equals'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getDenoisingStartAndEnd, @@ -50,8 +50,8 @@ export const addOutpaint = async ({ denoise.denoising_start = denoising_start; denoise.denoising_end = denoising_end; - const params = selectParamsSlice(state); - const canvasSettings = selectCanvasSettingsSlice(state); + const params = selectActiveTabParams(state); + const canvasSettings = selectCanvasSettingsByCanvasId(state, manager.canvasId); const { originalSize, scaledSize, rect } = getOriginalAndScaledSizesForOtherModes(state); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts index a38c9757cea..22cf1289b11 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLLoRAs.ts @@ -1,5 +1,6 @@ import type { RootState } from 'app/store/store'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation, S } from 'services/api/types'; @@ -13,7 +14,7 @@ export const addSDXLLoRAs = ( posCond: Invocation<'sdxl_compel_prompt'>, negCond: Invocation<'sdxl_compel_prompt'> ): void => { - const enabledLoRAs = state.loras.loras.filter((l) => l.isEnabled && l.model.base === 'sdxl'); + const enabledLoRAs = selectAddedLoRAs(state).filter((l) => l.isEnabled && l.model.base === 'sdxl'); const loraCount = enabledLoRAs.length; if (loraCount === 0) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts index 5485834db13..442610d6d8b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts @@ -1,5 +1,6 @@ import type { RootState } from 'app/store/store'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation } from 'services/api/types'; @@ -23,7 +24,7 @@ export const addSDXLRefiner = async ( refinerScheduler, refinerCFGScale, refinerStart, - } = state.params; + } = selectActiveTabParams(state); assert(refinerModel, 'No refiner model found in state'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts index 167de85062b..9391630dfa0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSeamless.ts @@ -1,5 +1,6 @@ import type { RootState } from 'app/store/store'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation } from 'services/api/types'; @@ -21,7 +22,7 @@ export const addSeamless = ( modelLoader: Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'>, vaeLoader: Invocation<'vae_loader'> | null ): Invocation<'seamless'> | null => { - const { seamlessXAxis: seamless_x, seamlessYAxis: seamless_y } = state.params; + const { seamlessXAxis: seamless_x, seamlessYAxis: seamless_y } = selectActiveTabParams(state); // Always write seamless metadata to ensure recalling all parameters will reset the seamless settings g.upsertMetadata({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts index c579fc05bc1..2b0b9d07d74 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildChatGPT4oGraph.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasMetadata } from 'features/controlLayers/store/selectors'; +import { selectActiveTab, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { isChatGPT4oAspectRatioID, isChatGPT4oReferenceImageConfig } from 'features/controlLayers/store/types'; import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators'; import { type ImageField, zImageField, zModelIdentifierField } from 'features/nodes/types/common'; @@ -14,7 +14,6 @@ import { } from 'features/nodes/util/graph/graphBuilderUtils'; import type { GraphBuilderArg, GraphBuilderReturn } from 'features/nodes/util/graph/types'; import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { t } from 'i18next'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts index 6adee057545..0e2c03b28b5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts @@ -1,7 +1,7 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; -import { selectCanvasMetadata } from 'features/controlLayers/store/selectors'; +import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTab, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage'; import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint'; @@ -12,7 +12,6 @@ import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermar import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { selectCanvasOutputFields, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils'; import type { GraphBuilderArg, GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import type { Invocation } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import type { Equals } from 'tsafe'; @@ -29,7 +28,7 @@ export const buildCogView4Graph = async (arg: GraphBuilderArg): Promise>(false); } - if (manager !== null) { + if (manager !== null && canvas !== null) { const controlNetCollector = g.addNode({ type: 'collect', id: getPrefixedId('control_net_collector'), @@ -308,7 +307,7 @@ export const buildFLUXGraph = async (arg: GraphBuilderArg): Promise 0, 'Runway video requires positive prompt to have at least one character'); 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 eae07532011..fb8f50c0cf3 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 @@ -1,8 +1,8 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectActiveTab, selectCanvasByCanvasId, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage'; import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint'; @@ -17,7 +17,6 @@ import { addWatermarker } from 'features/nodes/util/graph/generation/addWatermar import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { selectCanvasOutputFields, selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils'; import type { GraphBuilderArg, GraphBuilderReturn, ImageOutputNodes } from 'features/nodes/util/graph/types'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import type { Invocation } from 'services/api/types'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; @@ -35,8 +34,8 @@ export const buildSD1Graph = async (arg: GraphBuilderArg): Promise>(false); } - if (manager !== null) { + if (manager !== null && canvas !== null) { const controlNetCollector = g.addNode({ type: 'collect', id: getPrefixedId('control_net_collector'), @@ -281,7 +280,7 @@ export const buildSD1Graph = async (arg: GraphBuilderArg): Promise>(false); } - if (manager !== null) { + if (manager !== null && canvas !== null) { const controlNetCollector = g.addNode({ type: 'collect', id: getPrefixedId('control_net_collector'), @@ -284,7 +283,7 @@ export const buildSDXLGraph = async (arg: GraphBuilderArg): Promise assert(model, 'No model selected'); assert(model.base === 'veo3', 'Selected model is not a Veo3 model'); - const params = selectParamsSlice(state); + const params = selectActiveTabParams(state); const videoParams = selectVideoSlice(state); const prompts = selectPresetModifiedPrompts(state); assert(prompts.positive.length > 0, 'Veo3 video requires positive prompt to have at least one character'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index a98faea93a5..0487a8deffd 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -2,22 +2,21 @@ import { createSelector } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectSaveAllImagesToGallery } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectCanvasStagingAreaSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { + selectActiveTabParams, selectImg2imgStrength, selectMainModelConfig, selectOptimizedDenoisingEnabled, - selectParamsSlice, selectRefinerModel, selectRefinerStart, } from 'features/controlLayers/store/paramsSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectActiveTab } from 'features/controlLayers/store/selectors'; import type { ParamsState } from 'features/controlLayers/store/types'; import type { BoardField } from 'features/nodes/types/common'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; import { selectStylePresetSlice } from 'features/stylePresets/store/stylePresetSlice'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { selectListStylePresetsRequestState } from 'services/api/endpoints/stylePresets'; import type { Invocation } from 'services/api/types'; import { assert } from 'tsafe'; @@ -63,7 +62,7 @@ export const selectCanvasOutputFields = (state: RootState) => { * Select the destination to use for canvas queue items. * */ -export const selectCanvasDestination = (state: RootState) => { +export const selectCanvasDestination = (state: RootState, canvasId: string) => { // The canvas will stage images that have its session ID as the destination. When the user has enabled saving all // images to gallery, we want to bypass the staging area. So we use 'canvas' as a generic destination. Images will // go directly to the gallery. @@ -73,14 +72,15 @@ export const selectCanvasDestination = (state: RootState) => { if (saveAllImagesToGallery) { return 'canvas'; } - return selectCanvasSessionId(state); + + return selectCanvasStagingAreaSessionId(state, canvasId); }; /** * Gets the prompts, modified for the active style preset. */ export const selectPresetModifiedPrompts = createSelector( - selectParamsSlice, + selectActiveTabParams, selectStylePresetSlice, selectListStylePresetsRequestState, (params, stylePresetSlice, listStylePresetsRequestState) => { @@ -120,10 +120,10 @@ export const selectPresetModifiedPrompts = createSelector( export const getOriginalAndScaledSizesForTextToImage = (state: RootState) => { const tab = selectActiveTab(state); - const params = selectParamsSlice(state); - const canvas = selectCanvasSlice(state); + const params = selectActiveTabParams(state); if (tab === 'canvas') { + const canvas = selectActiveCanvas(state); const { rect, aspectRatio } = canvas.bbox; const { width, height } = rect; const originalSize = { width, height }; @@ -143,10 +143,10 @@ export const getOriginalAndScaledSizesForTextToImage = (state: RootState) => { export const getOriginalAndScaledSizesForOtherModes = (state: RootState) => { const tab = selectActiveTab(state); - const canvas = selectCanvasSlice(state); assert(tab === 'canvas', `Cannot get sizes for tab ${tab} - this function is only for the Canvas tab`); + const canvas = selectActiveCanvas(state); const { rect, aspectRatio } = canvas.bbox; const { width, height } = rect; const originalSize = { width, height }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx index c601e3b9b60..519898f6fb5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx @@ -9,8 +9,8 @@ import { useTranslation } from 'react-i18next'; const ParamCFGRescaleMultiplier = () => { const cfgRescaleMultiplier = useAppSelector(selectCFGRescaleMultiplier); const config = useAppSelector(selectCFGRescaleMultiplierConfig); - const dispatch = useAppDispatch(); + const { t } = useTranslation(); const handleChange = useCallback((v: number) => dispatch(setCfgRescaleMultiplier(v)), [dispatch]); diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx index 1d5eacc669c..e53d7c34b1a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx @@ -11,8 +11,8 @@ const ParamClipSkip = () => { const clipSkip = useAppSelector(selectCLIPSkip); const config = useAppSelector(selectCLIPSkipConfig); const model = useAppSelector(selectModel); - const dispatch = useAppDispatch(); + const { t } = useTranslation(); const handleClipSkipChange = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx index 40145839085..35316450d5a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxAspectRatioSelect.tsx @@ -1,8 +1,8 @@ import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { selectIsChatGPT4o, selectIsFluxKontext, diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxHeight.tsx index dd8e319447d..fcba968a9da 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxHeight.tsx @@ -2,7 +2,8 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { bboxHeightChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectGridSize, selectHeight, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/paramsSlice'; +import { selectHeight } from 'features/controlLayers/store/selectors'; import { useIsBboxSizeLocked } from 'features/parameters/components/Bbox/use-is-bbox-size-locked'; import { selectHeightConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxLockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxLockAspectRatioButton.tsx index 11d7fe66330..3286c21653c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxLockAspectRatioButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxLockAspectRatioButton.tsx @@ -2,13 +2,13 @@ import { IconButton } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { bboxAspectRatioLockToggled } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import { useIsBboxSizeLocked } from 'features/parameters/components/Bbox/use-is-bbox-size-locked'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi'; -const selectAspectRatioIsLocked = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.isLocked); +const selectAspectRatioIsLocked = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.aspectRatio.isLocked); export const BboxLockAspectRatioButton = memo(() => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaleMethod.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaleMethod.tsx index 839f9e0b030..d6c6ad1f895 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaleMethod.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaleMethod.tsx @@ -4,13 +4,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { bboxScaleMethodChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import { isBoundingBoxScaleMethod } from 'features/controlLayers/store/types'; import { useIsBboxSizeLocked } from 'features/parameters/components/Bbox/use-is-bbox-size-locked'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -const selectScaleMethod = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaleMethod); +const selectScaleMethod = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.scaleMethod); const BboxScaleMethod = () => { const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledHeight.tsx index da7338e72e3..1be7f34a5b0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledHeight.tsx @@ -2,14 +2,15 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { bboxScaledHeightChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import { useIsBboxSizeLocked } from 'features/parameters/components/Bbox/use-is-bbox-size-locked'; import { selectConfigSlice } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -const selectIsManual = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaleMethod === 'manual'); -const selectScaledHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaledSize.height); +const selectIsManual = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.scaleMethod === 'manual'); +const selectScaledHeight = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.scaledSize.height); const selectScaledBoundingBoxHeightConfig = createSelector( selectConfigSlice, (config) => config.sd.scaledBoundingBoxHeight diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledWidth.tsx index c4d14be98b7..0c78e6c2025 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledWidth.tsx @@ -2,14 +2,15 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { bboxScaledWidthChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import { useIsBboxSizeLocked } from 'features/parameters/components/Bbox/use-is-bbox-size-locked'; import { selectConfigSlice } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -const selectIsManual = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaleMethod === 'manual'); -const selectScaledWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaledSize.width); +const selectIsManual = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.scaleMethod === 'manual'); +const selectScaledWidth = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.scaledSize.width); const selectScaledBoundingBoxWidthConfig = createSelector( selectConfigSlice, (config) => config.sd.scaledBoundingBoxWidth diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSetOptimalSizeButton.tsx index 287daa6cc17..0fdd5b85375 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSetOptimalSizeButton.tsx @@ -2,15 +2,16 @@ import { IconButton } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { bboxSizeOptimized } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { selectOptimalDimension } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import { useIsBboxSizeLocked } from 'features/parameters/components/Bbox/use-is-bbox-size-locked'; import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiSparkleFill } from 'react-icons/pi'; -const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width); -const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height); +const selectWidth = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.rect.width); +const selectHeight = createSelector(selectActiveCanvas, (canvas) => canvas.bbox.rect.height); export const BboxSetOptimalSizeButton = memo(() => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx index 54614419a57..6e6eacfbac8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxSwapDimensionsButton.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { bboxDimensionsSwapped } from 'features/controlLayers/store/canvasSlice'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsDownUpBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxWidth.tsx index 8ad457da7ac..3eb3e81aab1 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxWidth.tsx @@ -2,7 +2,8 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; -import { selectGridSize, selectOptimalDimension, selectWidth } from 'features/controlLayers/store/selectors'; +import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/paramsSlice'; +import { selectWidth } from 'features/controlLayers/store/selectors'; import { useIsBboxSizeLocked } from 'features/parameters/components/Bbox/use-is-bbox-size-locked'; import { selectWidthConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts b/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts index 57b55d8a21e..f62c26e344b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts +++ b/invokeai/frontend/web/src/features/parameters/components/Bbox/use-is-bbox-size-locked.ts @@ -1,5 +1,5 @@ import { useAppSelector } from 'app/store/storeHooks'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { selectIsApiBaseModel } from 'features/controlLayers/store/paramsSlice'; export const useIsBboxSizeLocked = () => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx index bea45ae2828..c7c2942da2b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx @@ -12,7 +12,6 @@ import { useTranslation } from 'react-i18next'; const ParamInfillColorOptions = () => { const dispatch = useAppDispatch(); - const infillColor = useAppSelector(selectInfillColorValue); const infillMethod = useAppSelector(selectInfillMethod); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx index 9dc09f802d0..9f03b4e79ac 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx @@ -8,7 +8,6 @@ import { PiPlusMinusBold } from 'react-icons/pi'; export const NegativePromptToggleButton = memo(() => { const { t } = useTranslation(); const hasNegativePrompt = useAppSelector(selectHasNegativePrompt); - const dispatch = useAppDispatch(); const onClick = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx index 8001d81c9f2..5b3784f20d6 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -8,6 +8,7 @@ import { selectPositivePrompt, selectPositivePromptHistory, } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { promptGenerationFromImageDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton'; @@ -27,7 +28,6 @@ import { } from 'features/stylePresets/store/stylePresetSlice'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; import { selectAllowPromptExpansion } from 'features/system/store/configSlice'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import React, { memo, useCallback, useMemo, useRef } from 'react'; import type { HotkeyCallback } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -44,6 +44,7 @@ const persistOptions: Parameters[2] = { const usePromptHistory = () => { const store = useAppStore(); + const dispatch = useAppDispatch(); const history = useAppSelector(selectPositivePromptHistory); /** @@ -79,8 +80,8 @@ const usePromptHistory = () => { // Shouldn't happen return; } - store.dispatch(positivePromptChanged(newPrompt)); - }, [history, store]); + dispatch(positivePromptChanged(newPrompt)); + }, [dispatch, history, store]); const next = useCallback(() => { if (history.length === 0) { // No history, nothing to do @@ -94,7 +95,7 @@ const usePromptHistory = () => { state.historyIdx = state.historyIdx - 1; if (state.historyIdx < 0) { // Overshot to the "current" stashed prompt - store.dispatch(positivePromptChanged(state.stashedPrompt)); + dispatch(positivePromptChanged(state.stashedPrompt)); // Clear state bc we're back to current prompt stateRef.current = null; return; @@ -105,8 +106,8 @@ const usePromptHistory = () => { // Shouldn't happen return; } - store.dispatch(positivePromptChanged(newPrompt)); - }, [history, store]); + dispatch(positivePromptChanged(newPrompt)); + }, [dispatch, history]); const reset = useCallback(() => { // Clear stashed state - used when user clicks away or types in the prompt box stateRef.current = null; diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsHeight.tsx index a2f84f360a0..9fe8fca622e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsHeight.tsx @@ -1,8 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { heightChanged, selectHeight, selectIsApiBaseModel } from 'features/controlLayers/store/paramsSlice'; -import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { + heightChanged, + selectGridSize, + selectHeight, + selectIsApiBaseModel, + selectOptimalDimension, +} from 'features/controlLayers/store/paramsSlice'; import { selectHeightConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSetOptimalSizeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSetOptimalSizeButton.tsx index eda44ba925d..058522e3818 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSetOptimalSizeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSetOptimalSizeButton.tsx @@ -3,10 +3,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectHeight, selectIsApiBaseModel, + selectOptimalDimension, selectWidth, sizeOptimized, } from 'features/controlLayers/store/paramsSlice'; -import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx index 817a81996f7..4f7fe86eb03 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx @@ -8,6 +8,7 @@ import { PiArrowsDownUpBold } from 'react-icons/pi'; export const DimensionsSwapButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const onClick = useCallback(() => { dispatch(dimensionsSwapped()); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsWidth.tsx index eb2f96af96a..4af35abd917 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsWidth.tsx @@ -1,10 +1,15 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectIsApiBaseModel, selectWidth, widthChanged } from 'features/controlLayers/store/paramsSlice'; -import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import { + selectGridSize, + selectIsApiBaseModel, + selectOptimalDimension, + selectWidth, + widthChanged, +} from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { selectWidthConfig } from 'features/system/store/configSlice'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx b/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx index 08b24de1034..77b29424586 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx @@ -23,7 +23,7 @@ import { useDisclosure } from 'common/hooks/useBoolean'; import { typedMemo } from 'common/util/typedMemo'; import { uniq } from 'es-toolkit/compat'; import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore'; import { BASE_COLOR_MAP } from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge'; import ModelImage from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelImage'; @@ -39,7 +39,7 @@ import { PiCaretDownBold, PiLinkSimple } from 'react-icons/pi'; import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships'; import type { AnyModelConfig, BaseModelType } from 'services/api/types'; -const selectSelectedModelKeys = createMemoizedSelector(selectParamsSlice, selectLoRAsSlice, (params, loras) => { +const selectSelectedModelKeys = createMemoizedSelector(selectActiveTabParams, selectLoRAsSlice, (params, loras) => { const keys: string[] = []; const main = params.model; const vae = params.vae; diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx index 3393c0e14e7..a845a77e641 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx @@ -6,9 +6,9 @@ import { selectModelSupportsNegativePrompt, selectModelSupportsRefImages, } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt'; import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; export const Prompts = memo(() => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx index aced3654a16..85b058f8022 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx @@ -9,7 +9,6 @@ import { useTranslation } from 'react-i18next'; const ParamSeamlessXAxis = () => { const { t } = useTranslation(); const seamlessXAxis = useAppSelector(selectSeamlessXAxis); - const dispatch = useAppDispatch(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx index ca84094da38..6cfa8c5d346 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx @@ -8,8 +8,9 @@ import { useTranslation } from 'react-i18next'; const ParamSeamlessYAxis = () => { const { t } = useTranslation(); - const seamlessYAxis = useAppSelector(selectSeamlessYAxis); const dispatch = useAppDispatch(); + const seamlessYAxis = useAppSelector(selectSeamlessYAxis); + const handleChange = useCallback( (e: ChangeEvent) => { dispatch(setSeamlessYAxis(e.target.checked)); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx index d7e5ac2eccf..a57078c2f39 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx @@ -9,10 +9,8 @@ import { useTranslation } from 'react-i18next'; export const ParamSeedNumberInput = memo(() => { const seed = useAppSelector(selectSeed); const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); - - const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const { t } = useTranslation(); const handleChangeSeed = useCallback((v: number) => dispatch(setSeed(v)), [dispatch]); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx index f261f5273b5..b5280ee2cfb 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx @@ -8,9 +8,8 @@ import { useTranslation } from 'react-i18next'; import { PiShuffleBold } from 'react-icons/pi'; export const ParamSeedShuffle = memo(() => { - const dispatch = useAppDispatch(); const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); - + const dispatch = useAppDispatch(); const { t } = useTranslation(); const handleClickRandomizeSeed = useCallback( diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx index fdc18cde45a..baa009357ec 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx @@ -4,6 +4,7 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { debounce } from 'es-toolkit/compat'; import { selectIterations } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { selectDynamicPromptsIsLoading } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; import { $isInPublishFlow, useIsWorkflowPublished } from 'features/nodes/components/sidePanel/workflow/publish'; @@ -13,7 +14,6 @@ import type { BatchSizeResult } from 'features/nodes/util/node/resolveBatchValue import { getBatchSize } from 'features/nodes/util/node/resolveBatchValue'; import type { Reason } from 'features/queue/store/readiness'; import { $isReadyToEnqueue, $reasonsWhyCannotEnqueue, selectPromptsCount } from 'features/queue/store/readiness'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import type { PropsWithChildren } from 'react'; import { memo, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx index 4906ccc095b..345dedf5942 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx @@ -1,6 +1,7 @@ import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { SessionMenuItems } from 'common/components/SessionMenuItems'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; import { QueueCountBadge } from 'features/queue/components/QueueCountBadge'; import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; @@ -8,7 +9,6 @@ import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor'; import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { navigationApi } from 'features/ui/layouts/navigation-api'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { PiListBold, PiPauseFill, PiPlayFill, PiQueueBold, PiTrashBold, PiXBold, PiXCircle } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx index 3c2bd72bcc0..8ff94197678 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx @@ -6,9 +6,10 @@ import { selectIterationsConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; export const QueueIterationsNumberInput = memo(() => { + const dispatch = useAppDispatch(); const iterations = useAppSelector(selectIterations); const config = useAppSelector(selectIterationsConfig); - const dispatch = useAppDispatch(); + const handleChange = useCallback( (v: number) => { dispatch(setIterations(v)); diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts index 9d5a589f056..595aae81e9d 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts @@ -7,7 +7,11 @@ import { extractMessageFromAssertionError } from 'common/util/extractMessageFrom import { withResult, withResultAsync } from 'common/util/result'; import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice'; +import { + positivePromptAddedToHistory, + selectActiveTabParams, + selectPositivePrompt, +} from 'features/controlLayers/store/paramsSlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph'; import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph'; @@ -37,10 +41,10 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep dispatch(enqueueRequestedCanvas()); const state = getState(); + const destination = selectCanvasDestination(state, canvasManager.canvasId); + const params = selectActiveTabParams(state); - const destination = selectCanvasDestination(state); - - const model = state.params.model; + const model = params.model; if (!model) { log.error('No model found in state'); return; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts index 1529a87cff9..10c55dbbca1 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts @@ -5,7 +5,11 @@ import type { AppStore } from 'app/store/store'; import { useAppStore } from 'app/store/storeHooks'; import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError'; import { withResult, withResultAsync } from 'common/util/result'; -import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice'; +import { + positivePromptAddedToHistory, + selectActiveTabParams, + selectPositivePrompt, +} from 'features/controlLayers/store/paramsSlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph'; import { buildCogView4Graph } from 'features/nodes/util/graph/generation/buildCogView4Graph'; @@ -35,8 +39,9 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => { dispatch(enqueueRequestedGenerate()); const state = getState(); + const params = selectActiveTabParams(state); - const model = state.params.model; + const model = params.model; if (!model) { log.error('No model found in state'); return; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts index 01f278d98db..e551f037064 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts @@ -2,7 +2,11 @@ import { createAction } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { AppStore } from 'app/store/store'; import { useAppStore } from 'app/store/storeHooks'; -import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice'; +import { + positivePromptAddedToHistory, + selectActiveTabParams, + selectPositivePrompt, +} from 'features/controlLayers/store/paramsSlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildMultidiffusionUpscaleGraph } from 'features/nodes/util/graph/buildMultidiffusionUpscaleGraph'; import { useCallback } from 'react'; @@ -18,8 +22,9 @@ const enqueueUpscaling = async (store: AppStore, prepend: boolean) => { dispatch(enqueueRequestedUpscaling()); const state = getState(); + const params = selectActiveTabParams(state); - const model = state.params.model; + const model = params.model; if (!model) { log.error('No model found in state'); return; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueWorkflows.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueWorkflows.ts index 85272f4768a..146401d65f9 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueWorkflows.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueWorkflows.ts @@ -2,6 +2,7 @@ import { createAction } from '@reduxjs/toolkit'; import type { AppDispatch, AppStore, RootState } from 'app/store/store'; import { useAppStore } from 'app/store/storeHooks'; import { groupBy } from 'es-toolkit/compat'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import { $outputNodeId, getPublishInputs, @@ -132,13 +133,14 @@ const enqueueWorkflows = async ( const nodesState = selectNodesSlice(state); const graph = buildNodesGraph(state, templates); const workflow = buildWorkflowWithValidation(nodesState); + const params = selectActiveTabParams(state); if (workflow) { // embedded workflows don't have an id delete workflow.id; } - const runs = state.params.iterations; + const runs = params.iterations; const data = await getBatchDataForWorkflowGeneration(state, dispatch); const batchConfig: EnqueueBatchArg = { diff --git a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts index 3f1d29cf78a..8b70686184a 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts @@ -3,12 +3,12 @@ import { logger } from 'app/logging/logger'; import { useAppSelector } from 'app/store/storeHooks'; import { withResultAsync } from 'common/util/result'; import { selectSaveAllImagesToGallery } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked'; import { useEnqueueWorkflows } from 'features/queue/hooks/useEnqueueWorkflows'; import { $isReadyToEnqueue } from 'features/queue/store/readiness'; import { navigationApi } from 'features/ui/layouts/navigation-api'; import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useCallback } from 'react'; import { serializeError } from 'serialize-error'; import { enqueueMutationFixedCacheKeyOptions, useEnqueueBatchMutation } from 'services/api/endpoints/queue'; diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index e8a22e104ea..63b5be289aa 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -10,10 +10,10 @@ import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { debounce, groupBy, upperFirst } from 'es-toolkit/compat'; import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; -import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; -import type { CanvasState, LoRA, ParamsState, RefImagesState } from 'features/controlLayers/store/types'; +import { selectActiveCanvas, selectActiveTab } from 'features/controlLayers/store/selectors'; +import type { CanvasEntity, LoRA, ParamsState, RefImagesState, TabName } from 'features/controlLayers/store/types'; import { getControlLayerWarnings, getGlobalReferenceImageWarnings, @@ -42,8 +42,6 @@ import type { ParameterModel } from 'features/parameters/types/parameterSchemas' import { getGridSize } from 'features/parameters/util/optimalDimension'; import { promptExpansionApi, type PromptExpansionRequestState } from 'features/prompt/PromptExpansion/state'; import { selectAllowVideo, selectConfigSlice } from 'features/system/store/configSlice'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import type { TabName } from 'features/ui/store/uiTypes'; import i18n from 'i18next'; import { atom, computed } from 'nanostores'; import { useEffect } from 'react'; @@ -77,7 +75,7 @@ export const $isReadyToEnqueue = computed($reasonsWhyCannotEnqueue, (reasons) => type UpdateReasonsArg = { tab: TabName; isConnected: boolean; - canvas: CanvasState; + canvas: CanvasEntity; params: ParamsState; refImages: RefImagesState; dynamicPrompts: DynamicPromptsState; @@ -198,8 +196,8 @@ export const useReadinessWatcher = () => { const store = useAppStore(); const canvasManager = useCanvasManagerSafe(); const tab = useAppSelector(selectActiveTab); - const canvas = useAppSelector(selectCanvasSlice); - const params = useAppSelector(selectParamsSlice); + const canvas = useAppSelector(selectActiveCanvas); + const params = useAppSelector(selectActiveTabParams); const refImages = useAppSelector(selectRefImagesSlice); const dynamicPrompts = useAppSelector(selectDynamicPromptsSlice); const nodes = useAppSelector(selectNodesSlice); @@ -552,7 +550,7 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: { const getReasonsWhyCannotEnqueueCanvasTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; - canvas: CanvasState; + canvas: CanvasEntity; params: ParamsState; refImages: RefImagesState; loras: LoRA[]; @@ -821,7 +819,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: { }; export const selectPromptsCount = createSelector( - selectParamsSlice, + selectActiveTabParams, selectDynamicPromptsSlice, (params, dynamicPrompts) => (getShouldProcessPrompt(params.positivePrompt) ? dynamicPrompts.prompts.length : 1) ); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index 37475ad6aa8..3de4e7b12f9 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -3,7 +3,12 @@ import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-libra import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectIsFLUX, selectIsSD3, selectParamsSlice, selectVAEKey } from 'features/controlLayers/store/paramsSlice'; +import { + selectActiveTabParams, + selectIsFLUX, + selectIsSD3, + selectVAEKey, +} from 'features/controlLayers/store/paramsSlice'; import ParamCFGRescaleMultiplier from 'features/parameters/components/Advanced/ParamCFGRescaleMultiplier'; import ParamCLIPEmbedModelSelect from 'features/parameters/components/Advanced/ParamCLIPEmbedModelSelect'; import ParamCLIPGEmbedModelSelect from 'features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect'; @@ -36,7 +41,7 @@ export const AdvancedSettingsAccordion = memo(() => { const selectBadges = useMemo( () => - createMemoizedSelector([selectParamsSlice, selectIsFLUX], (params, isFLUX) => { + createMemoizedSelector([selectActiveTabParams, selectIsFLUX], (params, isFLUX) => { const badges: (string | number)[] = []; if (isFLUX) { if (vaeConfig) { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx index b9e1fe61b73..8edf482cd6e 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx @@ -2,7 +2,12 @@ import { Flex, StandaloneAccordion } from '@invoke-ai/ui-library'; import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectIsFLUX, selectIsSD3, selectParamsSlice, selectVAEKey } from 'features/controlLayers/store/paramsSlice'; +import { + selectActiveTabParams, + selectIsFLUX, + selectIsSD3, + selectVAEKey, +} from 'features/controlLayers/store/paramsSlice'; import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed'; import ParamTileControlNetModel from 'features/parameters/components/Upscale/ParamTileControlNetModel'; import ParamTileOverlap from 'features/parameters/components/Upscale/ParamTileOverlap'; @@ -23,7 +28,7 @@ export const UpscaleTabAdvancedSettingsAccordion = memo(() => { const selectBadges = useMemo( () => - createMemoizedSelector([selectParamsSlice, selectIsFLUX], (params, isFLUX) => { + createMemoizedSelector([selectActiveTabParams, selectIsFLUX], (params, isFLUX) => { const badges: (string | number)[] = []; if (isFLUX) { if (vaeConfig) { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx index 050722e3bcf..693e929fd70 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx @@ -2,7 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Flex, FormControlGroup, StandaloneAccordion, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectIsRefinerModelSelected, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams, selectIsRefinerModelSelected } from 'features/controlLayers/store/paramsSlice'; import ParamSDXLRefinerCFGScale from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale'; import ParamSDXLRefinerModelSelect from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect'; import ParamSDXLRefinerNegativeAestheticScore from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore'; @@ -23,7 +23,7 @@ const stepsScaleLabelProps: FormLabelProps = { minW: '5rem', }; -const selectBadges = createMemoizedSelector(selectParamsSlice, (params) => +const selectBadges = createMemoizedSelector(selectActiveTabParams, (params) => params.refinerModel ? ['Enabled'] : undefined ); diff --git a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx index e3cf278edf0..c17149cd5fd 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx @@ -31,6 +31,7 @@ export const ActiveStylePreset = () => { }); const dispatch = useAppDispatch(); + const { t } = useTranslation(); const { presetModifiedPositivePrompt, presetModifiedNegativePrompt } = usePresetModifiedPrompts(); diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 1be280fa40e..ca650ec2984 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -81,6 +81,7 @@ const [useSettingsModal] = buildUseBoolean(false); const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps) => { const dispatch = useAppDispatch(); + const { t } = useTranslation(); const { isNSFWCheckerAvailable, isWatermarkerAvailable } = useGetAppConfigQuery(undefined, { diff --git a/invokeai/frontend/web/src/features/system/hooks/useFeatureStatus.ts b/invokeai/frontend/web/src/features/system/hooks/useFeatureStatus.ts index 0126d0d94e6..872f98b6bee 100644 --- a/invokeai/frontend/web/src/features/system/hooks/useFeatureStatus.ts +++ b/invokeai/frontend/web/src/features/system/hooks/useFeatureStatus.ts @@ -1,8 +1,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import type { AppFeature, SDFeature } from 'app/types/invokeai'; +import type { TabName } from 'features/controlLayers/store/types'; import { selectConfigSlice } from 'features/system/store/configSlice'; -import type { TabName } from 'features/ui/store/uiTypes'; import { useMemo } from 'react'; export const useFeatureStatus = (feature: AppFeature | SDFeature | TabName) => { diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx index 5650e316bfb..d90d27d0e17 100644 --- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx @@ -5,6 +5,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import Loading from 'common/components/Loading/Loading'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { selectWithCanvasTab, selectWithGenerateTab, @@ -23,7 +24,6 @@ import { QueueTabAutoLayout } from 'features/ui/layouts/queue-tab-auto-layout'; import { UpscalingTabAutoLayout } from 'features/ui/layouts/upscaling-tab-auto-layout'; import { VideoTabAutoLayout } from 'features/ui/layouts/video-tab-auto-layout'; import { WorkflowsTabAutoLayout } from 'features/ui/layouts/workflows-tab-auto-layout'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; export const AppContent = memo(() => { diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx index adc0d4ecfc2..12ece8aabf4 100644 --- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx @@ -2,9 +2,9 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; +import type { TabName } from 'features/controlLayers/store/types'; import { navigationApi } from 'features/ui/layouts/navigation-api'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import type { TabName } from 'features/ui/store/uiTypes'; import type { ReactElement } from 'react'; import { memo, useCallback, useRef } from 'react'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx new file mode 100644 index 00000000000..fbf0ba979e7 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx @@ -0,0 +1,71 @@ +import { Flex, IconButton, Input, Text } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useBoolean } from 'common/hooks/useBoolean'; +import { useEditable } from 'common/hooks/useEditable'; +import { canvasNameChanged } from 'features/controlLayers/store/canvasSlice'; +import { memo, useCallback, useRef } from 'react'; +import { PiPencilBold } from 'react-icons/pi'; + +interface CanvasTabEditableTitleProps { + name: string; + isActive: boolean; +} + +export const CanvasTabEditableTitle = memo(({ name, isActive }: CanvasTabEditableTitleProps) => { + const dispatch = useAppDispatch(); + const isHovering = useBoolean(false); + const inputRef = useRef(null); + + const onChange = useCallback( + (value: string) => { + dispatch(canvasNameChanged({ name: value })); + }, + [dispatch] + ); + + const editable = useEditable({ + value: name, + defaultValue: name, + onChange, + inputRef, + onStartEditing: isHovering.setTrue, + }); + + if (!editable.isEditing) { + return ( + + + {editable.value} + + {isHovering.isTrue && ( + } + size="sm" + variant="ghost" + onClick={editable.startEditing} + /> + )} + + ); + } + + return ( + + ); +}); +CanvasTabEditableTitle.displayName = 'CanvasTabEditableTitle'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx new file mode 100644 index 00000000000..0e8f9c319f3 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx @@ -0,0 +1,123 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Box, Flex, IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { canvasActivated, canvasAdded, canvasDeleted } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvases } from 'features/controlLayers/store/selectors'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiPlusBold, PiXBold } from 'react-icons/pi'; + +import { CanvasTabEditableTitle } from './CanvasTabEditableTitle'; + +const _hover: SystemStyleObject = { + bg: 'base.650', +}; + +const AddCanvasButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const onClick = useCallback(() => { + dispatch(canvasAdded({ isSelected: true })); + }, [dispatch]); + + return ( + } + bg="base.650" + w={8} + h={8} + /> + ); +}); +AddCanvasButton.displayName = 'AddCanvasButton'; + +interface CloseCanvasButtonProps { + canvasId: string; + canDelete: boolean; +} + +const CloseCanvasButton = memo(({ canvasId, canDelete }: CloseCanvasButtonProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const onClick = useCallback(() => { + dispatch(canvasDeleted({ canvasId })); + }, [dispatch, canvasId]); + + return ( + } + disabled={!canDelete} + variant="link" + w={8} + h={8} + /> + ); +}); +CloseCanvasButton.displayName = 'CloseCanvasButton'; + +interface CanvasTabProps { + id: string; + name: string; + isActive: boolean; + canDelete: boolean; +} + +const CanvasTab = memo(({ id, name, isActive, canDelete }: CanvasTabProps) => { + const dispatch = useAppDispatch(); + + const onClick = useCallback(() => { + if (!isActive) { + dispatch(canvasActivated({ canvasId: id })); + } + }, [dispatch, id, isActive]); + + return ( + + + + + + + + + + + ); +}); +CanvasTab.displayName = 'CanvasTab'; + +export const CanvasTabs = () => { + const canvases = useAppSelector(selectCanvases); + + return ( + + + {canvases.map(({ id, name, isActive, canDelete }) => ( + + ))} + + ); +}; diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx index 2c8a516f982..3dbbf50e7b7 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx @@ -18,10 +18,10 @@ import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasT import { Transform } from 'features/controlLayers/components/Transform/Transform'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { memo, useCallback } from 'react'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; +import { CanvasTabs } from './CanvasTabs'; import { StagingArea } from './StagingArea'; const MenuContent = memo(() => { @@ -48,32 +48,17 @@ const canvasBgSx = { }, }; -export const CanvasWorkspacePanel = memo(() => { - const dynamicGrid = useAppSelector(selectDynamicGrid); - const showHUD = useAppSelector(selectShowHUD); - const sessionId = useAppSelector(selectCanvasSessionId); +const ActiveCanvas = memo(() => { + const dynamicGrid = useAppSelector((state) => selectDynamicGrid(state)); + const showHUD = useAppSelector((state) => selectShowHUD(state)); const renderMenu = useCallback(() => { return ; }, []); return ( - - - - - - + + renderMenu={renderMenu} withLongPress={false}> {(ref) => ( @@ -119,8 +104,32 @@ export const CanvasWorkspacePanel = memo(() => { - - + + + ); +}); +ActiveCanvas.displayName = 'ActiveCanvas'; + +export const CanvasWorkspacePanel = memo(() => { + return ( + + + + + + + + ); }); CanvasWorkspacePanel.displayName = 'CanvasWorkspacePanel'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx index 440847d7451..dddbf9d887a 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx @@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { setFocusedRegion } from 'common/hooks/focus'; import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter'; import type { IDockviewPanelHeaderProps } from 'dockview'; -import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectActiveCanvasStagingAreaSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { useCurrentQueueItemDestination } from 'features/queue/hooks/useCurrentQueueItemDestination'; import ProgressBar from 'features/system/components/ProgressBar'; import { memo, useCallback, useRef } from 'react'; @@ -15,7 +15,7 @@ import type { DockviewPanelParameters } from './auto-layout-context'; export const DockviewTabCanvasWorkspace = memo((props: IDockviewPanelHeaderProps) => { const { t } = useTranslation(); const isGenerationInProgress = useIsGenerationInProgress(); - const canvasSessionId = useAppSelector(selectCanvasSessionId); + const canvasSessionId = useAppSelector(selectActiveCanvasStagingAreaSessionId); const currentQueueItemDestination = useCurrentQueueItemDestination(); const ref = useRef(null); diff --git a/invokeai/frontend/web/src/features/ui/layouts/DockviewTabLaunchpad.tsx b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabLaunchpad.tsx index 47cc9b9ac67..bbf56c5c989 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/DockviewTabLaunchpad.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabLaunchpad.tsx @@ -3,8 +3,8 @@ import { useAppSelector } from 'app/store/storeHooks'; import { setFocusedRegion } from 'common/hooks/focus'; import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter'; import type { IDockviewPanelHeaderProps } from 'dockview'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import type { TabName } from 'features/ui/store/uiTypes'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; +import type { TabName } from 'features/controlLayers/store/types'; import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import type { IconType } from 'react-icons'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/LaunchpadEditImageButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/LaunchpadEditImageButton.tsx index 9bdf044aa43..ded806562a9 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/LaunchpadEditImageButton.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/LaunchpadEditImageButton.tsx @@ -1,7 +1,7 @@ import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library'; import { useAppStore } from 'app/store/storeHooks'; import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { newCanvasFromImageDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; import { newCanvasFromImage } from 'features/imageActions/actions'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/LaunchpadGenerateFromTextButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/LaunchpadGenerateFromTextButton.tsx index 856ec3d9b1a..744885eaf03 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/LaunchpadGenerateFromTextButton.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/LaunchpadGenerateFromTextButton.tsx @@ -1,8 +1,8 @@ import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; import { LaunchpadButton } from 'features/ui/layouts/LaunchpadButton'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCursorTextBold, PiTextAaBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/LaunchpadUseALayoutImageButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/LaunchpadUseALayoutImageButton.tsx index 71af2634600..52f45a2bd7f 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/LaunchpadUseALayoutImageButton.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/LaunchpadUseALayoutImageButton.tsx @@ -1,7 +1,7 @@ import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library'; import { useAppStore } from 'app/store/storeHooks'; import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { newCanvasFromImageDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; import { newCanvasFromImage } from 'features/imageActions/actions'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx b/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx index f262a25daa8..35e54182eac 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/StagingArea.tsx @@ -1,7 +1,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { StagingAreaItemsList } from 'features/controlLayers/components/StagingArea/StagingAreaItemsList'; import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { memo } from 'react'; export const StagingArea = memo(() => { diff --git a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx index cbb489bdb96..bebaf8b0d08 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx @@ -1,6 +1,6 @@ import type { FocusRegionName } from 'common/hooks/focus'; import type { IDockviewPanelProps, IGridviewPanelProps } from 'dockview'; -import type { TabName } from 'features/ui/store/uiTypes'; +import type { TabName } from 'features/controlLayers/store/types'; import type { FunctionComponent, PropsWithChildren } from 'react'; import { createContext, memo, useContext, useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx index b622b77e4de..252c6afa273 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx @@ -1,6 +1,7 @@ import type { DockviewApi, GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview'; import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview'; import { CanvasLayersPanel } from 'features/controlLayers/components/CanvasLayersPanelContent'; +import type { TabName } from 'features/controlLayers/store/types'; import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent'; import { GalleryPanel } from 'features/gallery/components/Gallery'; import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel'; @@ -15,7 +16,6 @@ import type { } from 'features/ui/layouts/auto-layout-context'; import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context'; import { CanvasLaunchpadPanel } from 'features/ui/layouts/CanvasLaunchpadPanel'; -import type { TabName } from 'features/ui/store/uiTypes'; import { dockviewTheme } from 'features/ui/styles/theme'; import { t } from 'i18next'; import { memo, useCallback, useEffect } from 'react'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx index 81cf2885474..3fbbccce2f5 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx @@ -1,5 +1,6 @@ import type { DockviewApi, GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview'; import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview'; +import type { TabName } from 'features/controlLayers/store/types'; import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent'; import { GalleryPanel } from 'features/gallery/components/Gallery'; import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel'; @@ -13,7 +14,6 @@ import type { RootLayoutGridviewComponents, } from 'features/ui/layouts/auto-layout-context'; import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context'; -import type { TabName } from 'features/ui/store/uiTypes'; import { dockviewTheme } from 'features/ui/styles/theme'; import { t } from 'i18next'; import { memo, useCallback, useEffect } from 'react'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx index e5ffcab0042..43dabf09fd0 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx @@ -1,9 +1,9 @@ import type { GridviewApi, IGridviewReactProps } from 'dockview'; import { GridviewReact, LayoutPriority, Orientation } from 'dockview'; +import type { TabName } from 'features/controlLayers/store/types'; import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab'; import type { RootLayoutGridviewComponents } from 'features/ui/layouts/auto-layout-context'; import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context'; -import type { TabName } from 'features/ui/store/uiTypes'; import { memo, useCallback, useEffect } from 'react'; import { navigationApi } from './navigation-api'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts index daa191acd79..64871b1f6ad 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts +++ b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts @@ -4,7 +4,8 @@ import { parseify } from 'common/util/serialize'; import type { GridviewApi, IDockviewPanel, IGridviewPanel } from 'dockview'; import { DockviewApi, GridviewPanel } from 'dockview'; import { debounce } from 'es-toolkit'; -import type { Serializable, TabName } from 'features/ui/store/uiTypes'; +import type { TabName } from 'features/controlLayers/store/types'; +import type { Serializable } from 'features/ui/store/uiTypes'; import type { Atom } from 'nanostores'; import { atom } from 'nanostores'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx index c5223035b7a..2d56e40be31 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx @@ -1,9 +1,9 @@ import type { GridviewApi, IGridviewReactProps } from 'dockview'; import { GridviewReact, LayoutPriority, Orientation } from 'dockview'; +import type { TabName } from 'features/controlLayers/store/types'; import QueueTab from 'features/ui/components/tabs/QueueTab'; import type { RootLayoutGridviewComponents } from 'features/ui/layouts/auto-layout-context'; import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context'; -import type { TabName } from 'features/ui/store/uiTypes'; import { memo, useCallback, useEffect } from 'react'; import { navigationApi } from './navigation-api'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx index 7820187119c..4148335ebbc 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/upscaling-tab-auto-layout.tsx @@ -1,5 +1,6 @@ import type { DockviewApi, GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview'; import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview'; +import type { TabName } from 'features/controlLayers/store/types'; import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent'; import { GalleryPanel } from 'features/gallery/components/Gallery'; import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel'; @@ -14,7 +15,6 @@ import type { } from 'features/ui/layouts/auto-layout-context'; import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context'; import { DockviewTab } from 'features/ui/layouts/DockviewTab'; -import type { TabName } from 'features/ui/store/uiTypes'; import { dockviewTheme } from 'features/ui/styles/theme'; import { t } from 'i18next'; import { memo, useCallback, useEffect } from 'react'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts b/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts index aa730a54a8c..689a8375a04 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts +++ b/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts @@ -1,5 +1,5 @@ import { GridviewPanel, type GridviewPanelApi, type IGridviewPanel } from 'dockview'; -import type { TabName } from 'features/ui/store/uiTypes'; +import type { TabName } from 'features/controlLayers/store/types'; import { atom } from 'nanostores'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-gallery-panel.ts b/invokeai/frontend/web/src/features/ui/layouts/use-gallery-panel.ts index 274f4bd5954..b68f328a776 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/use-gallery-panel.ts +++ b/invokeai/frontend/web/src/features/ui/layouts/use-gallery-panel.ts @@ -1,4 +1,4 @@ -import type { TabName } from 'features/ui/store/uiTypes'; +import type { TabName } from 'features/controlLayers/store/types'; import { GALLERY_PANEL_DEFAULT_HEIGHT_PX, diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-navigation-api.tsx b/invokeai/frontend/web/src/features/ui/layouts/use-navigation-api.tsx index 238147a0c13..b7ddbd6c5a8 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/use-navigation-api.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/use-navigation-api.tsx @@ -1,8 +1,9 @@ import { useAppStore } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import { dockviewStorageKeyChanged, setActiveTab } from 'features/ui/store/uiSlice'; -import type { TabName } from 'features/ui/store/uiTypes'; +import { selectActiveTab } from 'features/controlLayers/store/selectors'; +import { setActiveTab } from 'features/controlLayers/store/tabSlice'; +import type { TabName } from 'features/controlLayers/store/types'; +import { dockviewStorageKeyChanged } from 'features/ui/store/uiSlice'; import { useEffect, useMemo } from 'react'; import type { JsonObject } from 'type-fest'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/video-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/video-tab-auto-layout.tsx index dbf9b44e4c7..d02db1c8c51 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/video-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/video-tab-auto-layout.tsx @@ -1,5 +1,6 @@ import type { DockviewApi, GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview'; import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview'; +import type { TabName } from 'features/controlLayers/store/types'; import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent'; import { GalleryPanel } from 'features/gallery/components/Gallery'; import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel'; @@ -13,7 +14,6 @@ import type { RootLayoutGridviewComponents, } from 'features/ui/layouts/auto-layout-context'; import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context'; -import type { TabName } from 'features/ui/store/uiTypes'; import { dockviewTheme } from 'features/ui/styles/theme'; import { t } from 'i18next'; import { memo, useCallback, useEffect } from 'react'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx index 06ae423e447..59c9bcdcecb 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/workflows-tab-auto-layout.tsx @@ -1,5 +1,6 @@ import type { DockviewApi, GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview'; import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockview'; +import type { TabName } from 'features/controlLayers/store/types'; import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent'; import { GalleryPanel } from 'features/gallery/components/Gallery'; import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel'; @@ -16,7 +17,6 @@ import type { } from 'features/ui/layouts/auto-layout-context'; import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'features/ui/layouts/auto-layout-context'; import { DockviewTab } from 'features/ui/layouts/DockviewTab'; -import type { TabName } from 'features/ui/store/uiTypes'; import { dockviewTheme } from 'features/ui/styles/theme'; import { t } from 'i18next'; import { memo, useCallback, useEffect } from 'react'; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts index abaef76c55c..cb5382a34a7 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts @@ -1,7 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { selectUiSlice } from 'features/ui/store/uiSlice'; -export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTab); export const selectShouldShowItemDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowItemDetails); export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer); export const selectPickerCompactViewStates = createSelector(selectUiSlice, (ui) => ui.pickerCompactViewStates); diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index b1f3fe9fae6..5f79741a67a 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -1,8 +1,9 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { SliceConfig } from 'app/store/types'; +import type { SerializedStateFromDenyList, SliceConfig } from 'app/store/types'; import { isPlainObject } from 'es-toolkit'; +import { merge, omit } from 'es-toolkit/compat'; import { assert } from 'tsafe'; import { getInitialUIState, type UIState, zUIState } from './uiTypes'; @@ -11,9 +12,6 @@ const slice = createSlice({ name: 'ui', initialState: getInitialUIState(), reducers: { - setActiveTab: (state, action: PayloadAction) => { - state.activeTab = action.payload; - }, setShouldShowItemDetails: (state, action: PayloadAction) => { state.shouldShowItemDetails = action.payload; }, @@ -74,7 +72,6 @@ const slice = createSlice({ }); export const { - setActiveTab, setShouldShowItemDetails, setShouldShowProgressInViewer, accordionStateChanged, @@ -87,7 +84,10 @@ export const { export const selectUiSlice = (state: RootState) => state.ui; -export const uiSliceConfig: SliceConfig = { +const denyList = ['shouldShowItemDetails'] as const; +type SerializedUIState = SerializedStateFromDenyList; + +export const uiSliceConfig: SliceConfig = { slice, schema: zUIState, getInitialState: getInitialUIState, @@ -111,6 +111,11 @@ export const uiSliceConfig: SliceConfig = { } return zUIState.parse(state); }, - persistDenylist: ['shouldShowItemDetails'], + serialize: (state) => omit(state, denyList), + deserialize: (state) => { + const uiState = state as SerializedUIState; + + return merge(uiState, getInitialUIState()); + }, }, }; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 95f7603821e..695c2f9145e 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,9 +1,6 @@ import { isPlainObject } from 'es-toolkit'; import { z } from 'zod'; -export const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue', 'video']); -export type TabName = z.infer; - const zPartialDimensions = z.object({ width: z.number().optional(), height: z.number().optional(), @@ -14,7 +11,6 @@ export type Serializable = z.infer; export const zUIState = z.object({ _version: z.literal(4), - activeTab: zTabName, shouldShowItemDetails: z.boolean(), shouldShowProgressInViewer: z.boolean(), accordions: z.record(z.string(), z.boolean()), @@ -27,7 +23,6 @@ export const zUIState = z.object({ export type UIState = z.infer; export const getInitialUIState = (): UIState => ({ _version: 4 as const, - activeTab: 'generate' as const, shouldShowItemDetails: false, shouldShowProgressInViewer: true, accordions: {},