From dd8e8398c09921e157ff02efa8aa2c54ca8139ed Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Wed, 10 Sep 2025 13:44:50 +0200 Subject: [PATCH 01/16] canvas slice refactored to support tabbed canvases --- invokeai/frontend/web/package.json | 2 +- invokeai/frontend/web/pnpm-lock.yaml | 10 +- .../listeners/boardAndImagesDeleted.ts | 4 +- .../listeners/modelSelected.ts | 4 +- .../listeners/modelsLoaded.ts | 8 +- .../CanvasAlertsSelectedEntityStatus.tsx | 6 +- ...tityListSelectedEntityActionBarOpacity.tsx | 4 +- .../ControlLayer/ControlLayerBadges.tsx | 4 +- .../ControlLayerControlAdapter.tsx | 4 +- .../ControlLayer/ControlLayerEntityList.tsx | 4 +- ...ontrolLayerMenuItemsTransparencyEffect.tsx | 4 +- .../InpaintMaskDenoiseLimitSlider.tsx | 4 +- .../InpaintMask/InpaintMaskList.tsx | 4 +- .../InpaintMask/InpaintMaskNoiseSlider.tsx | 4 +- .../InpaintMask/InpaintMaskSettings.tsx | 6 +- .../RasterLayer/RasterLayerEntityList.tsx | 4 +- .../RegionalGuidanceBadges.tsx | 4 +- .../RegionalGuidanceEntityList.tsx | 4 +- .../RegionalGuidanceIPAdapterSettings.tsx | 6 +- .../RegionalGuidanceIPAdapters.tsx | 4 +- .../RegionalGuidanceMenuItemsAutoNegative.tsx | 4 +- .../RegionalGuidanceNegativePrompt.tsx | 4 +- .../RegionalGuidancePositivePrompt.tsx | 4 +- .../RegionalGuidanceSettings.tsx | 4 +- .../common/CanvasEntityHeaderWarnings.tsx | 4 +- .../common/CanvasEntityMenuItemsArrange.tsx | 4 +- .../common/CanvasEntityPreviewImage.tsx | 4 +- .../controlLayers/hooks/addLayerHooks.ts | 4 +- .../useEntityIsBookmarkedForQuickSwitch.ts | 4 +- .../controlLayers/hooks/useEntityIsEnabled.ts | 4 +- .../controlLayers/hooks/useEntityIsLocked.ts | 4 +- .../controlLayers/hooks/useEntityTitle.ts | 4 +- .../controlLayers/hooks/useEntityTypeCount.ts | 4 +- .../hooks/useEntityTypeIsHidden.ts | 4 +- .../controlLayers/hooks/useNextPrevEntity.ts | 6 +- .../useNextRenderableEntityIdentifier.ts | 4 +- .../CanvasEntity/CanvasEntityAdapterBase.ts | 4 +- .../konva/CanvasEntityRendererModule.ts | 6 +- .../konva/CanvasStateApiModule.ts | 4 +- .../konva/CanvasTool/CanvasToolModule.ts | 4 +- .../controlLayers/store/canvasSlice.ts | 819 +++++++++++------- .../features/controlLayers/store/selectors.ts | 62 +- .../src/features/controlLayers/store/types.ts | 26 +- .../features/deleteImageModal/store/state.ts | 8 +- .../components/Boards/DeleteBoardModal.tsx | 4 +- .../util/graph/generation/buildFLUXGraph.ts | 4 +- .../util/graph/generation/buildSD1Graph.ts | 4 +- .../util/graph/generation/buildSDXLGraph.ts | 4 +- .../nodes/util/graph/graphBuilderUtils.ts | 6 +- .../Bbox/BboxLockAspectRatioButton.tsx | 4 +- .../components/Bbox/BboxScaleMethod.tsx | 4 +- .../components/Bbox/BboxScaledHeight.tsx | 6 +- .../components/Bbox/BboxScaledWidth.tsx | 6 +- .../Bbox/BboxSetOptimalSizeButton.tsx | 6 +- .../web/src/features/queue/store/readiness.ts | 4 +- 55 files changed, 693 insertions(+), 452 deletions(-) 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/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index d185a03f220..23da5fd094c 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 { selectSelectedCanvas } 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,7 +19,7 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS const state = getState(); const nodes = selectNodesSlice(state); - const canvas = selectCanvasSlice(state); + const canvas = selectSelectedCanvas(state); const upscale = selectUpscaleSlice(state); const refImages = selectRefImagesSlice(state); 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..96a7a214713 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 @@ -8,7 +8,7 @@ import { refImageModelChanged, selectReferenceImageEntities } from 'features/con import { selectAllEntitiesOfType, selectBboxModelBase, - selectCanvasSlice, + selectSelectedCanvas, } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { modelSelected } from 'features/parameters/store/actions'; @@ -118,7 +118,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 = selectSelectedCanvas(state); const canvasRegionalGuidanceEntities = selectAllEntitiesOfType(canvasState, 'regional_guidance'); for (const entity of canvasRegionalGuidanceEntities) { for (const refImage of entity.referenceImages) { 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..6ab79d35da0 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 @@ -11,7 +11,7 @@ import { vaeSelected, } from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier, isFLUXReduxConfig, @@ -221,7 +221,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) => { + selectSelectedCanvas(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 +256,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => { dispatch(refImageModelChanged({ id: entity.id, modelConfig: null })); }); - selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => { + selectSelectedCanvas(state).regionalGuidance.entities.forEach((entity) => { entity.referenceImages.forEach(({ id: referenceImageId, config }) => { if (!isRegionalGuidanceIPAdapterConfig(config)) { return; @@ -299,7 +299,7 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => { dispatch(refImageModelChanged({ id: entity.id, modelConfig: null })); }); - selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => { + selectSelectedCanvas(state).regionalGuidance.entities.forEach((entity) => { entity.referenceImages.forEach(({ id: referenceImageId, config }) => { if (!isRegionalGuidanceFLUXReduxConfig(config)) { return; 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..4bcff6a02ff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx @@ -9,8 +9,8 @@ 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, selectEntityOrThrow, + selectSelectedCanvas, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; @@ -32,13 +32,13 @@ type AlertData = { const buildSelectIsEnabled = (entityIdentifier: CanvasEntityIdentifier) => createSelector( - selectCanvasSlice, + selectSelectedCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isEnabled ); const buildSelectIsLocked = (entityIdentifier: CanvasEntityIdentifier) => createSelector( - selectCanvasSlice, + selectSelectedCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isLocked ); 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..bd6749da463 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx @@ -20,8 +20,8 @@ import { clamp, round } from 'es-toolkit/compat'; import { snapToNearest } from 'features/controlLayers/konva/util'; import { entityOpacityChanged } from 'features/controlLayers/store/canvasSlice'; import { - selectCanvasSlice, selectEntity, + selectSelectedCanvas, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; import type { KeyboardEvent } from 'react'; @@ -61,7 +61,7 @@ const sliderDefaultValue = mapRawValueToSliderValue(1); const snapCandidates = marks.slice(1, marks.length - 1); -const selectOpacity = createSelector(selectCanvasSlice, (canvas) => { +const selectOpacity = createSelector(selectSelectedCanvas, (canvas) => { const selectedEntityIdentifier = canvas.selectedEntityIdentifier; if (!selectedEntityIdentifier) { return 1; // fallback to 100% opacity 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..0bb1fb62e86 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 { selectEntityOrThrow, selectSelectedCanvas } 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, + selectSelectedCanvas, (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..32c2e94686f 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 { selectEntityOrThrow, selectSelectedCanvas } 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(selectSelectedCanvas, (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..039cb19526a 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 { selectSelectedCanvas, 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(selectSelectedCanvas, (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..e17c718abd1 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 { selectEntityOrThrow, selectSelectedCanvas } 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, + selectSelectedCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerMenuItemsTransparencyEffect').withTransparencyEffect ); 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..8e851e2fbc6 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 { selectEntityOrThrow, selectSelectedCanvas } 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, + selectSelectedCanvas, (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..66425d25894 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 { selectSelectedCanvas, 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(selectSelectedCanvas, (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..3e16e0acdd9 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 { selectEntityOrThrow, selectSelectedCanvas } 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, + selectSelectedCanvas, (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..34864b65682 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 { selectEntityOrThrow, selectSelectedCanvas } 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(selectSelectedCanvas, (canvas) => { const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings'); return entity.denoiseLimit !== undefined; }); const buildSelectHasNoiseLevel = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) => - createSelector(selectCanvasSlice, (canvas) => { + createSelector(selectSelectedCanvas, (canvas) => { const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings'); return entity.noiseLevel !== undefined; }); 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..f2b22982668 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 { selectSelectedCanvas, 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(selectSelectedCanvas, (canvas) => { return canvas.rasterLayers.entities.map(getEntityIdentifier).toReversed(); }); const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => { 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..d5e7a3f818f 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 { selectEntityOrThrow, selectSelectedCanvas } 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, + selectSelectedCanvas, (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..bf75a03d8b0 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 { selectSelectedCanvas, 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(selectSelectedCanvas, (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..5aa3f8edde2 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 { selectRegionalGuidanceReferenceImage, selectSelectedCanvas } 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(selectSelectedCanvas, (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(selectSelectedCanvas, (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..6354a57867f 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 { selectEntityOrThrow, selectSelectedCanvas } 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(selectSelectedCanvas, (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..048fd36ac84 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 { selectEntityOrThrow, selectSelectedCanvas } 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, + selectSelectedCanvas, (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..61d79af2ce9 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 { selectEntityOrThrow, selectSelectedCanvas } 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, + selectSelectedCanvas, (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..808ae104efb 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 { selectEntityOrThrow, selectSelectedCanvas } 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, + selectSelectedCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidancePositivePrompt').positivePrompt ?? '' ), [entityIdentifier] 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..844c021c079 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 { selectEntityOrThrow, selectSelectedCanvas } 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(selectSelectedCanvas, (canvas) => { const entity = selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidanceSettings'); return { hasPositivePrompt: entity.positivePrompt !== null, 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..2a7747bfff3 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 { selectEntityOrThrow, selectSelectedCanvas } 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(selectSelectedCanvas, 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..cd3c6059ee0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx @@ -9,7 +9,7 @@ import { entityArrangedToBack, entityArrangedToFront, } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier, CanvasState } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -54,7 +54,7 @@ export const CanvasEntityMenuItemsArrange = memo(() => { const isBusy = useCanvasIsBusy(); const selectValidActions = useMemo( () => - createMemoizedSelector(selectCanvasSlice, (canvas) => { + createMemoizedSelector(selectSelectedCanvas, (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..86f7819013d 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 { selectEntity, selectSelectedCanvas } 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(selectSelectedCanvas, (state) => { const entity = selectEntity(state, entityIdentifier); if (!entity) { return 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..d14d72c7e8b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -17,8 +17,8 @@ import { } from 'features/controlLayers/store/canvasSlice'; import { selectBase, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { - selectCanvasSlice, selectEntity, + selectSelectedCanvas, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; import type { @@ -270,7 +270,7 @@ export const useAddInpaintMaskDenoiseLimit = (entityIdentifier: CanvasEntityIden export const buildSelectValidRegionalGuidanceActions = ( entityIdentifier: CanvasEntityIdentifier<'regional_guidance'> ) => { - return createMemoizedSelector(selectCanvasSlice, (canvas) => { + return createMemoizedSelector(selectSelectedCanvas, (canvas) => { const entity = selectEntity(canvas, entityIdentifier); return { canAddPositivePrompt: entity?.positivePrompt === null, diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts index 5c497bf6c35..ef3776dfc25 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 { selectSelectedCanvas } 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(selectSelectedCanvas, (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..127edee97eb 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 { selectEntity, selectSelectedCanvas } 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(selectSelectedCanvas, (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..45081fa63aa 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 { selectEntity, selectSelectedCanvas } 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(selectSelectedCanvas, (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..83883467144 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 { selectEntity, selectSelectedCanvas } 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(selectSelectedCanvas, (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..4e6160777be 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 { selectSelectedCanvas } 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(selectSelectedCanvas, (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..b5843d52efb 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 { selectSelectedCanvas } 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(selectSelectedCanvas, (canvas) => { switch (type) { case 'control_layer': return canvas.controlLayers.isHidden; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useNextPrevEntity.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useNextPrevEntity.ts index 000ecc53725..9d373913823 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 { selectAllEntities, selectSelectedCanvas } 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(selectSelectedCanvas, (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(selectSelectedCanvas, (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..74793a6ac47 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 { selectEntityIdentifierBelowThisOne, selectSelectedCanvas } 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(selectSelectedCanvas, (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..1ed4e8371cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts @@ -20,8 +20,8 @@ import { buildSelectIsSelected, getSelectIsTypeHidden, selectBboxRect, - selectCanvasSlice, selectEntity, + selectSelectedCanvas, } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier, @@ -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..67a278baaea 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts @@ -2,11 +2,11 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { - selectCanvasSlice, selectControlLayerEntities, selectInpaintMaskEntities, selectRasterLayerEntities, selectRegionalGuidanceEntities, + selectSelectedCanvas, } from 'features/controlLayers/store/selectors'; import type { CanvasControlLayerState, @@ -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(selectSelectedCanvas, 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(selectSelectedCanvas), null); }; createNewRasterLayers = (entities: CanvasRasterLayerState[]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 57027aaa8f9..5f82c2ff1bc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -33,8 +33,8 @@ import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasSta import { selectAllRenderableEntities, selectBbox, - selectCanvasSlice, selectGridSize, + selectSelectedCanvas, } from 'features/controlLayers/store/selectors'; import type { CanvasState, @@ -128,7 +128,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { * The state is stored in redux. */ getCanvasState = (): CanvasState => { - return this.runSelector(selectCanvasSlice); + return this.runSelector(selectSelectedCanvas); }; /** 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..779109b8ac9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts @@ -13,7 +13,7 @@ import { getPrefixedId, } from 'features/controlLayers/konva/util'; import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasControlLayerState, CanvasInpaintMaskState, @@ -136,7 +136,7 @@ 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(selectSelectedCanvas, this.render)); this.subscriptions.add( this.$tool.listen(() => { // On tool switch, reset mouse state diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index ee1a9c6ba44..a5acf2b0184 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -17,6 +17,7 @@ import { import type { CanvasEntityStateFromType, CanvasEntityType, + CanvasesState, CanvasInpaintMaskState, CanvasMetadata, ChannelName, @@ -78,7 +79,6 @@ import { FLUX_KONTEXT_ASPECT_RATIOS, GEMINI_2_5_ASPECT_RATIOS, getEntityIdentifier, - getInitialCanvasState, IMAGEN_ASPECT_RATIOS, isChatGPT4oAspectRatioID, isFluxKontextAspectRatioID, @@ -86,7 +86,7 @@ import { isImagenAspectRatioID, isRegionalGuidanceFLUXReduxConfig, isRegionalGuidanceIPAdapterConfig, - zCanvasState, + zCanvasesState, } from './types'; import { converters, @@ -104,11 +104,93 @@ import { makeDefaultRasterLayerAdjustments, } from './util'; +const getInitialCanvasesState = (): CanvasesState => { + const canvasId = getPrefixedId('canvas'); + const canvasName = 'default'; + const canvas = getCanvasState(canvasId, canvasName); + + return { + _version: 3, + selectedCanvasId: canvas.id, + canvases: [canvas], + }; +}; + +const getCanvasState = (id: string, name: string): CanvasState => ({ + id, + name, + 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 slice = createSlice({ name: 'canvas', - initialState: getInitialCanvasState(), + initialState: getInitialCanvasesState(), reducers: { - // undoable canvas state + // undoable canvases state + //#region Canvases + canvasAdded: { + reducer: (state, action: PayloadAction<{ id: string; isSelected?: boolean }>) => { + const { id, isSelected } = action.payload; + + const name = 'default'; + const canvasState = getCanvasState(id, name); + state.canvases.push(canvasState); + + if (isSelected) { + state.selectedCanvasId = id; + } + }, + prepare: (payload: { isSelected?: boolean }) => { + return { + payload: { ...payload, id: getPrefixedId('canvas') }, + }; + }, + }, + canvasSelected: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + + const canvas = getCanvasById(state, id); + if (!canvas) { + return; + } + + state.selectedCanvasId = canvas.id; + }, + canvasNameChanged: (state, action: PayloadAction<{ id: string; name: string }>) => { + const { id, name } = action.payload; + + const canvas = getCanvasById(state, id); + if (!canvas) { + return; + } + + canvas.name = name; + }, + canvasDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + + if (state.canvases.length === 1) { + throw new Error('Last canvas cannot be deleted'); + } + + const index = state.canvases.findIndex((canvas) => canvas.id === id); + const nextIndex = (index + 1) % state.canvases.length; + + state.selectedCanvasId = state.canvases[nextIndex]!.id; + state.canvases = state.canvases.filter((canvas) => canvas.id !== id); + }, //#region Raster layers rasterLayerAdjustmentsSet: ( state, @@ -213,15 +295,16 @@ const slice = createSlice({ }> ) => { const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [], addAfter } = action.payload; + const canvas = getSelectedCanvas(state); const entityState = getRasterLayerState(id, overrides); const index = addAfter - ? state.rasterLayers.entities.findIndex((e) => e.id === addAfter) + 1 - : state.rasterLayers.entities.length; - state.rasterLayers.entities.splice(index, 0, entityState); + ? canvas.rasterLayers.entities.findIndex((e) => e.id === addAfter) + 1 + : canvas.rasterLayers.entities.length; + canvas.rasterLayers.entities.splice(index, 0, entityState); if (mergedEntitiesToDelete.length > 0) { - state.rasterLayers.entities = state.rasterLayers.entities.filter( + canvas.rasterLayers.entities = canvas.rasterLayers.entities.filter( (entity) => !mergedEntitiesToDelete.includes(entity.id) ); } @@ -229,11 +312,11 @@ const slice = createSlice({ const entityIdentifier = getEntityIdentifier(entityState); if (isSelected || mergedEntitiesToDelete.length > 0) { - state.selectedEntityIdentifier = entityIdentifier; + canvas.selectedEntityIdentifier = entityIdentifier; } if (isBookmarked) { - state.bookmarkedEntityIdentifier = entityIdentifier; + canvas.bookmarkedEntityIdentifier = entityIdentifier; } }, prepare: (payload: { @@ -248,8 +331,10 @@ const slice = createSlice({ }, rasterLayerRecalled: (state, action: PayloadAction<{ data: CanvasRasterLayerState }>) => { const { data } = action.payload; - state.rasterLayers.entities.push(data); - state.selectedEntityIdentifier = getEntityIdentifier(data); + const canvas = getSelectedCanvas(state); + + canvas.rasterLayers.entities.push(data); + canvas.selectedEntityIdentifier = getEntityIdentifier(data); }, rasterLayerConvertedToControlLayer: { reducer: ( @@ -262,7 +347,8 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const layer = selectEntity(state, entityIdentifier); + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -272,13 +358,15 @@ const slice = createSlice({ if (replace) { // Remove the raster layer - state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); + canvas.rasterLayers.entities = canvas.rasterLayers.entities.filter( + (layer) => layer.id !== entityIdentifier.id + ); } // Add the converted control layer - state.controlLayers.entities.push(controlLayerState); + canvas.controlLayers.entities.push(controlLayerState); - state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id }; + canvas.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -300,7 +388,8 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const layer = selectEntity(state, entityIdentifier); + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -310,13 +399,15 @@ const slice = createSlice({ if (replace) { // Remove the raster layer - state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); + canvas.rasterLayers.entities = canvas.rasterLayers.entities.filter( + (layer) => layer.id !== entityIdentifier.id + ); } // Add the converted inpaint mask - state.inpaintMasks.entities.push(inpaintMaskState); + canvas.inpaintMasks.entities.push(inpaintMaskState); - state.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; + canvas.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -338,7 +429,8 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const layer = selectEntity(state, entityIdentifier); + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -348,13 +440,15 @@ const slice = createSlice({ if (replace) { // Remove the raster layer - state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); + canvas.rasterLayers.entities = canvas.rasterLayers.entities.filter( + (layer) => layer.id !== entityIdentifier.id + ); } // Add the converted inpaint mask - state.regionalGuidance.entities.push(regionalGuidanceState); + canvas.regionalGuidance.entities.push(regionalGuidanceState); - state.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; + canvas.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -380,26 +474,27 @@ const slice = createSlice({ ) => { const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [], addAfter } = action.payload; + const canvas = getSelectedCanvas(state); const entityState = getControlLayerState(id, overrides); const index = addAfter - ? state.controlLayers.entities.findIndex((e) => e.id === addAfter) + 1 - : state.controlLayers.entities.length; - state.controlLayers.entities.splice(index, 0, entityState); + ? canvas.controlLayers.entities.findIndex((e) => e.id === addAfter) + 1 + : canvas.controlLayers.entities.length; + canvas.controlLayers.entities.splice(index, 0, entityState); if (mergedEntitiesToDelete.length > 0) { - state.controlLayers.entities = state.controlLayers.entities.filter( + canvas.controlLayers.entities = canvas.controlLayers.entities.filter( (entity) => !mergedEntitiesToDelete.includes(entity.id) ); } const entityIdentifier = getEntityIdentifier(entityState); if (isSelected || mergedEntitiesToDelete.length > 0) { - state.selectedEntityIdentifier = entityIdentifier; + canvas.selectedEntityIdentifier = entityIdentifier; } if (isBookmarked) { - state.bookmarkedEntityIdentifier = entityIdentifier; + canvas.bookmarkedEntityIdentifier = entityIdentifier; } }, prepare: (payload: { @@ -414,8 +509,10 @@ const slice = createSlice({ }, controlLayerRecalled: (state, action: PayloadAction<{ data: CanvasControlLayerState }>) => { const { data } = action.payload; - state.controlLayers.entities.push(data); - state.selectedEntityIdentifier = { type: 'control_layer', id: data.id }; + const canvas = getSelectedCanvas(state); + + canvas.controlLayers.entities.push(data); + canvas.selectedEntityIdentifier = { type: 'control_layer', id: data.id }; }, controlLayerConvertedToRasterLayer: { reducer: ( @@ -428,7 +525,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const layer = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -438,15 +537,15 @@ const slice = createSlice({ if (replace) { // Remove the control layer - state.controlLayers.entities = state.controlLayers.entities.filter( + canvas.controlLayers.entities = canvas.controlLayers.entities.filter( (layer) => layer.id !== entityIdentifier.id ); } // Add the new raster layer - state.rasterLayers.entities.push(rasterLayerState); + canvas.rasterLayers.entities.push(rasterLayerState); - state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id }; + canvas.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -468,7 +567,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const layer = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -478,15 +579,15 @@ const slice = createSlice({ if (replace) { // Remove the control layer - state.controlLayers.entities = state.controlLayers.entities.filter( + canvas.controlLayers.entities = canvas.controlLayers.entities.filter( (layer) => layer.id !== entityIdentifier.id ); } // Add the new inpaint mask - state.inpaintMasks.entities.push(inpaintMaskState); + canvas.inpaintMasks.entities.push(inpaintMaskState); - state.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; + canvas.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -508,7 +609,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const layer = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -518,15 +621,15 @@ const slice = createSlice({ if (replace) { // Remove the control layer - state.controlLayers.entities = state.controlLayers.entities.filter( + canvas.controlLayers.entities = canvas.controlLayers.entities.filter( (layer) => layer.id !== entityIdentifier.id ); } // Add the new regional guidance - state.regionalGuidance.entities.push(regionalGuidanceState); + canvas.regionalGuidance.entities.push(regionalGuidanceState); - state.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; + canvas.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -549,7 +652,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, modelConfig } = action.payload; - const layer = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer || !layer.controlAdapter) { return; } @@ -629,7 +734,9 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, controlMode } = action.payload; - const layer = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') { return; } @@ -640,7 +747,9 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, weight } = action.payload; - const layer = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer || !layer.controlAdapter) { return; } @@ -651,7 +760,9 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, beginEndStepPct } = action.payload; - const layer = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer || !layer.controlAdapter || layer.controlAdapter.type === 'control_lora') { return; } @@ -662,7 +773,9 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier } = action.payload; - const layer = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -683,26 +796,27 @@ const slice = createSlice({ ) => { const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [], addAfter } = action.payload; + const canvas = getSelectedCanvas(state); const entityState = getRegionalGuidanceState(id, overrides); const index = addAfter - ? state.regionalGuidance.entities.findIndex((e) => e.id === addAfter) + 1 - : state.regionalGuidance.entities.length; - state.regionalGuidance.entities.splice(index, 0, entityState); + ? canvas.regionalGuidance.entities.findIndex((e) => e.id === addAfter) + 1 + : canvas.regionalGuidance.entities.length; + canvas.regionalGuidance.entities.splice(index, 0, entityState); if (mergedEntitiesToDelete.length > 0) { - state.regionalGuidance.entities = state.regionalGuidance.entities.filter( + canvas.regionalGuidance.entities = canvas.regionalGuidance.entities.filter( (entity) => !mergedEntitiesToDelete.includes(entity.id) ); } const entityIdentifier = getEntityIdentifier(entityState); if (isSelected || mergedEntitiesToDelete.length > 0) { - state.selectedEntityIdentifier = entityIdentifier; + canvas.selectedEntityIdentifier = entityIdentifier; } if (isBookmarked) { - state.bookmarkedEntityIdentifier = entityIdentifier; + canvas.bookmarkedEntityIdentifier = entityIdentifier; } }, prepare: (payload?: { @@ -717,8 +831,10 @@ const slice = createSlice({ }, rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => { const { data } = action.payload; - state.regionalGuidance.entities.push(data); - state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; + + const canvas = getSelectedCanvas(state); + canvas.regionalGuidance.entities.push(data); + canvas.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; }, rgConvertedToInpaintMask: { reducer: ( @@ -731,7 +847,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const layer = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -741,15 +859,15 @@ const slice = createSlice({ if (replace) { // Remove the regional guidance - state.regionalGuidance.entities = state.regionalGuidance.entities.filter( + canvas.regionalGuidance.entities = canvas.regionalGuidance.entities.filter( (layer) => layer.id !== entityIdentifier.id ); } // Add the new inpaint mask - state.inpaintMasks.entities.push(inpaintMaskState); + canvas.inpaintMasks.entities.push(inpaintMaskState); - state.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; + canvas.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -765,7 +883,9 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, prompt } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -776,7 +896,9 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, prompt } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -784,7 +906,9 @@ const slice = createSlice({ }, rgAutoNegativeToggled: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - const rg = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const rg = selectEntity(canvas, entityIdentifier); if (!rg) { return; } @@ -801,7 +925,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, overrides, referenceImageId } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -820,7 +946,9 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, referenceImageId } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -833,7 +961,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, imageDTO } = action.payload; - const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); + + const canvas = getSelectedCanvas(state); + const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -844,7 +974,9 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, referenceImageId, weight } = action.payload; - const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); + + const canvas = getSelectedCanvas(state); + const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -861,7 +993,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, beginEndStepPct } = action.payload; - const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); + + const canvas = getSelectedCanvas(state); + const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -877,7 +1011,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, method } = action.payload; - const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); + + const canvas = getSelectedCanvas(state); + const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -896,7 +1032,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, imageInfluence } = action.payload; - const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); + + const canvas = getSelectedCanvas(state); + const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -919,7 +1057,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, modelConfig } = action.payload; - const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); + + const canvas = getSelectedCanvas(state); + const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -968,7 +1108,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, clipVisionModel } = action.payload; - const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); + + const canvas = getSelectedCanvas(state); + const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -992,26 +1134,27 @@ const slice = createSlice({ ) => { const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [], addAfter } = action.payload; + const canvas = getSelectedCanvas(state); const entityState = getInpaintMaskState(id, overrides); const index = addAfter - ? state.inpaintMasks.entities.findIndex((e) => e.id === addAfter) + 1 - : state.inpaintMasks.entities.length; - state.inpaintMasks.entities.splice(index, 0, entityState); + ? canvas.inpaintMasks.entities.findIndex((e) => e.id === addAfter) + 1 + : canvas.inpaintMasks.entities.length; + canvas.inpaintMasks.entities.splice(index, 0, entityState); if (mergedEntitiesToDelete.length > 0) { - state.inpaintMasks.entities = state.inpaintMasks.entities.filter( + canvas.inpaintMasks.entities = canvas.inpaintMasks.entities.filter( (entity) => !mergedEntitiesToDelete.includes(entity.id) ); } const entityIdentifier = getEntityIdentifier(entityState); if (isSelected || mergedEntitiesToDelete.length > 0) { - state.selectedEntityIdentifier = entityIdentifier; + canvas.selectedEntityIdentifier = entityIdentifier; } if (isBookmarked) { - state.bookmarkedEntityIdentifier = entityIdentifier; + canvas.bookmarkedEntityIdentifier = entityIdentifier; } }, prepare: (payload?: { @@ -1026,12 +1169,16 @@ const slice = createSlice({ }, inpaintMaskRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { const { data } = action.payload; - state.inpaintMasks.entities = [data]; - state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; + + const canvas = getSelectedCanvas(state); + canvas.inpaintMasks.entities = [data]; + canvas.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, inpaintMaskNoiseAdded: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.noiseLevel = 0.15; // Default noise level } @@ -1041,14 +1188,18 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, noiseLevel } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.noiseLevel = noiseLevel; } }, inpaintMaskNoiseDeleted: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.noiseLevel = undefined; } @@ -1064,7 +1215,9 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const layer = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -1074,13 +1227,15 @@ const slice = createSlice({ if (replace) { // Remove the inpaint mask - state.inpaintMasks.entities = state.inpaintMasks.entities.filter((layer) => layer.id !== entityIdentifier.id); + canvas.inpaintMasks.entities = canvas.inpaintMasks.entities.filter( + (layer) => layer.id !== entityIdentifier.id + ); } // Add the new regional guidance - state.regionalGuidance.entities.push(regionalGuidanceState); + canvas.regionalGuidance.entities.push(regionalGuidanceState); - state.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; + canvas.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -1093,7 +1248,9 @@ const slice = createSlice({ }, inpaintMaskDenoiseLimitAdded: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.denoiseLimit = 1.0; // Default denoise limit } @@ -1103,58 +1260,66 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, denoiseLimit } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.denoiseLimit = denoiseLimit; } }, inpaintMaskDenoiseLimitDeleted: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.denoiseLimit = undefined; } }, //#region BBox bboxScaledWidthChanged: (state, action: PayloadAction) => { - const gridSize = getGridSize(state.bbox.modelBase); + const canvas = getSelectedCanvas(state); + const gridSize = getGridSize(canvas.bbox.modelBase); - state.bbox.scaledSize.width = roundToMultiple(action.payload, gridSize); + canvas.bbox.scaledSize.width = roundToMultiple(action.payload, gridSize); - if (state.bbox.aspectRatio.isLocked) { - state.bbox.scaledSize.height = roundToMultiple( - state.bbox.scaledSize.width / state.bbox.aspectRatio.value, + if (canvas.bbox.aspectRatio.isLocked) { + canvas.bbox.scaledSize.height = roundToMultiple( + canvas.bbox.scaledSize.width / canvas.bbox.aspectRatio.value, gridSize ); } }, bboxScaledHeightChanged: (state, action: PayloadAction) => { - const gridSize = getGridSize(state.bbox.modelBase); + const canvas = getSelectedCanvas(state); + const gridSize = getGridSize(canvas.bbox.modelBase); - state.bbox.scaledSize.height = roundToMultiple(action.payload, gridSize); + canvas.bbox.scaledSize.height = roundToMultiple(action.payload, gridSize); - if (state.bbox.aspectRatio.isLocked) { - state.bbox.scaledSize.width = roundToMultiple( - state.bbox.scaledSize.height * state.bbox.aspectRatio.value, + if (canvas.bbox.aspectRatio.isLocked) { + canvas.bbox.scaledSize.width = roundToMultiple( + canvas.bbox.scaledSize.height * canvas.bbox.aspectRatio.value, gridSize ); } }, bboxScaleMethodChanged: (state, action: PayloadAction) => { - state.bbox.scaleMethod = action.payload; - syncScaledSize(state); + const canvas = getSelectedCanvas(state); + canvas.bbox.scaleMethod = action.payload; + syncScaledSize(canvas); }, bboxChangedFromCanvas: (state, action: PayloadAction) => { + const canvas = getSelectedCanvas(state); const newBboxRect = action.payload; - const oldBboxRect = state.bbox.rect; + const oldBboxRect = canvas.bbox.rect; - state.bbox.rect = newBboxRect; + canvas.bbox.rect = newBboxRect; if (newBboxRect.width === oldBboxRect.width && newBboxRect.height === oldBboxRect.height) { return; } - const oldAspectRatio = state.bbox.aspectRatio.value; + const oldAspectRatio = canvas.bbox.aspectRatio.value; const newAspectRatio = newBboxRect.width / newBboxRect.height; if (oldAspectRatio === newAspectRatio) { @@ -1164,188 +1329,206 @@ const slice = createSlice({ // TODO(psyche): Figure out a way to handle this without resetting the aspect ratio on every change. // This action is dispatched when the user resizes or moves the bbox from the canvas. For now, when the user // resizes the bbox from the canvas, we unlock the aspect ratio. - state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; - state.bbox.aspectRatio.id = 'Free'; + canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; + canvas.bbox.aspectRatio.id = 'Free'; - syncScaledSize(state); + syncScaledSize(canvas); }, bboxWidthChanged: ( state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }> ) => { const { width, updateAspectRatio, clamp } = action.payload; - const gridSize = getGridSize(state.bbox.modelBase); - state.bbox.rect.width = clamp ? Math.max(roundDownToMultiple(width, gridSize), 64) : width; - if (state.bbox.aspectRatio.isLocked) { - state.bbox.rect.height = roundToMultiple(state.bbox.rect.width / state.bbox.aspectRatio.value, gridSize); + const canvas = getSelectedCanvas(state); + const gridSize = getGridSize(canvas.bbox.modelBase); + canvas.bbox.rect.width = clamp ? Math.max(roundDownToMultiple(width, gridSize), 64) : width; + + if (canvas.bbox.aspectRatio.isLocked) { + canvas.bbox.rect.height = roundToMultiple(canvas.bbox.rect.width / canvas.bbox.aspectRatio.value, gridSize); } - if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) { - state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; - state.bbox.aspectRatio.id = 'Free'; - state.bbox.aspectRatio.isLocked = false; + if (updateAspectRatio || !canvas.bbox.aspectRatio.isLocked) { + canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; + canvas.bbox.aspectRatio.id = 'Free'; + canvas.bbox.aspectRatio.isLocked = false; } - syncScaledSize(state); + syncScaledSize(canvas); }, bboxHeightChanged: ( state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }> ) => { const { height, updateAspectRatio, clamp } = action.payload; - const gridSize = getGridSize(state.bbox.modelBase); - state.bbox.rect.height = clamp ? Math.max(roundDownToMultiple(height, gridSize), 64) : height; - if (state.bbox.aspectRatio.isLocked) { - state.bbox.rect.width = roundToMultiple(state.bbox.rect.height * state.bbox.aspectRatio.value, gridSize); + const canvas = getSelectedCanvas(state); + const gridSize = getGridSize(canvas.bbox.modelBase); + canvas.bbox.rect.height = clamp ? Math.max(roundDownToMultiple(height, gridSize), 64) : height; + + if (canvas.bbox.aspectRatio.isLocked) { + canvas.bbox.rect.width = roundToMultiple(canvas.bbox.rect.height * canvas.bbox.aspectRatio.value, gridSize); } - if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) { - state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; - state.bbox.aspectRatio.id = 'Free'; - state.bbox.aspectRatio.isLocked = false; + if (updateAspectRatio || !canvas.bbox.aspectRatio.isLocked) { + canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; + canvas.bbox.aspectRatio.id = 'Free'; + canvas.bbox.aspectRatio.isLocked = false; } - syncScaledSize(state); + syncScaledSize(canvas); }, bboxSizeRecalled: (state, action: PayloadAction<{ width: number; height: number }>) => { const { width, height } = action.payload; - const gridSize = getGridSize(state.bbox.modelBase); - state.bbox.rect.width = Math.max(roundDownToMultiple(width, gridSize), 64); - state.bbox.rect.height = Math.max(roundDownToMultiple(height, gridSize), 64); - state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; - state.bbox.aspectRatio.id = 'Free'; - state.bbox.aspectRatio.isLocked = true; + + const canvas = getSelectedCanvas(state); + const gridSize = getGridSize(canvas.bbox.modelBase); + canvas.bbox.rect.width = Math.max(roundDownToMultiple(width, gridSize), 64); + canvas.bbox.rect.height = Math.max(roundDownToMultiple(height, gridSize), 64); + canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; + canvas.bbox.aspectRatio.id = 'Free'; + canvas.bbox.aspectRatio.isLocked = true; }, bboxAspectRatioLockToggled: (state) => { - state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked; - syncScaledSize(state); + const canvas = getSelectedCanvas(state); + canvas.bbox.aspectRatio.isLocked = !canvas.bbox.aspectRatio.isLocked; + syncScaledSize(canvas); }, bboxAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { const { id } = action.payload; - state.bbox.aspectRatio.id = id; + + const canvas = getSelectedCanvas(state); + canvas.bbox.aspectRatio.id = id; if (id === 'Free') { - state.bbox.aspectRatio.isLocked = false; + canvas.bbox.aspectRatio.isLocked = false; } else if ( - (state.bbox.modelBase === 'imagen3' || state.bbox.modelBase === 'imagen4') && + (canvas.bbox.modelBase === 'imagen3' || canvas.bbox.modelBase === 'imagen4') && isImagenAspectRatioID(id) ) { const { width, height } = IMAGEN_ASPECT_RATIOS[id]; - state.bbox.rect.width = width; - state.bbox.rect.height = height; - state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; - state.bbox.aspectRatio.isLocked = true; - } else if (state.bbox.modelBase === 'chatgpt-4o' && isChatGPT4oAspectRatioID(id)) { + canvas.bbox.rect.width = width; + canvas.bbox.rect.height = height; + canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; + canvas.bbox.aspectRatio.isLocked = true; + } else if (canvas.bbox.modelBase === 'chatgpt-4o' && isChatGPT4oAspectRatioID(id)) { const { width, height } = CHATGPT_ASPECT_RATIOS[id]; - state.bbox.rect.width = width; - state.bbox.rect.height = height; - state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; - state.bbox.aspectRatio.isLocked = true; - } else if (state.bbox.modelBase === 'gemini-2.5' && isGemini2_5AspectRatioID(id)) { + canvas.bbox.rect.width = width; + canvas.bbox.rect.height = height; + canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; + canvas.bbox.aspectRatio.isLocked = true; + } else if (canvas.bbox.modelBase === 'gemini-2.5' && isGemini2_5AspectRatioID(id)) { const { width, height } = GEMINI_2_5_ASPECT_RATIOS[id]; - state.bbox.rect.width = width; - state.bbox.rect.height = height; - state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; - state.bbox.aspectRatio.isLocked = true; - } else if (state.bbox.modelBase === 'flux-kontext' && isFluxKontextAspectRatioID(id)) { + canvas.bbox.rect.width = width; + canvas.bbox.rect.height = height; + canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; + canvas.bbox.aspectRatio.isLocked = true; + } else if (canvas.bbox.modelBase === 'flux-kontext' && isFluxKontextAspectRatioID(id)) { const { width, height } = FLUX_KONTEXT_ASPECT_RATIOS[id]; - state.bbox.rect.width = width; - state.bbox.rect.height = height; - state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; - state.bbox.aspectRatio.isLocked = true; + canvas.bbox.rect.width = width; + canvas.bbox.rect.height = height; + canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; + canvas.bbox.aspectRatio.isLocked = true; } else { - state.bbox.aspectRatio.isLocked = true; - state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; + canvas.bbox.aspectRatio.isLocked = true; + canvas.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; const { width, height } = calculateNewSize( - state.bbox.aspectRatio.value, - state.bbox.rect.width * state.bbox.rect.height, - state.bbox.modelBase + canvas.bbox.aspectRatio.value, + canvas.bbox.rect.width * canvas.bbox.rect.height, + canvas.bbox.modelBase ); - state.bbox.rect.width = width; - state.bbox.rect.height = height; + canvas.bbox.rect.width = width; + canvas.bbox.rect.height = height; } - syncScaledSize(state); + syncScaledSize(canvas); }, bboxDimensionsSwapped: (state) => { - state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value; - if (state.bbox.aspectRatio.id === 'Free') { - const newWidth = state.bbox.rect.height; - const newHeight = state.bbox.rect.width; - state.bbox.rect.width = newWidth; - state.bbox.rect.height = newHeight; + const canvas = getSelectedCanvas(state); + canvas.bbox.aspectRatio.value = 1 / canvas.bbox.aspectRatio.value; + if (canvas.bbox.aspectRatio.id === 'Free') { + const newWidth = canvas.bbox.rect.height; + const newHeight = canvas.bbox.rect.width; + canvas.bbox.rect.width = newWidth; + canvas.bbox.rect.height = newHeight; } else { const { width, height } = calculateNewSize( - state.bbox.aspectRatio.value, - state.bbox.rect.width * state.bbox.rect.height, - state.bbox.modelBase + canvas.bbox.aspectRatio.value, + canvas.bbox.rect.width * canvas.bbox.rect.height, + canvas.bbox.modelBase ); - state.bbox.rect.width = width; - state.bbox.rect.height = height; - state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID; + canvas.bbox.rect.width = width; + canvas.bbox.rect.height = height; + canvas.bbox.aspectRatio.id = ASPECT_RATIO_MAP[canvas.bbox.aspectRatio.id].inverseID; } - syncScaledSize(state); + syncScaledSize(canvas); }, bboxSizeOptimized: (state) => { - const optimalDimension = getOptimalDimension(state.bbox.modelBase); - if (state.bbox.aspectRatio.isLocked) { + const canvas = getSelectedCanvas(state); + const optimalDimension = getOptimalDimension(canvas.bbox.modelBase); + if (canvas.bbox.aspectRatio.isLocked) { const { width, height } = calculateNewSize( - state.bbox.aspectRatio.value, + canvas.bbox.aspectRatio.value, optimalDimension * optimalDimension, - state.bbox.modelBase + canvas.bbox.modelBase ); - state.bbox.rect.width = width; - state.bbox.rect.height = height; + canvas.bbox.rect.width = width; + canvas.bbox.rect.height = height; } else { - state.bbox.aspectRatio = deepClone(DEFAULT_ASPECT_RATIO_CONFIG); - state.bbox.rect.width = optimalDimension; - state.bbox.rect.height = optimalDimension; + canvas.bbox.aspectRatio = deepClone(DEFAULT_ASPECT_RATIO_CONFIG); + canvas.bbox.rect.width = optimalDimension; + canvas.bbox.rect.height = optimalDimension; } - syncScaledSize(state); + syncScaledSize(canvas); }, bboxSyncedToOptimalDimension: (state) => { - const optimalDimension = getOptimalDimension(state.bbox.modelBase); + const canvas = getSelectedCanvas(state); + const optimalDimension = getOptimalDimension(canvas.bbox.modelBase); - if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, state.bbox.modelBase)) { + if (!getIsSizeOptimal(canvas.bbox.rect.width, canvas.bbox.rect.height, canvas.bbox.modelBase)) { const bboxDims = calculateNewSize( - state.bbox.aspectRatio.value, + canvas.bbox.aspectRatio.value, optimalDimension * optimalDimension, - state.bbox.modelBase + canvas.bbox.modelBase ); - state.bbox.rect.width = bboxDims.width; - state.bbox.rect.height = bboxDims.height; - syncScaledSize(state); + canvas.bbox.rect.width = bboxDims.width; + canvas.bbox.rect.height = bboxDims.height; + syncScaledSize(canvas); } }, //#region Shared entity entitySelected: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { // Cannot select a non-existent entity return; } - state.selectedEntityIdentifier = entityIdentifier; + canvas.selectedEntityIdentifier = entityIdentifier; }, bookmarkedEntityChanged: (state, action: PayloadAction<{ entityIdentifier: CanvasEntityIdentifier | null }>) => { const { entityIdentifier } = action.payload; + + const canvas = getSelectedCanvas(state); if (!entityIdentifier) { - state.bookmarkedEntityIdentifier = null; + canvas.bookmarkedEntityIdentifier = null; return; } - const entity = selectEntity(state, entityIdentifier); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { // Cannot select a non-existent entity return; } - state.bookmarkedEntityIdentifier = entityIdentifier; + canvas.bookmarkedEntityIdentifier = entityIdentifier; }, entityNameChanged: (state, action: PayloadAction>) => { const { entityIdentifier, name } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1353,7 +1536,9 @@ const slice = createSlice({ }, entityReset: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1363,7 +1548,9 @@ const slice = createSlice({ }, entityDuplicated: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1375,14 +1562,14 @@ const slice = createSlice({ switch (newEntity.type) { case 'raster_layer': { newEntity.id = getPrefixedId('raster_layer'); - const newEntityIndex = state.rasterLayers.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; - state.rasterLayers.entities.splice(newEntityIndex, 0, newEntity); + const newEntityIndex = canvas.rasterLayers.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; + canvas.rasterLayers.entities.splice(newEntityIndex, 0, newEntity); break; } case 'control_layer': { newEntity.id = getPrefixedId('control_layer'); - const newEntityIndex = state.controlLayers.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; - state.controlLayers.entities.splice(newEntityIndex, 0, newEntity); + const newEntityIndex = canvas.controlLayers.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; + canvas.controlLayers.entities.splice(newEntityIndex, 0, newEntity); break; } case 'regional_guidance': { @@ -1390,23 +1577,25 @@ const slice = createSlice({ for (const refImage of newEntity.referenceImages) { refImage.id = getPrefixedId('regional_guidance_ip_adapter'); } - const newEntityIndex = state.regionalGuidance.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; - state.regionalGuidance.entities.splice(newEntityIndex, 0, newEntity); + const newEntityIndex = canvas.regionalGuidance.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; + canvas.regionalGuidance.entities.splice(newEntityIndex, 0, newEntity); break; } case 'inpaint_mask': { newEntity.id = getPrefixedId('inpaint_mask'); - const newEntityIndex = state.inpaintMasks.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; - state.inpaintMasks.entities.splice(newEntityIndex, 0, newEntity); + const newEntityIndex = canvas.inpaintMasks.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; + canvas.inpaintMasks.entities.splice(newEntityIndex, 0, newEntity); break; } } - state.selectedEntityIdentifier = getEntityIdentifier(newEntity); + canvas.selectedEntityIdentifier = getEntityIdentifier(newEntity); }, entityIsEnabledToggled: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1414,7 +1603,9 @@ const slice = createSlice({ }, entityIsLockedToggled: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1425,7 +1616,9 @@ const slice = createSlice({ action: PayloadAction> ) => { const { color, entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1436,7 +1629,9 @@ const slice = createSlice({ action: PayloadAction> ) => { const { style, entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1444,7 +1639,9 @@ const slice = createSlice({ }, entityMovedTo: (state, action: PayloadAction) => { const { entityIdentifier, position } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1453,7 +1650,9 @@ const slice = createSlice({ }, entityMovedBy: (state, action: PayloadAction) => { const { entityIdentifier, offset } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1463,7 +1662,9 @@ const slice = createSlice({ }, entityRasterized: (state, action: PayloadAction) => { const { entityIdentifier, imageObject, position, replaceObjects, isSelected } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1474,12 +1675,14 @@ const slice = createSlice({ } if (isSelected) { - state.selectedEntityIdentifier = entityIdentifier; + canvas.selectedEntityIdentifier = entityIdentifier; } }, entityBrushLineAdded: (state, action: PayloadAction) => { const { entityIdentifier, brushLine } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1494,7 +1697,9 @@ const slice = createSlice({ }, entityEraserLineAdded: (state, action: PayloadAction) => { const { entityIdentifier, eraserLine } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1509,7 +1714,9 @@ const slice = createSlice({ }, entityRectAdded: (state, action: PayloadAction) => { const { entityIdentifier, rect } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1522,7 +1729,8 @@ const slice = createSlice({ const { entityIdentifier } = action.payload; let selectedEntityIdentifier: CanvasState['selectedEntityIdentifier'] = null; - const allEntities = selectAllEntities(state); + const canvas = getSelectedCanvas(state); + const allEntities = selectAllEntities(canvas); const index = allEntities.findIndex((entity) => entity.id === entityIdentifier.id); const nextIndex = allEntities.length > 1 ? (index + 1) % allEntities.length : -1; if (nextIndex !== -1) { @@ -1534,84 +1742,96 @@ const slice = createSlice({ switch (entityIdentifier.type) { case 'raster_layer': - state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); + canvas.rasterLayers.entities = canvas.rasterLayers.entities.filter( + (layer) => layer.id !== entityIdentifier.id + ); break; case 'control_layer': - state.controlLayers.entities = state.controlLayers.entities.filter((rg) => rg.id !== entityIdentifier.id); + canvas.controlLayers.entities = canvas.controlLayers.entities.filter((rg) => rg.id !== entityIdentifier.id); break; case 'regional_guidance': - state.regionalGuidance.entities = state.regionalGuidance.entities.filter( + canvas.regionalGuidance.entities = canvas.regionalGuidance.entities.filter( (rg) => rg.id !== entityIdentifier.id ); break; case 'inpaint_mask': - state.inpaintMasks.entities = state.inpaintMasks.entities.filter((rg) => rg.id !== entityIdentifier.id); + canvas.inpaintMasks.entities = canvas.inpaintMasks.entities.filter((rg) => rg.id !== entityIdentifier.id); break; } - state.selectedEntityIdentifier = selectedEntityIdentifier; + canvas.selectedEntityIdentifier = selectedEntityIdentifier; }, entityArrangedForwardOne: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } - moveOneToEnd(selectAllEntitiesOfType(state, entity.type), entity); + moveOneToEnd(selectAllEntitiesOfType(canvas, entity.type), entity); }, entityArrangedToFront: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } - moveToEnd(selectAllEntitiesOfType(state, entity.type), entity); + moveToEnd(selectAllEntitiesOfType(canvas, entity.type), entity); }, entityArrangedBackwardOne: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } - moveOneToStart(selectAllEntitiesOfType(state, entity.type), entity); + moveOneToStart(selectAllEntitiesOfType(canvas, entity.type), entity); }, entityArrangedToBack: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } - moveToStart(selectAllEntitiesOfType(state, entity.type), entity); + moveToStart(selectAllEntitiesOfType(canvas, entity.type), entity); }, entitiesReordered: ( - state: CanvasState, + state: CanvasesState, action: PayloadAction<{ type: T; entityIdentifiers: CanvasEntityIdentifier[] }> ) => { const { type, entityIdentifiers } = action.payload; + const canvas = getSelectedCanvas(state); + switch (type) { case 'raster_layer': { - state.rasterLayers.entities = reorderEntities( - state.rasterLayers.entities, + canvas.rasterLayers.entities = reorderEntities( + canvas.rasterLayers.entities, entityIdentifiers as CanvasEntityIdentifier<'raster_layer'>[] ); break; } case 'control_layer': - state.controlLayers.entities = reorderEntities( - state.controlLayers.entities, + canvas.controlLayers.entities = reorderEntities( + canvas.controlLayers.entities, entityIdentifiers as CanvasEntityIdentifier<'control_layer'>[] ); break; case 'inpaint_mask': - state.inpaintMasks.entities = reorderEntities( - state.inpaintMasks.entities, + canvas.inpaintMasks.entities = reorderEntities( + canvas.inpaintMasks.entities, entityIdentifiers as CanvasEntityIdentifier<'inpaint_mask'>[] ); break; case 'regional_guidance': - state.regionalGuidance.entities = reorderEntities( - state.regionalGuidance.entities, + canvas.regionalGuidance.entities = reorderEntities( + canvas.regionalGuidance.entities, entityIdentifiers as CanvasEntityIdentifier<'regional_guidance'>[] ); break; @@ -1619,7 +1839,9 @@ const slice = createSlice({ }, entityOpacityChanged: (state, action: PayloadAction>) => { const { entityIdentifier, opacity } = action.payload; - const entity = selectEntity(state, entityIdentifier); + + const canvas = getSelectedCanvas(state); + const entity = selectEntity(canvas, entityIdentifier); if (!entity) { return; } @@ -1628,45 +1850,51 @@ const slice = createSlice({ allEntitiesOfTypeIsHiddenToggled: (state, action: PayloadAction<{ type: CanvasEntityIdentifier['type'] }>) => { const { type } = action.payload; + const canvas = getSelectedCanvas(state); + switch (type) { case 'raster_layer': - state.rasterLayers.isHidden = !state.rasterLayers.isHidden; + canvas.rasterLayers.isHidden = !canvas.rasterLayers.isHidden; break; case 'control_layer': - state.controlLayers.isHidden = !state.controlLayers.isHidden; + canvas.controlLayers.isHidden = !canvas.controlLayers.isHidden; break; case 'inpaint_mask': - state.inpaintMasks.isHidden = !state.inpaintMasks.isHidden; + canvas.inpaintMasks.isHidden = !canvas.inpaintMasks.isHidden; break; case 'regional_guidance': - state.regionalGuidance.isHidden = !state.regionalGuidance.isHidden; + canvas.regionalGuidance.isHidden = !canvas.regionalGuidance.isHidden; break; } }, allNonRasterLayersIsHiddenToggled: (state) => { + const canvas = getSelectedCanvas(state); const hasVisibleNonRasterLayers = - !state.controlLayers.isHidden || !state.inpaintMasks.isHidden || !state.regionalGuidance.isHidden; + !canvas.controlLayers.isHidden || !canvas.inpaintMasks.isHidden || !canvas.regionalGuidance.isHidden; const shouldHide = hasVisibleNonRasterLayers; - state.controlLayers.isHidden = shouldHide; - state.inpaintMasks.isHidden = shouldHide; - state.regionalGuidance.isHidden = shouldHide; + canvas.controlLayers.isHidden = shouldHide; + canvas.inpaintMasks.isHidden = shouldHide; + canvas.regionalGuidance.isHidden = shouldHide; }, allEntitiesDeleted: (state) => { + const canvas = getSelectedCanvas(state); // Deleting all entities is equivalent to resetting the state for each entity type - const initialState = getInitialCanvasState(); - state.rasterLayers = initialState.rasterLayers; - state.controlLayers = initialState.controlLayers; - state.inpaintMasks = initialState.inpaintMasks; - state.regionalGuidance = initialState.regionalGuidance; + const initialState = getCanvasState('dummyID', 'dummyName'); + canvas.rasterLayers = initialState.rasterLayers; + canvas.controlLayers = initialState.controlLayers; + canvas.inpaintMasks = initialState.inpaintMasks; + canvas.regionalGuidance = initialState.regionalGuidance; }, canvasMetadataRecalled: (state, action: PayloadAction) => { const { controlLayers, inpaintMasks, rasterLayers, regionalGuidance } = action.payload; - state.controlLayers.entities = controlLayers; - state.inpaintMasks.entities = inpaintMasks; - state.rasterLayers.entities = rasterLayers; - state.regionalGuidance.entities = regionalGuidance; + + const canvas = getSelectedCanvas(state); + canvas.controlLayers.entities = controlLayers; + canvas.inpaintMasks.entities = inpaintMasks; + canvas.rasterLayers.entities = rasterLayers; + canvas.regionalGuidance.entities = regionalGuidance; return state; }, canvasUndo: () => {}, @@ -1675,9 +1903,11 @@ const slice = createSlice({ }, extraReducers(builder) { builder.addCase(canvasReset, (state) => { - return resetState(state); + const canvas = getSelectedCanvas(state); + resetCanvasState(canvas); }); builder.addCase(modelChanged, (state, action) => { + const canvas = getSelectedCanvas(state); const { model } = action.payload; /** * Because the bbox depends in part on the model, it needs to be in sync with the model. However, due to @@ -1698,24 +1928,24 @@ const slice = createSlice({ * - Provide a separate action that will update the bbox dimensions and be careful to not dispatch it when staging. */ const base = model?.base; - if (isMainModelBase(base) && state.bbox.modelBase !== base) { - state.bbox.modelBase = base; + if (isMainModelBase(base) && canvas.bbox.modelBase !== base) { + canvas.bbox.modelBase = base; if (API_BASE_MODELS.includes(base)) { - state.bbox.aspectRatio.isLocked = true; - state.bbox.aspectRatio.value = 1; - state.bbox.aspectRatio.id = '1:1'; - state.bbox.rect.width = 1024; - state.bbox.rect.height = 1024; + canvas.bbox.aspectRatio.isLocked = true; + canvas.bbox.aspectRatio.value = 1; + canvas.bbox.aspectRatio.id = '1:1'; + canvas.bbox.rect.width = 1024; + canvas.bbox.rect.height = 1024; } - syncScaledSize(state); + syncScaledSize(canvas); } }); }, }); -const resetState = (state: CanvasState) => { - const newState = getInitialCanvasState(); +const resetCanvasState = (state: CanvasState) => { + const newState = getCanvasState(state.id, state.name); // 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,16 +1958,23 @@ const resetState = (state: CanvasState) => { ); newState.bbox.rect.width = rect.width; newState.bbox.rect.height = rect.height; - syncScaledSize(newState); - return newState; + syncScaledSize(newState); }; +const getCanvasById = (state: CanvasesState, id: string) => state.canvases.find((canvas) => canvas.id === id); +const getSelectedCanvas = (state: CanvasesState) => getCanvasById(state, state.selectedCanvasId)!; + export const { canvasMetadataRecalled, canvasUndo, canvasRedo, canvasClearHistory, + // Canvas + canvasAdded, + canvasSelected, + canvasNameChanged, + canvasDeleted, // All entities entitySelected, bookmarkedEntityChanged, @@ -1831,28 +2068,28 @@ export const { // inpaintMaskRecalled, } = slice.actions; -const syncScaledSize = (state: CanvasState) => { - if (API_BASE_MODELS.includes(state.bbox.modelBase)) { +const syncScaledSize = (canvas: CanvasState) => { + if (API_BASE_MODELS.includes(canvas.bbox.modelBase)) { // Imagen3 has fixed sizes. Scaled bbox is not supported. return; } - if (state.bbox.scaleMethod === 'auto') { + if (canvas.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) { + const { width, height } = canvas.bbox.rect; + canvas.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, canvas.bbox.modelBase); + } else if (canvas.bbox.scaleMethod === 'manual' && canvas.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 + canvas.bbox.scaledSize = calculateNewSize( + canvas.bbox.aspectRatio.value, + canvas.bbox.scaledSize.width * canvas.bbox.scaledSize.height, + canvas.bbox.modelBase ); } }; let filter = true; -const canvasUndoableConfig: UndoableOptions = { +const canvasUndoableConfig: UndoableOptions = { limit: 64, undoType: canvasUndo.type, redoType: canvasRedo.type, @@ -1872,10 +2109,10 @@ const canvasUndoableConfig: UndoableOptions = { export const canvasSliceConfig: SliceConfig = { slice, - getInitialState: getInitialCanvasState, - schema: zCanvasState, + getInitialState: getInitialCanvasesState, + schema: zCanvasesState, persistConfig: { - migrate: (state) => zCanvasState.parse(state), + migrate: (state) => zCanvasesState.parse(state), }, undoableConfig: { reduxUndoOptions: canvasUndoableConfig, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 5c0abfdb892..da7eb7a1cef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -1,4 +1,3 @@ -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'; @@ -20,9 +19,15 @@ import { assert } from 'tsafe'; /** * Selects the canvas slice from the root state */ -export const selectCanvasSlice = (state: RootState) => state.canvas.present; +const selectCanvasSlice = (state: RootState) => state.canvas.present; -const createCanvasSelector = (selector: Selector) => createSelector(selectCanvasSlice, selector); +/** + * Selects the selected canvas + */ +export const selectSelectedCanvas = createSelector( + selectCanvasSlice, + (state) => state.canvases.find((canvas) => canvas.id === state.selectedCanvasId)! +); /** * Selects the total canvas entity count: @@ -34,7 +39,7 @@ const createCanvasSelector = (selector: Selector) => createSe * * All entities are counted, regardless of their state. */ -const selectEntityCountAll = createCanvasSelector((canvas) => { +const selectEntityCountAll = createSelector(selectSelectedCanvas, (canvas) => { return ( canvas.regionalGuidance.entities.length + canvas.rasterLayers.entities.length + @@ -45,22 +50,28 @@ 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(selectSelectedCanvas, (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( + selectSelectedCanvas, + (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(selectSelectedCanvas, (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( + selectSelectedCanvas, + (canvas) => canvas.regionalGuidance.entities +); export const selectActiveRegionalGuidanceEntities = createSelector(selectRegionalGuidanceEntities, (entities) => entities.filter(isVisibleEntity) ); @@ -174,7 +185,7 @@ export function selectEntityOrThrow( } export const selectEntityExists = (entityIdentifier: T) => { - return createCanvasSelector((canvas) => Boolean(selectEntity(canvas, entityIdentifier))); + return createSelector(selectSelectedCanvas, (canvas) => Boolean(selectEntity(canvas, entityIdentifier))); }; /** @@ -251,22 +262,22 @@ export function selectRegionalGuidanceReferenceImage( return entity.referenceImages.find(({ id }) => id === referenceImageId); } -export const selectBbox = createCanvasSelector((canvas) => canvas.bbox); +export const selectBbox = createSelector(selectSelectedCanvas, (canvas) => canvas.bbox); export const selectSelectedEntityIdentifier = createSelector( - selectCanvasSlice, + selectSelectedCanvas, (canvas) => canvas.selectedEntityIdentifier ); export const selectBookmarkedEntityIdentifier = createSelector( - selectCanvasSlice, + selectSelectedCanvas, (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 selectSelectedEntityFill = createSelector( - selectCanvasSlice, + selectSelectedCanvas, selectSelectedEntityIdentifier, (canvas, selectedEntityIdentifier) => { if (!selectedEntityIdentifier) { @@ -283,10 +294,13 @@ 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(selectSelectedCanvas, (canvas) => canvas.rasterLayers.isHidden); +const selectControlLayersIsHidden = createSelector(selectSelectedCanvas, (canvas) => canvas.controlLayers.isHidden); +const selectInpaintMasksIsHidden = createSelector(selectSelectedCanvas, (canvas) => canvas.inpaintMasks.isHidden); +const selectRegionalGuidanceIsHidden = createSelector( + selectSelectedCanvas, + (canvas) => canvas.regionalGuidance.isHidden +); /** * Returns the hidden selector for the given entity type. @@ -324,7 +338,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(selectSelectedCanvas, (canvas) => { const entity = selectEntity(canvas, entityIdentifier); if (!entity) { @@ -334,17 +348,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(selectSelectedCanvas, (canvas) => canvas.bbox.rect.width); +export const selectHeight = createSelector(selectSelectedCanvas, (canvas) => canvas.bbox.rect.height); +export const selectAspectRatioID = createSelector(selectSelectedCanvas, (canvas) => canvas.bbox.aspectRatio.id); +export const selectAspectRatioValue = createSelector(selectSelectedCanvas, (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, + selectSelectedCanvas, (canvas): { canvas_v2_metadata: CanvasMetadata } => { const canvas_v2_metadata: CanvasMetadata = { controlLayers: selectAllEntitiesOfType(canvas, 'control_layer'), @@ -360,6 +374,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(selectSelectedCanvas, (canvas) => { return canvas.controlLayers.isHidden && canvas.inpaintMasks.isHidden && canvas.regionalGuidance.isHidden; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 3163bd85b2a..76416a111a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -801,8 +801,9 @@ const zRegionalGuidance = z.object({ isHidden: z.boolean(), entities: z.array(zCanvasRegionalGuidanceState), }); -export const zCanvasState = z.object({ - _version: z.literal(3), +const zCanvasState = z.object({ + id: zId, + name: z.string().min(1), selectedEntityIdentifier: zCanvasEntityIdentifer.nullable(), bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable(), inpaintMasks: zInpaintMasks, @@ -812,23 +813,12 @@ export const zCanvasState = z.object({ 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 zCanvasesState = z.object({ + _version: z.literal(3), + selectedCanvasId: zId, + canvases: z.array(zCanvasState), }); - +export type CanvasesState = z.infer; export const zRefImagesState = z.object({ selectedEntityId: z.string().nullable(), isPanelOpen: z.boolean(), diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts index 38aa8b039f3..d3d59a6bb93 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts @@ -8,7 +8,7 @@ import { selectReferenceImageEntities, selectRefImagesSlice, } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasState, RefImagesState } from 'features/controlLayers/store/types'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; import { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors'; @@ -156,7 +156,7 @@ const getImageUsageFromImageNames = (image_names: string[], state: RootState): I } const nodes = selectNodesSlice(state); - const canvas = selectCanvasSlice(state); + const canvas = selectSelectedCanvas(state); const upscale = selectUpscaleSlice(state); const refImages = selectRefImagesSlice(state); @@ -220,7 +220,7 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, image_name: }; const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image_name: string) => { - selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => { + selectSelectedCanvas(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) { @@ -246,7 +246,7 @@ const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, image_na }; const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, image_name: string) => { - selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => { + selectSelectedCanvas(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) { 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..ff320007828 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 { selectSelectedCanvas } 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,7 +56,7 @@ const DeleteBoardModal = () => { const selectImageUsageSummary = useMemo( () => createMemoizedSelector( - [selectNodesSlice, selectCanvasSlice, selectUpscaleSlice, selectRefImagesSlice], + [selectNodesSlice, selectSelectedCanvas, selectUpscaleSlice, selectRefImagesSlice], (nodes, canvas, upscale, refImages) => { const allImageUsage = (boardImageNames ?? []).map((imageName) => getImageUsage(nodes, canvas, upscale, refImages, imageName) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts index 6b0140bf9e7..825d3e88fdd 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasMetadata, selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectCanvasMetadata, selectSelectedCanvas } from 'features/controlLayers/store/selectors'; import { isFluxKontextReferenceImageConfig } from 'features/controlLayers/store/types'; import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators'; import { zImageField } from 'features/nodes/types/common'; @@ -40,7 +40,7 @@ export const buildFLUXGraph = async (arg: GraphBuilderArg): Promise { const tab = selectActiveTab(state); const params = selectParamsSlice(state); - const canvas = selectCanvasSlice(state); + const canvas = selectSelectedCanvas(state); if (tab === 'canvas') { const { rect, aspectRatio } = canvas.bbox; @@ -143,7 +143,7 @@ export const getOriginalAndScaledSizesForTextToImage = (state: RootState) => { export const getOriginalAndScaledSizesForOtherModes = (state: RootState) => { const tab = selectActiveTab(state); - const canvas = selectCanvasSlice(state); + const canvas = selectSelectedCanvas(state); assert(tab === 'canvas', `Cannot get sizes for tab ${tab} - this function is only for the Canvas tab`); 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..32016bd2bf9 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 { selectSelectedCanvas } 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(selectSelectedCanvas, (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..446739582c5 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 { selectSelectedCanvas } 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(selectSelectedCanvas, (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..267a34a9ccd 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,14 @@ 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, selectSelectedCanvas } 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(selectSelectedCanvas, (canvas) => canvas.bbox.scaleMethod === 'manual'); +const selectScaledHeight = createSelector(selectSelectedCanvas, (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..2d05bb1fbd0 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,14 @@ 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, selectSelectedCanvas } 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(selectSelectedCanvas, (canvas) => canvas.bbox.scaleMethod === 'manual'); +const selectScaledWidth = createSelector(selectSelectedCanvas, (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..660fd6e1801 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,15 @@ 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, selectSelectedCanvas } 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(selectSelectedCanvas, (canvas) => canvas.bbox.rect.width); +const selectHeight = createSelector(selectSelectedCanvas, (canvas) => canvas.bbox.rect.height); export const BboxSetOptimalSizeButton = memo(() => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index e8a22e104ea..5e64bd92bb8 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -12,7 +12,7 @@ import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasMana import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasState, LoRA, ParamsState, RefImagesState } from 'features/controlLayers/store/types'; import { getControlLayerWarnings, @@ -198,7 +198,7 @@ export const useReadinessWatcher = () => { const store = useAppStore(); const canvasManager = useCanvasManagerSafe(); const tab = useAppSelector(selectActiveTab); - const canvas = useAppSelector(selectCanvasSlice); + const canvas = useAppSelector(selectSelectedCanvas); const params = useAppSelector(selectParamsSlice); const refImages = useAppSelector(selectRefImagesSlice); const dynamicPrompts = useAppSelector(selectDynamicPromptsSlice); From 15dce42e1a05ed26a7a399e6fcfcf5edac318b24 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Thu, 11 Sep 2025 10:40:47 +0200 Subject: [PATCH 02/16] canvas tabs added --- .../features/controlLayers/store/selectors.ts | 11 ++ .../ui/layouts/CanvasTabEditableTitle.tsx | 69 ++++++++++ .../src/features/ui/layouts/CanvasTabs.tsx | 123 ++++++++++++++++++ .../ui/layouts/CanvasWorkspacePanel.tsx | 2 + 4 files changed, 205 insertions(+) create mode 100644 invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx create mode 100644 invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index da7eb7a1cef..2524e81e254 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -21,6 +21,17 @@ import { assert } from 'tsafe'; */ const selectCanvasSlice = (state: RootState) => state.canvas.present; +/** + * Selects the canvases + */ +export const selectCanvases = createSelector(selectCanvasSlice, (state) => + state.canvases.map((canvas) => ({ + ...canvas, + isSelected: canvas.id === state.selectedCanvasId, + canDelete: state.canvases.length > 1, + })) +); + /** * Selects the selected canvas */ 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..ace7f38f35c --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx @@ -0,0 +1,69 @@ +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 { + id: string; + name: string; + isSelected: boolean; +} + +export const CanvasTabEditableTitle = memo(({ id, name, isSelected }: CanvasTabEditableTitleProps) => { + const dispatch = useAppDispatch(); + const isHovering = useBoolean(false); + const inputRef = useRef(null); + + const onChange = useCallback(() => { + dispatch(canvasNameChanged({ id, name })); + }, [dispatch, id, name]); + + 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..0fc5b0a8e77 --- /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 { canvasAdded, canvasDeleted, canvasSelected } 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 { + id: string; + canDelete: boolean; +} + +const CloseCanvasButton = memo(({ id, canDelete }: CloseCanvasButtonProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const onClick = useCallback(() => { + dispatch(canvasDeleted({ id })); + }, [dispatch, id]); + + return ( + } + disabled={!canDelete} + variant="link" + w={8} + h={8} + /> + ); +}); +CloseCanvasButton.displayName = 'CloseCanvasButton'; + +interface CanvasTabProps { + id: string; + name: string; + isSelected: boolean; + canDelete: boolean; +} + +const CanvasTab = memo(({ id, name, isSelected, canDelete }: CanvasTabProps) => { + const dispatch = useAppDispatch(); + + const onClick = useCallback(() => { + if (!isSelected) { + dispatch(canvasSelected({ id })); + } + }, [dispatch, id, isSelected]); + + return ( + + + + + + + + + + + ); +}); +CanvasTab.displayName = 'CanvasTab'; + +export const CanvasTabs = () => { + const canvases = useAppSelector(selectCanvases); + + return ( + + + {canvases.map(({ id, name, isSelected, 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..9c17dd1ba63 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx @@ -22,6 +22,7 @@ import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagin import { memo, useCallback } from 'react'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; +import { CanvasTabs } from './CanvasTabs'; import { StagingArea } from './StagingArea'; const MenuContent = memo(() => { @@ -74,6 +75,7 @@ export const CanvasWorkspacePanel = memo(() => { + renderMenu={renderMenu} withLongPress={false}> {(ref) => ( From 094c4dd88fd2a1213980694e626d88e33fdf39cf Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Thu, 11 Sep 2025 11:06:07 +0200 Subject: [PATCH 03/16] undo/redo handles multiple canvases --- invokeai/frontend/web/src/app/store/store.ts | 28 +- invokeai/frontend/web/src/app/store/types.ts | 26 +- .../RasterLayerAdjustmentsPanel.tsx | 12 +- .../RasterLayerCurvesAdjustmentsEditor.tsx | 6 +- .../RasterLayerMenuItemsAdjustments.tsx | 13 +- .../RasterLayerSimpleAdjustmentsEditor.tsx | 6 +- .../controlLayers/store/canvasSlice.ts | 895 ++++++++---------- .../features/controlLayers/store/selectors.ts | 17 +- .../src/features/controlLayers/store/types.ts | 27 +- .../src/features/nodes/store/nodesSlice.ts | 16 +- .../ui/layouts/CanvasTabEditableTitle.tsx | 9 +- 11 files changed, 510 insertions(+), 545 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 12fcfa5a406..1a7273355db 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -22,7 +22,7 @@ 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 { canvasSliceConfig, undoableCanvasesReducer } 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'; @@ -30,7 +30,7 @@ import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlic 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,7 +44,6 @@ 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'; @@ -91,22 +90,14 @@ 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]: undoableCanvasesReducer, [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, [modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig.slice.reducer, - // Undoable! - [nodesSliceConfig.slice.reducerPath]: undoable( - nodesSliceConfig.slice.reducer, - nodesSliceConfig.undoableConfig?.reduxUndoOptions - ), + [nodesSliceConfig.slice.reducerPath]: undoableNodesSliceReducer, [paramsSliceConfig.slice.reducerPath]: paramsSliceConfig.slice.reducer, [queueSliceConfig.slice.reducerPath]: queueSliceConfig.slice.reducer, [refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig.slice.reducer, @@ -128,7 +119,7 @@ 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(); @@ -160,12 +151,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.wrapState ? persistConfig.wrapState(state) : state; }; const serialize: SerializeFunction = (data, key) => { @@ -175,7 +161,7 @@ const serialize: SerializeFunction = (data, key) => { } const result = omit( - sliceConfig.undoableConfig ? data.present : data, + sliceConfig.persistConfig.unwrapState ? sliceConfig.persistConfig.unwrapState(data) : data, sliceConfig.persistConfig.persistDenylist ?? [] ); diff --git a/invokeai/frontend/web/src/app/store/types.ts b/invokeai/frontend/web/src/app/store/types.ts index 28b28e1889b..cdb203ab4fe 100644 --- a/invokeai/frontend/web/src/app/store/types.ts +++ b/invokeai/frontend/web/src/app/store/types.ts @@ -1,10 +1,9 @@ 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 SliceConfig = { +export type SliceConfig, TSerializedState = StateFromSlice> = { /** * The redux slice (return of createSlice). */ @@ -16,7 +15,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 +27,24 @@ 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. */ persistDenylist?: (keyof StateFromSlice)[]; - }; - /** - * The optional undoable configuration for this slice. If omitted, the slice will not be undoable. - */ - undoableConfig?: { /** - * The options to be passed into redux-undo. + * Wraps state into state with history + * + * @param state The state without history + * @returns The state with history + */ + wrapState?: (state: unknown) => TInternalState; + /** + * Unwraps state with history + * + * @param state The state with history + * @returns The state without history */ - reduxUndoOptions: UndoableOptions>; + unwrapState?: (state: TInternalState) => TSerializedState; }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel.tsx index 4b17767cfa0..00ce4807841 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel.tsx @@ -13,7 +13,7 @@ import { rasterLayerAdjustmentsReset, rasterLayerAdjustmentsSet, } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors'; +import { selectEntity, selectSelectedCanvas } from 'features/controlLayers/store/selectors'; import React, { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiCaretDownBold, PiCheckBold, PiTrashBold } from 'react-icons/pi'; @@ -25,14 +25,16 @@ export const RasterLayerAdjustmentsPanel = memo(() => { const canvasManager = useCanvasManager(); const selectHasAdjustments = useMemo(() => { - return createSelector(selectCanvasSlice, (canvas) => Boolean(selectEntity(canvas, entityIdentifier)?.adjustments)); + return createSelector(selectSelectedCanvas, (canvas) => + Boolean(selectEntity(canvas, entityIdentifier)?.adjustments) + ); }, [entityIdentifier]); const hasAdjustments = useAppSelector(selectHasAdjustments); const selectMode = useMemo(() => { return createSelector( - selectCanvasSlice, + selectSelectedCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.mode ?? 'simple' ); }, [entityIdentifier]); @@ -40,7 +42,7 @@ export const RasterLayerAdjustmentsPanel = memo(() => { const selectEnabled = useMemo(() => { return createSelector( - selectCanvasSlice, + selectSelectedCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled ?? false ); }, [entityIdentifier]); @@ -48,7 +50,7 @@ export const RasterLayerAdjustmentsPanel = memo(() => { const selectCollapsed = useMemo(() => { return createSelector( - selectCanvasSlice, + selectSelectedCanvas, (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..d3b796fd764 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 { selectEntity, selectSelectedCanvas } 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, + selectSelectedCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.curves ?? DEFAULT_CURVES ); }, [entityIdentifier]); @@ -80,7 +80,7 @@ export const RasterLayerCurvesAdjustmentsEditor = memo(() => { const selectIsDisabled = useMemo(() => { return createSelector( - selectCanvasSlice, + selectSelectedCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled !== true ); }, [entityIdentifier]); 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..b0d7ef9991b 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 { selectSelectedCanvas } 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(selectSelectedCanvas, (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..e8f2ca01f15 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 { selectEntity, selectSelectedCanvas } 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, + selectSelectedCanvas, (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, + selectSelectedCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled !== true ); }, [entityIdentifier]); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index a5acf2b0184..5a8a421daa2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -4,6 +4,7 @@ import type { SliceConfig } from 'app/store/types'; 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'; @@ -17,7 +18,8 @@ import { import type { CanvasEntityStateFromType, CanvasEntityType, - CanvasesState, + CanvasesStateWithHistory, + CanvasesStateWithoutHistory, CanvasInpaintMaskState, CanvasMetadata, ChannelName, @@ -40,7 +42,8 @@ import { isMainModelBase, zModelIdentifierField } from 'features/nodes/types/com 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 type { StateWithHistory, UndoableOptions } from 'redux-undo'; +import undoable, { newHistory } from 'redux-undo'; import { type ControlLoRAModelConfig, type ControlNetModelConfig, @@ -51,6 +54,7 @@ import { isIPAdapterModelConfig, type T2IAdapterModelConfig, } from 'services/api/types'; +import { assert } from 'tsafe'; import type { AspectRatioID, @@ -86,7 +90,8 @@ import { isImagenAspectRatioID, isRegionalGuidanceFLUXReduxConfig, isRegionalGuidanceIPAdapterConfig, - zCanvasesState, + zCanvasesStateWithHistory, + zCanvasesStateWithoutHistory, } from './types'; import { converters, @@ -104,19 +109,7 @@ import { makeDefaultRasterLayerAdjustments, } from './util'; -const getInitialCanvasesState = (): CanvasesState => { - const canvasId = getPrefixedId('canvas'); - const canvasName = 'default'; - const canvas = getCanvasState(canvasId, canvasName); - - return { - _version: 3, - selectedCanvasId: canvas.id, - canvases: [canvas], - }; -}; - -const getCanvasState = (id: string, name: string): CanvasState => ({ +const getInitialCanvasState = (id: string, name: string): CanvasState => ({ id, name, selectedEntityIdentifier: null, @@ -134,19 +127,44 @@ const getCanvasState = (id: string, name: string): CanvasState => ({ }, }); -const slice = createSlice({ +const getInitialCanvasHistoryState = (id: string, name: string): StateWithHistory => { + const canvas = getInitialCanvasState(id, name); + + return newHistory([], canvas, []); +}; + +const getInitialCanvasesState = (): CanvasesStateWithoutHistory => { + const canvasId = getPrefixedId('canvas'); + const canvasName = 'default'; + const canvas = getInitialCanvasState(canvasId, canvasName); + + return { + _version: 4, + selectedCanvasId: canvasId, + canvases: [canvas], + }; +}; + +const getInitialCanvasesHistoryState = (): CanvasesStateWithHistory => { + const state = getInitialCanvasesState(); + + return { + ...state, + canvases: state.canvases.map((canvas) => newHistory([], canvas, [])), + }; +}; + +const canvasesSlice = createSlice({ name: 'canvas', - initialState: getInitialCanvasesState(), + initialState: getInitialCanvasesHistoryState(), reducers: { - // undoable canvases state - //#region Canvases canvasAdded: { reducer: (state, action: PayloadAction<{ id: string; isSelected?: boolean }>) => { const { id, isSelected } = action.payload; const name = 'default'; - const canvasState = getCanvasState(id, name); - state.canvases.push(canvasState); + const canvas = getInitialCanvasHistoryState(id, name); + state.canvases.push(canvas); if (isSelected) { state.selectedCanvasId = id; @@ -185,12 +203,19 @@ const slice = createSlice({ throw new Error('Last canvas cannot be deleted'); } - const index = state.canvases.findIndex((canvas) => canvas.id === id); + const index = state.canvases.findIndex((canvas) => canvas.present.id === id); const nextIndex = (index + 1) % state.canvases.length; - state.selectedCanvasId = state.canvases[nextIndex]!.id; - state.canvases = state.canvases.filter((canvas) => canvas.id !== id); + state.selectedCanvasId = state.canvases[nextIndex]!.present.id; + state.canvases = state.canvases.filter((canvas) => canvas.present.id !== id); }, + }, +}); + +const canvasSlice = createSlice({ + name: 'canvas', + initialState: {} as CanvasState, + reducers: { //#region Raster layers rasterLayerAdjustmentsSet: ( state, @@ -295,16 +320,15 @@ const slice = createSlice({ }> ) => { const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [], addAfter } = action.payload; - const canvas = getSelectedCanvas(state); const entityState = getRasterLayerState(id, overrides); const index = addAfter - ? canvas.rasterLayers.entities.findIndex((e) => e.id === addAfter) + 1 - : canvas.rasterLayers.entities.length; - canvas.rasterLayers.entities.splice(index, 0, entityState); + ? state.rasterLayers.entities.findIndex((e) => e.id === addAfter) + 1 + : state.rasterLayers.entities.length; + state.rasterLayers.entities.splice(index, 0, entityState); if (mergedEntitiesToDelete.length > 0) { - canvas.rasterLayers.entities = canvas.rasterLayers.entities.filter( + state.rasterLayers.entities = state.rasterLayers.entities.filter( (entity) => !mergedEntitiesToDelete.includes(entity.id) ); } @@ -312,11 +336,11 @@ const slice = createSlice({ const entityIdentifier = getEntityIdentifier(entityState); if (isSelected || mergedEntitiesToDelete.length > 0) { - canvas.selectedEntityIdentifier = entityIdentifier; + state.selectedEntityIdentifier = entityIdentifier; } if (isBookmarked) { - canvas.bookmarkedEntityIdentifier = entityIdentifier; + state.bookmarkedEntityIdentifier = entityIdentifier; } }, prepare: (payload: { @@ -331,10 +355,8 @@ const slice = createSlice({ }, rasterLayerRecalled: (state, action: PayloadAction<{ data: CanvasRasterLayerState }>) => { const { data } = action.payload; - const canvas = getSelectedCanvas(state); - - canvas.rasterLayers.entities.push(data); - canvas.selectedEntityIdentifier = getEntityIdentifier(data); + state.rasterLayers.entities.push(data); + state.selectedEntityIdentifier = getEntityIdentifier(data); }, rasterLayerConvertedToControlLayer: { reducer: ( @@ -347,8 +369,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -358,15 +379,13 @@ const slice = createSlice({ if (replace) { // Remove the raster layer - canvas.rasterLayers.entities = canvas.rasterLayers.entities.filter( - (layer) => layer.id !== entityIdentifier.id - ); + state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); } // Add the converted control layer - canvas.controlLayers.entities.push(controlLayerState); + state.controlLayers.entities.push(controlLayerState); - canvas.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id }; + state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -388,8 +407,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -399,15 +417,13 @@ const slice = createSlice({ if (replace) { // Remove the raster layer - canvas.rasterLayers.entities = canvas.rasterLayers.entities.filter( - (layer) => layer.id !== entityIdentifier.id - ); + state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); } // Add the converted inpaint mask - canvas.inpaintMasks.entities.push(inpaintMaskState); + state.inpaintMasks.entities.push(inpaintMaskState); - canvas.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; + state.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -429,8 +445,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -440,15 +455,13 @@ const slice = createSlice({ if (replace) { // Remove the raster layer - canvas.rasterLayers.entities = canvas.rasterLayers.entities.filter( - (layer) => layer.id !== entityIdentifier.id - ); + state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); } // Add the converted inpaint mask - canvas.regionalGuidance.entities.push(regionalGuidanceState); + state.regionalGuidance.entities.push(regionalGuidanceState); - canvas.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; + state.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -474,27 +487,26 @@ const slice = createSlice({ ) => { const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [], addAfter } = action.payload; - const canvas = getSelectedCanvas(state); const entityState = getControlLayerState(id, overrides); const index = addAfter - ? canvas.controlLayers.entities.findIndex((e) => e.id === addAfter) + 1 - : canvas.controlLayers.entities.length; - canvas.controlLayers.entities.splice(index, 0, entityState); + ? state.controlLayers.entities.findIndex((e) => e.id === addAfter) + 1 + : state.controlLayers.entities.length; + state.controlLayers.entities.splice(index, 0, entityState); if (mergedEntitiesToDelete.length > 0) { - canvas.controlLayers.entities = canvas.controlLayers.entities.filter( + state.controlLayers.entities = state.controlLayers.entities.filter( (entity) => !mergedEntitiesToDelete.includes(entity.id) ); } const entityIdentifier = getEntityIdentifier(entityState); if (isSelected || mergedEntitiesToDelete.length > 0) { - canvas.selectedEntityIdentifier = entityIdentifier; + state.selectedEntityIdentifier = entityIdentifier; } if (isBookmarked) { - canvas.bookmarkedEntityIdentifier = entityIdentifier; + state.bookmarkedEntityIdentifier = entityIdentifier; } }, prepare: (payload: { @@ -509,10 +521,8 @@ const slice = createSlice({ }, controlLayerRecalled: (state, action: PayloadAction<{ data: CanvasControlLayerState }>) => { const { data } = action.payload; - const canvas = getSelectedCanvas(state); - - canvas.controlLayers.entities.push(data); - canvas.selectedEntityIdentifier = { type: 'control_layer', id: data.id }; + state.controlLayers.entities.push(data); + state.selectedEntityIdentifier = { type: 'control_layer', id: data.id }; }, controlLayerConvertedToRasterLayer: { reducer: ( @@ -525,9 +535,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -537,15 +545,15 @@ const slice = createSlice({ if (replace) { // Remove the control layer - canvas.controlLayers.entities = canvas.controlLayers.entities.filter( + state.controlLayers.entities = state.controlLayers.entities.filter( (layer) => layer.id !== entityIdentifier.id ); } // Add the new raster layer - canvas.rasterLayers.entities.push(rasterLayerState); + state.rasterLayers.entities.push(rasterLayerState); - canvas.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id }; + state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -567,9 +575,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -579,15 +585,15 @@ const slice = createSlice({ if (replace) { // Remove the control layer - canvas.controlLayers.entities = canvas.controlLayers.entities.filter( + state.controlLayers.entities = state.controlLayers.entities.filter( (layer) => layer.id !== entityIdentifier.id ); } // Add the new inpaint mask - canvas.inpaintMasks.entities.push(inpaintMaskState); + state.inpaintMasks.entities.push(inpaintMaskState); - canvas.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; + state.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -609,9 +615,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -621,15 +625,15 @@ const slice = createSlice({ if (replace) { // Remove the control layer - canvas.controlLayers.entities = canvas.controlLayers.entities.filter( + state.controlLayers.entities = state.controlLayers.entities.filter( (layer) => layer.id !== entityIdentifier.id ); } // Add the new regional guidance - canvas.regionalGuidance.entities.push(regionalGuidanceState); + state.regionalGuidance.entities.push(regionalGuidanceState); - canvas.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; + state.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -652,9 +656,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, modelConfig } = action.payload; - - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer || !layer.controlAdapter) { return; } @@ -734,9 +736,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, controlMode } = action.payload; - - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') { return; } @@ -747,9 +747,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, weight } = action.payload; - - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer || !layer.controlAdapter) { return; } @@ -760,9 +758,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, beginEndStepPct } = action.payload; - - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer || !layer.controlAdapter || layer.controlAdapter.type === 'control_lora') { return; } @@ -773,9 +769,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -796,27 +790,26 @@ const slice = createSlice({ ) => { const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [], addAfter } = action.payload; - const canvas = getSelectedCanvas(state); const entityState = getRegionalGuidanceState(id, overrides); const index = addAfter - ? canvas.regionalGuidance.entities.findIndex((e) => e.id === addAfter) + 1 - : canvas.regionalGuidance.entities.length; - canvas.regionalGuidance.entities.splice(index, 0, entityState); + ? state.regionalGuidance.entities.findIndex((e) => e.id === addAfter) + 1 + : state.regionalGuidance.entities.length; + state.regionalGuidance.entities.splice(index, 0, entityState); if (mergedEntitiesToDelete.length > 0) { - canvas.regionalGuidance.entities = canvas.regionalGuidance.entities.filter( + state.regionalGuidance.entities = state.regionalGuidance.entities.filter( (entity) => !mergedEntitiesToDelete.includes(entity.id) ); } const entityIdentifier = getEntityIdentifier(entityState); if (isSelected || mergedEntitiesToDelete.length > 0) { - canvas.selectedEntityIdentifier = entityIdentifier; + state.selectedEntityIdentifier = entityIdentifier; } if (isBookmarked) { - canvas.bookmarkedEntityIdentifier = entityIdentifier; + state.bookmarkedEntityIdentifier = entityIdentifier; } }, prepare: (payload?: { @@ -831,10 +824,8 @@ const slice = createSlice({ }, rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => { const { data } = action.payload; - - const canvas = getSelectedCanvas(state); - canvas.regionalGuidance.entities.push(data); - canvas.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; + state.regionalGuidance.entities.push(data); + state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; }, rgConvertedToInpaintMask: { reducer: ( @@ -847,9 +838,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -859,15 +848,15 @@ const slice = createSlice({ if (replace) { // Remove the regional guidance - canvas.regionalGuidance.entities = canvas.regionalGuidance.entities.filter( + state.regionalGuidance.entities = state.regionalGuidance.entities.filter( (layer) => layer.id !== entityIdentifier.id ); } // Add the new inpaint mask - canvas.inpaintMasks.entities.push(inpaintMaskState); + state.inpaintMasks.entities.push(inpaintMaskState); - canvas.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; + state.selectedEntityIdentifier = { type: inpaintMaskState.type, id: inpaintMaskState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -883,9 +872,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, prompt } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -896,9 +883,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, prompt } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -906,9 +891,7 @@ const slice = createSlice({ }, rgAutoNegativeToggled: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const rg = selectEntity(canvas, entityIdentifier); + const rg = selectEntity(state, entityIdentifier); if (!rg) { return; } @@ -925,9 +908,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, overrides, referenceImageId } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -946,9 +927,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, referenceImageId } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -961,9 +940,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, imageDTO } = action.payload; - - const canvas = getSelectedCanvas(state); - const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); + const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -974,9 +951,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, referenceImageId, weight } = action.payload; - - const canvas = getSelectedCanvas(state); - const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); + const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -993,9 +968,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, beginEndStepPct } = action.payload; - - const canvas = getSelectedCanvas(state); - const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); + const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -1011,9 +984,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, method } = action.payload; - - const canvas = getSelectedCanvas(state); - const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); + const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -1032,9 +1003,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, imageInfluence } = action.payload; - - const canvas = getSelectedCanvas(state); - const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); + const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -1057,9 +1026,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, modelConfig } = action.payload; - - const canvas = getSelectedCanvas(state); - const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); + const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -1108,9 +1075,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, referenceImageId, clipVisionModel } = action.payload; - - const canvas = getSelectedCanvas(state); - const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId); + const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId); if (!referenceImage) { return; } @@ -1134,27 +1099,26 @@ const slice = createSlice({ ) => { const { id, overrides, isSelected, isBookmarked, mergedEntitiesToDelete = [], addAfter } = action.payload; - const canvas = getSelectedCanvas(state); const entityState = getInpaintMaskState(id, overrides); const index = addAfter - ? canvas.inpaintMasks.entities.findIndex((e) => e.id === addAfter) + 1 - : canvas.inpaintMasks.entities.length; - canvas.inpaintMasks.entities.splice(index, 0, entityState); + ? state.inpaintMasks.entities.findIndex((e) => e.id === addAfter) + 1 + : state.inpaintMasks.entities.length; + state.inpaintMasks.entities.splice(index, 0, entityState); if (mergedEntitiesToDelete.length > 0) { - canvas.inpaintMasks.entities = canvas.inpaintMasks.entities.filter( + state.inpaintMasks.entities = state.inpaintMasks.entities.filter( (entity) => !mergedEntitiesToDelete.includes(entity.id) ); } const entityIdentifier = getEntityIdentifier(entityState); if (isSelected || mergedEntitiesToDelete.length > 0) { - canvas.selectedEntityIdentifier = entityIdentifier; + state.selectedEntityIdentifier = entityIdentifier; } if (isBookmarked) { - canvas.bookmarkedEntityIdentifier = entityIdentifier; + state.bookmarkedEntityIdentifier = entityIdentifier; } }, prepare: (payload?: { @@ -1169,16 +1133,12 @@ const slice = createSlice({ }, inpaintMaskRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { const { data } = action.payload; - - const canvas = getSelectedCanvas(state); - canvas.inpaintMasks.entities = [data]; - canvas.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; + state.inpaintMasks.entities = [data]; + state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, inpaintMaskNoiseAdded: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.noiseLevel = 0.15; // Default noise level } @@ -1188,18 +1148,14 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, noiseLevel } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.noiseLevel = noiseLevel; } }, inpaintMaskNoiseDeleted: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.noiseLevel = undefined; } @@ -1215,9 +1171,7 @@ const slice = createSlice({ > ) => { const { entityIdentifier, newId, overrides, replace } = action.payload; - - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -1227,15 +1181,13 @@ const slice = createSlice({ if (replace) { // Remove the inpaint mask - canvas.inpaintMasks.entities = canvas.inpaintMasks.entities.filter( - (layer) => layer.id !== entityIdentifier.id - ); + state.inpaintMasks.entities = state.inpaintMasks.entities.filter((layer) => layer.id !== entityIdentifier.id); } // Add the new regional guidance - canvas.regionalGuidance.entities.push(regionalGuidanceState); + state.regionalGuidance.entities.push(regionalGuidanceState); - canvas.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; + state.selectedEntityIdentifier = { type: regionalGuidanceState.type, id: regionalGuidanceState.id }; }, prepare: ( payload: EntityIdentifierPayload< @@ -1248,9 +1200,7 @@ const slice = createSlice({ }, inpaintMaskDenoiseLimitAdded: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.denoiseLimit = 1.0; // Default denoise limit } @@ -1260,66 +1210,58 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, denoiseLimit } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.denoiseLimit = denoiseLimit; } }, inpaintMaskDenoiseLimitDeleted: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (entity && entity.type === 'inpaint_mask') { entity.denoiseLimit = undefined; } }, //#region BBox bboxScaledWidthChanged: (state, action: PayloadAction) => { - const canvas = getSelectedCanvas(state); - const gridSize = getGridSize(canvas.bbox.modelBase); + const gridSize = getGridSize(state.bbox.modelBase); - canvas.bbox.scaledSize.width = roundToMultiple(action.payload, gridSize); + state.bbox.scaledSize.width = roundToMultiple(action.payload, gridSize); - if (canvas.bbox.aspectRatio.isLocked) { - canvas.bbox.scaledSize.height = roundToMultiple( - canvas.bbox.scaledSize.width / canvas.bbox.aspectRatio.value, + if (state.bbox.aspectRatio.isLocked) { + state.bbox.scaledSize.height = roundToMultiple( + state.bbox.scaledSize.width / state.bbox.aspectRatio.value, gridSize ); } }, bboxScaledHeightChanged: (state, action: PayloadAction) => { - const canvas = getSelectedCanvas(state); - const gridSize = getGridSize(canvas.bbox.modelBase); + const gridSize = getGridSize(state.bbox.modelBase); - canvas.bbox.scaledSize.height = roundToMultiple(action.payload, gridSize); + state.bbox.scaledSize.height = roundToMultiple(action.payload, gridSize); - if (canvas.bbox.aspectRatio.isLocked) { - canvas.bbox.scaledSize.width = roundToMultiple( - canvas.bbox.scaledSize.height * canvas.bbox.aspectRatio.value, + if (state.bbox.aspectRatio.isLocked) { + state.bbox.scaledSize.width = roundToMultiple( + state.bbox.scaledSize.height * state.bbox.aspectRatio.value, gridSize ); } }, bboxScaleMethodChanged: (state, action: PayloadAction) => { - const canvas = getSelectedCanvas(state); - canvas.bbox.scaleMethod = action.payload; - syncScaledSize(canvas); + state.bbox.scaleMethod = action.payload; + syncScaledSize(state); }, bboxChangedFromCanvas: (state, action: PayloadAction) => { - const canvas = getSelectedCanvas(state); const newBboxRect = action.payload; - const oldBboxRect = canvas.bbox.rect; + const oldBboxRect = state.bbox.rect; - canvas.bbox.rect = newBboxRect; + state.bbox.rect = newBboxRect; if (newBboxRect.width === oldBboxRect.width && newBboxRect.height === oldBboxRect.height) { return; } - const oldAspectRatio = canvas.bbox.aspectRatio.value; + const oldAspectRatio = state.bbox.aspectRatio.value; const newAspectRatio = newBboxRect.width / newBboxRect.height; if (oldAspectRatio === newAspectRatio) { @@ -1329,206 +1271,188 @@ const slice = createSlice({ // TODO(psyche): Figure out a way to handle this without resetting the aspect ratio on every change. // This action is dispatched when the user resizes or moves the bbox from the canvas. For now, when the user // resizes the bbox from the canvas, we unlock the aspect ratio. - canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; - canvas.bbox.aspectRatio.id = 'Free'; + state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; + state.bbox.aspectRatio.id = 'Free'; - syncScaledSize(canvas); + syncScaledSize(state); }, bboxWidthChanged: ( state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }> ) => { const { width, updateAspectRatio, clamp } = action.payload; + const gridSize = getGridSize(state.bbox.modelBase); + state.bbox.rect.width = clamp ? Math.max(roundDownToMultiple(width, gridSize), 64) : width; - const canvas = getSelectedCanvas(state); - const gridSize = getGridSize(canvas.bbox.modelBase); - canvas.bbox.rect.width = clamp ? Math.max(roundDownToMultiple(width, gridSize), 64) : width; - - if (canvas.bbox.aspectRatio.isLocked) { - canvas.bbox.rect.height = roundToMultiple(canvas.bbox.rect.width / canvas.bbox.aspectRatio.value, gridSize); + if (state.bbox.aspectRatio.isLocked) { + state.bbox.rect.height = roundToMultiple(state.bbox.rect.width / state.bbox.aspectRatio.value, gridSize); } - if (updateAspectRatio || !canvas.bbox.aspectRatio.isLocked) { - canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; - canvas.bbox.aspectRatio.id = 'Free'; - canvas.bbox.aspectRatio.isLocked = false; + if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) { + state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; + state.bbox.aspectRatio.id = 'Free'; + state.bbox.aspectRatio.isLocked = false; } - syncScaledSize(canvas); + syncScaledSize(state); }, bboxHeightChanged: ( state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }> ) => { const { height, updateAspectRatio, clamp } = action.payload; + const gridSize = getGridSize(state.bbox.modelBase); + state.bbox.rect.height = clamp ? Math.max(roundDownToMultiple(height, gridSize), 64) : height; - const canvas = getSelectedCanvas(state); - const gridSize = getGridSize(canvas.bbox.modelBase); - canvas.bbox.rect.height = clamp ? Math.max(roundDownToMultiple(height, gridSize), 64) : height; - - if (canvas.bbox.aspectRatio.isLocked) { - canvas.bbox.rect.width = roundToMultiple(canvas.bbox.rect.height * canvas.bbox.aspectRatio.value, gridSize); + if (state.bbox.aspectRatio.isLocked) { + state.bbox.rect.width = roundToMultiple(state.bbox.rect.height * state.bbox.aspectRatio.value, gridSize); } - if (updateAspectRatio || !canvas.bbox.aspectRatio.isLocked) { - canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; - canvas.bbox.aspectRatio.id = 'Free'; - canvas.bbox.aspectRatio.isLocked = false; + if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) { + state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; + state.bbox.aspectRatio.id = 'Free'; + state.bbox.aspectRatio.isLocked = false; } - syncScaledSize(canvas); + syncScaledSize(state); }, bboxSizeRecalled: (state, action: PayloadAction<{ width: number; height: number }>) => { const { width, height } = action.payload; - - const canvas = getSelectedCanvas(state); - const gridSize = getGridSize(canvas.bbox.modelBase); - canvas.bbox.rect.width = Math.max(roundDownToMultiple(width, gridSize), 64); - canvas.bbox.rect.height = Math.max(roundDownToMultiple(height, gridSize), 64); - canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; - canvas.bbox.aspectRatio.id = 'Free'; - canvas.bbox.aspectRatio.isLocked = true; + const gridSize = getGridSize(state.bbox.modelBase); + state.bbox.rect.width = Math.max(roundDownToMultiple(width, gridSize), 64); + state.bbox.rect.height = Math.max(roundDownToMultiple(height, gridSize), 64); + state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; + state.bbox.aspectRatio.id = 'Free'; + state.bbox.aspectRatio.isLocked = true; }, bboxAspectRatioLockToggled: (state) => { - const canvas = getSelectedCanvas(state); - canvas.bbox.aspectRatio.isLocked = !canvas.bbox.aspectRatio.isLocked; - syncScaledSize(canvas); + state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked; + syncScaledSize(state); }, bboxAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { const { id } = action.payload; - - const canvas = getSelectedCanvas(state); - canvas.bbox.aspectRatio.id = id; + state.bbox.aspectRatio.id = id; if (id === 'Free') { - canvas.bbox.aspectRatio.isLocked = false; + state.bbox.aspectRatio.isLocked = false; } else if ( - (canvas.bbox.modelBase === 'imagen3' || canvas.bbox.modelBase === 'imagen4') && + (state.bbox.modelBase === 'imagen3' || state.bbox.modelBase === 'imagen4') && isImagenAspectRatioID(id) ) { const { width, height } = IMAGEN_ASPECT_RATIOS[id]; - canvas.bbox.rect.width = width; - canvas.bbox.rect.height = height; - canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; - canvas.bbox.aspectRatio.isLocked = true; - } else if (canvas.bbox.modelBase === 'chatgpt-4o' && isChatGPT4oAspectRatioID(id)) { + state.bbox.rect.width = width; + state.bbox.rect.height = height; + state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; + state.bbox.aspectRatio.isLocked = true; + } else if (state.bbox.modelBase === 'chatgpt-4o' && isChatGPT4oAspectRatioID(id)) { const { width, height } = CHATGPT_ASPECT_RATIOS[id]; - canvas.bbox.rect.width = width; - canvas.bbox.rect.height = height; - canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; - canvas.bbox.aspectRatio.isLocked = true; - } else if (canvas.bbox.modelBase === 'gemini-2.5' && isGemini2_5AspectRatioID(id)) { + state.bbox.rect.width = width; + state.bbox.rect.height = height; + state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; + state.bbox.aspectRatio.isLocked = true; + } else if (state.bbox.modelBase === 'gemini-2.5' && isGemini2_5AspectRatioID(id)) { const { width, height } = GEMINI_2_5_ASPECT_RATIOS[id]; - canvas.bbox.rect.width = width; - canvas.bbox.rect.height = height; - canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; - canvas.bbox.aspectRatio.isLocked = true; - } else if (canvas.bbox.modelBase === 'flux-kontext' && isFluxKontextAspectRatioID(id)) { + state.bbox.rect.width = width; + state.bbox.rect.height = height; + state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; + state.bbox.aspectRatio.isLocked = true; + } else if (state.bbox.modelBase === 'flux-kontext' && isFluxKontextAspectRatioID(id)) { const { width, height } = FLUX_KONTEXT_ASPECT_RATIOS[id]; - canvas.bbox.rect.width = width; - canvas.bbox.rect.height = height; - canvas.bbox.aspectRatio.value = canvas.bbox.rect.width / canvas.bbox.rect.height; - canvas.bbox.aspectRatio.isLocked = true; + state.bbox.rect.width = width; + state.bbox.rect.height = height; + state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height; + state.bbox.aspectRatio.isLocked = true; } else { - canvas.bbox.aspectRatio.isLocked = true; - canvas.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; + state.bbox.aspectRatio.isLocked = true; + state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio; const { width, height } = calculateNewSize( - canvas.bbox.aspectRatio.value, - canvas.bbox.rect.width * canvas.bbox.rect.height, - canvas.bbox.modelBase + state.bbox.aspectRatio.value, + state.bbox.rect.width * state.bbox.rect.height, + state.bbox.modelBase ); - canvas.bbox.rect.width = width; - canvas.bbox.rect.height = height; + state.bbox.rect.width = width; + state.bbox.rect.height = height; } - syncScaledSize(canvas); + syncScaledSize(state); }, bboxDimensionsSwapped: (state) => { - const canvas = getSelectedCanvas(state); - canvas.bbox.aspectRatio.value = 1 / canvas.bbox.aspectRatio.value; - if (canvas.bbox.aspectRatio.id === 'Free') { - const newWidth = canvas.bbox.rect.height; - const newHeight = canvas.bbox.rect.width; - canvas.bbox.rect.width = newWidth; - canvas.bbox.rect.height = newHeight; + state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value; + if (state.bbox.aspectRatio.id === 'Free') { + const newWidth = state.bbox.rect.height; + const newHeight = state.bbox.rect.width; + state.bbox.rect.width = newWidth; + state.bbox.rect.height = newHeight; } else { const { width, height } = calculateNewSize( - canvas.bbox.aspectRatio.value, - canvas.bbox.rect.width * canvas.bbox.rect.height, - canvas.bbox.modelBase + state.bbox.aspectRatio.value, + state.bbox.rect.width * state.bbox.rect.height, + state.bbox.modelBase ); - canvas.bbox.rect.width = width; - canvas.bbox.rect.height = height; - canvas.bbox.aspectRatio.id = ASPECT_RATIO_MAP[canvas.bbox.aspectRatio.id].inverseID; + state.bbox.rect.width = width; + state.bbox.rect.height = height; + state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID; } - syncScaledSize(canvas); + syncScaledSize(state); }, bboxSizeOptimized: (state) => { - const canvas = getSelectedCanvas(state); - const optimalDimension = getOptimalDimension(canvas.bbox.modelBase); - if (canvas.bbox.aspectRatio.isLocked) { + const optimalDimension = getOptimalDimension(state.bbox.modelBase); + if (state.bbox.aspectRatio.isLocked) { const { width, height } = calculateNewSize( - canvas.bbox.aspectRatio.value, + state.bbox.aspectRatio.value, optimalDimension * optimalDimension, - canvas.bbox.modelBase + state.bbox.modelBase ); - canvas.bbox.rect.width = width; - canvas.bbox.rect.height = height; + state.bbox.rect.width = width; + state.bbox.rect.height = height; } else { - canvas.bbox.aspectRatio = deepClone(DEFAULT_ASPECT_RATIO_CONFIG); - canvas.bbox.rect.width = optimalDimension; - canvas.bbox.rect.height = optimalDimension; + state.bbox.aspectRatio = deepClone(DEFAULT_ASPECT_RATIO_CONFIG); + state.bbox.rect.width = optimalDimension; + state.bbox.rect.height = optimalDimension; } - syncScaledSize(canvas); + syncScaledSize(state); }, bboxSyncedToOptimalDimension: (state) => { - const canvas = getSelectedCanvas(state); - const optimalDimension = getOptimalDimension(canvas.bbox.modelBase); + const optimalDimension = getOptimalDimension(state.bbox.modelBase); - if (!getIsSizeOptimal(canvas.bbox.rect.width, canvas.bbox.rect.height, canvas.bbox.modelBase)) { + if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, state.bbox.modelBase)) { const bboxDims = calculateNewSize( - canvas.bbox.aspectRatio.value, + state.bbox.aspectRatio.value, optimalDimension * optimalDimension, - canvas.bbox.modelBase + state.bbox.modelBase ); - canvas.bbox.rect.width = bboxDims.width; - canvas.bbox.rect.height = bboxDims.height; - syncScaledSize(canvas); + state.bbox.rect.width = bboxDims.width; + state.bbox.rect.height = bboxDims.height; + syncScaledSize(state); } }, //#region Shared entity entitySelected: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { // Cannot select a non-existent entity return; } - canvas.selectedEntityIdentifier = entityIdentifier; + state.selectedEntityIdentifier = entityIdentifier; }, bookmarkedEntityChanged: (state, action: PayloadAction<{ entityIdentifier: CanvasEntityIdentifier | null }>) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); if (!entityIdentifier) { - canvas.bookmarkedEntityIdentifier = null; + state.bookmarkedEntityIdentifier = null; return; } - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { // Cannot select a non-existent entity return; } - canvas.bookmarkedEntityIdentifier = entityIdentifier; + state.bookmarkedEntityIdentifier = entityIdentifier; }, entityNameChanged: (state, action: PayloadAction>) => { const { entityIdentifier, name } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1536,9 +1460,7 @@ const slice = createSlice({ }, entityReset: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1548,9 +1470,7 @@ const slice = createSlice({ }, entityDuplicated: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1562,14 +1482,14 @@ const slice = createSlice({ switch (newEntity.type) { case 'raster_layer': { newEntity.id = getPrefixedId('raster_layer'); - const newEntityIndex = canvas.rasterLayers.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; - canvas.rasterLayers.entities.splice(newEntityIndex, 0, newEntity); + const newEntityIndex = state.rasterLayers.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; + state.rasterLayers.entities.splice(newEntityIndex, 0, newEntity); break; } case 'control_layer': { newEntity.id = getPrefixedId('control_layer'); - const newEntityIndex = canvas.controlLayers.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; - canvas.controlLayers.entities.splice(newEntityIndex, 0, newEntity); + const newEntityIndex = state.controlLayers.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; + state.controlLayers.entities.splice(newEntityIndex, 0, newEntity); break; } case 'regional_guidance': { @@ -1577,25 +1497,23 @@ const slice = createSlice({ for (const refImage of newEntity.referenceImages) { refImage.id = getPrefixedId('regional_guidance_ip_adapter'); } - const newEntityIndex = canvas.regionalGuidance.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; - canvas.regionalGuidance.entities.splice(newEntityIndex, 0, newEntity); + const newEntityIndex = state.regionalGuidance.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; + state.regionalGuidance.entities.splice(newEntityIndex, 0, newEntity); break; } case 'inpaint_mask': { newEntity.id = getPrefixedId('inpaint_mask'); - const newEntityIndex = canvas.inpaintMasks.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; - canvas.inpaintMasks.entities.splice(newEntityIndex, 0, newEntity); + const newEntityIndex = state.inpaintMasks.entities.findIndex((e) => e.id === entityIdentifier.id) + 1; + state.inpaintMasks.entities.splice(newEntityIndex, 0, newEntity); break; } } - canvas.selectedEntityIdentifier = getEntityIdentifier(newEntity); + state.selectedEntityIdentifier = getEntityIdentifier(newEntity); }, entityIsEnabledToggled: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1603,9 +1521,7 @@ const slice = createSlice({ }, entityIsLockedToggled: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1616,9 +1532,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { color, entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1629,9 +1543,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { style, entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1639,9 +1551,7 @@ const slice = createSlice({ }, entityMovedTo: (state, action: PayloadAction) => { const { entityIdentifier, position } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1650,9 +1560,7 @@ const slice = createSlice({ }, entityMovedBy: (state, action: PayloadAction) => { const { entityIdentifier, offset } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1662,9 +1570,7 @@ const slice = createSlice({ }, entityRasterized: (state, action: PayloadAction) => { const { entityIdentifier, imageObject, position, replaceObjects, isSelected } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1675,14 +1581,12 @@ const slice = createSlice({ } if (isSelected) { - canvas.selectedEntityIdentifier = entityIdentifier; + state.selectedEntityIdentifier = entityIdentifier; } }, entityBrushLineAdded: (state, action: PayloadAction) => { const { entityIdentifier, brushLine } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1697,9 +1601,7 @@ const slice = createSlice({ }, entityEraserLineAdded: (state, action: PayloadAction) => { const { entityIdentifier, eraserLine } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1714,9 +1616,7 @@ const slice = createSlice({ }, entityRectAdded: (state, action: PayloadAction) => { const { entityIdentifier, rect } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1729,8 +1629,7 @@ const slice = createSlice({ const { entityIdentifier } = action.payload; let selectedEntityIdentifier: CanvasState['selectedEntityIdentifier'] = null; - const canvas = getSelectedCanvas(state); - const allEntities = selectAllEntities(canvas); + const allEntities = selectAllEntities(state); const index = allEntities.findIndex((entity) => entity.id === entityIdentifier.id); const nextIndex = allEntities.length > 1 ? (index + 1) % allEntities.length : -1; if (nextIndex !== -1) { @@ -1742,96 +1641,84 @@ const slice = createSlice({ switch (entityIdentifier.type) { case 'raster_layer': - canvas.rasterLayers.entities = canvas.rasterLayers.entities.filter( - (layer) => layer.id !== entityIdentifier.id - ); + state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id); break; case 'control_layer': - canvas.controlLayers.entities = canvas.controlLayers.entities.filter((rg) => rg.id !== entityIdentifier.id); + state.controlLayers.entities = state.controlLayers.entities.filter((rg) => rg.id !== entityIdentifier.id); break; case 'regional_guidance': - canvas.regionalGuidance.entities = canvas.regionalGuidance.entities.filter( + state.regionalGuidance.entities = state.regionalGuidance.entities.filter( (rg) => rg.id !== entityIdentifier.id ); break; case 'inpaint_mask': - canvas.inpaintMasks.entities = canvas.inpaintMasks.entities.filter((rg) => rg.id !== entityIdentifier.id); + state.inpaintMasks.entities = state.inpaintMasks.entities.filter((rg) => rg.id !== entityIdentifier.id); break; } - canvas.selectedEntityIdentifier = selectedEntityIdentifier; + state.selectedEntityIdentifier = selectedEntityIdentifier; }, entityArrangedForwardOne: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } - moveOneToEnd(selectAllEntitiesOfType(canvas, entity.type), entity); + moveOneToEnd(selectAllEntitiesOfType(state, entity.type), entity); }, entityArrangedToFront: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } - moveToEnd(selectAllEntitiesOfType(canvas, entity.type), entity); + moveToEnd(selectAllEntitiesOfType(state, entity.type), entity); }, entityArrangedBackwardOne: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } - moveOneToStart(selectAllEntitiesOfType(canvas, entity.type), entity); + moveOneToStart(selectAllEntitiesOfType(state, entity.type), entity); }, entityArrangedToBack: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } - moveToStart(selectAllEntitiesOfType(canvas, entity.type), entity); + moveToStart(selectAllEntitiesOfType(state, entity.type), entity); }, entitiesReordered: ( - state: CanvasesState, + state: CanvasState, action: PayloadAction<{ type: T; entityIdentifiers: CanvasEntityIdentifier[] }> ) => { const { type, entityIdentifiers } = action.payload; - const canvas = getSelectedCanvas(state); - switch (type) { case 'raster_layer': { - canvas.rasterLayers.entities = reorderEntities( - canvas.rasterLayers.entities, + state.rasterLayers.entities = reorderEntities( + state.rasterLayers.entities, entityIdentifiers as CanvasEntityIdentifier<'raster_layer'>[] ); break; } case 'control_layer': - canvas.controlLayers.entities = reorderEntities( - canvas.controlLayers.entities, + state.controlLayers.entities = reorderEntities( + state.controlLayers.entities, entityIdentifiers as CanvasEntityIdentifier<'control_layer'>[] ); break; case 'inpaint_mask': - canvas.inpaintMasks.entities = reorderEntities( - canvas.inpaintMasks.entities, + state.inpaintMasks.entities = reorderEntities( + state.inpaintMasks.entities, entityIdentifiers as CanvasEntityIdentifier<'inpaint_mask'>[] ); break; case 'regional_guidance': - canvas.regionalGuidance.entities = reorderEntities( - canvas.regionalGuidance.entities, + state.regionalGuidance.entities = reorderEntities( + state.regionalGuidance.entities, entityIdentifiers as CanvasEntityIdentifier<'regional_guidance'>[] ); break; @@ -1839,9 +1726,7 @@ const slice = createSlice({ }, entityOpacityChanged: (state, action: PayloadAction>) => { const { entityIdentifier, opacity } = action.payload; - - const canvas = getSelectedCanvas(state); - const entity = selectEntity(canvas, entityIdentifier); + const entity = selectEntity(state, entityIdentifier); if (!entity) { return; } @@ -1850,51 +1735,45 @@ const slice = createSlice({ allEntitiesOfTypeIsHiddenToggled: (state, action: PayloadAction<{ type: CanvasEntityIdentifier['type'] }>) => { const { type } = action.payload; - const canvas = getSelectedCanvas(state); - switch (type) { case 'raster_layer': - canvas.rasterLayers.isHidden = !canvas.rasterLayers.isHidden; + state.rasterLayers.isHidden = !state.rasterLayers.isHidden; break; case 'control_layer': - canvas.controlLayers.isHidden = !canvas.controlLayers.isHidden; + state.controlLayers.isHidden = !state.controlLayers.isHidden; break; case 'inpaint_mask': - canvas.inpaintMasks.isHidden = !canvas.inpaintMasks.isHidden; + state.inpaintMasks.isHidden = !state.inpaintMasks.isHidden; break; case 'regional_guidance': - canvas.regionalGuidance.isHidden = !canvas.regionalGuidance.isHidden; + state.regionalGuidance.isHidden = !state.regionalGuidance.isHidden; break; } }, allNonRasterLayersIsHiddenToggled: (state) => { - const canvas = getSelectedCanvas(state); const hasVisibleNonRasterLayers = - !canvas.controlLayers.isHidden || !canvas.inpaintMasks.isHidden || !canvas.regionalGuidance.isHidden; + !state.controlLayers.isHidden || !state.inpaintMasks.isHidden || !state.regionalGuidance.isHidden; const shouldHide = hasVisibleNonRasterLayers; - canvas.controlLayers.isHidden = shouldHide; - canvas.inpaintMasks.isHidden = shouldHide; - canvas.regionalGuidance.isHidden = shouldHide; + state.controlLayers.isHidden = shouldHide; + state.inpaintMasks.isHidden = shouldHide; + state.regionalGuidance.isHidden = shouldHide; }, allEntitiesDeleted: (state) => { - const canvas = getSelectedCanvas(state); // Deleting all entities is equivalent to resetting the state for each entity type - const initialState = getCanvasState('dummyID', 'dummyName'); - canvas.rasterLayers = initialState.rasterLayers; - canvas.controlLayers = initialState.controlLayers; - canvas.inpaintMasks = initialState.inpaintMasks; - canvas.regionalGuidance = initialState.regionalGuidance; + const initialState = getInitialCanvasState('dummyId', 'dummyName'); + state.rasterLayers = initialState.rasterLayers; + state.controlLayers = initialState.controlLayers; + state.inpaintMasks = initialState.inpaintMasks; + state.regionalGuidance = initialState.regionalGuidance; }, canvasMetadataRecalled: (state, action: PayloadAction) => { const { controlLayers, inpaintMasks, rasterLayers, regionalGuidance } = action.payload; - - const canvas = getSelectedCanvas(state); - canvas.controlLayers.entities = controlLayers; - canvas.inpaintMasks.entities = inpaintMasks; - canvas.rasterLayers.entities = rasterLayers; - canvas.regionalGuidance.entities = regionalGuidance; + state.controlLayers.entities = controlLayers; + state.inpaintMasks.entities = inpaintMasks; + state.rasterLayers.entities = rasterLayers; + state.regionalGuidance.entities = regionalGuidance; return state; }, canvasUndo: () => {}, @@ -1903,11 +1782,9 @@ const slice = createSlice({ }, extraReducers(builder) { builder.addCase(canvasReset, (state) => { - const canvas = getSelectedCanvas(state); - resetCanvasState(canvas); + return resetCanvasState(state); }); builder.addCase(modelChanged, (state, action) => { - const canvas = getSelectedCanvas(state); const { model } = action.payload; /** * Because the bbox depends in part on the model, it needs to be in sync with the model. However, due to @@ -1928,24 +1805,24 @@ const slice = createSlice({ * - Provide a separate action that will update the bbox dimensions and be careful to not dispatch it when staging. */ const base = model?.base; - if (isMainModelBase(base) && canvas.bbox.modelBase !== base) { - canvas.bbox.modelBase = base; + if (isMainModelBase(base) && state.bbox.modelBase !== base) { + state.bbox.modelBase = base; if (API_BASE_MODELS.includes(base)) { - canvas.bbox.aspectRatio.isLocked = true; - canvas.bbox.aspectRatio.value = 1; - canvas.bbox.aspectRatio.id = '1:1'; - canvas.bbox.rect.width = 1024; - canvas.bbox.rect.height = 1024; + state.bbox.aspectRatio.isLocked = true; + state.bbox.aspectRatio.value = 1; + state.bbox.aspectRatio.id = '1:1'; + state.bbox.rect.width = 1024; + state.bbox.rect.height = 1024; } - syncScaledSize(canvas); + syncScaledSize(state); } }); }, }); const resetCanvasState = (state: CanvasState) => { - const newState = getCanvasState(state.id, state.name); + const newState = getInitialCanvasState(state.id, state.name); // 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. @@ -1962,19 +1839,41 @@ const resetCanvasState = (state: CanvasState) => { syncScaledSize(newState); }; -const getCanvasById = (state: CanvasesState, id: string) => state.canvases.find((canvas) => canvas.id === id); -const getSelectedCanvas = (state: CanvasesState) => getCanvasById(state, state.selectedCanvasId)!; +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 + ); + } +}; + +const getCanvasById = (state: CanvasesStateWithHistory, id: string) => + state.canvases.find((canvas) => canvas.present.id === id)?.present; export const { - canvasMetadataRecalled, - canvasUndo, - canvasRedo, - canvasClearHistory, // Canvas canvasAdded, canvasSelected, canvasNameChanged, canvasDeleted, +} = canvasesSlice.actions; + +export const { + canvasMetadataRecalled, + canvasUndo, + canvasRedo, + canvasClearHistory, // All entities entitySelected, bookmarkedEntityChanged, @@ -2066,37 +1965,20 @@ export const { inpaintMaskDenoiseLimitChanged, inpaintMaskDenoiseLimitDeleted, // inpaintMaskRecalled, -} = slice.actions; +} = canvasSlice.actions; -const syncScaledSize = (canvas: CanvasState) => { - if (API_BASE_MODELS.includes(canvas.bbox.modelBase)) { - // Imagen3 has fixed sizes. Scaled bbox is not supported. - return; - } - if (canvas.bbox.scaleMethod === 'auto') { - // Sync both aspect ratio and size - const { width, height } = canvas.bbox.rect; - canvas.bbox.scaledSize = getScaledBoundingBoxDimensions({ width, height }, canvas.bbox.modelBase); - } else if (canvas.bbox.scaleMethod === 'manual' && canvas.bbox.aspectRatio.isLocked) { - // Only sync the aspect ratio if manual & locked - canvas.bbox.scaledSize = calculateNewSize( - canvas.bbox.aspectRatio.value, - canvas.bbox.scaledSize.width * canvas.bbox.scaledSize.height, - canvas.bbox.modelBase - ); - } -}; +const isCanvasSliceAction = isAnyOf(...Object.values(canvasSlice.actions)); let filter = true; -const canvasUndoableConfig: UndoableOptions = { +const canvasUndoableConfig: 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(canvasSlice.name)) { return false; } // Throttle rapid actions of the same type @@ -2107,15 +1989,72 @@ const canvasUndoableConfig: UndoableOptions = { // debug: import.meta.env.MODE === 'development', }; -export const canvasSliceConfig: SliceConfig = { - slice, +const undoableCanvasReducer = undoable(canvasSlice.reducer, canvasUndoableConfig); + +export const undoableCanvasesReducer = ( + state: CanvasesStateWithHistory, + action: UnknownAction +): CanvasesStateWithHistory => { + state = canvasesSlice.reducer(state, action); + + if (!isCanvasSliceAction(action)) { + return state; + } + + return { + ...state, + canvases: state.canvases.map((c) => + c.present.id === state.selectedCanvasId ? undoableCanvasReducer(c, action) : c + ), + }; +}; + +export const canvasSliceConfig: SliceConfig< + typeof canvasesSlice, + CanvasesStateWithHistory, + CanvasesStateWithoutHistory +> = { + slice: canvasesSlice, getInitialState: getInitialCanvasesState, - schema: zCanvasesState, + schema: zCanvasesStateWithHistory, persistConfig: { - migrate: (state) => zCanvasesState.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 = 'default'; + + const canvas = { + id: canvasId, + name: canvasName, + ...state, + } as CanvasState; + + state = { + _version: 4, + selectedCanvasId: canvas.id, + canvases: [canvas], + }; + } + return zCanvasesStateWithoutHistory.parse(state); + }, + wrapState: (state) => { + const canvasesState = state as CanvasesStateWithoutHistory; + + return { + _version: canvasesState._version, + selectedCanvasId: canvasesState.selectedCanvasId, + canvases: canvasesState.canvases.map((canvas) => newHistory([], canvas, [])), + }; + }, + unwrapState: (state) => { + return { + _version: state._version, + selectedCanvasId: state.selectedCanvasId, + canvases: state.canvases.map((canvas) => canvas.present), + }; + }, }, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 2524e81e254..c05be8cfee9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -19,13 +19,13 @@ import { assert } from 'tsafe'; /** * Selects the canvas slice from the root state */ -const selectCanvasSlice = (state: RootState) => state.canvas.present; +const selectCanvasSlice = (state: RootState) => state.canvas; /** * Selects the canvases */ export const selectCanvases = createSelector(selectCanvasSlice, (state) => - state.canvases.map((canvas) => ({ + state.canvases.map(({ present: canvas }) => ({ ...canvas, isSelected: canvas.id === state.selectedCanvasId, canDelete: state.canvases.length > 1, @@ -35,11 +35,13 @@ export const selectCanvases = createSelector(selectCanvasSlice, (state) => /** * Selects the selected canvas */ -export const selectSelectedCanvas = createSelector( +const selectSelectedCanvasWithHistory = createSelector( selectCanvasSlice, - (state) => state.canvases.find((canvas) => canvas.id === state.selectedCanvasId)! + (state) => state.canvases.find(({ present: canvas }) => canvas.id === state.selectedCanvasId)! ); +export const selectSelectedCanvas = createSelector(selectSelectedCanvasWithHistory, (canvas) => canvas.present); + /** * Selects the total canvas entity count: * - Regions @@ -285,8 +287,11 @@ export const selectBookmarkedEntityIdentifier = createSelector( (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(selectSelectedCanvasWithHistory, (canvas) => canvas.past.length > 0); +export const selectCanvasMayRedo = createSelector( + selectSelectedCanvasWithHistory, + (canvas) => canvas.future.length > 0 +); export const selectSelectedEntityFill = createSelector( selectSelectedCanvas, selectSelectedEntityIdentifier, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 76416a111a9..c014e6b79c8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -801,6 +801,16 @@ const zRegionalGuidance = z.object({ isHidden: z.boolean(), entities: z.array(zCanvasRegionalGuidanceState), }); +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 zCanvasState = z.object({ id: zId, name: z.string().min(1), @@ -813,12 +823,17 @@ const zCanvasState = z.object({ bbox: zBboxState, }); export type CanvasState = z.infer; -export const zCanvasesState = z.object({ - _version: z.literal(3), - selectedCanvasId: zId, - canvases: z.array(zCanvasState), -}); -export type CanvasesState = z.infer; +const zCanvasStateWithHistory = zStateWithHistory(zCanvasState); +const zCanvasesState = (canvasStateSchema: T) => + z.object({ + _version: z.literal(4), + selectedCanvasId: zId, + canvases: z.array(canvasStateSchema), + }); +export const zCanvasesStateWithHistory = zCanvasesState(zCanvasStateWithHistory); +export type CanvasesStateWithHistory = z.infer; +export const zCanvasesStateWithoutHistory = zCanvasesState(zCanvasState); +export type CanvasesStateWithoutHistory = z.infer; export const zRefImagesState = z.object({ selectedEntityId: z.string().nullable(), isPanelOpen: z.boolean(), diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 20c27d2cd6e..8ac1f64d7e5 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, + wrapState: (state) => { + const nodesState = state as NodesState; + + return newHistory([], nodesState, []); + }, + unwrapState: (state) => state.present, }, }; diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx index ace7f38f35c..3539bc60a0b 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx @@ -17,9 +17,12 @@ export const CanvasTabEditableTitle = memo(({ id, name, isSelected }: CanvasTabE const isHovering = useBoolean(false); const inputRef = useRef(null); - const onChange = useCallback(() => { - dispatch(canvasNameChanged({ id, name })); - }, [dispatch, id, name]); + const onChange = useCallback( + (value: string) => { + dispatch(canvasNameChanged({ id, name: value })); + }, + [dispatch, id] + ); const editable = useEditable({ value: name, From dd7283555b6876eb8d3a294951a63e8f199c6aa0 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Thu, 18 Sep 2025 10:06:19 +0200 Subject: [PATCH 04/16] staging area multi-canvas support --- .../listeners/boardAndImagesDeleted.ts | 6 +- .../listeners/modelSelected.ts | 9 +- .../listeners/setDefaultSettings.ts | 9 +- invokeai/frontend/web/src/app/store/store.ts | 5 +- .../common/components/SessionMenuItems.tsx | 7 +- .../CanvasAlerts/CanvasAlertsPreserveMask.tsx | 2 +- .../CanvasAlertsSaveAllImagesToGallery.tsx | 2 +- .../components/CanvasAutoProcessSwitch.tsx | 2 +- ...vasOperationIsolatedLayerPreviewSwitch.tsx | 2 +- .../components/Filters/Filter.tsx | 2 +- .../components/InvokeCanvasComponent.tsx | 8 +- .../components/RefImage/RefImageImage.tsx | 2 +- .../components/RefImage/RefImageList.tsx | 4 +- .../RegionalGuidanceRefImageImage.tsx | 2 +- .../SelectObjectActionButtons.tsx | 2 +- .../CanvasSettingsBboxOverlaySwitch.tsx | 2 +- .../CanvasSettingsClipToBboxCheckbox.tsx | 9 +- .../CanvasSettingsDynamicGridSwitch.tsx | 2 +- .../Settings/CanvasSettingsGridSize.tsx | 2 +- .../CanvasSettingsInvertScrollCheckbox.tsx | 12 +- ...nvasSettingsIsolatedLayerPreviewSwitch.tsx | 2 +- ...asSettingsIsolatedStagingPreviewSwitch.tsx | 2 +- ...ettingsOutputOnlyMaskedRegionsCheckbox.tsx | 2 +- .../CanvasSettingsPreserveMaskCheckbox.tsx | 2 +- .../CanvasSettingsPressureSensitivity.tsx | 2 +- .../CanvasSettingsRuleOfThirdsGuideSwitch.tsx | 2 +- ...SettingsSaveAllImagesToGalleryCheckbox.tsx | 2 +- .../Settings/CanvasSettingsShowHUDSwitch.tsx | 7 +- ...nvasSettingsShowProgressOnCanvasSwitch.tsx | 2 +- .../StagingArea/QueueItemPreviewMini.tsx | 4 +- .../StagingAreaAutoSwitchButtons.tsx | 9 +- .../components/StagingArea/context.tsx | 46 ++- .../components/Tool/ToolFillColorPicker.tsx | 31 +- .../components/Tool/ToolWidthPicker.tsx | 19 +- .../CanvasInstanceContextProvider.tsx | 13 + .../contexts/CanvasManagerProviderGate.tsx | 30 +- .../controlLayers/hooks/useCanvasId.ts | 10 + .../controlLayers/hooks/useCanvasIsBusy.ts | 20 +- .../controlLayers/hooks/useCanvasIsStaging.ts | 12 + .../controlLayers/hooks/useCanvasSessionId.ts | 14 + .../controlLayers/hooks/useInvokeCanvas.ts | 19 +- .../controlLayers/konva/CanvasManager.ts | 15 +- .../konva/CanvasStateApiModule.ts | 16 +- .../konva/CanvasTool/CanvasToolModule.ts | 18 +- .../store/canvasSettingsSlice.ts | 302 +++++++++++++----- .../controlLayers/store/canvasSlice.ts | 86 +++-- .../store/canvasStagingAreaSlice.ts | 138 ++++++-- .../features/controlLayers/store/ephemeral.ts | 4 +- .../features/controlLayers/store/selectors.ts | 7 + .../src/features/controlLayers/store/types.ts | 5 + .../features/deleteImageModal/store/state.ts | 78 +++-- .../components/Boards/DeleteBoardModal.tsx | 8 +- ...ntextMenuItemNewCanvasFromImageSubMenu.tsx | 28 +- ...ontextMenuItemNewLayerFromImageSubMenu.tsx | 4 +- .../features/gallery/hooks/useEditImage.ts | 2 +- .../hooks/useRecallAllImageMetadata.ts | 2 +- .../gallery/hooks/useRecallDimensions.ts | 2 +- .../features/gallery/hooks/useRecallRemix.ts | 2 +- .../web/src/features/imageActions/actions.ts | 13 +- .../util/graph/generation/addFLUXFill.ts | 4 +- .../nodes/util/graph/generation/addInpaint.ts | 4 +- .../util/graph/generation/addOutpaint.ts | 4 +- .../util/graph/generation/buildFLUXGraph.ts | 8 +- .../util/graph/generation/buildSD1Graph.ts | 8 +- .../util/graph/generation/buildSDXLGraph.ts | 8 +- .../nodes/util/graph/graphBuilderUtils.ts | 9 +- .../components/Bbox/BboxAspectRatioSelect.tsx | 2 +- .../Bbox/BboxSwapDimensionsButton.tsx | 2 +- .../Bbox/use-is-bbox-size-locked.ts | 2 +- .../features/queue/hooks/useEnqueueCanvas.ts | 3 +- .../src/features/ui/layouts/CanvasTabs.tsx | 6 +- .../ui/layouts/CanvasWorkspacePanel.tsx | 151 +++++---- .../ui/layouts/DockviewTabCanvasWorkspace.tsx | 5 +- .../ui/layouts/LaunchpadEditImageButton.tsx | 2 +- .../LaunchpadGenerateFromTextButton.tsx | 2 +- .../LaunchpadUseALayoutImageButton.tsx | 2 +- .../src/features/ui/layouts/StagingArea.tsx | 2 +- 77 files changed, 827 insertions(+), 468 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsStaging.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts 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 23da5fd094c..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 { selectSelectedCanvas } 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 = selectSelectedCanvas(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 96a7a214713..96cbe27be46 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,7 +1,10 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/store'; import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice'; -import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { + buildSelectIsStagingBySessionId, + selectSelectedCanvasSessionId, +} from 'features/controlLayers/store/canvasStagingAreaSlice'; import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice'; @@ -159,7 +162,9 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = if (modelBase !== state.params.model?.base) { // Sync generate tab settings whenever the model base changes dispatch(syncedToOptimalDimension()); - const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state); + const sessionId = selectSelectedCanvasSessionId(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/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index f568bfe10c4..1550a1394ed 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,7 +1,10 @@ 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, + selectSelectedCanvasSessionId, +} from 'features/controlLayers/store/canvasStagingAreaSlice'; import { heightChanged, setCfgRescaleMultiplier, @@ -115,7 +118,9 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni } const setSizeOptions = { updateAspectRatio: true, clamp: true }; - const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state); + const sessionId = selectSelectedCanvasSessionId(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 1a7273355db..a9864938f2f 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -22,7 +22,7 @@ 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, undoableCanvasesReducer } from 'features/controlLayers/store/canvasSlice'; +import { canvasSliceConfig, migrateCanvas, undoableCanvasesReducer } 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'; @@ -219,8 +219,9 @@ export const createStore = (options?: { persist?: boolean; persistDebounce?: num // Once-off listener to support waiting for rehydration before rendering the app startAppListening({ actionCreator: createAction(REMEMBER_REHYDRATED), - effect: (action, { unsubscribe }) => { + effect: (action, { dispatch, unsubscribe }) => { unsubscribe(); + dispatch(migrateCanvas()); options?.onRehydrated?.(); }, }); diff --git a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx index 0018a78622c..952fa4a1f0b 100644 --- a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx +++ b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx @@ -1,7 +1,7 @@ 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 { memo, useCallback } from 'react'; @@ -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/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/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/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/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/InvokeCanvasComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx index 871b9e055f1..d425ebe7770 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx @@ -2,8 +2,12 @@ import { Box } from '@invoke-ai/ui-library'; import { useInvokeCanvas } from 'features/controlLayers/hooks/useInvokeCanvas'; import { memo } from 'react'; -export const InvokeCanvasComponent = memo(() => { - const ref = useInvokeCanvas(); +interface InvokeCanvasComponent { + canvasId: string; +} + +export const InvokeCanvasComponent = memo(({ canvasId }: InvokeCanvasComponent) => { + const ref = useInvokeCanvas(canvasId); return ( { const { t } = useTranslation(); - const isBusy = useCanvasIsBusySafe(); + const isBusy = useCanvasIsBusy(); const newGlobalReferenceImageFromBbox = useNewGlobalReferenceImageFromBbox(); return ( 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..46812f0ced5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx @@ -3,8 +3,8 @@ import { useStore } from '@nanostores/react'; import { skipToken } from '@reduxjs/toolkit/query'; import { 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 type { ImageWithDims } from 'features/controlLayers/store/types'; import type { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd'; 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..d6f677dd19c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx @@ -1,19 +1,16 @@ 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)), + (e: ChangeEvent) => dispatch(settingsClipToBboxChanged({ clipToBbox: e.target.checked })), [dispatch] ); return ( 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..018bff88be5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx @@ -1,26 +1,20 @@ 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)); + dispatch(settingsInvertScrollForToolWidthChanged({ invertScrollForToolWidth: e.target.checked })); }, [dispatch] ); 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..9a133183b68 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); @@ -55,7 +55,7 @@ export const QueueItemPreviewMini = memo(({ item, index }: Props) => { const onDoubleClick = useCallback(() => { if (autoSwitch !== 'off') { - dispatch(settingsStagingAreaAutoSwitchChanged('off')); + dispatch(settingsStagingAreaAutoSwitchChanged({ stagingAreaAutoSwitch: 'off' })); toast({ title: 'Auto-Switch Disabled', }); 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..849ee82e51b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx @@ -12,18 +12,17 @@ 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(() => { - dispatch(settingsStagingAreaAutoSwitchChanged('off')); + dispatch(settingsStagingAreaAutoSwitchChanged({ stagingAreaAutoSwitch: 'off' })); }, [dispatch]); const onClickSwitchOnStart = useCallback(() => { - dispatch(settingsStagingAreaAutoSwitchChanged('switch_on_start')); + dispatch(settingsStagingAreaAutoSwitchChanged({ stagingAreaAutoSwitch: 'switch_on_start' })); }, [dispatch]); const onClickSwitchOnFinished = useCallback(() => { - dispatch(settingsStagingAreaAutoSwitchChanged('switch_on_finish')); + dispatch(settingsStagingAreaAutoSwitchChanged({ stagingAreaAutoSwitch: 'switch_on_finish' })); }, [dispatch]); return ( 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..d5cb3fb2b0a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx @@ -1,12 +1,13 @@ import { useStore } from '@nanostores/react'; import { useAppStore } from 'app/store/storeHooks'; +import { useScopedCanvasSessionId } from 'features/controlLayers/hooks/useCanvasSessionId'; import { selectStagingAreaAutoSwitch, settingsStagingAreaAutoSwitchChanged, } from 'features/controlLayers/store/canvasSettingsSlice'; import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice'; import { - buildSelectCanvasQueueItems, + buildSelectCanvasQueueItemsBySessionId, canvasQueueItemDiscarded, canvasSessionReset, } from 'features/controlLayers/store/canvasStagingAreaSlice'; @@ -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(({ canvasId, children }: PropsWithChildren<{ canvasId: string }>) => { const store = useAppStore(); const socket = useStore($socket); - const stagingAreaAppApi = useMemo(() => { - const selectQueueItems = buildSelectCanvasQueueItems(sessionId); + const sessionId = useScopedCanvasSessionId(canvasId); + 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); @@ -58,16 +62,18 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi }); }, onDiscard: ({ item_id, status }) => { - store.dispatch(canvasQueueItemDiscarded({ itemId: item_id })); + store.dispatch(canvasQueueItemDiscarded({ canvasId, itemId: item_id })); if (status === 'in_progress' || status === 'pending') { store.dispatch(queueApi.endpoints.cancelQueueItem.initiate({ item_id }, { track: false })); } }, onDiscardAll: () => { - store.dispatch(canvasSessionReset()); - store.dispatch( - queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false }) - ); + store.dispatch(canvasSessionReset({ canvasId })); + if (sessionId) { + store.dispatch( + queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false }) + ); + } }, onAccept: (item, imageDTO) => { const bboxRect = selectBboxRect(store.getState()); @@ -80,22 +86,30 @@ 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 }) - ); + store.dispatch(canvasSessionReset({ canvasId })); + if (sessionId) { + store.dispatch( + queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false }) + ); + } }, onAutoSwitchChange: (mode) => { - store.dispatch(settingsStagingAreaAutoSwitchChanged(mode)); + store.dispatch(settingsStagingAreaAutoSwitchChanged({ stagingAreaAutoSwitch: mode })); }, }; return _stagingAreaAppApi; - }, [sessionId, socket, store]); + }, [canvasId, 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/Tool/ToolFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx index c192687e2e9..e60abfab74e 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,14 @@ 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 { useCanvasId } from 'features/controlLayers/hooks/useCanvasId'; import { - selectCanvasSettingsSlice, + selectActiveColor, + selectBgColor, + selectFgColor, settingsActiveColorToggled, settingsBgColorChanged, settingsColorsSetToDefault, @@ -25,15 +27,12 @@ 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 canvasId = useCanvasId(); + const activeColorType = useAppSelector((state) => selectActiveColor(state, canvasId)); + const bgColor = useAppSelector((state) => selectBgColor(state, canvasId)); + const fgColor = useAppSelector((state) => selectFgColor(state, canvasId)); const { activeColor, tooltip, bgColorzIndex, fgColorzIndex } = useMemo(() => { if (activeColorType === 'bgColor') { return { activeColor: bgColor, tooltip: t('controlLayers.fill.bgFillColor'), bgColorzIndex: 2, fgColorzIndex: 1 }; @@ -45,28 +44,28 @@ export const ToolFillColorPicker = memo(() => { const onColorChange = useCallback( (color: RgbaColor) => { if (activeColorType === 'bgColor') { - dispatch(settingsBgColorChanged(color)); + dispatch(settingsBgColorChanged({ canvasId, bgColor: color })); } else { - dispatch(settingsFgColorChanged(color)); + dispatch(settingsFgColorChanged({ canvasId, fgColor: color })); } }, - [activeColorType, dispatch] + [activeColorType, canvasId, dispatch] ); useRegisteredHotkeys({ id: 'setFillColorsToDefault', category: 'canvas', - callback: () => dispatch(settingsColorsSetToDefault()), + callback: () => dispatch(settingsColorsSetToDefault({ canvasId })), options: { preventDefault: true }, - dependencies: [dispatch], + dependencies: [canvasId, dispatch], }); useRegisteredHotkeys({ id: 'toggleFillColor', category: 'canvas', - callback: () => dispatch(settingsActiveColorToggled()), + callback: () => dispatch(settingsActiveColorToggled({ canvasId })), options: { preventDefault: true }, - dependencies: [dispatch], + dependencies: [canvasId, dispatch], }); return ( 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..e3da696f91f 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,12 @@ 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 { useCanvasId } from 'features/controlLayers/hooks/useCanvasId'; import { - selectCanvasSettingsSlice, + selectBrushWidth, + selectEraserWidth, settingsBrushWidthChanged, settingsEraserWidthChanged, } from 'features/controlLayers/store/canvasSettingsSlice'; @@ -180,19 +181,17 @@ 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(); + const canvasId = useCanvasId(); const isBrushSelected = useToolIsSelected('brush'); const isEraserSelected = useToolIsSelected('eraser'); const isToolSelected = useMemo(() => { return isBrushSelected || isEraserSelected; }, [isBrushSelected, isEraserSelected]); - const brushWidth = useAppSelector(selectBrushWidth); - const eraserWidth = useAppSelector(selectEraserWidth); + const brushWidth = useAppSelector((state) => selectBrushWidth(state, canvasId)); + const eraserWidth = useAppSelector((state) => selectEraserWidth(state, canvasId)); const width = useMemo(() => { if (isBrushSelected) { return brushWidth; @@ -229,12 +228,12 @@ export const ToolWidthPicker = memo(() => { const onValueChange = useCallback( (value: number) => { if (isBrushSelected) { - dispatch(settingsBrushWidthChanged(value)); + dispatch(settingsBrushWidthChanged({ canvasId, brushWidth: value })); } else if (isEraserSelected) { - dispatch(settingsEraserWidthChanged(value)); + dispatch(settingsEraserWidthChanged({ canvasId, eraserWidth: value })); } }, - [isBrushSelected, isEraserSelected, dispatch] + [isBrushSelected, isEraserSelected, canvasId, dispatch] ); const onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx new file mode 100644 index 00000000000..4b3a6ecd8e8 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx @@ -0,0 +1,13 @@ +import type { PropsWithChildren } from 'react'; +import { createContext, memo, useContext } from 'react'; + +const CanvasInstanceContext = createContext(null); + +export const CanvasInstanceContextProvider = memo(({ canvasId, children }: PropsWithChildren<{ canvasId: string }>) => { + return {children}; +}); +CanvasInstanceContextProvider.displayName = 'CanvasInstanceContextProvider'; + +export const useScopedCanvasIdSafe = () => { + return useContext(CanvasInstanceContext); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx index ca3528c7a0e..3dd30284ae3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx @@ -1,31 +1,35 @@ import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useCanvasId } from 'features/controlLayers/hooks/useCanvasId'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { $canvasManager } from 'features/controlLayers/store/ephemeral'; +import { $canvasManagers } from 'features/controlLayers/store/ephemeral'; +import { selectSelectedCanvasId } 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(selectSelectedCanvasId); - 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 +37,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 = useCanvasId(); + + return canvasManagers[canvasId] ?? null; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts new file mode 100644 index 00000000000..65a75009489 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts @@ -0,0 +1,10 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { useScopedCanvasIdSafe } from 'features/controlLayers/contexts/CanvasInstanceContextProvider'; +import { selectSelectedCanvasId } from 'features/controlLayers/store/selectors'; + +export const useCanvasId = () => { + const scopedCanvasId = useScopedCanvasIdSafe(); + const canvasId = useAppSelector(selectSelectedCanvasId); + + return scopedCanvasId ?? canvasId; +}; 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..c1c2e47a2ad --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsStaging.ts @@ -0,0 +1,12 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { buildSelectIsStagingBySessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useMemo } from 'react'; + +import { useCanvasSessionId } from './useCanvasSessionId'; + +export const useCanvasIsStaging = () => { + const sessionId = useCanvasSessionId(); + const selectIsStagingBySessionIdSelector = useMemo(() => buildSelectIsStagingBySessionId(sessionId), [sessionId]); + + return useAppSelector(selectIsStagingBySessionIdSelector); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts new file mode 100644 index 00000000000..30208347431 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts @@ -0,0 +1,14 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; + +import { useCanvasId } from './useCanvasId'; + +export const useCanvasSessionId = () => { + const canvasId = useCanvasId(); + + return useAppSelector((state) => selectCanvasSessionId(state, canvasId)); +}; + +export const useScopedCanvasSessionId = (canvasId: string) => { + return useAppSelector((state) => selectCanvasSessionId(state, canvasId)); +}; 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/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 5f82c2ff1bc..e73e35af0f4 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, + buildSelectCanvasSettingsByCanvasId, settingsBgColorChanged, settingsBrushWidthChanged, settingsEraserWidthChanged, @@ -29,7 +29,7 @@ import { rasterLayerAdded, rgAdded, } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectCanvasSessionByCanvasId } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { selectAllRenderableEntities, selectBbox, @@ -219,14 +219,14 @@ export class CanvasStateApiModule extends CanvasModuleBase { * Sets the brush width, pushing state to redux. */ setBrushWidth = (width: number) => { - this.store.dispatch(settingsBrushWidthChanged(width)); + this.store.dispatch(settingsBrushWidthChanged({ canvasId: this.manager.canvasId, brushWidth: width })); }; /** * Sets the eraser width, pushing state to redux. */ setEraserWidth = (width: number) => { - this.store.dispatch(settingsEraserWidthChanged(width)); + this.store.dispatch(settingsEraserWidthChanged({ canvasId: this.manager.canvasId, eraserWidth: width })); }; /** @@ -234,8 +234,8 @@ export class CanvasStateApiModule extends CanvasModuleBase { */ setColor = (color: Partial) => { return this.getSettings().activeColor === 'bgColor' - ? this.store.dispatch(settingsBgColorChanged(color)) - : this.store.dispatch(settingsFgColorChanged(color)); + ? this.store.dispatch(settingsBgColorChanged({ canvasId: this.manager.canvasId, bgColor: color })) + : this.store.dispatch(settingsFgColorChanged({ canvasId: this.manager.canvasId, fgColor: color })); }; /** @@ -312,7 +312,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { * Gets the canvas settings from redux. */ getSettings = () => { - return this.runSelector(selectCanvasSettingsSlice); + return this.runSelector(buildSelectCanvasSettingsByCanvasId(this.manager.canvasId)); }; /** @@ -371,7 +371,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { * Gets the canvas staging area state from redux. */ getStagingArea = () => { - return this.runSelector(selectCanvasSessionSlice); + return this.runSelector((state) => selectCanvasSessionByCanvasId(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 779109b8ac9..bcfa2e07136 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 { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectCanvasById } 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(selectSelectedCanvas, this.render)); + this.subscriptions.add( + this.manager.stateApi.createStoreSubscription( + (state) => selectCanvasById(state, this.manager.canvasId), + this.render + ) + ); + this.subscriptions.add( + this.manager.stateApi.createStoreSubscription( + buildSelectCanvasSettingsByCanvasId(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/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index bbeac05a1d2..bfdda00af5b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -2,14 +2,23 @@ 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 { isPlainObject } from 'es-toolkit'; import type { RgbaColor } from 'features/controlLayers/store/types'; import { RGBA_BLACK, RGBA_WHITE, zRgbaColor } from 'features/controlLayers/store/types'; +import { assert } from 'tsafe'; import { z } from 'zod'; +import { + canvasCreated, + canvasMultiCanvasMigrated, + canvasRemoved, + MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, +} from './canvasSlice'; + const zAutoSwitchMode = z.enum(['off', 'switch_on_start', 'switch_on_finish']); export type AutoSwitchMode = z.infer; -const zCanvasSettingsState = z.object({ +const zCanvasSharedSettingsState = z.object({ /** * Whether to show HUD (Heads-Up Display) on the canvas. */ @@ -27,20 +36,6 @@ const zCanvasSettingsState = z.object({ * 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. * @@ -94,18 +89,39 @@ const zCanvasSettingsState = z.object({ */ stagingAreaAutoSwitch: zAutoSwitchMode, }); +type CanvasSharedSettingsState = z.infer; +const zCanvasInstanceSettingsState = z.object({ + canvasId: z.string(), + /** + * 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, +}); +type CanvasInstanceSettingsState = z.infer; + +const zCanvasSettingsState = z.object({ + _version: z.literal(1), + shared: zCanvasSharedSettingsState, + canvases: z.array(zCanvasInstanceSettingsState), +}); type CanvasSettingsState = z.infer; -const getInitialState = (): CanvasSettingsState => ({ + +const getInitialCanvasSharedSettingsState = (): CanvasSharedSettingsState => ({ showHUD: true, clipToBbox: false, dynamicGrid: false, invertScrollForToolWidth: false, - brushWidth: 50, - eraserWidth: 50, - activeColor: 'fgColor', - bgColor: RGBA_BLACK, - fgColor: RGBA_WHITE, outputOnlyMaskedRegions: true, autoProcess: true, snapToGrid: true, @@ -119,85 +135,162 @@ const getInitialState = (): CanvasSettingsState => ({ saveAllImagesToGallery: false, stagingAreaAutoSwitch: 'switch_on_start', }); +const getInitialCanvasInstanceSettingsState = (canvasId: string): CanvasInstanceSettingsState => ({ + canvasId, + brushWidth: 50, + eraserWidth: 50, + activeColor: 'fgColor', + bgColor: RGBA_BLACK, + fgColor: RGBA_WHITE, +}); +const getInitialState = (): CanvasSettingsState => ({ + _version: 1, + shared: getInitialCanvasSharedSettingsState(), + canvases: [], +}); + +type CanvasPayload = { canvasId: string } & T; +type CanvasPayloadAction = PayloadAction>; const slice = createSlice({ name: 'canvasSettings', initialState: getInitialState(), reducers: { - settingsClipToBboxChanged: (state, action: PayloadAction) => { - state.clipToBbox = action.payload; + settingsClipToBboxChanged: (state, action: PayloadAction<{ clipToBbox: boolean }>) => { + const { clipToBbox } = action.payload; + + state.shared.clipToBbox = clipToBbox; }, settingsDynamicGridToggled: (state) => { - state.dynamicGrid = !state.dynamicGrid; + state.shared.dynamicGrid = !state.shared.dynamicGrid; }, settingsShowHUDToggled: (state) => { - state.showHUD = !state.showHUD; + state.shared.showHUD = !state.shared.showHUD; }, - settingsBrushWidthChanged: (state, action: PayloadAction) => { - state.brushWidth = Math.round(action.payload); + settingsBrushWidthChanged: (state, action: CanvasPayloadAction<{ brushWidth: number }>) => { + const { canvasId, brushWidth } = action.payload; + + const settings = state.canvases.find((settings) => settings.canvasId === canvasId); + if (!settings) { + return; + } + + settings.brushWidth = Math.round(brushWidth); }, - settingsEraserWidthChanged: (state, action: PayloadAction) => { - state.eraserWidth = Math.round(action.payload); + settingsEraserWidthChanged: (state, action: CanvasPayloadAction<{ eraserWidth: number }>) => { + const { canvasId, eraserWidth } = action.payload; + + const settings = state.canvases.find((settings) => settings.canvasId === canvasId); + if (!settings) { + return; + } + + settings.eraserWidth = Math.round(eraserWidth); }, - settingsActiveColorToggled: (state) => { - state.activeColor = state.activeColor === 'bgColor' ? 'fgColor' : 'bgColor'; + settingsActiveColorToggled: (state, action: CanvasPayloadAction) => { + const { canvasId } = action.payload; + + const settings = state.canvases.find((settings) => settings.canvasId === canvasId); + if (!settings) { + return; + } + + settings.activeColor = settings.activeColor === 'bgColor' ? 'fgColor' : 'bgColor'; }, - settingsBgColorChanged: (state, action: PayloadAction>) => { - state.bgColor = { ...state.bgColor, ...action.payload }; + settingsBgColorChanged: (state, action: CanvasPayloadAction<{ bgColor: Partial }>) => { + const { canvasId, bgColor } = action.payload; + + const settings = state.canvases.find((settings) => settings.canvasId === canvasId); + if (!settings) { + return; + } + + settings.bgColor = { ...settings.bgColor, ...bgColor }; }, - settingsFgColorChanged: (state, action: PayloadAction>) => { - state.fgColor = { ...state.fgColor, ...action.payload }; + settingsFgColorChanged: (state, action: CanvasPayloadAction<{ fgColor: Partial }>) => { + const { canvasId, fgColor } = action.payload; + + const settings = state.canvases.find((settings) => settings.canvasId === canvasId); + if (!settings) { + return; + } + + settings.fgColor = { ...settings.fgColor, ...fgColor }; }, - settingsColorsSetToDefault: (state) => { - state.bgColor = RGBA_BLACK; - state.fgColor = RGBA_WHITE; + settingsColorsSetToDefault: (state, action: CanvasPayloadAction) => { + const { canvasId } = action.payload; + + const settings = state.canvases.find((settings) => settings.canvasId === canvasId); + if (!settings) { + return; + } + + settings.bgColor = RGBA_BLACK; + settings.fgColor = RGBA_WHITE; }, - settingsInvertScrollForToolWidthChanged: ( - state, - action: PayloadAction - ) => { - state.invertScrollForToolWidth = action.payload; + settingsInvertScrollForToolWidthChanged: (state, action: PayloadAction<{ invertScrollForToolWidth: boolean }>) => { + const { invertScrollForToolWidth } = action.payload; + + state.shared.invertScrollForToolWidth = invertScrollForToolWidth; }, settingsOutputOnlyMaskedRegionsToggled: (state) => { - state.outputOnlyMaskedRegions = !state.outputOnlyMaskedRegions; + state.shared.outputOnlyMaskedRegions = !state.shared.outputOnlyMaskedRegions; }, settingsAutoProcessToggled: (state) => { - state.autoProcess = !state.autoProcess; + state.shared.autoProcess = !state.shared.autoProcess; }, settingsSnapToGridToggled: (state) => { - state.snapToGrid = !state.snapToGrid; + state.shared.snapToGrid = !state.shared.snapToGrid; }, settingsShowProgressOnCanvasToggled: (state) => { - state.showProgressOnCanvas = !state.showProgressOnCanvas; + state.shared.showProgressOnCanvas = !state.shared.showProgressOnCanvas; }, settingsBboxOverlayToggled: (state) => { - state.bboxOverlay = !state.bboxOverlay; + state.shared.bboxOverlay = !state.shared.bboxOverlay; }, settingsPreserveMaskToggled: (state) => { - state.preserveMask = !state.preserveMask; + state.shared.preserveMask = !state.shared.preserveMask; }, settingsIsolatedStagingPreviewToggled: (state) => { - state.isolatedStagingPreview = !state.isolatedStagingPreview; + state.shared.isolatedStagingPreview = !state.shared.isolatedStagingPreview; }, settingsIsolatedLayerPreviewToggled: (state) => { - state.isolatedLayerPreview = !state.isolatedLayerPreview; + state.shared.isolatedLayerPreview = !state.shared.isolatedLayerPreview; }, settingsPressureSensitivityToggled: (state) => { - state.pressureSensitivity = !state.pressureSensitivity; + state.shared.pressureSensitivity = !state.shared.pressureSensitivity; }, settingsRuleOfThirdsToggled: (state) => { - state.ruleOfThirds = !state.ruleOfThirds; + state.shared.ruleOfThirds = !state.shared.ruleOfThirds; }, settingsSaveAllImagesToGalleryToggled: (state) => { - state.saveAllImagesToGallery = !state.saveAllImagesToGallery; + state.shared.saveAllImagesToGallery = !state.shared.saveAllImagesToGallery; }, settingsStagingAreaAutoSwitchChanged: ( state, - action: PayloadAction + action: PayloadAction<{ stagingAreaAutoSwitch: CanvasSharedSettingsState['stagingAreaAutoSwitch'] }> ) => { - state.stagingAreaAutoSwitch = action.payload; + const { stagingAreaAutoSwitch } = action.payload; + + state.shared.stagingAreaAutoSwitch = stagingAreaAutoSwitch; }, }, + extraReducers(builder) { + builder.addCase(canvasCreated, (state, action) => { + const canvasSettings = getInitialCanvasInstanceSettingsState(action.payload.id); + state.canvases.push(canvasSettings); + }); + builder.addCase(canvasRemoved, (state, action) => { + state.canvases = state.canvases.filter((settings) => settings.canvasId !== action.payload.id); + }); + builder.addCase(canvasMultiCanvasMigrated, (state, action) => { + const settings = state.canvases.find((settings) => settings.canvasId === MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER); + if (!settings) { + return; + } + settings.canvasId = action.payload.id; + }); + }, }); export const { @@ -230,29 +323,88 @@ export const canvasSettingsSliceConfig: SliceConfig = { schema: zCanvasSettingsState, getInitialState, persistConfig: { - migrate: (state) => zCanvasSettingsState.parse(state), + migrate: (state) => { + assert(isPlainObject(state)); + if (!('_version' in state)) { + // Migrate from v1: slice represented a canvas settings instance -> slice represents multiple canvas settings instances + const canvas = { + canvasId: MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, + ...state, + } as CanvasInstanceSettingsState; + + state = { + _version: 1, + shared: { + ...state, + }, + canvases: [canvas], + }; + } + + return zCanvasSettingsState.parse(state); + }, }, }; -export const selectCanvasSettingsSlice = (s: RootState) => s.canvasSettings; -const createCanvasSettingsSelector = (selector: Selector) => - createSelector(selectCanvasSettingsSlice, selector); +export const buildSelectCanvasSettingsByCanvasId = (canvasId: string) => + createSelector( + selectCanvasSharedSettings, + (state: RootState) => selectCanvasInstanceSettings(state, canvasId), + (sharedSettings, instanceSettings) => { + return { + ...sharedSettings, + ...instanceSettings, + }; + } + ); +const selectCanvasSharedSettings = (state: RootState) => state.canvasSettings.shared; +const selectCanvasInstanceSettings = (state: RootState, canvasId: string) => { + const settings = state.canvasSettings.canvases.find((settings) => settings.canvasId === canvasId); + assert(settings, 'Settings must exist for a canvas once the canvas has been created'); + return settings; +}; -export const selectPreserveMask = createCanvasSettingsSelector((settings) => settings.preserveMask); -export const selectOutputOnlyMaskedRegions = createCanvasSettingsSelector( +const buildCanvasSharedSettingsSelector = + (selector: Selector) => + (state: RootState) => + selector(selectCanvasSharedSettings(state)); +const buildCanvasInstanceSettingsSelector = + (selector: Selector) => + (state: RootState, canvasId: string) => + selector(selectCanvasInstanceSettings(state, canvasId)); + +export const selectPreserveMask = buildCanvasSharedSettingsSelector((settings) => settings.preserveMask); +export const selectOutputOnlyMaskedRegions = buildCanvasSharedSettingsSelector( (settings) => settings.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 = buildCanvasSharedSettingsSelector((settings) => settings.dynamicGrid); +export const selectInvertScrollForToolWidth = buildCanvasSharedSettingsSelector( + (settings) => settings.invertScrollForToolWidth +); +export const selectBboxOverlay = buildCanvasSharedSettingsSelector((settings) => settings.bboxOverlay); +export const selectShowHUD = buildCanvasSharedSettingsSelector((settings) => settings.showHUD); +export const selectClipToBbox = buildCanvasSharedSettingsSelector((settings) => settings.clipToBbox); +export const selectAutoProcess = buildCanvasSharedSettingsSelector((settings) => settings.autoProcess); +export const selectSnapToGrid = buildCanvasSharedSettingsSelector((settings) => settings.snapToGrid); +export const selectShowProgressOnCanvas = buildCanvasSharedSettingsSelector( + (settings) => settings.showProgressOnCanvas +); +export const selectIsolatedStagingPreview = buildCanvasSharedSettingsSelector( + (settings) => settings.isolatedStagingPreview +); +export const selectIsolatedLayerPreview = buildCanvasSharedSettingsSelector( + (settings) => settings.isolatedLayerPreview +); +export const selectPressureSensitivity = buildCanvasSharedSettingsSelector((settings) => settings.pressureSensitivity); +export const selectRuleOfThirds = buildCanvasSharedSettingsSelector((settings) => settings.ruleOfThirds); +export const selectSaveAllImagesToGallery = buildCanvasSharedSettingsSelector( + (settings) => settings.saveAllImagesToGallery +); +export const selectStagingAreaAutoSwitch = buildCanvasSharedSettingsSelector( + (settings) => settings.stagingAreaAutoSwitch ); -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 selectActiveColor = buildCanvasInstanceSettingsSelector((settings) => settings.activeColor); +export const selectBgColor = buildCanvasInstanceSettingsSelector((settings) => settings.bgColor); +export const selectFgColor = buildCanvasInstanceSettingsSelector((settings) => settings.fgColor); +export const selectBrushWidth = buildCanvasInstanceSettingsSelector((settings) => settings.brushWidth); +export const selectEraserWidth = buildCanvasInstanceSettingsSelector((settings) => settings.eraserWidth); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 5a8a421daa2..594cd1d4c3a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -1,5 +1,6 @@ -import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; +import type { CaseReducer, PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; +import type { AppDispatch, RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; @@ -22,6 +23,7 @@ import type { CanvasesStateWithoutHistory, CanvasInpaintMaskState, CanvasMetadata, + CanvasStateWithHistory, ChannelName, ChannelPoints, ControlLoRAConfig, @@ -135,7 +137,7 @@ const getInitialCanvasHistoryState = (id: string, name: string): StateWithHistor const getInitialCanvasesState = (): CanvasesStateWithoutHistory => { const canvasId = getPrefixedId('canvas'); - const canvasName = 'default'; + const canvasName = getNextCanvasName([]); const canvas = getInitialCanvasState(canvasId, canvasName); return { @@ -154,6 +156,15 @@ const getInitialCanvasesHistoryState = (): CanvasesStateWithHistory => { }; }; +const getNextCanvasName = (canvases: CanvasStateWithHistory[]): string => { + for (let i = 1; ; i++) { + const name = `Canvas-${i}`; + if (!canvases.some((c) => c.present.name === name)) { + return name; + } + } +}; + const canvasesSlice = createSlice({ name: 'canvas', initialState: getInitialCanvasesHistoryState(), @@ -162,7 +173,7 @@ const canvasesSlice = createSlice({ reducer: (state, action: PayloadAction<{ id: string; isSelected?: boolean }>) => { const { id, isSelected } = action.payload; - const name = 'default'; + const name = getNextCanvasName(state.canvases); const canvas = getInitialCanvasHistoryState(id, name); state.canvases.push(canvas); @@ -176,26 +187,21 @@ const canvasesSlice = createSlice({ }; }, }, + canvasCreated: (_state, _action: PayloadAction<{ id: string }>) => {}, + canvasMigrated: (state) => { + delete state.migration; + }, + canvasMultiCanvasMigrated: (_state, _action: PayloadAction<{ id: string }>) => {}, canvasSelected: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const canvas = getCanvasById(state, id); + const canvas = state.canvases.find((canvas) => canvas.present.id === id)?.present; if (!canvas) { return; } state.selectedCanvasId = canvas.id; }, - canvasNameChanged: (state, action: PayloadAction<{ id: string; name: string }>) => { - const { id, name } = action.payload; - - const canvas = getCanvasById(state, id); - if (!canvas) { - return; - } - - canvas.name = name; - }, canvasDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; @@ -209,13 +215,22 @@ const canvasesSlice = createSlice({ state.selectedCanvasId = state.canvases[nextIndex]!.present.id; state.canvases = state.canvases.filter((canvas) => canvas.present.id !== id); }, + canvasRemoved: (_state, _action: PayloadAction<{ id: string }>) => {}, }, }); +type WithId

= P & { id: string }; +type IdCaseReducer = CaseReducer>>; + const canvasSlice = createSlice({ name: 'canvas', initialState: {} as CanvasState, reducers: { + canvasNameChanged: ((state, action: PayloadAction<{ name: string }>) => { + const { name } = action.payload; + + state.name = name; + }) as IdCaseReducer, //#region Raster layers rasterLayerAdjustmentsSet: ( state, @@ -1858,18 +1873,41 @@ const syncScaledSize = (state: CanvasState) => { } }; -const getCanvasById = (state: CanvasesStateWithHistory, id: string) => - state.canvases.find((canvas) => canvas.present.id === id)?.present; +export const addCanvas = (payload: { isSelected?: boolean }) => (dispatch: AppDispatch) => { + const action = canvasesSlice.actions.canvasAdded(payload); + dispatch(action); + + const { id } = action.payload; + dispatch(canvasesSlice.actions.canvasCreated({ id })); +}; + +export const MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER = 'multi-canvas-id-placeholder'; + +export const migrateCanvas = () => (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + + if (state.canvas.migration?.isMultiCanvasMigrationPending) { + dispatch(canvasesSlice.actions.canvasMultiCanvasMigrated({ id: state.canvas.canvases[0]!.present.id })); + } + + dispatch(canvasesSlice.actions.canvasMigrated()); +}; + +export const deleteCanvas = (payload: { id: string }) => (dispatch: AppDispatch) => { + dispatch(canvasesSlice.actions.canvasDeleted(payload)); + dispatch(canvasesSlice.actions.canvasRemoved(payload)); +}; export const { // Canvas - canvasAdded, + canvasCreated, + canvasMultiCanvasMigrated, + canvasRemoved, canvasSelected, - canvasNameChanged, - canvasDeleted, } = canvasesSlice.actions; export const { + canvasNameChanged, canvasMetadataRecalled, canvasUndo, canvasRedo, @@ -1970,6 +2008,7 @@ export const { const isCanvasSliceAction = isAnyOf(...Object.values(canvasSlice.actions)); let filter = true; +const isActionFileterd = isAnyOf(canvasNameChanged, entitySelected); const canvasUndoableConfig: UndoableOptions = { limit: 64, @@ -1978,7 +2017,7 @@ const canvasUndoableConfig: UndoableOptions = { clearHistoryType: canvasClearHistory.type, filter: (action, _state, _history) => { // Ignore both all actions from other slices and canvas management actions - if (!action.type.startsWith(canvasSlice.name)) { + if (!action.type.startsWith(canvasSlice.name) || isActionFileterd(action)) { return false; } // Throttle rapid actions of the same type @@ -2023,7 +2062,7 @@ export const canvasSliceConfig: SliceConfig< 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 = 'default'; + const canvasName = getNextCanvasName([]); const canvas = { id: canvasId, @@ -2035,6 +2074,9 @@ export const canvasSliceConfig: SliceConfig< _version: 4, selectedCanvasId: canvas.id, canvases: [canvas], + migration: { + isMultiCanvasMigrationPending: true, + }, }; } return zCanvasesStateWithoutHistory.parse(state); @@ -2046,6 +2088,7 @@ export const canvasSliceConfig: SliceConfig< _version: canvasesState._version, selectedCanvasId: canvasesState.selectedCanvasId, canvases: canvasesState.canvases.map((canvas) => newHistory([], canvas, [])), + migration: canvasesState.migration, }; }, unwrapState: (state) => { @@ -2053,6 +2096,7 @@ export const canvasSliceConfig: SliceConfig< _version: state._version, selectedCanvasId: state.selectedCanvasId, canvases: state.canvases.map((canvas) => canvas.present), + migration: state.migration, }; }, }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 694abcda1c6..881e2bf7266 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -1,53 +1,101 @@ import { createSelector, createSlice, type PayloadAction } 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), +import { + canvasCreated, + canvasMultiCanvasMigrated, + canvasRemoved, + MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, +} from './canvasSlice'; +import { selectSelectedCanvasId } from './selectors'; + +const zCanvasSessionState = z.object({ + canvasId: z.string(), canvasSessionId: z.string(), canvasDiscardedQueueItems: z.array(z.number().int()), }); +type CanvasSessionState = z.infer; +const zCanvasStagingAreaState = z.object({ + _version: z.literal(2), + sessions: z.array(zCanvasSessionState), +}); type CanvasStagingAreaState = z.infer; -const getInitialState = (): CanvasStagingAreaState => ({ - _version: 1, +type CanvasPayload = { canvasId: string } & T; +type CanvasPayloadAction = PayloadAction>; + +const getInitialCanvasSessionState = (canvasId: string): CanvasSessionState => ({ + canvasId, canvasSessionId: getPrefixedId('canvas'), canvasDiscardedQueueItems: [], }); +const getInitialState = (): CanvasStagingAreaState => ({ + _version: 2, + sessions: [], +}); + const slice = createSlice({ name: 'canvasSession', initialState: getInitialState(), reducers: { - canvasQueueItemDiscarded: (state, action: PayloadAction<{ itemId: number }>) => { - const { itemId } = action.payload; - if (!state.canvasDiscardedQueueItems.includes(itemId)) { - state.canvasDiscardedQueueItems.push(itemId); + canvasQueueItemDiscarded: (state, action: CanvasPayloadAction<{ itemId: number }>) => { + const { canvasId, itemId } = action.payload; + + const session = state.sessions.find((session) => session.canvasId === canvasId); + if (!session) { + return; + } + + if (!session.canvasDiscardedQueueItems.includes(itemId)) { + session.canvasDiscardedQueueItems.push(itemId); } }, canvasSessionReset: { - reducer: (state, action: PayloadAction<{ canvasSessionId: string }>) => { - const { canvasSessionId } = action.payload; - state.canvasSessionId = canvasSessionId; - state.canvasDiscardedQueueItems = []; + reducer: (state, action: CanvasPayloadAction<{ canvasSessionId: string }>) => { + const { canvasId, canvasSessionId } = action.payload; + + const session = state.sessions.find((session) => session.canvasId === canvasId); + if (!session) { + return; + } + + session.canvasSessionId = canvasSessionId; + session.canvasDiscardedQueueItems = []; }, - prepare: () => { + prepare: (payload: CanvasPayload) => { return { payload: { + ...payload, canvasSessionId: getPrefixedId('canvas'), }, }; }, }, }, + extraReducers(builder) { + builder.addCase(canvasCreated, (state, action) => { + const session = getInitialCanvasSessionState(action.payload.id); + state.sessions.push(session); + }); + builder.addCase(canvasRemoved, (state, action) => { + state.sessions = state.sessions.filter((session) => session.canvasId !== action.payload.id); + }); + builder.addCase(canvasMultiCanvasMigrated, (state, action) => { + const session = state.sessions.find((session) => session.canvasId === MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER); + if (!session) { + return; + } + session.canvasId = action.payload.id; + }); + }, }); export const { canvasSessionReset, canvasQueueItemDiscarded } = slice.actions; @@ -62,6 +110,17 @@ export const canvasSessionSliceConfig: SliceConfig = { if (!('_version' in state)) { state._version = 1; state.canvasSessionId = state.canvasSessionId ?? getPrefixedId('canvas'); + } else if (state._version === 1) { + // Migrate from v1 to v2: slice represented a canvas session instance -> slice represents multiple canvas session instances + const session = { + canvasId: MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, + ...state, + } as CanvasSessionState; + + state = { + _version: 2, + sessions: [session], + }; } return zCanvasStagingAreaState.parse(state); @@ -69,33 +128,44 @@ export const canvasSessionSliceConfig: SliceConfig = { }, }; -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) => +const findSessionByCanvasId = (sessions: CanvasSessionState[], canvasId: string) => { + const session = sessions.find((s) => s.canvasId === canvasId); + assert(session, 'Session must exist for a canvas once the canvas has been created'); + return session; +}; +export const selectCanvasSessionByCanvasId = (state: RootState, canvasId: string) => + findSessionByCanvasId(state.canvasSession.sessions, canvasId); +const selectSelectedCanvasSession = (state: RootState) => { + const canvasId = selectSelectedCanvasId(state); + return findSessionByCanvasId(state.canvasSession.sessions, canvasId); +}; +export const selectCanvasSessionId = (state: RootState, canvasId: string) => { + const session = selectCanvasSessionByCanvasId(state, canvasId); + return session.canvasSessionId; +}; +export const selectSelectedCanvasSessionId = (state: RootState) => { + const session = selectSelectedCanvasSession(state); + return session.canvasSessionId; +}; +const selectCanvasSessionDiscardedItemsBySessionId = (state: RootState, sessionId: string) => { + const session = state.canvasSession.sessions.find((s) => s.canvasSessionId === sessionId); + assert(session, 'Session does not exist'); + return session.canvasDiscardedQueueItems; +}; +export const buildSelectCanvasQueueItemsBySessionId = (sessionId: string) => createSelector( - [queueApi.endpoints.listAllQueueItems.select({ destination: sessionId }), selectDiscardedItems], + queueApi.endpoints.listAllQueueItems.select({ destination: sessionId }), + (state: RootState) => selectCanvasSessionDiscardedItemsBySessionId(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/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index c05be8cfee9..d6353993830 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -41,6 +41,13 @@ const selectSelectedCanvasWithHistory = createSelector( ); export const selectSelectedCanvas = createSelector(selectSelectedCanvasWithHistory, (canvas) => canvas.present); +export const selectSelectedCanvasId = createSelector(selectSelectedCanvas, (canvas) => canvas.id); + +export const selectCanvasById = (state: RootState, canvasId: string) => { + const canvas = state.canvas.canvases.find((canvas) => canvas.present.id === canvasId); + assert(canvas, 'Canvas does not exist'); + return canvas.present; +}; /** * Selects the total canvas entity count: diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index c014e6b79c8..6613f1a2e57 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -824,11 +824,16 @@ const zCanvasState = z.object({ }); export type CanvasState = z.infer; const zCanvasStateWithHistory = zStateWithHistory(zCanvasState); +export type CanvasStateWithHistory = z.infer; +const zCanvasesStateMigration = z.object({ + isMultiCanvasMigrationPending: z.boolean().optional(), +}); const zCanvasesState = (canvasStateSchema: T) => z.object({ _version: z.literal(4), selectedCanvasId: zId, canvases: z.array(canvasStateSchema), + migration: zCanvasesStateMigration.optional(), }); export const zCanvasesStateWithHistory = zCanvasesState(zCanvasStateWithHistory); export type CanvasesStateWithHistory = z.infer; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts index d3d59a6bb93..0aafe2769f3 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts @@ -8,7 +8,7 @@ import { selectReferenceImageEntities, selectRefImagesSlice, } from 'features/controlLayers/store/refImagesSlice'; -import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectCanvases } from 'features/controlLayers/store/selectors'; import type { CanvasState, RefImagesState } from 'features/controlLayers/store/types'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; import { selectGetImageNamesQueryArgs } from 'features/gallery/store/gallerySelectors'; @@ -156,11 +156,11 @@ const getImageUsageFromImageNames = (image_names: string[], state: RootState): I } const nodes = selectNodesSlice(state); - const canvas = selectSelectedCanvas(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) => { - selectSelectedCanvas(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) => { - selectSelectedCanvas(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: CanvasState[], 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/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index ff320007828..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 { selectSelectedCanvas } 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, selectSelectedCanvas, 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/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/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..7dd64b77e4f 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallAllImageMetadata.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallAllImageMetadata.ts @@ -1,5 +1,5 @@ import { useAppSelector, useAppStore } from 'app/store/storeHooks'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useCallback, useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts index 3feb074a35e..0b189c6414b 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallDimensions.ts @@ -1,5 +1,5 @@ import { useAppSelector, useAppStore } from 'app/store/storeHooks'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { MetadataUtils } from 'features/metadata/parsing'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useCallback, useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts index 8f7ea9db6d7..d839154d907 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useRecallRemix.ts @@ -1,5 +1,5 @@ import { useAppSelector, useAppStore } from 'app/store/storeHooks'; -import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useCallback, useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts index 14d27e900c1..85940ed725a 100644 --- a/invokeai/frontend/web/src/features/imageActions/actions.ts +++ b/invokeai/frontend/web/src/features/imageActions/actions.ts @@ -3,8 +3,8 @@ 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 { + addCanvas, bboxChangedFromCanvas, canvasClearHistory, controlLayerAdded, @@ -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(addCanvas({ 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(addCanvas({ 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(addCanvas({ 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(addCanvas({ 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(addCanvas({ isSelected: true })); dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true })); if (withInpaintMask) { dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true })); 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..352dfe35bd0 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,7 +2,7 @@ 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 { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { @@ -36,7 +36,7 @@ export const addFLUXFill = async ({ denoise.height = scaledSize.height; const params = selectParamsSlice(state); - const canvasSettings = selectCanvasSettingsSlice(state); + const canvasSettings = buildSelectCanvasSettingsByCanvasId(manager.canvasId)(state); 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/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index ac71ec4b0cb..2b86c7763fb 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,7 +2,7 @@ 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 { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { @@ -49,7 +49,7 @@ export const addInpaint = async ({ denoise.denoising_end = denoising_end; const params = selectParamsSlice(state); - const canvasSettings = selectCanvasSettingsSlice(state); + const canvasSettings = buildSelectCanvasSettingsByCanvasId(manager.canvasId)(state); const { originalSize, scaledSize, rect } = getOriginalAndScaledSizesForOtherModes(state); 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..3ac047c0918 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,7 +2,7 @@ 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 { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { @@ -51,7 +51,7 @@ export const addOutpaint = async ({ denoise.denoising_end = denoising_end; const params = selectParamsSlice(state); - const canvasSettings = selectCanvasSettingsSlice(state); + const canvasSettings = buildSelectCanvasSettingsByCanvasId(manager.canvasId)(state); const { originalSize, scaledSize, rect } = getOriginalAndScaledSizesForOtherModes(state); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts index 825d3e88fdd..e6e2b9efecf 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasMetadata, selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectCanvasById, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { isFluxKontextReferenceImageConfig } from 'features/controlLayers/store/types'; import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators'; import { zImageField } from 'features/nodes/types/common'; @@ -40,7 +40,7 @@ export const buildFLUXGraph = 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 +308,7 @@ export const buildFLUXGraph = 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 +281,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 +284,7 @@ export const buildSDXLGraph = async (arg: GraphBuilderArg): Promise { * 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,7 +73,8 @@ export const selectCanvasDestination = (state: RootState) => { if (saveAllImagesToGallery) { return 'canvas'; } - return selectCanvasSessionId(state); + + return selectCanvasSessionId(state, canvasId); }; /** @@ -121,9 +122,9 @@ export const selectPresetModifiedPrompts = createSelector( export const getOriginalAndScaledSizesForTextToImage = (state: RootState) => { const tab = selectActiveTab(state); const params = selectParamsSlice(state); - const canvas = selectSelectedCanvas(state); if (tab === 'canvas') { + const canvas = selectSelectedCanvas(state); const { rect, aspectRatio } = canvas.bbox; const { width, height } = rect; const originalSize = { width, height }; @@ -143,10 +144,10 @@ export const getOriginalAndScaledSizesForTextToImage = (state: RootState) => { export const getOriginalAndScaledSizesForOtherModes = (state: RootState) => { const tab = selectActiveTab(state); - const canvas = selectSelectedCanvas(state); assert(tab === 'canvas', `Cannot get sizes for tab ${tab} - this function is only for the Canvas tab`); + const canvas = selectSelectedCanvas(state); const { rect, aspectRatio } = canvas.bbox; const { width, height } = rect; const originalSize = { width, height }; 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/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/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/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts index 9d5a589f056..64c3791f9e6 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts @@ -38,7 +38,8 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep const state = getState(); - const destination = selectCanvasDestination(state); + const destination = selectCanvasDestination(state, canvasManager.canvasId); + assert(destination, 'Destination must exist when CanvasManager has already been created'); const model = state.params.model; if (!model) { diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx index 0fc5b0a8e77..e2b54fe50e2 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx @@ -1,7 +1,7 @@ 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 { canvasAdded, canvasDeleted, canvasSelected } from 'features/controlLayers/store/canvasSlice'; +import { addCanvas, canvasSelected, deleteCanvas } from 'features/controlLayers/store/canvasSlice'; import { selectCanvases } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,7 +18,7 @@ const AddCanvasButton = memo(() => { const dispatch = useAppDispatch(); const onClick = useCallback(() => { - dispatch(canvasAdded({ isSelected: true })); + dispatch(addCanvas({ isSelected: true })); }, [dispatch]); return ( @@ -46,7 +46,7 @@ const CloseCanvasButton = memo(({ id, canDelete }: CloseCanvasButtonProps) => { const dispatch = useAppDispatch(); const onClick = useCallback(() => { - dispatch(canvasDeleted({ id })); + dispatch(deleteCanvas({ id })); }, [dispatch, id]); return ( diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx index 9c17dd1ba63..870f33aee3a 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx @@ -16,9 +16,10 @@ import { SelectObject } from 'features/controlLayers/components/SelectObject/Sel import { StagingAreaContextProvider } from 'features/controlLayers/components/StagingArea/context'; import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar'; import { Transform } from 'features/controlLayers/components/Transform/Transform'; +import { CanvasInstanceContextProvider } from 'features/controlLayers/contexts/CanvasInstanceContextProvider'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectSelectedCanvasId } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; @@ -49,80 +50,96 @@ const canvasBgSx = { }, }; -export const CanvasWorkspacePanel = memo(() => { - const dynamicGrid = useAppSelector(selectDynamicGrid); - const showHUD = useAppSelector(selectShowHUD); - const sessionId = useAppSelector(selectCanvasSessionId); +interface CanvasProps { + canvasId: string; +} + +const Canvas = memo(({ canvasId }: CanvasProps) => { + const dynamicGrid = useAppSelector((state) => selectDynamicGrid(state)); + const showHUD = useAppSelector((state) => selectShowHUD(state)); const renderMenu = useCallback(() => { return ; }, []); return ( - - - - - - - - renderMenu={renderMenu} withLongPress={false}> - {(ref) => ( - - - - - {showHUD && } - - - - - - - - - } colorScheme="base" /> - - - - - - - )} - - - - - + + + + renderMenu={renderMenu} withLongPress={false}> + {(ref) => ( + + + + + {showHUD && } + + + + + + + + + } colorScheme="base" /> + + + + + + + )} + - - - + - - - - - - + + + + + + + + + + + + + + ); +}); +Canvas.displayName = 'Canvas'; + +export const CanvasWorkspacePanel = memo(() => { + const canvasId = useAppSelector(selectSelectedCanvasId); + + 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..11a664f6585 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx @@ -1,9 +1,8 @@ import { Flex, Text } from '@invoke-ai/ui-library'; -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 { useCanvasSessionId } from 'features/controlLayers/hooks/useCanvasSessionId'; import { useCurrentQueueItemDestination } from 'features/queue/hooks/useCurrentQueueItemDestination'; import ProgressBar from 'features/system/components/ProgressBar'; import { memo, useCallback, useRef } from 'react'; @@ -15,7 +14,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 = useCanvasSessionId(); const currentQueueItemDestination = useCurrentQueueItemDestination(); const ref = useRef(null); 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..98e77dac8a8 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/LaunchpadGenerateFromTextButton.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/LaunchpadGenerateFromTextButton.tsx @@ -1,6 +1,6 @@ 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 { LaunchpadButton } from 'features/ui/layouts/LaunchpadButton'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; 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(() => { From 7f66f7c12437b805c867770eb12b16ae8e25c961 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Sat, 27 Sep 2025 11:38:15 +0200 Subject: [PATCH 05/16] canvasSlice refactored --- .../listeners/modelSelected.ts | 4 +- .../listeners/modelsLoaded.ts | 8 +- .../CanvasAlertsSelectedEntityStatus.tsx | 6 +- ...tityListSelectedEntityActionBarOpacity.tsx | 4 +- .../ControlLayer/ControlLayerBadges.tsx | 4 +- .../ControlLayerControlAdapter.tsx | 4 +- .../ControlLayer/ControlLayerEntityList.tsx | 4 +- ...ontrolLayerMenuItemsTransparencyEffect.tsx | 4 +- .../InpaintMaskDenoiseLimitSlider.tsx | 4 +- .../InpaintMask/InpaintMaskList.tsx | 4 +- .../InpaintMask/InpaintMaskNoiseSlider.tsx | 4 +- .../InpaintMask/InpaintMaskSettings.tsx | 6 +- .../RasterLayerAdjustmentsPanel.tsx | 12 +- .../RasterLayerCurvesAdjustmentsEditor.tsx | 6 +- .../RasterLayer/RasterLayerEntityList.tsx | 4 +- .../RasterLayerMenuItemsAdjustments.tsx | 4 +- .../RasterLayerSimpleAdjustmentsEditor.tsx | 6 +- .../RegionalGuidanceBadges.tsx | 4 +- .../RegionalGuidanceEntityList.tsx | 4 +- .../RegionalGuidanceIPAdapterSettings.tsx | 6 +- .../RegionalGuidanceIPAdapters.tsx | 4 +- .../RegionalGuidanceMenuItemsAutoNegative.tsx | 4 +- .../RegionalGuidanceNegativePrompt.tsx | 4 +- .../RegionalGuidancePositivePrompt.tsx | 4 +- .../RegionalGuidanceSettings.tsx | 4 +- .../common/CanvasEntityHeaderWarnings.tsx | 4 +- .../common/CanvasEntityMenuItemsArrange.tsx | 4 +- .../common/CanvasEntityPreviewImage.tsx | 4 +- .../contexts/CanvasManagerProviderGate.tsx | 4 +- .../controlLayers/hooks/addLayerHooks.ts | 4 +- .../controlLayers/hooks/useCanvasId.ts | 4 +- .../useEntityIsBookmarkedForQuickSwitch.ts | 4 +- .../controlLayers/hooks/useEntityIsEnabled.ts | 4 +- .../controlLayers/hooks/useEntityIsLocked.ts | 4 +- .../controlLayers/hooks/useEntityTitle.ts | 4 +- .../controlLayers/hooks/useEntityTypeCount.ts | 4 +- .../hooks/useEntityTypeIsHidden.ts | 4 +- .../controlLayers/hooks/useNextPrevEntity.ts | 6 +- .../useNextRenderableEntityIdentifier.ts | 4 +- .../CanvasEntity/CanvasEntityAdapterBase.ts | 4 +- .../konva/CanvasEntityRendererModule.ts | 6 +- .../konva/CanvasStateApiModule.ts | 4 +- .../store/canvasSettingsSlice.ts | 6 +- .../controlLayers/store/canvasSlice.ts | 113 +++++++++++------- .../store/canvasStagingAreaSlice.ts | 10 +- .../features/controlLayers/store/selectors.ts | 73 +++++------ .../src/features/controlLayers/store/types.ts | 4 +- .../nodes/util/graph/graphBuilderUtils.ts | 6 +- .../Bbox/BboxLockAspectRatioButton.tsx | 4 +- .../components/Bbox/BboxScaleMethod.tsx | 4 +- .../components/Bbox/BboxScaledHeight.tsx | 6 +- .../components/Bbox/BboxScaledWidth.tsx | 6 +- .../Bbox/BboxSetOptimalSizeButton.tsx | 6 +- .../web/src/features/queue/store/readiness.ts | 4 +- .../ui/layouts/CanvasTabEditableTitle.tsx | 12 +- .../src/features/ui/layouts/CanvasTabs.tsx | 30 ++--- .../ui/layouts/CanvasWorkspacePanel.tsx | 4 +- 57 files changed, 248 insertions(+), 234 deletions(-) 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 96cbe27be46..2c62551a8b8 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 @@ -9,9 +9,9 @@ import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice'; import { + selectActiveCanvas, selectAllEntitiesOfType, selectBboxModelBase, - selectSelectedCanvas, } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier } from 'features/controlLayers/store/types'; import { modelSelected } from 'features/parameters/store/actions'; @@ -121,7 +121,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 = selectSelectedCanvas(state); + const canvasState = selectActiveCanvas(state); const canvasRegionalGuidanceEntities = selectAllEntitiesOfType(canvasState, 'regional_guidance'); for (const entity of canvasRegionalGuidanceEntities) { for (const refImage of entity.referenceImages) { 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 6ab79d35da0..05a3551bbb5 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 @@ -11,7 +11,7 @@ import { vaeSelected, } from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import { getEntityIdentifier, isFLUXReduxConfig, @@ -221,7 +221,7 @@ const handleVideoModels: ModelHandler = (models, state, dispatch, log) => { const handleControlAdapterModels: ModelHandler = (models, state, dispatch, log) => { const caModels = models.filter(isControlLayerModelConfig); - selectSelectedCanvas(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 +256,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => { dispatch(refImageModelChanged({ id: entity.id, modelConfig: null })); }); - selectSelectedCanvas(state).regionalGuidance.entities.forEach((entity) => { + selectActiveCanvas(state).regionalGuidance.entities.forEach((entity) => { entity.referenceImages.forEach(({ id: referenceImageId, config }) => { if (!isRegionalGuidanceIPAdapterConfig(config)) { return; @@ -299,7 +299,7 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => { dispatch(refImageModelChanged({ id: entity.id, modelConfig: null })); }); - selectSelectedCanvas(state).regionalGuidance.entities.forEach((entity) => { + selectActiveCanvas(state).regionalGuidance.entities.forEach((entity) => { entity.referenceImages.forEach(({ id: referenceImageId, config }) => { if (!isRegionalGuidanceFLUXReduxConfig(config)) { return; 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 4bcff6a02ff..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,8 +9,8 @@ import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle'; import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden'; import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; import { + selectActiveCanvas, selectEntityOrThrow, - selectSelectedCanvas, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; @@ -32,13 +32,13 @@ type AlertData = { const buildSelectIsEnabled = (entityIdentifier: CanvasEntityIdentifier) => createSelector( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isEnabled ); const buildSelectIsLocked = (entityIdentifier: CanvasEntityIdentifier) => createSelector( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isLocked ); 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 bd6749da463..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,8 +20,8 @@ import { clamp, round } from 'es-toolkit/compat'; import { snapToNearest } from 'features/controlLayers/konva/util'; import { entityOpacityChanged } from 'features/controlLayers/store/canvasSlice'; import { + selectActiveCanvas, selectEntity, - selectSelectedCanvas, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; import type { KeyboardEvent } from 'react'; @@ -61,7 +61,7 @@ const sliderDefaultValue = mapRawValueToSliderValue(1); const snapCandidates = marks.slice(1, marks.length - 1); -const selectOpacity = createSelector(selectSelectedCanvas, (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/ControlLayer/ControlLayerBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx index 0bb1fb62e86..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 { selectEntityOrThrow, selectSelectedCanvas } 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( - selectSelectedCanvas, + 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 32c2e94686f..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 { selectEntityOrThrow, selectSelectedCanvas } 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(selectSelectedCanvas, (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 039cb19526a..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 { selectSelectedCanvas, 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(selectSelectedCanvas, (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 e17c718abd1..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 { selectEntityOrThrow, selectSelectedCanvas } 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( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'ControlLayerMenuItemsTransparencyEffect').withTransparencyEffect ); 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 8e851e2fbc6..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 { selectEntityOrThrow, selectSelectedCanvas } 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( - selectSelectedCanvas, + 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 66425d25894..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 { selectSelectedCanvas, 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(selectSelectedCanvas, (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 3e16e0acdd9..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 { selectEntityOrThrow, selectSelectedCanvas } 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( - selectSelectedCanvas, + 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 34864b65682..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 { selectEntityOrThrow, selectSelectedCanvas } 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(selectSelectedCanvas, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings'); return entity.denoiseLimit !== undefined; }); const buildSelectHasNoiseLevel = (entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>) => - createSelector(selectSelectedCanvas, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { const entity = selectEntityOrThrow(canvas, entityIdentifier, 'InpaintMaskSettings'); return entity.noiseLevel !== undefined; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel.tsx index 00ce4807841..8b18a96c482 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel.tsx @@ -13,7 +13,7 @@ import { rasterLayerAdjustmentsReset, rasterLayerAdjustmentsSet, } from 'features/controlLayers/store/canvasSlice'; -import { selectEntity, selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectEntity } from 'features/controlLayers/store/selectors'; import React, { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiCaretDownBold, PiCheckBold, PiTrashBold } from 'react-icons/pi'; @@ -25,16 +25,14 @@ export const RasterLayerAdjustmentsPanel = memo(() => { const canvasManager = useCanvasManager(); const selectHasAdjustments = useMemo(() => { - return createSelector(selectSelectedCanvas, (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( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.mode ?? 'simple' ); }, [entityIdentifier]); @@ -42,7 +40,7 @@ export const RasterLayerAdjustmentsPanel = memo(() => { const selectEnabled = useMemo(() => { return createSelector( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled ?? false ); }, [entityIdentifier]); @@ -50,7 +48,7 @@ export const RasterLayerAdjustmentsPanel = memo(() => { const selectCollapsed = useMemo(() => { return createSelector( - selectSelectedCanvas, + 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 d3b796fd764..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 { selectEntity, selectSelectedCanvas } 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( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.curves ?? DEFAULT_CURVES ); }, [entityIdentifier]); @@ -80,7 +80,7 @@ export const RasterLayerCurvesAdjustmentsEditor = memo(() => { const selectIsDisabled = useMemo(() => { return createSelector( - selectSelectedCanvas, + 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 f2b22982668..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 { selectSelectedCanvas, 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(selectSelectedCanvas, (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 b0d7ef9991b..21fdae49e35 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsAdjustments.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsAdjustments.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 { rasterLayerAdjustmentsCancel, rasterLayerAdjustmentsSet } from 'features/controlLayers/store/canvasSlice'; -import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +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, useMemo } from 'react'; @@ -15,7 +15,7 @@ export const RasterLayerMenuItemsAdjustments = memo(() => { const entityIdentifier = useEntityIdentifierContext<'raster_layer'>(); const { t } = useTranslation(); const selectRasterLayer = useMemo(() => { - return createSelector(selectSelectedCanvas, (canvas) => + return createSelector(selectActiveCanvas, (canvas) => canvas.rasterLayers.entities.find((e: CanvasRasterLayerState) => e.id === entityIdentifier.id) ); }, [entityIdentifier]); 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 e8f2ca01f15..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 { selectEntity, selectSelectedCanvas } 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( - selectSelectedCanvas, + 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( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled !== true ); }, [entityIdentifier]); 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 d5e7a3f818f..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 { selectEntityOrThrow, selectSelectedCanvas } 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( - selectSelectedCanvas, + 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 bf75a03d8b0..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 { selectSelectedCanvas, 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(selectSelectedCanvas, (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 5aa3f8edde2..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 { selectRegionalGuidanceReferenceImage, selectSelectedCanvas } 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(selectSelectedCanvas, (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(selectSelectedCanvas, (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 6354a57867f..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 { selectEntityOrThrow, selectSelectedCanvas } 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(selectSelectedCanvas, (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 048fd36ac84..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 { selectEntityOrThrow, selectSelectedCanvas } 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( - selectSelectedCanvas, + 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 61d79af2ce9..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 { selectEntityOrThrow, selectSelectedCanvas } 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( - selectSelectedCanvas, + 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 808ae104efb..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 { selectEntityOrThrow, selectSelectedCanvas } 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( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'RegionalGuidancePositivePrompt').positivePrompt ?? '' ), [entityIdentifier] 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 844c021c079..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 { selectEntityOrThrow, selectSelectedCanvas } 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(selectSelectedCanvas, (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/common/CanvasEntityHeaderWarnings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderWarnings.tsx index 2a7747bfff3..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 { selectEntityOrThrow, selectSelectedCanvas } 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(selectSelectedCanvas, 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 cd3c6059ee0..3346ee6a05c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx @@ -9,7 +9,7 @@ import { entityArrangedToBack, entityArrangedToFront, } from 'features/controlLayers/store/canvasSlice'; -import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier, CanvasState } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -54,7 +54,7 @@ export const CanvasEntityMenuItemsArrange = memo(() => { const isBusy = useCanvasIsBusy(); const selectValidActions = useMemo( () => - createMemoizedSelector(selectSelectedCanvas, (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 86f7819013d..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 { selectEntity, selectSelectedCanvas } 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(selectSelectedCanvas, (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 3dd30284ae3..f7c19300fe7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx @@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useCanvasId } from 'features/controlLayers/hooks/useCanvasId'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { $canvasManagers } from 'features/controlLayers/store/ephemeral'; -import { selectSelectedCanvasId } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; import type { PropsWithChildren } from 'react'; import { createContext, memo } from 'react'; import { assert } from 'tsafe'; @@ -12,7 +12,7 @@ const CanvasManagerContext = createContext<{ [canvasId: string]: CanvasManager } export const CanvasManagerProviderGate = memo(({ children }: PropsWithChildren) => { const canvasManagers = useStore($canvasManagers); - const selectedCanvasId = useAppSelector(selectSelectedCanvasId); + const selectedCanvasId = useAppSelector(selectActiveCanvasId); if (Object.keys(canvasManagers).length === 0 || !canvasManagers[selectedCanvasId]) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index d14d72c7e8b..f209a89dcec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -17,8 +17,8 @@ import { } from 'features/controlLayers/store/canvasSlice'; import { selectBase, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { + selectActiveCanvas, selectEntity, - selectSelectedCanvas, selectSelectedEntityIdentifier, } from 'features/controlLayers/store/selectors'; import type { @@ -270,7 +270,7 @@ export const useAddInpaintMaskDenoiseLimit = (entityIdentifier: CanvasEntityIden export const buildSelectValidRegionalGuidanceActions = ( entityIdentifier: CanvasEntityIdentifier<'regional_guidance'> ) => { - return createMemoizedSelector(selectSelectedCanvas, (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/useCanvasId.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts index 65a75009489..fd29b1ae989 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts @@ -1,10 +1,10 @@ import { useAppSelector } from 'app/store/storeHooks'; import { useScopedCanvasIdSafe } from 'features/controlLayers/contexts/CanvasInstanceContextProvider'; -import { selectSelectedCanvasId } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; export const useCanvasId = () => { const scopedCanvasId = useScopedCanvasIdSafe(); - const canvasId = useAppSelector(selectSelectedCanvasId); + const canvasId = useAppSelector(selectActiveCanvasId); return scopedCanvasId ?? canvasId; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch.ts index ef3776dfc25..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 { selectSelectedCanvas } 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(selectSelectedCanvas, (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 127edee97eb..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 { selectEntity, selectSelectedCanvas } 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(selectSelectedCanvas, (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 45081fa63aa..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 { selectEntity, selectSelectedCanvas } 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(selectSelectedCanvas, (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 83883467144..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 { selectEntity, selectSelectedCanvas } 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(selectSelectedCanvas, (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 4e6160777be..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 { selectSelectedCanvas } 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(selectSelectedCanvas, (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 b5843d52efb..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 { selectSelectedCanvas } 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(selectSelectedCanvas, (canvas) => { + createSelector(selectActiveCanvas, (canvas) => { switch (type) { case 'control_layer': return canvas.controlLayers.isHidden; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useNextPrevEntity.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useNextPrevEntity.ts index 9d373913823..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, selectSelectedCanvas } 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(selectSelectedCanvas, (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(selectSelectedCanvas, return getEntityIdentifier(nextEntity); }); -const selectPrevEntityIdentifier = createMemoizedSelector(selectSelectedCanvas, (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 74793a6ac47..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 { selectEntityIdentifierBelowThisOne, selectSelectedCanvas } 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(selectSelectedCanvas, (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 1ed4e8371cb..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,9 +19,9 @@ import { import { buildSelectIsSelected, getSelectIsTypeHidden, + selectActiveCanvas, selectBboxRect, selectEntity, - selectSelectedCanvas, } from 'features/controlLayers/store/selectors'; import type { CanvasEntityIdentifier, @@ -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 67a278baaea..69357a24732 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts @@ -2,11 +2,11 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { + selectActiveCanvas, selectControlLayerEntities, selectInpaintMaskEntities, selectRasterLayerEntities, selectRegionalGuidanceEntities, - selectSelectedCanvas, } from 'features/controlLayers/store/selectors'; import type { CanvasControlLayerState, @@ -54,7 +54,7 @@ export class CanvasEntityRendererModule extends CanvasModuleBase { this.manager.stateApi.createStoreSubscription(selectRegionalGuidanceEntities, this.createNewRegionalGuidance) ); - this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectSelectedCanvas, 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(selectSelectedCanvas), null); + this.arrangeEntities(this.manager.stateApi.runSelector(selectActiveCanvas), null); }; createNewRasterLayers = (entities: CanvasRasterLayerState[]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index e73e35af0f4..d997b2a4fb2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -31,10 +31,10 @@ import { } from 'features/controlLayers/store/canvasSlice'; import { selectCanvasSessionByCanvasId } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { + selectActiveCanvas, selectAllRenderableEntities, selectBbox, selectGridSize, - selectSelectedCanvas, } from 'features/controlLayers/store/selectors'; import type { CanvasState, @@ -128,7 +128,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { * The state is stored in redux. */ getCanvasState = (): CanvasState => { - return this.runSelector(selectSelectedCanvas); + return this.runSelector(selectActiveCanvas); }; /** diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index bfdda00af5b..7fb44425726 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -277,18 +277,18 @@ const slice = createSlice({ }, extraReducers(builder) { builder.addCase(canvasCreated, (state, action) => { - const canvasSettings = getInitialCanvasInstanceSettingsState(action.payload.id); + const canvasSettings = getInitialCanvasInstanceSettingsState(action.payload.canvasId); state.canvases.push(canvasSettings); }); builder.addCase(canvasRemoved, (state, action) => { - state.canvases = state.canvases.filter((settings) => settings.canvasId !== action.payload.id); + state.canvases = state.canvases.filter((settings) => settings.canvasId !== action.payload.canvasId); }); builder.addCase(canvasMultiCanvasMigrated, (state, action) => { const settings = state.canvases.find((settings) => settings.canvasId === MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER); if (!settings) { return; } - settings.canvasId = action.payload.id; + settings.canvasId = action.payload.canvasId; }); }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 594cd1d4c3a..6d0bf5719f0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -1,4 +1,4 @@ -import type { CaseReducer, PayloadAction, UnknownAction } from '@reduxjs/toolkit'; +import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { AppDispatch, RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; @@ -142,8 +142,8 @@ const getInitialCanvasesState = (): CanvasesStateWithoutHistory => { return { _version: 4, - selectedCanvasId: canvasId, - canvases: [canvas], + activeCanvasId: canvasId, + canvases: { [canvasId]: canvas }, }; }; @@ -152,7 +152,9 @@ const getInitialCanvasesHistoryState = (): CanvasesStateWithHistory => { return { ...state, - canvases: state.canvases.map((canvas) => newHistory([], canvas, [])), + canvases: Object.fromEntries( + Object.entries(state.canvases).map(([canvasId, canvas]) => [canvasId, newHistory([], canvas, [])]) + ), }; }; @@ -165,72 +167,79 @@ const getNextCanvasName = (canvases: CanvasStateWithHistory[]): string => { } }; +type PayloadWithCanvasId

= P & { canvasId: string }; +type PayloadActionWithCanvasId

= PayloadAction>; + const canvasesSlice = createSlice({ name: 'canvas', initialState: getInitialCanvasesHistoryState(), reducers: { canvasAdded: { - reducer: (state, action: PayloadAction<{ id: string; isSelected?: boolean }>) => { - const { id, isSelected } = action.payload; + reducer: (state, action: PayloadActionWithCanvasId<{ isSelected?: boolean }>) => { + const { canvasId, isSelected } = action.payload; + + const name = getNextCanvasName(Object.values(state.canvases)); + const canvas = getInitialCanvasHistoryState(canvasId, name); - const name = getNextCanvasName(state.canvases); - const canvas = getInitialCanvasHistoryState(id, name); - state.canvases.push(canvas); + state.canvases[canvasId] = canvas; if (isSelected) { - state.selectedCanvasId = id; + state.activeCanvasId = canvasId; } }, prepare: (payload: { isSelected?: boolean }) => { return { - payload: { ...payload, id: getPrefixedId('canvas') }, + payload: { ...payload, canvasId: getPrefixedId('canvas') }, }; }, }, - canvasCreated: (_state, _action: PayloadAction<{ id: string }>) => {}, + canvasCreated: (_state, _action: PayloadActionWithCanvasId) => {}, canvasMigrated: (state) => { delete state.migration; }, - canvasMultiCanvasMigrated: (_state, _action: PayloadAction<{ id: string }>) => {}, - canvasSelected: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; + canvasMultiCanvasMigrated: (_state, _action: PayloadActionWithCanvasId) => {}, + canvasActivated: (state, action: PayloadActionWithCanvasId) => { + const { canvasId } = action.payload; - const canvas = state.canvases.find((canvas) => canvas.present.id === id)?.present; + const canvas = state.canvases[canvasId]?.present; if (!canvas) { return; } - state.selectedCanvasId = canvas.id; + state.activeCanvasId = canvas.id; }, - canvasDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; + canvasDeleted: (state, action: PayloadActionWithCanvasId) => { + const { canvasId } = action.payload; + const canvasIds = Object.keys(state.canvases); + + const canvas = state.canvases[canvasId]?.present; + if (!canvas) { + return; + } - if (state.canvases.length === 1) { + if (canvasIds.length === 1) { throw new Error('Last canvas cannot be deleted'); } - const index = state.canvases.findIndex((canvas) => canvas.present.id === id); - const nextIndex = (index + 1) % state.canvases.length; + const index = canvasIds.indexOf(canvas.id); + const nextIndex = index > 0 ? index - 1 : index + 1; - state.selectedCanvasId = state.canvases[nextIndex]!.present.id; - state.canvases = state.canvases.filter((canvas) => canvas.present.id !== id); + state.activeCanvasId = canvasIds[nextIndex]!; + delete state.canvases[canvas.id]; }, - canvasRemoved: (_state, _action: PayloadAction<{ id: string }>) => {}, + canvasRemoved: (_state, _action: PayloadActionWithCanvasId) => {}, }, }); -type WithId

= P & { id: string }; -type IdCaseReducer = CaseReducer>>; - const canvasSlice = createSlice({ name: 'canvas', initialState: {} as CanvasState, reducers: { - canvasNameChanged: ((state, action: PayloadAction<{ name: string }>) => { + canvasNameChanged: (state, action: PayloadActionWithCanvasId<{ name: string }>) => { const { name } = action.payload; state.name = name; - }) as IdCaseReducer, + }, //#region Raster layers rasterLayerAdjustmentsSet: ( state, @@ -1877,8 +1886,8 @@ export const addCanvas = (payload: { isSelected?: boolean }) => (dispatch: AppDi const action = canvasesSlice.actions.canvasAdded(payload); dispatch(action); - const { id } = action.payload; - dispatch(canvasesSlice.actions.canvasCreated({ id })); + const { canvasId } = action.payload; + dispatch(canvasesSlice.actions.canvasCreated({ canvasId })); }; export const MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER = 'multi-canvas-id-placeholder'; @@ -1887,13 +1896,13 @@ export const migrateCanvas = () => (dispatch: AppDispatch, getState: () => RootS const state = getState(); if (state.canvas.migration?.isMultiCanvasMigrationPending) { - dispatch(canvasesSlice.actions.canvasMultiCanvasMigrated({ id: state.canvas.canvases[0]!.present.id })); + dispatch(canvasesSlice.actions.canvasMultiCanvasMigrated({ canvasId: Object.keys(state.canvas.canvases)[0]! })); } dispatch(canvasesSlice.actions.canvasMigrated()); }; -export const deleteCanvas = (payload: { id: string }) => (dispatch: AppDispatch) => { +export const deleteCanvas = (payload: { canvasId: string }) => (dispatch: AppDispatch) => { dispatch(canvasesSlice.actions.canvasDeleted(payload)); dispatch(canvasesSlice.actions.canvasRemoved(payload)); }; @@ -1903,7 +1912,7 @@ export const { canvasCreated, canvasMultiCanvasMigrated, canvasRemoved, - canvasSelected, + canvasActivated, } = canvasesSlice.actions; export const { @@ -2040,14 +2049,26 @@ export const undoableCanvasesReducer = ( return state; } + const canvasId = isPayloadActionWithCanvasId(action) ? action.payload.canvasId : state.activeCanvasId; + return { ...state, - canvases: state.canvases.map((c) => - c.present.id === state.selectedCanvasId ? undoableCanvasReducer(c, action) : c - ), + canvases: { + ...state.canvases, + [canvasId]: undoableCanvasReducer(state.canvases[canvasId], action), + }, }; }; +const isPayloadActionWithCanvasId = (action: UnknownAction): action is PayloadActionWithCanvasId => { + return ( + typeof action.payload === 'object' && + action.payload !== null && + 'canvasId' in action.payload && + typeof (action.payload as { canvasId: unknown }).canvasId === 'string' + ); +}; + export const canvasSliceConfig: SliceConfig< typeof canvasesSlice, CanvasesStateWithHistory, @@ -2072,8 +2093,8 @@ export const canvasSliceConfig: SliceConfig< state = { _version: 4, - selectedCanvasId: canvas.id, - canvases: [canvas], + activeCanvasId: canvas.id, + canvases: { [canvasId]: canvas }, migration: { isMultiCanvasMigrationPending: true, }, @@ -2086,16 +2107,20 @@ export const canvasSliceConfig: SliceConfig< return { _version: canvasesState._version, - selectedCanvasId: canvasesState.selectedCanvasId, - canvases: canvasesState.canvases.map((canvas) => newHistory([], canvas, [])), + activeCanvasId: canvasesState.activeCanvasId, + canvases: Object.fromEntries( + Object.entries(canvasesState.canvases).map(([canvasId, canvas]) => [canvasId, newHistory([], canvas, [])]) + ), migration: canvasesState.migration, }; }, unwrapState: (state) => { return { _version: state._version, - selectedCanvasId: state.selectedCanvasId, - canvases: state.canvases.map((canvas) => canvas.present), + activeCanvasId: state.activeCanvasId, + canvases: Object.fromEntries( + Object.entries(state.canvases).map(([canvasId, canvas]) => [canvasId, canvas.present]) + ), migration: state.migration, }; }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 881e2bf7266..7d673ba874a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -14,7 +14,7 @@ import { canvasRemoved, MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, } from './canvasSlice'; -import { selectSelectedCanvasId } from './selectors'; +import { selectActiveCanvasId } from './selectors'; const zCanvasSessionState = z.object({ canvasId: z.string(), @@ -82,18 +82,18 @@ const slice = createSlice({ }, extraReducers(builder) { builder.addCase(canvasCreated, (state, action) => { - const session = getInitialCanvasSessionState(action.payload.id); + const session = getInitialCanvasSessionState(action.payload.canvasId); state.sessions.push(session); }); builder.addCase(canvasRemoved, (state, action) => { - state.sessions = state.sessions.filter((session) => session.canvasId !== action.payload.id); + state.sessions = state.sessions.filter((session) => session.canvasId !== action.payload.canvasId); }); builder.addCase(canvasMultiCanvasMigrated, (state, action) => { const session = state.sessions.find((session) => session.canvasId === MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER); if (!session) { return; } - session.canvasId = action.payload.id; + session.canvasId = action.payload.canvasId; }); }, }); @@ -136,7 +136,7 @@ const findSessionByCanvasId = (sessions: CanvasSessionState[], canvasId: string) export const selectCanvasSessionByCanvasId = (state: RootState, canvasId: string) => findSessionByCanvasId(state.canvasSession.sessions, canvasId); const selectSelectedCanvasSession = (state: RootState) => { - const canvasId = selectSelectedCanvasId(state); + const canvasId = selectActiveCanvasId(state); return findSessionByCanvasId(state.canvasSession.sessions, canvasId); }; export const selectCanvasSessionId = (state: RootState, canvasId: string) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index d6353993830..e108eed3352 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -25,26 +25,26 @@ const selectCanvasSlice = (state: RootState) => state.canvas; * Selects the canvases */ export const selectCanvases = createSelector(selectCanvasSlice, (state) => - state.canvases.map(({ present: canvas }) => ({ + Object.values(state.canvases).map(({ present: canvas }) => ({ ...canvas, - isSelected: canvas.id === state.selectedCanvasId, - canDelete: state.canvases.length > 1, + isActive: canvas.id === state.activeCanvasId, + canDelete: Object.keys(state.canvases).length > 1, })) ); /** - * Selects the selected canvas + * Selects the active canvas with history */ -const selectSelectedCanvasWithHistory = createSelector( +const selectActiveCanvasWithHistory = createSelector( selectCanvasSlice, - (state) => state.canvases.find(({ present: canvas }) => canvas.id === state.selectedCanvasId)! + (state) => state.canvases[state.activeCanvasId]! ); -export const selectSelectedCanvas = createSelector(selectSelectedCanvasWithHistory, (canvas) => canvas.present); -export const selectSelectedCanvasId = createSelector(selectSelectedCanvas, (canvas) => canvas.id); +export const selectActiveCanvas = createSelector(selectActiveCanvasWithHistory, (canvas) => canvas.present); +export const selectActiveCanvasId = createSelector(selectActiveCanvas, (canvas) => canvas.id); export const selectCanvasById = (state: RootState, canvasId: string) => { - const canvas = state.canvas.canvases.find((canvas) => canvas.present.id === canvasId); + const canvas = selectCanvasSlice(state).canvases[canvasId]; assert(canvas, 'Canvas does not exist'); return canvas.present; }; @@ -59,7 +59,7 @@ export const selectCanvasById = (state: RootState, canvasId: string) => { * * All entities are counted, regardless of their state. */ -const selectEntityCountAll = createSelector(selectSelectedCanvas, (canvas) => { +const selectEntityCountAll = createSelector(selectActiveCanvas, (canvas) => { return ( canvas.regionalGuidance.entities.length + canvas.rasterLayers.entities.length + @@ -70,26 +70,23 @@ const selectEntityCountAll = createSelector(selectSelectedCanvas, (canvas) => { const isVisibleEntity = (entity: CanvasEntityState) => entity.isEnabled && entity.objects.length > 0; -export const selectRasterLayerEntities = createSelector(selectSelectedCanvas, (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 = createSelector( - selectSelectedCanvas, - (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 = createSelector(selectSelectedCanvas, (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 = createSelector( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => canvas.regionalGuidance.entities ); export const selectActiveRegionalGuidanceEntities = createSelector(selectRegionalGuidanceEntities, (entities) => @@ -205,7 +202,7 @@ export function selectEntityOrThrow( } export const selectEntityExists = (entityIdentifier: T) => { - return createSelector(selectSelectedCanvas, (canvas) => Boolean(selectEntity(canvas, entityIdentifier))); + return createSelector(selectActiveCanvas, (canvas) => Boolean(selectEntity(canvas, entityIdentifier))); }; /** @@ -282,25 +279,22 @@ export function selectRegionalGuidanceReferenceImage( return entity.referenceImages.find(({ id }) => id === referenceImageId); } -export const selectBbox = createSelector(selectSelectedCanvas, (canvas) => canvas.bbox); +export const selectBbox = createSelector(selectActiveCanvas, (canvas) => canvas.bbox); export const selectSelectedEntityIdentifier = createSelector( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => canvas.selectedEntityIdentifier ); export const selectBookmarkedEntityIdentifier = createSelector( - selectSelectedCanvas, + selectActiveCanvas, (canvas) => canvas.bookmarkedEntityIdentifier ); -export const selectCanvasMayUndo = createSelector(selectSelectedCanvasWithHistory, (canvas) => canvas.past.length > 0); -export const selectCanvasMayRedo = createSelector( - selectSelectedCanvasWithHistory, - (canvas) => 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( - selectSelectedCanvas, + selectActiveCanvas, selectSelectedEntityIdentifier, (canvas, selectedEntityIdentifier) => { if (!selectedEntityIdentifier) { @@ -317,13 +311,10 @@ export const selectSelectedEntityFill = createSelector( } ); -const selectRasterLayersIsHidden = createSelector(selectSelectedCanvas, (canvas) => canvas.rasterLayers.isHidden); -const selectControlLayersIsHidden = createSelector(selectSelectedCanvas, (canvas) => canvas.controlLayers.isHidden); -const selectInpaintMasksIsHidden = createSelector(selectSelectedCanvas, (canvas) => canvas.inpaintMasks.isHidden); -const selectRegionalGuidanceIsHidden = createSelector( - selectSelectedCanvas, - (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. @@ -361,7 +352,7 @@ export const buildSelectIsSelected = (entityIdentifier: CanvasEntityIdentifier) * Other entities are considered empty if they have no objects. */ export const buildSelectHasObjects = (entityIdentifier: CanvasEntityIdentifier) => { - return createSelector(selectSelectedCanvas, (canvas) => { + return createSelector(selectActiveCanvas, (canvas) => { const entity = selectEntity(canvas, entityIdentifier); if (!entity) { @@ -371,17 +362,17 @@ export const buildSelectHasObjects = (entityIdentifier: CanvasEntityIdentifier) }); }; -export const selectWidth = createSelector(selectSelectedCanvas, (canvas) => canvas.bbox.rect.width); -export const selectHeight = createSelector(selectSelectedCanvas, (canvas) => canvas.bbox.rect.height); -export const selectAspectRatioID = createSelector(selectSelectedCanvas, (canvas) => canvas.bbox.aspectRatio.id); -export const selectAspectRatioValue = createSelector(selectSelectedCanvas, (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( - selectSelectedCanvas, + selectActiveCanvas, (canvas): { canvas_v2_metadata: CanvasMetadata } => { const canvas_v2_metadata: CanvasMetadata = { controlLayers: selectAllEntitiesOfType(canvas, 'control_layer'), @@ -397,6 +388,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(selectSelectedCanvas, (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/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 6613f1a2e57..287db719b4f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -831,8 +831,8 @@ const zCanvasesStateMigration = z.object({ const zCanvasesState = (canvasStateSchema: T) => z.object({ _version: z.literal(4), - selectedCanvasId: zId, - canvases: z.array(canvasStateSchema), + activeCanvasId: zId, + canvases: z.record(zId, canvasStateSchema), migration: zCanvasesStateMigration.optional(), }); export const zCanvasesStateWithHistory = zCanvasesState(zCanvasStateWithHistory); 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 2f05014c4aa..3ef707bee77 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -11,7 +11,7 @@ import { selectRefinerModel, selectRefinerStart, } from 'features/controlLayers/store/paramsSlice'; -import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas } 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'; @@ -124,7 +124,7 @@ export const getOriginalAndScaledSizesForTextToImage = (state: RootState) => { const params = selectParamsSlice(state); if (tab === 'canvas') { - const canvas = selectSelectedCanvas(state); + const canvas = selectActiveCanvas(state); const { rect, aspectRatio } = canvas.bbox; const { width, height } = rect; const originalSize = { width, height }; @@ -147,7 +147,7 @@ export const getOriginalAndScaledSizesForOtherModes = (state: RootState) => { assert(tab === 'canvas', `Cannot get sizes for tab ${tab} - this function is only for the Canvas tab`); - const canvas = selectSelectedCanvas(state); + 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/Bbox/BboxLockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxLockAspectRatioButton.tsx index 32016bd2bf9..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 { selectSelectedCanvas } 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(selectSelectedCanvas, (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 446739582c5..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 { selectSelectedCanvas } 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(selectSelectedCanvas, (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 267a34a9ccd..d4cc0c08b7b 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,14 @@ 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 { selectGridSize, selectOptimalDimension, selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectGridSize, selectOptimalDimension } 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(selectSelectedCanvas, (canvas) => canvas.bbox.scaleMethod === 'manual'); -const selectScaledHeight = createSelector(selectSelectedCanvas, (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 2d05bb1fbd0..de08f1b4f73 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,14 @@ 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 { selectGridSize, selectOptimalDimension, selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectGridSize, selectOptimalDimension } 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(selectSelectedCanvas, (canvas) => canvas.bbox.scaleMethod === 'manual'); -const selectScaledWidth = createSelector(selectSelectedCanvas, (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 660fd6e1801..cc96408ea7b 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,15 @@ 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 { selectOptimalDimension, selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas, selectOptimalDimension } 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(selectSelectedCanvas, (canvas) => canvas.bbox.rect.width); -const selectHeight = createSelector(selectSelectedCanvas, (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/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index 5e64bd92bb8..dc71ebfdde7 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -12,7 +12,7 @@ import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasMana import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasState, LoRA, ParamsState, RefImagesState } from 'features/controlLayers/store/types'; import { getControlLayerWarnings, @@ -198,7 +198,7 @@ export const useReadinessWatcher = () => { const store = useAppStore(); const canvasManager = useCanvasManagerSafe(); const tab = useAppSelector(selectActiveTab); - const canvas = useAppSelector(selectSelectedCanvas); + const canvas = useAppSelector(selectActiveCanvas); const params = useAppSelector(selectParamsSlice); const refImages = useAppSelector(selectRefImagesSlice); const dynamicPrompts = useAppSelector(selectDynamicPromptsSlice); diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx index 3539bc60a0b..d115de7bd6e 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx @@ -7,21 +7,21 @@ import { memo, useCallback, useRef } from 'react'; import { PiPencilBold } from 'react-icons/pi'; interface CanvasTabEditableTitleProps { - id: string; + canvasId: string; name: string; - isSelected: boolean; + isActive: boolean; } -export const CanvasTabEditableTitle = memo(({ id, name, isSelected }: CanvasTabEditableTitleProps) => { +export const CanvasTabEditableTitle = memo(({ canvasId, name, isActive }: CanvasTabEditableTitleProps) => { const dispatch = useAppDispatch(); const isHovering = useBoolean(false); const inputRef = useRef(null); const onChange = useCallback( (value: string) => { - dispatch(canvasNameChanged({ id, name: value })); + dispatch(canvasNameChanged({ canvasId, name: value })); }, - [dispatch, id] + [dispatch, canvasId] ); const editable = useEditable({ @@ -39,7 +39,7 @@ export const CanvasTabEditableTitle = memo(({ id, name, isSelected }: CanvasTabE size="sm" fontWeight="semibold" userSelect="none" - color={isSelected ? 'base.100' : 'base.300'} + color={isActive ? 'base.100' : 'base.300'} onDoubleClick={editable.startEditing} cursor="text" noOfLines={1} diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx index e2b54fe50e2..8a98f769350 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx @@ -1,7 +1,7 @@ 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 { addCanvas, canvasSelected, deleteCanvas } from 'features/controlLayers/store/canvasSlice'; +import { addCanvas, canvasActivated, deleteCanvas } from 'features/controlLayers/store/canvasSlice'; import { selectCanvases } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -37,17 +37,17 @@ const AddCanvasButton = memo(() => { AddCanvasButton.displayName = 'AddCanvasButton'; interface CloseCanvasButtonProps { - id: string; + canvasId: string; canDelete: boolean; } -const CloseCanvasButton = memo(({ id, canDelete }: CloseCanvasButtonProps) => { +const CloseCanvasButton = memo(({ canvasId, canDelete }: CloseCanvasButtonProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const onClick = useCallback(() => { - dispatch(deleteCanvas({ id })); - }, [dispatch, id]); + dispatch(deleteCanvas({ canvasId })); + }, [dispatch, canvasId]); return ( { +const CanvasTab = memo(({ id, name, isActive, canDelete }: CanvasTabProps) => { const dispatch = useAppDispatch(); const onClick = useCallback(() => { - if (!isSelected) { - dispatch(canvasSelected({ id })); + if (!isActive) { + dispatch(canvasActivated({ canvasId: id })); } - }, [dispatch, id, isSelected]); + }, [dispatch, id, isActive]); return ( @@ -92,16 +92,16 @@ const CanvasTab = memo(({ id, name, isSelected, canDelete }: CanvasTabProps) => ps={1} pe={1} gap={4} - bg={isSelected ? 'base.650' : 'base.850'} + bg={isActive ? 'base.650' : 'base.850'} _hover={_hover} w="full" h="full" > - + - + @@ -115,8 +115,8 @@ export const CanvasTabs = () => { return ( - {canvases.map(({ id, name, isSelected, canDelete }) => ( - + {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 870f33aee3a..22be4e3e4db 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx @@ -19,7 +19,7 @@ import { Transform } from 'features/controlLayers/components/Transform/Transform import { CanvasInstanceContextProvider } from 'features/controlLayers/contexts/CanvasInstanceContextProvider'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectSelectedCanvasId } from 'features/controlLayers/store/selectors'; +import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; @@ -119,7 +119,7 @@ const Canvas = memo(({ canvasId }: CanvasProps) => { Canvas.displayName = 'Canvas'; export const CanvasWorkspacePanel = memo(() => { - const canvasId = useAppSelector(selectSelectedCanvasId); + const canvasId = useAppSelector(selectActiveCanvasId); return ( Date: Sat, 27 Sep 2025 12:59:34 +0200 Subject: [PATCH 06/16] canvasStagingAreaSlice refactored --- .../listeners/modelSelected.ts | 4 +- .../listeners/setDefaultSettings.ts | 4 +- .../store/canvasStagingAreaSlice.ts | 46 +++++++++++-------- 3 files changed, 30 insertions(+), 24 deletions(-) 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 2c62551a8b8..aa572597e15 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 @@ -3,7 +3,7 @@ import type { AppStartListening } from 'app/store/store'; import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice'; import { buildSelectIsStagingBySessionId, - selectSelectedCanvasSessionId, + selectActiveCanvasSessionId, } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice'; @@ -162,7 +162,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = if (modelBase !== state.params.model?.base) { // Sync generate tab settings whenever the model base changes dispatch(syncedToOptimalDimension()); - const sessionId = selectSelectedCanvasSessionId(state); + const sessionId = selectActiveCanvasSessionId(state); const selectIsStaging = buildSelectIsStagingBySessionId(sessionId); const isStaging = selectIsStaging(state); if (!isStaging) { 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 1550a1394ed..b8406a5c1fd 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 @@ -3,7 +3,7 @@ import { isNil } from 'es-toolkit'; import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; import { buildSelectIsStagingBySessionId, - selectSelectedCanvasSessionId, + selectActiveCanvasSessionId, } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { heightChanged, @@ -118,7 +118,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni } const setSizeOptions = { updateAspectRatio: true, clamp: true }; - const sessionId = selectSelectedCanvasSessionId(state); + const sessionId = selectActiveCanvasSessionId(state); const selectIsStaging = buildSelectIsStagingBySessionId(sessionId); const isStaging = selectIsStaging(state); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 7d673ba874a..66ee0c80f7b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -17,14 +17,14 @@ import { import { selectActiveCanvasId } from './selectors'; const zCanvasSessionState = z.object({ - canvasId: z.string(), + canvasId: z.string().min(1), canvasSessionId: z.string(), canvasDiscardedQueueItems: z.array(z.number().int()), }); type CanvasSessionState = z.infer; const zCanvasStagingAreaState = z.object({ _version: z.literal(2), - sessions: z.array(zCanvasSessionState), + sessions: z.record(z.string(), zCanvasSessionState), }); type CanvasStagingAreaState = z.infer; @@ -39,17 +39,17 @@ const getInitialCanvasSessionState = (canvasId: string): CanvasSessionState => ( const getInitialState = (): CanvasStagingAreaState => ({ _version: 2, - sessions: [], + sessions: {}, }); -const slice = createSlice({ +const canvasStagingAreaSlice = createSlice({ name: 'canvasSession', initialState: getInitialState(), reducers: { canvasQueueItemDiscarded: (state, action: CanvasPayloadAction<{ itemId: number }>) => { const { canvasId, itemId } = action.payload; - const session = state.sessions.find((session) => session.canvasId === canvasId); + const session = state.sessions[canvasId]; if (!session) { return; } @@ -62,7 +62,7 @@ const slice = createSlice({ reducer: (state, action: CanvasPayloadAction<{ canvasSessionId: string }>) => { const { canvasId, canvasSessionId } = action.payload; - const session = state.sessions.find((session) => session.canvasId === canvasId); + const session = state.sessions[canvasId]; if (!session) { return; } @@ -83,25 +83,27 @@ const slice = createSlice({ extraReducers(builder) { builder.addCase(canvasCreated, (state, action) => { const session = getInitialCanvasSessionState(action.payload.canvasId); - state.sessions.push(session); + state.sessions[session.canvasId] = session; }); builder.addCase(canvasRemoved, (state, action) => { - state.sessions = state.sessions.filter((session) => session.canvasId !== action.payload.canvasId); + delete state.sessions[action.payload.canvasId]; }); builder.addCase(canvasMultiCanvasMigrated, (state, action) => { - const session = state.sessions.find((session) => session.canvasId === MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER); + const session = state.sessions[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; if (!session) { return; } session.canvasId = action.payload.canvasId; + state.sessions[session.canvasId] = session; + delete state.sessions[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; }); }, }); -export const { canvasSessionReset, canvasQueueItemDiscarded } = slice.actions; +export const { canvasSessionReset, canvasQueueItemDiscarded } = canvasStagingAreaSlice.actions; -export const canvasSessionSliceConfig: SliceConfig = { - slice, +export const canvasSessionSliceConfig: SliceConfig = { + slice: canvasStagingAreaSlice, schema: zCanvasStagingAreaState, getInitialState, persistConfig: { @@ -119,7 +121,7 @@ export const canvasSessionSliceConfig: SliceConfig = { state = { _version: 2, - sessions: [session], + sessions: { [session.canvasId]: session }, }; } @@ -128,28 +130,32 @@ export const canvasSessionSliceConfig: SliceConfig = { }, }; -const findSessionByCanvasId = (sessions: CanvasSessionState[], canvasId: string) => { - const session = sessions.find((s) => s.canvasId === canvasId); +const findSessionByCanvasId = (sessions: Record, canvasId: string) => { + const session = sessions[canvasId]; assert(session, 'Session must exist for a canvas once the canvas has been created'); return session; }; export const selectCanvasSessionByCanvasId = (state: RootState, canvasId: string) => findSessionByCanvasId(state.canvasSession.sessions, canvasId); -const selectSelectedCanvasSession = (state: RootState) => { +const selectActiveCanvasSession = (state: RootState) => { const canvasId = selectActiveCanvasId(state); return findSessionByCanvasId(state.canvasSession.sessions, canvasId); }; +export const selectCanvasSessionBySessionId = (state: RootState, sessionId: string) => { + const session = Object.values(state.canvasSession.sessions).find((s) => s.canvasSessionId === sessionId); + assert(session, 'Session does not exist'); + return session; +}; export const selectCanvasSessionId = (state: RootState, canvasId: string) => { const session = selectCanvasSessionByCanvasId(state, canvasId); return session.canvasSessionId; }; -export const selectSelectedCanvasSessionId = (state: RootState) => { - const session = selectSelectedCanvasSession(state); +export const selectActiveCanvasSessionId = (state: RootState) => { + const session = selectActiveCanvasSession(state); return session.canvasSessionId; }; const selectCanvasSessionDiscardedItemsBySessionId = (state: RootState, sessionId: string) => { - const session = state.canvasSession.sessions.find((s) => s.canvasSessionId === sessionId); - assert(session, 'Session does not exist'); + const session = selectCanvasSessionBySessionId(state, sessionId); return session.canvasDiscardedQueueItems; }; export const buildSelectCanvasQueueItemsBySessionId = (sessionId: string) => From 96b7ba2ebd97d8647b8eb18055969a77d8b921c3 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Sat, 27 Sep 2025 16:30:56 +0200 Subject: [PATCH 07/16] canvasSettingsSlice refactored --- invokeai/frontend/web/src/app/store/store.ts | 4 +- .../store/canvasSettingsSlice.ts | 248 +++++++++--------- .../controlLayers/store/canvasSlice.ts | 20 +- .../store/canvasStagingAreaSlice.ts | 2 +- 4 files changed, 131 insertions(+), 143 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index a9864938f2f..9b17cc479e9 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -21,7 +21,7 @@ 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 { canvasSettingsReducer, canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice'; import { canvasSliceConfig, migrateCanvas, undoableCanvasesReducer } from 'features/controlLayers/store/canvasSlice'; import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; @@ -89,7 +89,7 @@ const SLICE_CONFIGS = { const ALL_REDUCERS = { [api.reducerPath]: api.reducer, [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer, - [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer, + [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsReducer, [canvasSliceConfig.slice.reducerPath]: undoableCanvasesReducer, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer, [configSliceConfig.slice.reducerPath]: configSliceConfig.slice.reducer, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index 7fb44425726..aa8f9b1c5c5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -1,5 +1,5 @@ -import type { PayloadAction, Selector } from '@reduxjs/toolkit'; -import { createSelector, createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector, UnknownAction } 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 { isPlainObject } from 'es-toolkit'; @@ -18,7 +18,7 @@ import { const zAutoSwitchMode = z.enum(['off', 'switch_on_start', 'switch_on_finish']); export type AutoSwitchMode = z.infer; -const zCanvasSharedSettingsState = z.object({ +const zCanvasSharedSettings = z.object({ /** * Whether to show HUD (Heads-Up Display) on the canvas. */ @@ -89,9 +89,9 @@ const zCanvasSharedSettingsState = z.object({ */ stagingAreaAutoSwitch: zAutoSwitchMode, }); -type CanvasSharedSettingsState = z.infer; +type CanvasSharedSettings = z.infer; -const zCanvasInstanceSettingsState = z.object({ +const zCanvasInstanceSettings = z.object({ canvasId: z.string(), /** * The width of the brush tool. @@ -108,16 +108,16 @@ const zCanvasInstanceSettingsState = z.object({ bgColor: zRgbaColor, fgColor: zRgbaColor, }); -type CanvasInstanceSettingsState = z.infer; +type CanvasInstanceSettings = z.infer; const zCanvasSettingsState = z.object({ _version: z.literal(1), - shared: zCanvasSharedSettingsState, - canvases: z.array(zCanvasInstanceSettingsState), + shared: zCanvasSharedSettings, + canvases: z.record(z.string(), zCanvasInstanceSettings), }); type CanvasSettingsState = z.infer; -const getInitialCanvasSharedSettingsState = (): CanvasSharedSettingsState => ({ +const getInitialCanvasSharedSettings = (): CanvasSharedSettings => ({ showHUD: true, clipToBbox: false, dynamicGrid: false, @@ -135,7 +135,7 @@ const getInitialCanvasSharedSettingsState = (): CanvasSharedSettingsState => ({ saveAllImagesToGallery: false, stagingAreaAutoSwitch: 'switch_on_start', }); -const getInitialCanvasInstanceSettingsState = (canvasId: string): CanvasInstanceSettingsState => ({ +const getInitialCanvasInstanceSettings = (canvasId: string): CanvasInstanceSettings => ({ canvasId, brushWidth: 50, eraserWidth: 50, @@ -143,18 +143,18 @@ const getInitialCanvasInstanceSettingsState = (canvasId: string): CanvasInstance bgColor: RGBA_BLACK, fgColor: RGBA_WHITE, }); -const getInitialState = (): CanvasSettingsState => ({ +const getInitialCanvasSettingsState = (): CanvasSettingsState => ({ _version: 1, - shared: getInitialCanvasSharedSettingsState(), - canvases: [], + shared: getInitialCanvasSharedSettings(), + canvases: {}, }); -type CanvasPayload = { canvasId: string } & T; -type CanvasPayloadAction = PayloadAction>; +type PayloadWithCanvasId

= P & { canvasId: string }; +type CanvasPayloadAction

= PayloadAction>; -const slice = createSlice({ +const canvasSettingsSlice = createSlice({ name: 'canvasSettings', - initialState: getInitialState(), + initialState: getInitialCanvasSettingsState(), reducers: { settingsClipToBboxChanged: (state, action: PayloadAction<{ clipToBbox: boolean }>) => { const { clipToBbox } = action.payload; @@ -167,67 +167,6 @@ const slice = createSlice({ settingsShowHUDToggled: (state) => { state.shared.showHUD = !state.shared.showHUD; }, - settingsBrushWidthChanged: (state, action: CanvasPayloadAction<{ brushWidth: number }>) => { - const { canvasId, brushWidth } = action.payload; - - const settings = state.canvases.find((settings) => settings.canvasId === canvasId); - if (!settings) { - return; - } - - settings.brushWidth = Math.round(brushWidth); - }, - settingsEraserWidthChanged: (state, action: CanvasPayloadAction<{ eraserWidth: number }>) => { - const { canvasId, eraserWidth } = action.payload; - - const settings = state.canvases.find((settings) => settings.canvasId === canvasId); - if (!settings) { - return; - } - - settings.eraserWidth = Math.round(eraserWidth); - }, - settingsActiveColorToggled: (state, action: CanvasPayloadAction) => { - const { canvasId } = action.payload; - - const settings = state.canvases.find((settings) => settings.canvasId === canvasId); - if (!settings) { - return; - } - - settings.activeColor = settings.activeColor === 'bgColor' ? 'fgColor' : 'bgColor'; - }, - settingsBgColorChanged: (state, action: CanvasPayloadAction<{ bgColor: Partial }>) => { - const { canvasId, bgColor } = action.payload; - - const settings = state.canvases.find((settings) => settings.canvasId === canvasId); - if (!settings) { - return; - } - - settings.bgColor = { ...settings.bgColor, ...bgColor }; - }, - settingsFgColorChanged: (state, action: CanvasPayloadAction<{ fgColor: Partial }>) => { - const { canvasId, fgColor } = action.payload; - - const settings = state.canvases.find((settings) => settings.canvasId === canvasId); - if (!settings) { - return; - } - - settings.fgColor = { ...settings.fgColor, ...fgColor }; - }, - settingsColorsSetToDefault: (state, action: CanvasPayloadAction) => { - const { canvasId } = action.payload; - - const settings = state.canvases.find((settings) => settings.canvasId === canvasId); - if (!settings) { - return; - } - - settings.bgColor = RGBA_BLACK; - settings.fgColor = RGBA_WHITE; - }, settingsInvertScrollForToolWidthChanged: (state, action: PayloadAction<{ invertScrollForToolWidth: boolean }>) => { const { invertScrollForToolWidth } = action.payload; @@ -268,7 +207,7 @@ const slice = createSlice({ }, settingsStagingAreaAutoSwitchChanged: ( state, - action: PayloadAction<{ stagingAreaAutoSwitch: CanvasSharedSettingsState['stagingAreaAutoSwitch'] }> + action: PayloadAction<{ stagingAreaAutoSwitch: CanvasSharedSettings['stagingAreaAutoSwitch'] }> ) => { const { stagingAreaAutoSwitch } = action.payload; @@ -277,32 +216,62 @@ const slice = createSlice({ }, extraReducers(builder) { builder.addCase(canvasCreated, (state, action) => { - const canvasSettings = getInitialCanvasInstanceSettingsState(action.payload.canvasId); - state.canvases.push(canvasSettings); + const canvasSettings = getInitialCanvasInstanceSettings(action.payload.canvasId); + state.canvases[canvasSettings.canvasId] = canvasSettings; }); builder.addCase(canvasRemoved, (state, action) => { - state.canvases = state.canvases.filter((settings) => settings.canvasId !== action.payload.canvasId); + delete state.canvases[action.payload.canvasId]; }); builder.addCase(canvasMultiCanvasMigrated, (state, action) => { - const settings = state.canvases.find((settings) => settings.canvasId === MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER); + const settings = state.canvases[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; if (!settings) { return; } settings.canvasId = action.payload.canvasId; + state.canvases[settings.canvasId] = settings; + delete state.canvases[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; }); }, }); +const canvasInstanceSettingsSlice = createSlice({ + name: 'canvasSettings', + initialState: {} as CanvasInstanceSettings, + reducers: { + settingsBrushWidthChanged: (state, action: CanvasPayloadAction<{ brushWidth: number }>) => { + const { brushWidth } = action.payload; + + state.brushWidth = Math.round(brushWidth); + }, + settingsEraserWidthChanged: (state, action: CanvasPayloadAction<{ eraserWidth: number }>) => { + const { eraserWidth } = action.payload; + + state.eraserWidth = Math.round(eraserWidth); + }, + settingsActiveColorToggled: (state, _action: CanvasPayloadAction) => { + state.activeColor = state.activeColor === 'bgColor' ? 'fgColor' : 'bgColor'; + }, + settingsBgColorChanged: (state, action: CanvasPayloadAction<{ bgColor: Partial }>) => { + const { bgColor } = action.payload; + + state.bgColor = { ...state.bgColor, ...bgColor }; + }, + settingsFgColorChanged: (state, action: CanvasPayloadAction<{ fgColor: Partial }>) => { + const { fgColor } = action.payload; + + state.fgColor = { ...state.fgColor, ...fgColor }; + }, + settingsColorsSetToDefault: (state, _action: CanvasPayloadAction) => { + state.bgColor = RGBA_BLACK; + state.fgColor = RGBA_WHITE; + }, + }, +}); + export const { settingsClipToBboxChanged, settingsDynamicGridToggled, settingsShowHUDToggled, - settingsBrushWidthChanged, - settingsEraserWidthChanged, - settingsActiveColorToggled, - settingsBgColorChanged, - settingsFgColorChanged, - settingsColorsSetToDefault, settingsInvertScrollForToolWidthChanged, settingsOutputOnlyMaskedRegionsToggled, settingsAutoProcessToggled, @@ -316,28 +285,57 @@ export const { settingsRuleOfThirdsToggled, settingsSaveAllImagesToGalleryToggled, settingsStagingAreaAutoSwitchChanged, -} = slice.actions; +} = canvasSettingsSlice.actions; + +export const { + settingsBrushWidthChanged, + settingsEraserWidthChanged, + settingsActiveColorToggled, + settingsBgColorChanged, + settingsFgColorChanged, + settingsColorsSetToDefault, +} = canvasInstanceSettingsSlice.actions; + +const isCanvasInstanceSettingsAction = isAnyOf(...Object.values(canvasInstanceSettingsSlice.actions)); + +export const canvasSettingsReducer = (state: CanvasSettingsState, action: UnknownAction): CanvasSettingsState => { + state = canvasSettingsSlice.reducer(state, action); -export const canvasSettingsSliceConfig: SliceConfig = { - slice, + if (!isCanvasInstanceSettingsAction(action)) { + return state; + } + + const canvasId = action.payload.canvasId; + + return { + ...state, + canvases: { + ...state.canvases, + [canvasId]: canvasInstanceSettingsSlice.reducer(state.canvases[canvasId], action), + }, + }; +}; + +export const canvasSettingsSliceConfig: SliceConfig = { + slice: canvasSettingsSlice, schema: zCanvasSettingsState, - getInitialState, + getInitialState: getInitialCanvasSettingsState, persistConfig: { migrate: (state) => { assert(isPlainObject(state)); if (!('_version' in state)) { // Migrate from v1: slice represented a canvas settings instance -> slice represents multiple canvas settings instances - const canvas = { + const settings = { canvasId: MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, ...state, - } as CanvasInstanceSettingsState; + } as CanvasInstanceSettings; state = { _version: 1, shared: { ...state, }, - canvases: [canvas], + canvases: { [settings.canvasId]: settings }, }; } @@ -359,52 +357,42 @@ export const buildSelectCanvasSettingsByCanvasId = (canvasId: string) => ); const selectCanvasSharedSettings = (state: RootState) => state.canvasSettings.shared; const selectCanvasInstanceSettings = (state: RootState, canvasId: string) => { - const settings = state.canvasSettings.canvases.find((settings) => settings.canvasId === canvasId); + const settings = state.canvasSettings.canvases[canvasId]; assert(settings, 'Settings must exist for a canvas once the canvas has been created'); return settings; }; const buildCanvasSharedSettingsSelector = - (selector: Selector) => + (selector: Selector) => (state: RootState) => selector(selectCanvasSharedSettings(state)); const buildCanvasInstanceSettingsSelector = - (selector: Selector) => + (selector: Selector) => (state: RootState, canvasId: string) => selector(selectCanvasInstanceSettings(state, canvasId)); -export const selectPreserveMask = buildCanvasSharedSettingsSelector((settings) => settings.preserveMask); +export const selectPreserveMask = buildCanvasSharedSettingsSelector((state) => state.preserveMask); export const selectOutputOnlyMaskedRegions = buildCanvasSharedSettingsSelector( - (settings) => settings.outputOnlyMaskedRegions + (state) => state.outputOnlyMaskedRegions ); -export const selectDynamicGrid = buildCanvasSharedSettingsSelector((settings) => settings.dynamicGrid); +export const selectDynamicGrid = buildCanvasSharedSettingsSelector((state) => state.dynamicGrid); export const selectInvertScrollForToolWidth = buildCanvasSharedSettingsSelector( - (settings) => settings.invertScrollForToolWidth -); -export const selectBboxOverlay = buildCanvasSharedSettingsSelector((settings) => settings.bboxOverlay); -export const selectShowHUD = buildCanvasSharedSettingsSelector((settings) => settings.showHUD); -export const selectClipToBbox = buildCanvasSharedSettingsSelector((settings) => settings.clipToBbox); -export const selectAutoProcess = buildCanvasSharedSettingsSelector((settings) => settings.autoProcess); -export const selectSnapToGrid = buildCanvasSharedSettingsSelector((settings) => settings.snapToGrid); -export const selectShowProgressOnCanvas = buildCanvasSharedSettingsSelector( - (settings) => settings.showProgressOnCanvas -); -export const selectIsolatedStagingPreview = buildCanvasSharedSettingsSelector( - (settings) => settings.isolatedStagingPreview -); -export const selectIsolatedLayerPreview = buildCanvasSharedSettingsSelector( - (settings) => settings.isolatedLayerPreview -); -export const selectPressureSensitivity = buildCanvasSharedSettingsSelector((settings) => settings.pressureSensitivity); -export const selectRuleOfThirds = buildCanvasSharedSettingsSelector((settings) => settings.ruleOfThirds); -export const selectSaveAllImagesToGallery = buildCanvasSharedSettingsSelector( - (settings) => settings.saveAllImagesToGallery -); -export const selectStagingAreaAutoSwitch = buildCanvasSharedSettingsSelector( - (settings) => settings.stagingAreaAutoSwitch + (state) => state.invertScrollForToolWidth ); -export const selectActiveColor = buildCanvasInstanceSettingsSelector((settings) => settings.activeColor); -export const selectBgColor = buildCanvasInstanceSettingsSelector((settings) => settings.bgColor); -export const selectFgColor = buildCanvasInstanceSettingsSelector((settings) => settings.fgColor); -export const selectBrushWidth = buildCanvasInstanceSettingsSelector((settings) => settings.brushWidth); -export const selectEraserWidth = buildCanvasInstanceSettingsSelector((settings) => settings.eraserWidth); +export const selectBboxOverlay = buildCanvasSharedSettingsSelector((state) => state.bboxOverlay); +export const selectShowHUD = buildCanvasSharedSettingsSelector((state) => state.showHUD); +export const selectClipToBbox = buildCanvasSharedSettingsSelector((state) => state.clipToBbox); +export const selectAutoProcess = buildCanvasSharedSettingsSelector((state) => state.autoProcess); +export const selectSnapToGrid = buildCanvasSharedSettingsSelector((state) => state.snapToGrid); +export const selectShowProgressOnCanvas = buildCanvasSharedSettingsSelector((state) => state.showProgressOnCanvas); +export const selectIsolatedStagingPreview = buildCanvasSharedSettingsSelector((state) => state.isolatedStagingPreview); +export const selectIsolatedLayerPreview = buildCanvasSharedSettingsSelector((state) => state.isolatedLayerPreview); +export const selectPressureSensitivity = buildCanvasSharedSettingsSelector((state) => state.pressureSensitivity); +export const selectRuleOfThirds = buildCanvasSharedSettingsSelector((state) => state.ruleOfThirds); +export const selectSaveAllImagesToGallery = buildCanvasSharedSettingsSelector((state) => state.saveAllImagesToGallery); +export const selectStagingAreaAutoSwitch = buildCanvasSharedSettingsSelector((state) => state.stagingAreaAutoSwitch); +export const selectActiveColor = buildCanvasInstanceSettingsSelector((state) => state.activeColor); +export const selectBgColor = buildCanvasInstanceSettingsSelector((state) => state.bgColor); +export const selectFgColor = buildCanvasInstanceSettingsSelector((state) => state.fgColor); +export const selectBrushWidth = buildCanvasInstanceSettingsSelector((state) => state.brushWidth); +export const selectEraserWidth = buildCanvasInstanceSettingsSelector((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 6d0bf5719f0..07b72ba1972 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -168,14 +168,14 @@ const getNextCanvasName = (canvases: CanvasStateWithHistory[]): string => { }; type PayloadWithCanvasId

= P & { canvasId: string }; -type PayloadActionWithCanvasId

= PayloadAction>; +type CanvasPayloadAction

= PayloadAction>; const canvasesSlice = createSlice({ name: 'canvas', initialState: getInitialCanvasesHistoryState(), reducers: { canvasAdded: { - reducer: (state, action: PayloadActionWithCanvasId<{ isSelected?: boolean }>) => { + reducer: (state, action: CanvasPayloadAction<{ isSelected?: boolean }>) => { const { canvasId, isSelected } = action.payload; const name = getNextCanvasName(Object.values(state.canvases)); @@ -193,12 +193,12 @@ const canvasesSlice = createSlice({ }; }, }, - canvasCreated: (_state, _action: PayloadActionWithCanvasId) => {}, + canvasCreated: (_state, _action: CanvasPayloadAction) => {}, canvasMigrated: (state) => { delete state.migration; }, - canvasMultiCanvasMigrated: (_state, _action: PayloadActionWithCanvasId) => {}, - canvasActivated: (state, action: PayloadActionWithCanvasId) => { + canvasMultiCanvasMigrated: (_state, _action: CanvasPayloadAction) => {}, + canvasActivated: (state, action: CanvasPayloadAction) => { const { canvasId } = action.payload; const canvas = state.canvases[canvasId]?.present; @@ -208,7 +208,7 @@ const canvasesSlice = createSlice({ state.activeCanvasId = canvas.id; }, - canvasDeleted: (state, action: PayloadActionWithCanvasId) => { + canvasDeleted: (state, action: CanvasPayloadAction) => { const { canvasId } = action.payload; const canvasIds = Object.keys(state.canvases); @@ -227,7 +227,7 @@ const canvasesSlice = createSlice({ state.activeCanvasId = canvasIds[nextIndex]!; delete state.canvases[canvas.id]; }, - canvasRemoved: (_state, _action: PayloadActionWithCanvasId) => {}, + canvasRemoved: (_state, _action: CanvasPayloadAction) => {}, }, }); @@ -235,7 +235,7 @@ const canvasSlice = createSlice({ name: 'canvas', initialState: {} as CanvasState, reducers: { - canvasNameChanged: (state, action: PayloadActionWithCanvasId<{ name: string }>) => { + canvasNameChanged: (state, action: CanvasPayloadAction<{ name: string }>) => { const { name } = action.payload; state.name = name; @@ -2049,7 +2049,7 @@ export const undoableCanvasesReducer = ( return state; } - const canvasId = isPayloadActionWithCanvasId(action) ? action.payload.canvasId : state.activeCanvasId; + const canvasId = isCanvasPayloadAction(action) ? action.payload.canvasId : state.activeCanvasId; return { ...state, @@ -2060,7 +2060,7 @@ export const undoableCanvasesReducer = ( }; }; -const isPayloadActionWithCanvasId = (action: UnknownAction): action is PayloadActionWithCanvasId => { +const isCanvasPayloadAction = (action: UnknownAction): action is CanvasPayloadAction => { return ( typeof action.payload === 'object' && action.payload !== null && diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 66ee0c80f7b..04f882fdcd7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -141,7 +141,7 @@ const selectActiveCanvasSession = (state: RootState) => { const canvasId = selectActiveCanvasId(state); return findSessionByCanvasId(state.canvasSession.sessions, canvasId); }; -export const selectCanvasSessionBySessionId = (state: RootState, sessionId: string) => { +const selectCanvasSessionBySessionId = (state: RootState, sessionId: string) => { const session = Object.values(state.canvasSession.sessions).find((s) => s.canvasSessionId === sessionId); assert(session, 'Session does not exist'); return session; From 16caef902cacd8c01e541f15fbdec4e9946dcac9 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Sun, 28 Sep 2025 09:05:58 +0200 Subject: [PATCH 08/16] paramsSlice refactored --- .../frontend/web/.storybook/ReduxInit.tsx | 11 +- .../listeners/appConfigReceived.ts | 9 +- .../listeners/modelSelected.ts | 24 +- .../listeners/modelsLoaded.ts | 35 +- .../listeners/setDefaultSettings.ts | 29 +- invokeai/frontend/web/src/app/store/store.ts | 4 +- .../common/components/SessionMenuItems.tsx | 7 +- .../common/hooks/useGroupedModelCombobox.ts | 4 +- .../components/ParamDenoisingStrength.tsx | 10 +- .../components/RefImage/RefImageImage.tsx | 9 +- .../RegionalGuidanceRefImageImage.tsx | 9 +- .../controlLayers/hooks/useCanvasId.ts | 4 +- .../konva/CanvasStateApiModule.ts | 8 +- .../features/controlLayers/store/actions.ts | 9 + .../store/canvasSettingsSlice.ts | 8 +- .../controlLayers/store/canvasSlice.ts | 24 +- .../store/canvasStagingAreaSlice.ts | 8 +- .../controlLayers/store/paramsSlice.ts | 604 ++++++++++++------ .../features/controlLayers/store/selectors.ts | 18 - .../src/features/controlLayers/store/types.ts | 41 +- .../web/src/features/metadata/parsing.tsx | 45 +- .../util/graph/buildLinearBatchConfig.ts | 3 +- .../graph/buildMultidiffusionUpscaleGraph.ts | 10 +- .../util/graph/generation/addFLUXFill.ts | 4 +- .../nodes/util/graph/generation/addInpaint.ts | 4 +- .../util/graph/generation/addOutpaint.ts | 4 +- .../util/graph/generation/addSDXLRefiner.ts | 3 +- .../util/graph/generation/addSeamless.ts | 3 +- .../graph/generation/buildCogView4Graph.ts | 4 +- .../util/graph/generation/buildFLUXGraph.ts | 4 +- .../graph/generation/buildRunwayVideoGraph.ts | 4 +- .../util/graph/generation/buildSD1Graph.ts | 4 +- .../util/graph/generation/buildSD3Graph.ts | 4 +- .../util/graph/generation/buildSDXLGraph.ts | 4 +- .../graph/generation/buildVeo3VideoGraph.ts | 4 +- .../nodes/util/graph/graphBuilderUtils.ts | 10 +- .../Advanced/ParamCFGRescaleMultiplier.tsx | 12 +- .../Advanced/ParamCLIPEmbedModelSelect.tsx | 14 +- .../Advanced/ParamCLIPGEmbedModelSelect.tsx | 14 +- .../Advanced/ParamCLIPLEmbedModelSelect.tsx | 14 +- .../components/Advanced/ParamClipSkip.tsx | 10 +- .../ParamOptimizedDenoisingToggle.tsx | 9 +- .../Advanced/ParamT5EncoderModelSelect.tsx | 14 +- .../parameters/components/Bbox/BboxHeight.tsx | 3 +- .../components/Bbox/BboxScaledHeight.tsx | 3 +- .../components/Bbox/BboxScaledWidth.tsx | 3 +- .../Bbox/BboxSetOptimalSizeButton.tsx | 3 +- .../parameters/components/Bbox/BboxWidth.tsx | 3 +- .../ParamCanvasCoherenceEdgeSize.tsx | 14 +- .../ParamCanvasCoherenceMinDenoise.tsx | 9 +- .../ParamCanvasCoherenceMode.tsx | 14 +- .../MaskAdjustment/ParamMaskBlur.tsx | 10 +- .../ParamInfillColorOptions.tsx | 9 +- .../InfillAndScaling/ParamInfillMethod.tsx | 10 +- .../ParamInfillPatchmatchDownscaleSize.tsx | 9 +- .../InfillAndScaling/ParamInfillTilesize.tsx | 15 +- .../Core/NegativePromptToggleButton.tsx | 16 +- .../components/Core/ParamCFGScale.tsx | 8 +- .../components/Core/ParamGuidance.tsx | 8 +- .../components/Core/ParamNegativePrompt.tsx | 14 +- .../components/Core/ParamPositivePrompt.tsx | 20 +- .../components/Core/ParamScheduler.tsx | 10 +- .../parameters/components/Core/ParamSteps.tsx | 10 +- .../components/Core/PositivePromptHistory.tsx | 19 +- .../DimensionsAspectRatioSelect.tsx | 9 +- .../Dimensions/DimensionsHeight.tsx | 18 +- .../DimensionsLockAspectRatioButton.tsx | 9 +- .../DimensionsSetOptimalSizeButton.tsx | 11 +- .../Dimensions/DimensionsSwapButton.tsx | 9 +- .../components/Dimensions/DimensionsWidth.tsx | 18 +- .../parameters/components/ModelPicker.tsx | 4 +- .../Seamless/ParamSeamlessXAxis.tsx | 10 +- .../Seamless/ParamSeamlessYAxis.tsx | 10 +- .../components/Seed/ParamSeedNumberInput.tsx | 13 +- .../components/Seed/ParamSeedRandomize.tsx | 14 +- .../components/Seed/ParamSeedShuffle.tsx | 10 +- .../Upscale/ParamUpscaleCFGScale.tsx | 8 +- .../Upscale/ParamUpscaleScheduler.tsx | 14 +- .../VAEModel/ParamFLUXVAEModelSelect.tsx | 10 +- .../VAEModel/ParamVAEModelSelect.tsx | 10 +- .../components/VAEModel/ParamVAEPrecision.tsx | 10 +- .../PromptExpansionResultOverlay.tsx | 18 +- .../components/QueueIterationsNumberInput.tsx | 10 +- .../features/queue/hooks/useEnqueueCanvas.ts | 14 +- .../queue/hooks/useEnqueueGenerate.ts | 12 +- .../queue/hooks/useEnqueueUpscaling.ts | 12 +- .../features/queue/hooks/useEnqueueVideo.ts | 8 +- .../queue/hooks/useEnqueueWorkflows.ts | 4 +- .../web/src/features/queue/store/readiness.ts | 18 +- .../SDXLRefiner/ParamSDXLRefinerCFGScale.tsx | 8 +- .../ParamSDXLRefinerModelSelect.tsx | 12 +- ...ParamSDXLRefinerNegativeAestheticScore.tsx | 10 +- ...ParamSDXLRefinerPositiveAestheticScore.tsx | 10 +- .../SDXLRefiner/ParamSDXLRefinerScheduler.tsx | 14 +- .../SDXLRefiner/ParamSDXLRefinerStart.tsx | 8 +- .../SDXLRefiner/ParamSDXLRefinerSteps.tsx | 10 +- .../AdvancedSettingsAccordion.tsx | 4 +- .../UpscaleTabAdvancedSettingsAccordion.tsx | 4 +- .../RefinerSettingsAccordion.tsx | 4 +- .../components/ActiveStylePreset.tsx | 13 +- .../SettingsModal/SettingsModal.tsx | 11 +- 101 files changed, 1018 insertions(+), 659 deletions(-) diff --git a/invokeai/frontend/web/.storybook/ReduxInit.tsx b/invokeai/frontend/web/.storybook/ReduxInit.tsx index b4989c75564..6a7c2c8593d 100644 --- a/invokeai/frontend/web/.storybook/ReduxInit.tsx +++ b/invokeai/frontend/web/.storybook/ReduxInit.tsx @@ -3,18 +3,19 @@ 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'; +import { useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; /** * Initializes some state for storybook. Must be in a different component * so that it is run inside the redux context. */ export const ReduxInit = memo(({ children }: PropsWithChildren) => { - const dispatch = useAppDispatch(); + const dispatch = useParamsDispatch(); useGlobalModifiersInit(); useEffect(() => { - dispatch( - modelChanged({ model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' } }) - ); + dispatch(modelChanged, { + model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' }, + }); }, [dispatch]); return children; 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..933f47d1401 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,20 +1,21 @@ import type { AppStartListening } from 'app/store/store'; -import { setInfillMethod } from 'features/controlLayers/store/paramsSlice'; +import { paramsDispatch, selectActiveParams, 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 = selectActiveParams(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'. // TODO(psyche): lama _should_ always be in the list, but the API doesn't guarantee it... const infillMethod = infill_methods.includes('lama') ? 'lama' : 'tile'; - dispatch(setInfillMethod(infillMethod)); + paramsDispatch(api, setInfillMethod, infillMethod); } if (!nsfw_methods.includes('nsfw_checker')) { 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 aa572597e15..9a7406efe1a 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,12 +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 { buildSelectIsStagingBySessionId, selectActiveCanvasSessionId, } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; -import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice'; +import { + paramsDispatch, + selectActiveParams, + syncedToOptimalDimension, + vaeSelected, +} from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice'; import { selectActiveCanvas, @@ -34,7 +40,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); @@ -45,7 +52,8 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = const newModel = result.data; const newBase = newModel.base; - const didBaseModelChange = state.params.model?.base !== newBase; + const params = selectActiveParams(state); + const didBaseModelChange = params.model?.base !== newBase; if (didBaseModelChange) { // we may need to reset some incompatible submodels @@ -60,9 +68,9 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = }); // handle incompatible vae - const { vae } = state.params; + const { vae } = params; if (vae && vae.base !== newBase) { - dispatch(vaeSelected(null)); + paramsDispatch(api, vaeSelected, null); modelsUpdatedDisabledOrCleared += 1; } @@ -155,13 +163,13 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } } - dispatch(modelChanged({ model: newModel, previousModel: state.params.model })); + paramsDispatch(api, 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()); + paramsDispatch(api, syncedToOptimalDimension); const sessionId = selectActiveCanvasSessionId(state); const selectIsStaging = buildSelectIsStagingBySessionId(sessionId); const isStaging = selectIsStaging(state); 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 05a3551bbb5..1113686b7e1 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,13 +1,16 @@ 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 { clipEmbedModelSelected, fluxVAESelected, - modelChanged, + paramsDispatch, refinerModelChanged, + selectActiveParams, t5EncoderModelSelected, + toStore, vaeSelected, } from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; @@ -103,7 +106,7 @@ type ModelHandler = ( ) => undefined; const handleMainModels: ModelHandler = (models, state, dispatch, log) => { - const selectedMainModel = state.params.model; + const selectedMainModel = selectActiveParams(state).model; const allMainModels = models.filter(isNonRefinerMainModelConfig).sort((a) => (a.base === 'sdxl' ? -1 : 1)); const firstModel = allMainModels[0]; @@ -113,7 +116,7 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { // Only clear the model if we have one currently selected if (selectedMainModel !== null) { log.debug({ selectedMainModel }, 'No main models available, clearing'); - dispatch(modelChanged({ model: null })); + paramsDispatch(toStore(state, dispatch), modelChanged, { model: null }); } return; } @@ -144,7 +147,7 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { }; const handleRefinerModels: ModelHandler = (models, state, dispatch, log) => { - const selectedRefinerModel = state.params.refinerModel; + const selectedRefinerModel = selectActiveParams(state).refinerModel; // `null` is a valid refiner model - no need to do anything. if (selectedRefinerModel === null) { @@ -163,12 +166,12 @@ const handleRefinerModels: ModelHandler = (models, state, dispatch, log) => { // Else, we need to clear the refiner model log.debug({ selectedRefinerModel }, 'Selected refiner model is not available, clearing'); - dispatch(refinerModelChanged(null)); + paramsDispatch(toStore(state, dispatch), refinerModelChanged, null); return; }; const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { - const selectedVAEModel = state.params.vae; + const selectedVAEModel = selectActiveParams(state).vae; // `null` is a valid VAE - it means "use the VAE baked into the currently-selected main model" if (selectedVAEModel === null) { @@ -187,7 +190,7 @@ const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { // Else, we need to clear the VAE model log.debug({ selectedVAEModel }, 'Selected VAE model is not available, clearing'); - dispatch(vaeSelected(null)); + paramsDispatch(toStore(state, dispatch), vaeSelected, null); return; }; @@ -417,7 +420,7 @@ const handleTileControlNetModel: ModelHandler = (models, state, dispatch, log) = }; const handleT5EncoderModels: ModelHandler = (models, state, dispatch, log) => { - const selectedT5EncoderModel = state.params.t5EncoderModel; + const selectedT5EncoderModel = selectActiveParams(state).t5EncoderModel; const t5EncoderModels = models.filter((m) => isT5EncoderModelConfig(m)); // If the currently selected model is available, we don't need to do anything @@ -432,20 +435,20 @@ const handleT5EncoderModels: ModelHandler = (models, state, dispatch, log) => { { selectedT5EncoderModel, firstModel }, 'No selected T5 encoder model or selected T5 encoder model is not available, selecting first available model' ); - dispatch(t5EncoderModelSelected(zParameterT5EncoderModel.parse(firstModel))); + paramsDispatch(toStore(state, dispatch), t5EncoderModelSelected, zParameterT5EncoderModel.parse(firstModel)); return; } // No available models, we should clear the selected model - but only if we have one selected if (selectedT5EncoderModel) { log.debug({ selectedT5EncoderModel }, 'Selected T5 encoder model is not available, clearing'); - dispatch(t5EncoderModelSelected(null)); + paramsDispatch(toStore(state, dispatch), t5EncoderModelSelected, null); return; } }; const handleCLIPEmbedModels: ModelHandler = (models, state, dispatch, log) => { - const selectedCLIPEmbedModel = state.params.clipEmbedModel; + const selectedCLIPEmbedModel = selectActiveParams(state).clipEmbedModel; const CLIPEmbedModels = models.filter((m) => isCLIPEmbedModelConfig(m)); // If the currently selected model is available, we don't need to do anything @@ -460,20 +463,20 @@ const handleCLIPEmbedModels: ModelHandler = (models, state, dispatch, log) => { { selectedCLIPEmbedModel, firstModel }, 'No selected CLIP embed model or selected CLIP embed model is not available, selecting first available model' ); - dispatch(clipEmbedModelSelected(zParameterCLIPEmbedModel.parse(firstModel))); + paramsDispatch(toStore(state, dispatch), clipEmbedModelSelected, zParameterCLIPEmbedModel.parse(firstModel)); return; } // No available models, we should clear the selected model - but only if we have one selected if (selectedCLIPEmbedModel) { log.debug({ selectedCLIPEmbedModel }, 'Selected CLIP embed model is not available, clearing'); - dispatch(clipEmbedModelSelected(null)); + paramsDispatch(toStore(state, dispatch), clipEmbedModelSelected, null); return; } }; const handleFLUXVAEModels: ModelHandler = (models, state, dispatch, log) => { - const selectedFLUXVAEModel = state.params.fluxVAE; + const selectedFLUXVAEModel = selectActiveParams(state).fluxVAE; const fluxVAEModels = models.filter((m) => isFluxVAEModelConfig(m)); // If the currently selected model is available, we don't need to do anything @@ -488,14 +491,14 @@ const handleFLUXVAEModels: ModelHandler = (models, state, dispatch, log) => { { selectedFLUXVAEModel, firstModel }, 'No selected FLUX VAE model or selected FLUX VAE model is not available, selecting first available model' ); - dispatch(fluxVAESelected(zParameterVAEModel.parse(firstModel))); + paramsDispatch(toStore(state, dispatch), fluxVAESelected, zParameterVAEModel.parse(firstModel)); return; } // No available models, we should clear the selected model - but only if we have one selected if (selectedFLUXVAEModel) { log.debug({ selectedFLUXVAEModel }, 'Selected FLUX VAE model is not available, clearing'); - dispatch(fluxVAESelected(null)); + paramsDispatch(toStore(state, dispatch), fluxVAESelected, null); return; } }; 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 b8406a5c1fd..6671d0190e6 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 @@ -7,6 +7,8 @@ import { } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { heightChanged, + paramsDispatch, + selectActiveParams, setCfgRescaleMultiplier, setCfgScale, setGuidance, @@ -37,10 +39,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 = selectActiveParams(state).model; if (!currentModel) { return; @@ -64,56 +67,56 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni // we store this as "default" within default settings // to distinguish it from no default set if (vae === 'default') { - dispatch(vaeSelected(null)); + paramsDispatch(api, vaeSelected, null); } else { const vaeModel = models.find((model) => model.key === vae); const result = zParameterVAEModel.safeParse(vaeModel); if (!result.success) { return; } - dispatch(vaeSelected(result.data)); + paramsDispatch(api, vaeSelected, result.data); } } if (vae_precision) { if (isParameterPrecision(vae_precision)) { - dispatch(vaePrecisionChanged(vae_precision)); + paramsDispatch(api, vaePrecisionChanged, vae_precision); } } if (guidance) { if (isParameterGuidance(guidance)) { - dispatch(setGuidance(guidance)); + paramsDispatch(api, setGuidance, guidance); } } if (cfg_scale) { if (isParameterCFGScale(cfg_scale)) { - dispatch(setCfgScale(cfg_scale)); + paramsDispatch(api, setCfgScale, cfg_scale); } } if (!isNil(cfg_rescale_multiplier)) { if (isParameterCFGRescaleMultiplier(cfg_rescale_multiplier)) { - dispatch(setCfgRescaleMultiplier(cfg_rescale_multiplier)); + paramsDispatch(api, setCfgRescaleMultiplier, cfg_rescale_multiplier); } } else { // Set this to 0 if it doesn't have a default. This value is // easy to miss in the UI when users are resetting defaults // and leaving it non-zero could lead to detrimental // effects. - dispatch(setCfgRescaleMultiplier(0)); + paramsDispatch(api, setCfgRescaleMultiplier, 0); } if (steps) { if (isParameterSteps(steps)) { - dispatch(setSteps(steps)); + paramsDispatch(api, setSteps, steps); } } if (scheduler) { if (isParameterScheduler(scheduler)) { - dispatch(setScheduler(scheduler)); + paramsDispatch(api, setScheduler, scheduler); } } const setSizeOptions = { updateAspectRatio: true, clamp: true }; @@ -125,10 +128,10 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni const activeTab = selectActiveTab(getState()); if (activeTab === 'generate') { if (isParameterWidth(width)) { - dispatch(widthChanged({ width, ...setSizeOptions })); + paramsDispatch(api, widthChanged, { width, ...setSizeOptions }); } if (isParameterHeight(height)) { - dispatch(heightChanged({ height, ...setSizeOptions })); + paramsDispatch(api, heightChanged, { height, ...setSizeOptions }); } } diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 9b17cc479e9..fa3c31a5e51 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -25,7 +25,7 @@ import { canvasSettingsReducer, canvasSettingsSliceConfig } from 'features/contr import { canvasSliceConfig, migrateCanvas, undoableCanvasesReducer } 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 { paramsSliceConfig, paramsSliceReducer } from 'features/controlLayers/store/paramsSlice'; import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice'; import { dynamicPromptsSliceConfig } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { gallerySliceConfig } from 'features/gallery/store/gallerySlice'; @@ -98,7 +98,7 @@ const ALL_REDUCERS = { [lorasSliceConfig.slice.reducerPath]: lorasSliceConfig.slice.reducer, [modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig.slice.reducer, [nodesSliceConfig.slice.reducerPath]: undoableNodesSliceReducer, - [paramsSliceConfig.slice.reducerPath]: paramsSliceConfig.slice.reducer, + [paramsSliceConfig.slice.reducerPath]: paramsSliceReducer, [queueSliceConfig.slice.reducerPath]: queueSliceConfig.slice.reducer, [refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig.slice.reducer, [stylePresetSliceConfig.slice.reducerPath]: stylePresetSliceConfig.slice.reducer, diff --git a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx index 952fa4a1f0b..6bee5eb632f 100644 --- a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx +++ b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx @@ -2,7 +2,7 @@ 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 { paramsReset } from 'features/controlLayers/store/paramsSlice'; +import { paramsReset, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,6 +11,7 @@ import { PiArrowsCounterClockwiseBold } from 'react-icons/pi'; export const SessionMenuItems = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const paramsDispatch = useParamsDispatch(); const tab = useAppSelector(selectActiveTab); const canvasManager = useCanvasManagerSafe(); @@ -20,8 +21,8 @@ export const SessionMenuItems = memo(() => { canvasManager?.stage.fitBboxToStage(); }, [dispatch, canvasManager]); const resetGenerationSettings = useCallback(() => { - dispatch(paramsReset()); - }, [dispatch]); + paramsDispatch(paramsReset); + }, [paramsDispatch]); return ( <> {tab === 'canvas' && ( diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index a7f8c812af2..e806a7b7f54 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 { selectActiveParams } 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(selectActiveParams, (params) => params.model?.base ?? 'sdxl'); export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx index bf4464bd5bd..6ead251dcab 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx @@ -8,10 +8,10 @@ import { useToken, } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import WavyLine from 'common/components/WavyLine'; -import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice'; +import { selectImg2imgStrength, setImg2imgStrength, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { selectActiveRasterLayerEntities } from 'features/controlLayers/store/selectors'; import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; @@ -25,15 +25,15 @@ const selectHasRasterLayersWithContent = createSelector( export const ParamDenoisingStrength = memo(() => { const img2imgStrength = useAppSelector(selectImg2imgStrength); - const dispatch = useAppDispatch(); + const paramsDispatch = useParamsDispatch(); const hasRasterLayersWithContent = useAppSelector(selectHasRasterLayersWithContent); const selectedModelConfig = useSelectedModelConfig(); const onChange = useCallback( (v: number) => { - dispatch(setImg2imgStrength(v)); + paramsDispatch(setImg2imgStrength, v); }, - [dispatch] + [paramsDispatch] ); const config = useAppSelector(selectImg2imgStrengthConfig); 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 11768a06f01..bef83fb4885 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx @@ -6,7 +6,7 @@ import { 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 { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice'; +import { sizeOptimized, sizeRecalled, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import type { CroppableImageWithDims } from 'features/controlLayers/store/types'; import { imageDTOToCroppableImage, imageDTOToImageWithDims } from 'features/controlLayers/store/util'; import { Editor } from 'features/cropper/lib/editor'; @@ -39,6 +39,7 @@ export const RefImageImage = memo( }: Props) => { const { t } = useTranslation(); const store = useAppStore(); + const paramsDispatch = useParamsDispatch(); 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()); + paramsDispatch(sizeRecalled, { width, height }); + paramsDispatch(sizeOptimized); } - }, [imageDTO, isStaging, store, tab]); + }, [paramsDispatch, imageDTO, isStaging, store, tab]); const edit = useCallback(() => { if (!originalImageDTO) { 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 46812f0ced5..8b6d567f365 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx @@ -5,7 +5,7 @@ import { 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 { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice'; +import { sizeOptimized, sizeRecalled, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import type { ImageWithDims } from 'features/controlLayers/store/types'; import type { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; @@ -29,6 +29,7 @@ type Props = { export const RegionalGuidanceRefImageImage = memo(({ image, onChangeImage, dndTarget, dndTargetData }: Props) => { const { t } = useTranslation(); const store = useAppStore(); + const paramsDispatch = useParamsDispatch(); 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()); + paramsDispatch(sizeRecalled, { width, height }); + paramsDispatch(sizeOptimized); } - }, [imageDTO, isStaging, store, tab]); + }, [paramsDispatch, imageDTO, isStaging, store, tab]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts index fd29b1ae989..e524a60faba 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts @@ -4,7 +4,7 @@ import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; export const useCanvasId = () => { const scopedCanvasId = useScopedCanvasIdSafe(); - const canvasId = useAppSelector(selectActiveCanvasId); + const activeCanvasId = useAppSelector(selectActiveCanvasId); - return scopedCanvasId ?? canvasId; + return scopedCanvasId ?? activeCanvasId; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index d997b2a4fb2..84d75e188c0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -30,12 +30,8 @@ import { rgAdded, } from 'features/controlLayers/store/canvasSlice'; import { selectCanvasSessionByCanvasId } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { - selectActiveCanvas, - selectAllRenderableEntities, - selectBbox, - selectGridSize, -} from 'features/controlLayers/store/selectors'; +import { selectGridSize } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveCanvas, selectAllRenderableEntities, selectBbox } from 'features/controlLayers/store/selectors'; import type { CanvasState, EntityBrushLineAddedPayload, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/actions.ts b/invokeai/frontend/web/src/features/controlLayers/store/actions.ts index 9e1d9734cda..86f1eee5cb6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/actions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/actions.ts @@ -1,4 +1,13 @@ import { createAction } from '@reduxjs/toolkit'; +import type { ParameterModel } from 'features/parameters/types/parameterSchemas'; + +import type { ParamsEnrichedPayload } from './types'; // 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>( + '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 aa8f9b1c5c5..20b13875bea 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -9,9 +9,9 @@ import { assert } from 'tsafe'; import { z } from 'zod'; import { - canvasCreated, + canvasAdding, + canvasDeleted, canvasMultiCanvasMigrated, - canvasRemoved, MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, } from './canvasSlice'; @@ -215,11 +215,11 @@ const canvasSettingsSlice = createSlice({ }, }, extraReducers(builder) { - builder.addCase(canvasCreated, (state, action) => { + builder.addCase(canvasAdding, (state, action) => { const canvasSettings = getInitialCanvasInstanceSettings(action.payload.canvasId); state.canvases[canvasSettings.canvasId] = canvasSettings; }); - builder.addCase(canvasRemoved, (state, action) => { + builder.addCase(canvasDeleted, (state, action) => { delete state.canvases[action.payload.canvasId]; }); builder.addCase(canvasMultiCanvasMigrated, (state, action) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 07b72ba1972..ce22c26630d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -8,8 +8,7 @@ import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMul 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, @@ -193,7 +192,7 @@ const canvasesSlice = createSlice({ }; }, }, - canvasCreated: (_state, _action: CanvasPayloadAction) => {}, + canvasAdding: (_state, _action: CanvasPayloadAction) => {}, canvasMigrated: (state) => { delete state.migration; }, @@ -208,7 +207,7 @@ const canvasesSlice = createSlice({ state.activeCanvasId = canvas.id; }, - canvasDeleted: (state, action: CanvasPayloadAction) => { + canvasDeleting: (state, action: CanvasPayloadAction) => { const { canvasId } = action.payload; const canvasIds = Object.keys(state.canvases); @@ -227,7 +226,7 @@ const canvasesSlice = createSlice({ state.activeCanvasId = canvasIds[nextIndex]!; delete state.canvases[canvas.id]; }, - canvasRemoved: (_state, _action: CanvasPayloadAction) => {}, + canvasDeleted: (_state, _action: CanvasPayloadAction) => {}, }, }); @@ -1809,7 +1808,7 @@ const canvasSlice = createSlice({ return resetCanvasState(state); }); builder.addCase(modelChanged, (state, action) => { - const { model } = action.payload; + const { model } = action.payload.value; /** * Because the bbox depends in part on the model, it needs to be in sync with the model. However, due to * complications with managing undo/redo history, we need to store the model in a separate slice from the canvas @@ -1883,11 +1882,10 @@ const syncScaledSize = (state: CanvasState) => { }; export const addCanvas = (payload: { isSelected?: boolean }) => (dispatch: AppDispatch) => { - const action = canvasesSlice.actions.canvasAdded(payload); - dispatch(action); + const canvasAdded = canvasesSlice.actions.canvasAdded(payload); - const { canvasId } = action.payload; - dispatch(canvasesSlice.actions.canvasCreated({ canvasId })); + dispatch(canvasesSlice.actions.canvasAdding({ canvasId: canvasAdded.payload.canvasId })); + dispatch(canvasAdded); }; export const MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER = 'multi-canvas-id-placeholder'; @@ -1903,15 +1901,15 @@ export const migrateCanvas = () => (dispatch: AppDispatch, getState: () => RootS }; export const deleteCanvas = (payload: { canvasId: string }) => (dispatch: AppDispatch) => { + dispatch(canvasesSlice.actions.canvasDeleting(payload)); dispatch(canvasesSlice.actions.canvasDeleted(payload)); - dispatch(canvasesSlice.actions.canvasRemoved(payload)); }; export const { // Canvas - canvasCreated, + canvasAdding, canvasMultiCanvasMigrated, - canvasRemoved, + canvasDeleted, canvasActivated, } = canvasesSlice.actions; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 04f882fdcd7..3004b71bfcc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -9,9 +9,9 @@ import { assert } from 'tsafe'; import z from 'zod'; import { - canvasCreated, + canvasAdding, + canvasDeleted, canvasMultiCanvasMigrated, - canvasRemoved, MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, } from './canvasSlice'; import { selectActiveCanvasId } from './selectors'; @@ -81,11 +81,11 @@ const canvasStagingAreaSlice = createSlice({ }, }, extraReducers(builder) { - builder.addCase(canvasCreated, (state, action) => { + builder.addCase(canvasAdding, (state, action) => { const session = getInitialCanvasSessionState(action.payload.canvasId); state.sessions[session.canvasId] = session; }); - builder.addCase(canvasRemoved, (state, action) => { + builder.addCase(canvasDeleted, (state, action) => { delete state.sessions[action.payload.canvasId]; }); builder.addCase(canvasMultiCanvasMigrated, (state, action) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index 3f148c5efbb..b51e275fd41 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -1,25 +1,37 @@ -import type { PayloadAction, Selector } from '@reduxjs/toolkit'; -import { createSelector, createSlice } from '@reduxjs/toolkit'; -import type { RootState } from 'app/store/store'; +import type { ActionCreatorWithPayload, Selector, UnknownAction } from '@reduxjs/toolkit'; +import { createSelector, createSlice, isAnyOf } from '@reduxjs/toolkit'; +import type { AppDispatch, RootState } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; 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 type { + AspectRatioID, + CanvasInstanceParamsState, + InstanceParamsState, + ParamsPayloadAction, + ParamsState, + RgbaColor, +} from 'features/controlLayers/store/types'; import { ASPECT_RATIO_MAP, CHATGPT_ASPECT_RATIOS, DEFAULT_ASPECT_RATIO_CONFIG, FLUX_KONTEXT_ASPECT_RATIOS, GEMINI_2_5_ASPECT_RATIOS, + getInitialCanvasInstanceParamsState, + getInitialInstanceParamsState, getInitialParamsState, IMAGEN_ASPECT_RATIOS, isChatGPT4oAspectRatioID, isFluxKontextAspectRatioID, isGemini2_5AspectRatioID, isImagenAspectRatioID, + isParamsTab, MAX_POSITIVE_PROMPT_HISTORY, + zInstanceParamsState, zParamsState, } from 'features/controlLayers/store/types'; import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; @@ -52,149 +64,158 @@ import type { ParameterVAEModel, } from 'features/parameters/types/parameterSchemas'; import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; +import type { TabName } from 'features/ui/store/uiTypes'; +import { useCallback } from 'react'; 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 { + canvasAdding, + canvasDeleted, + canvasMultiCanvasMigrated, + MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, +} from './canvasSlice'; +import { selectActiveCanvasId } from './selectors'; + +const paramsSlice = createSlice({ name: 'params', initialState: getInitialParamsState(), + reducers: {}, + extraReducers(builder) { + builder.addCase(canvasAdding, (state, action) => { + const canvasParams = getInitialCanvasInstanceParamsState(action.payload.canvasId); + state.canvases[canvasParams.canvasId] = canvasParams; + }); + builder.addCase(canvasDeleted, (state, action) => { + delete state.canvases[action.payload.canvasId]; + }); + builder.addCase(canvasMultiCanvasMigrated, (state, action) => { + const canvasParams = state.canvases[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; + if (!canvasParams) { + return; + } + canvasParams.canvasId = action.payload.canvasId; + state.canvases[canvasParams.canvasId] = canvasParams; + delete state.canvases[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; + }); + }, +}); + +const instanceParamsSlice = createSlice({ + name: 'params', + initialState: getInitialInstanceParamsState(), reducers: { - setIterations: (state, action: PayloadAction) => { - state.iterations = action.payload; + setIterations: (state, action: ParamsPayloadAction) => { + state.iterations = action.payload.value; }, - setSteps: (state, action: PayloadAction) => { - state.steps = action.payload; + setSteps: (state, action: ParamsPayloadAction) => { + state.steps = action.payload.value; }, - setCfgScale: (state, action: PayloadAction) => { - state.cfgScale = action.payload; + setCfgScale: (state, action: ParamsPayloadAction) => { + state.cfgScale = action.payload.value; }, - setUpscaleCfgScale: (state, action: PayloadAction) => { - state.upscaleCfgScale = action.payload; + setUpscaleCfgScale: (state, action: ParamsPayloadAction) => { + state.upscaleCfgScale = action.payload.value; }, - setGuidance: (state, action: PayloadAction) => { - state.guidance = action.payload; + setGuidance: (state, action: ParamsPayloadAction) => { + state.guidance = action.payload.value; }, - setCfgRescaleMultiplier: (state, action: PayloadAction) => { - state.cfgRescaleMultiplier = action.payload; + setCfgRescaleMultiplier: (state, action: ParamsPayloadAction) => { + state.cfgRescaleMultiplier = action.payload.value; }, - setScheduler: (state, action: PayloadAction) => { - state.scheduler = action.payload; + setScheduler: (state, action: ParamsPayloadAction) => { + state.scheduler = action.payload.value; }, - setUpscaleScheduler: (state, action: PayloadAction) => { - state.upscaleScheduler = action.payload; + setUpscaleScheduler: (state, action: ParamsPayloadAction) => { + state.upscaleScheduler = action.payload.value; }, - setSeed: (state, action: PayloadAction) => { - state.seed = action.payload; + setSeed: (state, action: ParamsPayloadAction) => { + state.seed = action.payload.value; state.shouldRandomizeSeed = false; }, - setImg2imgStrength: (state, action: PayloadAction) => { - state.img2imgStrength = action.payload; + setImg2imgStrength: (state, action: ParamsPayloadAction) => { + state.img2imgStrength = action.payload.value; }, - setOptimizedDenoisingEnabled: (state, action: PayloadAction) => { - state.optimizedDenoisingEnabled = action.payload; + setOptimizedDenoisingEnabled: (state, action: ParamsPayloadAction) => { + state.optimizedDenoisingEnabled = action.payload.value; }, - setSeamlessXAxis: (state, action: PayloadAction) => { - state.seamlessXAxis = action.payload; + setSeamlessXAxis: (state, action: ParamsPayloadAction) => { + state.seamlessXAxis = action.payload.value; }, - setSeamlessYAxis: (state, action: PayloadAction) => { - state.seamlessYAxis = action.payload; + setSeamlessYAxis: (state, action: ParamsPayloadAction) => { + state.seamlessYAxis = action.payload.value; }, - setShouldRandomizeSeed: (state, action: PayloadAction) => { - state.shouldRandomizeSeed = action.payload; + setShouldRandomizeSeed: (state, action: ParamsPayloadAction) => { + state.shouldRandomizeSeed = action.payload.value; }, - 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) => { + vaeSelected: (state, action: ParamsPayloadAction) => { // null is a valid VAE! - const result = zParamsState.shape.vae.safeParse(action.payload); + const result = zInstanceParamsState.shape.vae.safeParse(action.payload); if (!result.success) { return; } state.vae = result.data; }, - fluxVAESelected: (state, action: PayloadAction) => { - const result = zParamsState.shape.fluxVAE.safeParse(action.payload); + fluxVAESelected: (state, action: ParamsPayloadAction) => { + const result = zInstanceParamsState.shape.fluxVAE.safeParse(action.payload); if (!result.success) { return; } state.fluxVAE = result.data; }, - t5EncoderModelSelected: (state, action: PayloadAction) => { - const result = zParamsState.shape.t5EncoderModel.safeParse(action.payload); + t5EncoderModelSelected: (state, action: ParamsPayloadAction) => { + const result = zInstanceParamsState.shape.t5EncoderModel.safeParse(action.payload); if (!result.success) { return; } state.t5EncoderModel = result.data; }, - controlLoRAModelSelected: (state, action: PayloadAction) => { - const result = zParamsState.shape.controlLora.safeParse(action.payload); + controlLoRAModelSelected: (state, action: ParamsPayloadAction) => { + const result = zInstanceParamsState.shape.controlLora.safeParse(action.payload); if (!result.success) { return; } state.controlLora = result.data; }, - clipEmbedModelSelected: (state, action: PayloadAction) => { - const result = zParamsState.shape.clipEmbedModel.safeParse(action.payload); + clipEmbedModelSelected: (state, action: ParamsPayloadAction) => { + const result = zInstanceParamsState.shape.clipEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipEmbedModel = result.data; }, - clipLEmbedModelSelected: (state, action: PayloadAction) => { - const result = zParamsState.shape.clipLEmbedModel.safeParse(action.payload); + clipLEmbedModelSelected: (state, action: ParamsPayloadAction) => { + const result = zInstanceParamsState.shape.clipLEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipLEmbedModel = result.data; }, - clipGEmbedModelSelected: (state, action: PayloadAction) => { - const result = zParamsState.shape.clipGEmbedModel.safeParse(action.payload); + clipGEmbedModelSelected: (state, action: ParamsPayloadAction) => { + const result = zInstanceParamsState.shape.clipGEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipGEmbedModel = result.data; }, - vaePrecisionChanged: (state, action: PayloadAction) => { - state.vaePrecision = action.payload; + vaePrecisionChanged: (state, action: ParamsPayloadAction) => { + state.vaePrecision = action.payload.value; }, - setClipSkip: (state, action: PayloadAction) => { - applyClipSkip(state, state.model, action.payload); + setClipSkip: (state, action: ParamsPayloadAction) => { + applyClipSkip(state, state.model, action.payload.value); }, - shouldUseCpuNoiseChanged: (state, action: PayloadAction) => { - state.shouldUseCpuNoise = action.payload; + shouldUseCpuNoiseChanged: (state, action: ParamsPayloadAction) => { + state.shouldUseCpuNoise = action.payload.value; }, - positivePromptChanged: (state, action: PayloadAction) => { - state.positivePrompt = action.payload; + positivePromptChanged: (state, action: ParamsPayloadAction) => { + state.positivePrompt = action.payload.value; }, - positivePromptAddedToHistory: (state, action: PayloadAction) => { - const prompt = action.payload.trim(); + positivePromptAddedToHistory: (state, action: ParamsPayloadAction) => { + const prompt = action.payload.value.trim(); if (prompt.length === 0) { return; } @@ -205,68 +226,68 @@ const slice = createSlice({ state.positivePromptHistory = state.positivePromptHistory.slice(0, MAX_POSITIVE_PROMPT_HISTORY); } }, - promptRemovedFromHistory: (state, action: PayloadAction) => { - state.positivePromptHistory = state.positivePromptHistory.filter((p) => p !== action.payload); + promptRemovedFromHistory: (state, action: ParamsPayloadAction) => { + state.positivePromptHistory = state.positivePromptHistory.filter((p) => p !== action.payload.value); }, - promptHistoryCleared: (state) => { + promptHistoryCleared: (state, _action: ParamsPayloadAction) => { state.positivePromptHistory = []; }, - negativePromptChanged: (state, action: PayloadAction) => { - state.negativePrompt = action.payload; + negativePromptChanged: (state, action: ParamsPayloadAction) => { + state.negativePrompt = action.payload.value; }, - refinerModelChanged: (state, action: PayloadAction) => { - const result = zParamsState.shape.refinerModel.safeParse(action.payload); + refinerModelChanged: (state, action: ParamsPayloadAction) => { + const result = zInstanceParamsState.shape.refinerModel.safeParse(action.payload); if (!result.success) { return; } state.refinerModel = result.data; }, - setRefinerSteps: (state, action: PayloadAction) => { - state.refinerSteps = action.payload; + setRefinerSteps: (state, action: ParamsPayloadAction) => { + state.refinerSteps = action.payload.value; }, - setRefinerCFGScale: (state, action: PayloadAction) => { - state.refinerCFGScale = action.payload; + setRefinerCFGScale: (state, action: ParamsPayloadAction) => { + state.refinerCFGScale = action.payload.value; }, - setRefinerScheduler: (state, action: PayloadAction) => { - state.refinerScheduler = action.payload; + setRefinerScheduler: (state, action: ParamsPayloadAction) => { + state.refinerScheduler = action.payload.value; }, - setRefinerPositiveAestheticScore: (state, action: PayloadAction) => { - state.refinerPositiveAestheticScore = action.payload; + setRefinerPositiveAestheticScore: (state, action: ParamsPayloadAction) => { + state.refinerPositiveAestheticScore = action.payload.value; }, - setRefinerNegativeAestheticScore: (state, action: PayloadAction) => { - state.refinerNegativeAestheticScore = action.payload; + setRefinerNegativeAestheticScore: (state, action: ParamsPayloadAction) => { + state.refinerNegativeAestheticScore = action.payload.value; }, - setRefinerStart: (state, action: PayloadAction) => { - state.refinerStart = action.payload; + setRefinerStart: (state, action: ParamsPayloadAction) => { + state.refinerStart = action.payload.value; }, - setInfillMethod: (state, action: PayloadAction) => { - state.infillMethod = action.payload; + setInfillMethod: (state, action: ParamsPayloadAction) => { + state.infillMethod = action.payload.value; }, - setInfillTileSize: (state, action: PayloadAction) => { - state.infillTileSize = action.payload; + setInfillTileSize: (state, action: ParamsPayloadAction) => { + state.infillTileSize = action.payload.value; }, - setInfillPatchmatchDownscaleSize: (state, action: PayloadAction) => { - state.infillPatchmatchDownscaleSize = action.payload; + setInfillPatchmatchDownscaleSize: (state, action: ParamsPayloadAction) => { + state.infillPatchmatchDownscaleSize = action.payload.value; }, - setInfillColorValue: (state, action: PayloadAction) => { - state.infillColorValue = action.payload; + setInfillColorValue: (state, action: ParamsPayloadAction) => { + state.infillColorValue = action.payload.value; }, - setMaskBlur: (state, action: PayloadAction) => { - state.maskBlur = action.payload; + setMaskBlur: (state, action: ParamsPayloadAction) => { + state.maskBlur = action.payload.value; }, - setCanvasCoherenceMode: (state, action: PayloadAction) => { - state.canvasCoherenceMode = action.payload; + setCanvasCoherenceMode: (state, action: ParamsPayloadAction) => { + state.canvasCoherenceMode = action.payload.value; }, - setCanvasCoherenceEdgeSize: (state, action: PayloadAction) => { - state.canvasCoherenceEdgeSize = action.payload; + setCanvasCoherenceEdgeSize: (state, action: ParamsPayloadAction) => { + state.canvasCoherenceEdgeSize = action.payload.value; }, - setCanvasCoherenceMinDenoise: (state, action: PayloadAction) => { - state.canvasCoherenceMinDenoise = action.payload; + setCanvasCoherenceMinDenoise: (state, action: ParamsPayloadAction) => { + state.canvasCoherenceMinDenoise = action.payload.value; }, //#region Dimensions - sizeRecalled: (state, action: PayloadAction<{ width: number; height: number }>) => { - const { width, height } = action.payload; + sizeRecalled: (state, action: ParamsPayloadAction<{ width: number; height: number }>) => { + const { width, height } = action.payload.value; const gridSize = getGridSize(state.model?.base); state.dimensions.width = Math.max(roundDownToMultiple(width, gridSize), 64); state.dimensions.height = Math.max(roundDownToMultiple(height, gridSize), 64); @@ -274,8 +295,11 @@ const slice = createSlice({ state.dimensions.aspectRatio.id = 'Free'; state.dimensions.aspectRatio.isLocked = true; }, - widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { - const { width, updateAspectRatio, clamp } = action.payload; + widthChanged: ( + state, + action: ParamsPayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }> + ) => { + const { width, updateAspectRatio, clamp } = action.payload.value; const gridSize = getGridSize(state.model?.base); state.dimensions.width = clamp ? Math.max(roundDownToMultiple(width, gridSize), 64) : width; @@ -292,8 +316,11 @@ const slice = createSlice({ state.dimensions.aspectRatio.isLocked = false; } }, - heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { - const { height, updateAspectRatio, clamp } = action.payload; + heightChanged: ( + state, + action: ParamsPayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }> + ) => { + const { height, updateAspectRatio, clamp } = action.payload.value; const gridSize = getGridSize(state.model?.base); state.dimensions.height = clamp ? Math.max(roundDownToMultiple(height, gridSize), 64) : height; @@ -310,11 +337,11 @@ const slice = createSlice({ state.dimensions.aspectRatio.isLocked = false; } }, - aspectRatioLockToggled: (state) => { + aspectRatioLockToggled: (state, _action: ParamsPayloadAction) => { state.dimensions.aspectRatio.isLocked = !state.dimensions.aspectRatio.isLocked; }, - aspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { - const { id } = action.payload; + aspectRatioIdChanged: (state, action: ParamsPayloadAction<{ id: AspectRatioID }>) => { + const { id } = action.payload.value; state.dimensions.aspectRatio.id = id; if (id === 'Free') { state.dimensions.aspectRatio.isLocked = false; @@ -354,7 +381,7 @@ const slice = createSlice({ state.dimensions.height = height; } }, - dimensionsSwapped: (state) => { + dimensionsSwapped: (state, _action: ParamsPayloadAction) => { state.dimensions.aspectRatio.value = 1 / state.dimensions.aspectRatio.value; if (state.dimensions.aspectRatio.id === 'Free') { const newWidth = state.dimensions.height; @@ -372,7 +399,7 @@ const slice = createSlice({ state.dimensions.aspectRatio.id = ASPECT_RATIO_MAP[state.dimensions.aspectRatio.id].inverseID; } }, - sizeOptimized: (state) => { + sizeOptimized: (state, _action: ParamsPayloadAction) => { const optimalDimension = getOptimalDimension(state.model?.base); if (state.dimensions.aspectRatio.isLocked) { const { width, height } = calculateNewSize( @@ -388,7 +415,7 @@ const slice = createSlice({ state.dimensions.height = optimalDimension; } }, - syncedToOptimalDimension: (state) => { + syncedToOptimalDimension: (state, _action: ParamsPayloadAction) => { const optimalDimension = getOptimalDimension(state.model?.base); if (!getIsSizeOptimal(state.dimensions.width, state.dimensions.height, state.model?.base)) { @@ -401,7 +428,33 @@ const slice = createSlice({ state.dimensions.height = bboxDims.height; } }, - paramsReset: (state) => resetState(state), + paramsReset: (state, _action: ParamsPayloadAction) => resetState(state), + }, + extraReducers(builder) { + builder.addCase(modelChanged, (state, action) => { + const { previousModel } = action.payload.value; + const result = zInstanceParamsState.shape.model.safeParse(action.payload.value.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); + }); }, }); @@ -432,11 +485,11 @@ const getModelMaxClipSkip = (model: ParameterModel) => { return CLIP_SKIP_MAP[model.base]?.maxClip; }; -const resetState = (state: ParamsState): ParamsState => { +const resetState = (state: InstanceParamsState): InstanceParamsState => { // When a new session is requested, we need to keep the current model selections, plus dependent state // like VAE precision. Everything else gets reset to default. const oldState = deepClone(state); - const newState = getInitialParamsState(); + const newState = getInitialInstanceParamsState(); newState.dimensions = oldState.dimensions; newState.model = oldState.model; newState.vae = oldState.vae; @@ -492,7 +545,6 @@ export const { setRefinerPositiveAestheticScore, setRefinerNegativeAestheticScore, setRefinerStart, - modelChanged, // Dimensions sizeRecalled, @@ -505,10 +557,98 @@ export const { syncedToOptimalDimension, paramsReset, -} = slice.actions; +} = instanceParamsSlice.actions; + +const instanceParamsSliceActions = { ...instanceParamsSlice.actions, modelChanged }; + +type InstanceParamsAction = typeof instanceParamsSliceActions; +type InstanceParamsActionCreator = InstanceParamsAction[keyof InstanceParamsAction]; +type PayloadOf = AC extends ActionCreatorWithPayload ? P : never; +type ValueOf = PayloadOf extends { value: infer V } ? V : never; + +export const useParamsDispatch = () => { + const dispatch = useAppDispatch(); + const activeTab = useAppSelector(selectActiveTab); + const activeCanvasId = useAppSelector(selectActiveCanvasId); + + const paramsDispatch = ( + actionCreator: AC, + ...rest: ValueOf extends never ? [] : [value: ValueOf] + ): void => { + const value = rest[0]; + + dispatchParamsAction(actionCreator, activeTab, activeCanvasId, value, dispatch); + }; + + return useCallback(paramsDispatch, [activeTab, activeCanvasId, dispatch]); +}; + +export const toStore = (state: RootState, dispatch: AppDispatch) => ({ getState: () => state, dispatch }); -export const paramsSliceConfig: SliceConfig = { - slice, +export const paramsDispatch = ( + store: { getState: () => RootState; dispatch: AppDispatch }, + actionCreator: AC, + ...rest: ValueOf extends never ? [] : [value: ValueOf] +): void => { + const state = store.getState(); + const activeTab = selectActiveTab(state); + const activeCanvasId = selectActiveCanvasId(state); + const value = rest[0]; + + dispatchParamsAction(actionCreator, activeTab, activeCanvasId, value, store.dispatch); +}; + +const dispatchParamsAction = ( + actionCreator: AC, + tab: TabName, + canvasId: string, + value: ValueOf | undefined, + dispatch: AppDispatch +) => { + if (isParamsTab(tab)) { + // Type information simplified to help TS compiler cope with too complex type + const payload = value !== undefined ? { tab, canvasId, value } : ({ tab, canvasId } as unknown); + const action = (actionCreator as ActionCreatorWithPayload)(payload); + + dispatch(action); + } +}; + +const isInstanceParamsSliceAction = isAnyOf(...Object.values(instanceParamsSliceActions)); + +export const paramsSliceReducer = (state: ParamsState, action: UnknownAction): ParamsState => { + state = paramsSlice.reducer(state, action); + + if (!isInstanceParamsSliceAction(action)) { + return state; + } + + const { tab, canvasId } = action.payload; + + switch (tab) { + case 'generate': + return { + ...state, + generate: instanceParamsSlice.reducer(state.generate, action), + }; + case 'canvas': + return { + ...state, + canvases: { + ...state.canvases, + [canvasId]: { canvasId, ...instanceParamsSlice.reducer(state.canvases[canvasId], action) }, + }, + }; + case 'upscaling': + return { + ...state, + upscaling: instanceParamsSlice.reducer(state.upscaling, action), + }; + } +}; + +export const paramsSliceConfig: SliceConfig = { + slice: paramsSlice, schema: zParamsState, getInitialState: getInitialParamsState, persistConfig: { @@ -528,22 +668,60 @@ export const paramsSliceConfig: SliceConfig = { state.positivePromptHistory = []; } + if (state._version === 2) { + // Migrate from v2 to v3: slice represented shared params -> slice represents multiple tabs/canvases params + const canvasParams = { + canvasId: MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, + ...state, + } as CanvasInstanceParamsState; + + state = { + _version: 3, + generate: { ...state }, + canvases: { [canvasParams.canvasId]: canvasParams }, + upscaling: { ...state }, + }; + } + return zParamsState.parse(state); }, }, }; -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) => { +export const selectActiveParams = (state: RootState) => { + const tab = selectActiveTab(state); + const canvasId = selectActiveCanvasId(state); + assert( + tab === 'generate' || tab === 'canvas' || tab === 'upscaling', + `Unsupported tab ${tab} for params slice selector` + ); + + switch (tab) { + case 'generate': + return state.params.generate; + case 'canvas': { + const params = state.params.canvases[canvasId]; + assert(params, 'Params must exist for a canvas once the canvas has been created'); + return params; + } + case 'upscaling': + return state.params.upscaling; + } +}; + +const buildActiveParamsSelector = + (selector: Selector) => + (state: RootState) => + selector(selectActiveParams(state)); + +export const selectBase = buildActiveParamsSelector((params) => params.model?.base); +export const selectIsSDXL = buildActiveParamsSelector((params) => params.model?.base === 'sdxl'); +export const selectIsFLUX = buildActiveParamsSelector((params) => params.model?.base === 'flux'); +export const selectIsSD3 = buildActiveParamsSelector((params) => params.model?.base === 'sd-3'); +export const selectIsCogView4 = buildActiveParamsSelector((params) => params.model?.base === 'cogview4'); +export const selectIsImagen3 = buildActiveParamsSelector((params) => params.model?.base === 'imagen3'); +export const selectIsImagen4 = buildActiveParamsSelector((params) => params.model?.base === 'imagen4'); +export const selectIsFluxKontext = buildActiveParamsSelector((params) => { if (params.model?.base === 'flux-kontext') { return true; } @@ -552,42 +730,42 @@ 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 = buildActiveParamsSelector((params) => params.model?.base === 'chatgpt-4o'); +export const selectIsGemini2_5 = buildActiveParamsSelector((params) => params.model?.base === 'gemini-2.5'); + +export const selectModel = buildActiveParamsSelector((params) => params.model); +export const selectModelKey = buildActiveParamsSelector((params) => params.model?.key); +export const selectVAE = buildActiveParamsSelector((params) => params.vae); +export const selectFLUXVAE = buildActiveParamsSelector((params) => params.fluxVAE); +export const selectVAEKey = buildActiveParamsSelector((params) => params.vae?.key); +export const selectT5EncoderModel = buildActiveParamsSelector((params) => params.t5EncoderModel); +export const selectCLIPEmbedModel = buildActiveParamsSelector((params) => params.clipEmbedModel); +export const selectCLIPLEmbedModel = buildActiveParamsSelector((params) => params.clipLEmbedModel); + +export const selectCLIPGEmbedModel = buildActiveParamsSelector((params) => params.clipGEmbedModel); + +export const selectCFGScale = buildActiveParamsSelector((params) => params.cfgScale); +export const selectGuidance = buildActiveParamsSelector((params) => params.guidance); +export const selectSteps = buildActiveParamsSelector((params) => params.steps); +export const selectCFGRescaleMultiplier = buildActiveParamsSelector((params) => params.cfgRescaleMultiplier); +export const selectCLIPSkip = buildActiveParamsSelector((params) => params.clipSkip); +export const selectHasModelCLIPSkip = buildActiveParamsSelector((params) => hasModelClipSkip(params.model)); +export const selectCanvasCoherenceEdgeSize = buildActiveParamsSelector((params) => params.canvasCoherenceEdgeSize); +export const selectCanvasCoherenceMinDenoise = buildActiveParamsSelector((params) => params.canvasCoherenceMinDenoise); +export const selectCanvasCoherenceMode = buildActiveParamsSelector((params) => params.canvasCoherenceMode); +export const selectMaskBlur = buildActiveParamsSelector((params) => params.maskBlur); +export const selectInfillMethod = buildActiveParamsSelector((params) => params.infillMethod); +export const selectInfillTileSize = buildActiveParamsSelector((params) => params.infillTileSize); +export const selectInfillPatchmatchDownscaleSize = buildActiveParamsSelector( (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 = buildActiveParamsSelector((params) => params.infillColorValue); +export const selectImg2imgStrength = buildActiveParamsSelector((params) => params.img2imgStrength); +export const selectOptimizedDenoisingEnabled = buildActiveParamsSelector((params) => params.optimizedDenoisingEnabled); +export const selectPositivePrompt = buildActiveParamsSelector((params) => params.positivePrompt); +export const selectNegativePrompt = buildActiveParamsSelector((params) => params.negativePrompt); +export const selectNegativePromptWithFallback = buildActiveParamsSelector((params) => params.negativePrompt ?? ''); +export const selectHasNegativePrompt = buildActiveParamsSelector((params) => params.negativePrompt !== null); export const selectModelSupportsNegativePrompt = createSelector( selectModel, (model) => !!model && SUPPORTS_NEGATIVE_PROMPT_BASE_MODELS.includes(model.base) @@ -616,41 +794,45 @@ 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 = buildActiveParamsSelector((params) => params.scheduler); +export const selectSeamlessXAxis = buildActiveParamsSelector((params) => params.seamlessXAxis); +export const selectSeamlessYAxis = buildActiveParamsSelector((params) => params.seamlessYAxis); +export const selectSeed = buildActiveParamsSelector((params) => params.seed); +export const selectShouldRandomizeSeed = buildActiveParamsSelector((params) => params.shouldRandomizeSeed); +export const selectVAEPrecision = buildActiveParamsSelector((params) => params.vaePrecision); +export const selectIterations = buildActiveParamsSelector((params) => params.iterations); +export const selectShouldUseCPUNoise = buildActiveParamsSelector((params) => params.shouldUseCpuNoise); + +export const selectUpscaleScheduler = buildActiveParamsSelector((params) => params.upscaleScheduler); +export const selectUpscaleCfgScale = buildActiveParamsSelector((params) => params.upscaleCfgScale); + +export const selectPositivePromptHistory = buildActiveParamsSelector((params) => params.positivePromptHistory); +export const selectRefinerCFGScale = buildActiveParamsSelector((params) => params.refinerCFGScale); +export const selectRefinerModel = buildActiveParamsSelector((params) => params.refinerModel); +export const selectIsRefinerModelSelected = buildActiveParamsSelector((params) => Boolean(params.refinerModel)); +export const selectRefinerPositiveAestheticScore = buildActiveParamsSelector( (params) => params.refinerPositiveAestheticScore ); -export const selectRefinerNegativeAestheticScore = createParamsSelector( +export const selectRefinerNegativeAestheticScore = buildActiveParamsSelector( (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 = buildActiveParamsSelector((params) => params.refinerScheduler); +export const selectRefinerStart = buildActiveParamsSelector((params) => params.refinerStart); +export const selectRefinerSteps = buildActiveParamsSelector((params) => params.refinerSteps); + +export const selectWidth = buildActiveParamsSelector((params) => params.dimensions.width); +export const selectHeight = buildActiveParamsSelector((params) => params.dimensions.height); +export const selectAspectRatioID = buildActiveParamsSelector((params) => params.dimensions.aspectRatio.id); +export const selectAspectRatioValue = buildActiveParamsSelector((params) => params.dimensions.aspectRatio.value); +export const selectAspectRatioIsLocked = buildActiveParamsSelector((params) => params.dimensions.aspectRatio.isLocked); +export const selectOptimalDimension = buildActiveParamsSelector((params) => + getOptimalDimension(params.model?.base ?? null) +); +export const selectGridSize = buildActiveParamsSelector((params) => getGridSize(params.model?.base ?? null)); export const selectMainModelConfig = createSelector( selectModelConfigsQuery, - selectParamsSlice, + selectActiveParams, (modelConfigs, { model }) => { if (!modelConfigs.data) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index e108eed3352..96933ac6ab3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -1,6 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import type { CanvasControlLayerState, CanvasEntityIdentifier, @@ -12,7 +11,6 @@ import type { CanvasRegionalGuidanceState, CanvasState, } from 'features/controlLayers/store/types'; -import { getGridSize, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; @@ -98,22 +96,6 @@ 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. diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 287db719b4f..741ff162381 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,3 +1,4 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common'; @@ -24,6 +25,7 @@ import { zParameterT5EncoderModel, zParameterVAEModel, } from 'features/parameters/types/parameterSchemas'; +import type { TabName } from 'features/ui/store/uiTypes'; import type { JsonObject } from 'type-fest'; import { z } from 'zod'; @@ -683,8 +685,14 @@ const zPositivePromptHistory = z .array(zParameterPositivePrompt) .transform((arr) => arr.slice(0, MAX_POSITIVE_PROMPT_HISTORY)); -export const zParamsState = z.object({ - _version: z.literal(2), +type EnrichedPayload = P extends undefined ? T : T & { value: P }; + +export const isParamsTab = (tab: TabName) => tab === 'generate' || tab === 'canvas' || tab === 'upscaling'; +type ParamsTabName = 'generate' | 'canvas' | 'upscaling'; +export type ParamsEnrichedPayload

= EnrichedPayload<{ tab: ParamsTabName; canvasId: string }, P>; +export type ParamsPayloadAction

= PayloadAction>; + +export const zInstanceParamsState = z.object({ maskBlur: z.number(), maskBlurMethod: zParameterMaskBlurMethod, canvasCoherenceMode: zParameterCanvasCoherenceMode, @@ -731,9 +739,22 @@ export const zParamsState = z.object({ controlLora: zParameterControlLoRAModel.nullable(), dimensions: zDimensionsState, }); +export type InstanceParamsState = z.infer; + +const zCanvasInstanceParamsState = zInstanceParamsState.extend({ + canvasId: zId, +}); +export type CanvasInstanceParamsState = z.infer; + +export const zParamsState = z.object({ + _version: z.literal(3), + generate: zInstanceParamsState, + canvases: z.record(z.string(), zCanvasInstanceParamsState), + upscaling: zInstanceParamsState, +}); export type ParamsState = z.infer; -export const getInitialParamsState = (): ParamsState => ({ - _version: 2, + +export const getInitialInstanceParamsState = (): InstanceParamsState => ({ maskBlur: 16, maskBlurMethod: 'box', canvasCoherenceMode: 'Gaussian Blur', @@ -785,6 +806,18 @@ export const getInitialParamsState = (): ParamsState => ({ }, }); +export const getInitialCanvasInstanceParamsState = (canvasId: string): CanvasInstanceParamsState => ({ + canvasId, + ...getInitialInstanceParamsState(), +}); + +export const getInitialParamsState = (): ParamsState => ({ + _version: 3, + generate: getInitialInstanceParamsState(), + canvases: {}, + upscaling: getInitialInstanceParamsState(), +}); + const zInpaintMasks = z.object({ isHidden: z.boolean(), entities: z.array(zCanvasInpaintMaskState), diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index 10cd0e32f7f..c3c7f45e07a 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -10,6 +10,7 @@ import { loraAllDeleted, loraRecalled } from 'features/controlLayers/store/loras import { heightChanged, negativePromptChanged, + paramsDispatch, positivePromptChanged, refinerModelChanged, selectBase, @@ -276,7 +277,7 @@ const PositivePrompt: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(positivePromptChanged(value)); + paramsDispatch(store, positivePromptChanged, value); }, i18nKey: 'metadata.positivePrompt', LabelComponent: MetadataLabel, @@ -296,7 +297,7 @@ const NegativePrompt: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(negativePromptChanged(value || null)); + paramsDispatch(store, negativePromptChanged, value); }, i18nKey: 'metadata.negativePrompt', LabelComponent: MetadataLabel, @@ -316,7 +317,7 @@ const CFGScale: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setCfgScale(value)); + paramsDispatch(store, setCfgScale, value); }, i18nKey: 'metadata.cfgScale', LabelComponent: MetadataLabel, @@ -334,7 +335,7 @@ const CFGRescaleMultiplier: SingleMetadataHandler return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setCfgRescaleMultiplier(value)); + paramsDispatch(store, setCfgRescaleMultiplier, value); }, i18nKey: 'metadata.cfgRescaleMultiplier', LabelComponent: MetadataLabel, @@ -354,7 +355,7 @@ const CLIPSkip: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setClipSkip(value)); + paramsDispatch(store, setClipSkip, value); }, i18nKey: 'metadata.clipSkip', LabelComponent: MetadataLabel, @@ -372,7 +373,7 @@ const Guidance: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setGuidance(value)); + paramsDispatch(store, setGuidance, value); }, i18nKey: 'metadata.guidance', LabelComponent: MetadataLabel, @@ -390,7 +391,7 @@ const Scheduler: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setScheduler(value)); + paramsDispatch(store, setScheduler, value); }, i18nKey: 'metadata.scheduler', LabelComponent: MetadataLabel, @@ -412,7 +413,7 @@ const Width: SingleMetadataHandler = { if (activeTab === 'canvas') { store.dispatch(bboxWidthChanged({ width: value, updateAspectRatio: true, clamp: true })); } else if (activeTab === 'generate') { - store.dispatch(widthChanged({ width: value, updateAspectRatio: true, clamp: true })); + paramsDispatch(store, widthChanged, { width: value, updateAspectRatio: true, clamp: true }); } }, i18nKey: 'metadata.width', @@ -435,7 +436,7 @@ const Height: SingleMetadataHandler = { if (activeTab === 'canvas') { store.dispatch(bboxHeightChanged({ height: value, updateAspectRatio: true, clamp: true })); } else if (activeTab === 'generate') { - store.dispatch(heightChanged({ height: value, updateAspectRatio: true, clamp: true })); + paramsDispatch(store, heightChanged, { height: value, updateAspectRatio: true, clamp: true }); } }, i18nKey: 'metadata.height', @@ -454,7 +455,7 @@ const Seed: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setSeed(value)); + paramsDispatch(store, setSeed, value); }, i18nKey: 'metadata.seed', LabelComponent: MetadataLabel, @@ -472,7 +473,7 @@ const Steps: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setSteps(value)); + paramsDispatch(store, setSteps, value); }, i18nKey: 'metadata.steps', LabelComponent: MetadataLabel, @@ -490,7 +491,7 @@ const DenoisingStrength: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setImg2imgStrength(value)); + paramsDispatch(store, setImg2imgStrength, value); }, i18nKey: 'metadata.strength', LabelComponent: MetadataLabel, @@ -508,7 +509,7 @@ const SeamlessX: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setSeamlessXAxis(value)); + paramsDispatch(store, setSeamlessXAxis, value); }, i18nKey: 'metadata.seamlessXAxis', LabelComponent: MetadataLabel, @@ -526,7 +527,7 @@ const SeamlessY: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setSeamlessYAxis(value)); + paramsDispatch(store, setSeamlessYAxis, value); }, i18nKey: 'metadata.seamlessYAxis', LabelComponent: MetadataLabel, @@ -547,7 +548,7 @@ const RefinerModel: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(refinerModelChanged(value)); + paramsDispatch(store, refinerModelChanged, value); }, i18nKey: 'sdxl.refinermodel', LabelComponent: MetadataLabel, @@ -567,7 +568,7 @@ const RefinerSteps: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setRefinerSteps(value)); + paramsDispatch(store, setRefinerSteps, value); }, i18nKey: 'sdxl.refinerSteps', LabelComponent: MetadataLabel, @@ -585,7 +586,7 @@ const RefinerCFGScale: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setRefinerCFGScale(value)); + paramsDispatch(store, setRefinerCFGScale, value); }, i18nKey: 'sdxl.cfgScale', LabelComponent: MetadataLabel, @@ -603,7 +604,7 @@ const RefinerScheduler: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setRefinerScheduler(value)); + paramsDispatch(store, setRefinerScheduler, value); }, i18nKey: 'sdxl.scheduler', LabelComponent: MetadataLabel, @@ -621,7 +622,7 @@ const RefinerPositiveAestheticScore: SingleMetadataHandler { - store.dispatch(setRefinerPositiveAestheticScore(value)); + paramsDispatch(store, setRefinerPositiveAestheticScore, value); }, i18nKey: 'sdxl.posAestheticScore', LabelComponent: MetadataLabel, @@ -641,7 +642,7 @@ const RefinerNegativeAestheticScore: SingleMetadataHandler { - store.dispatch(setRefinerNegativeAestheticScore(value)); + paramsDispatch(store, setRefinerNegativeAestheticScore, value); }, i18nKey: 'sdxl.negAestheticScore', LabelComponent: MetadataLabel, @@ -661,7 +662,7 @@ const RefinerDenoisingStart: SingleMetadataHandler = return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(setRefinerStart(value)); + paramsDispatch(store, setRefinerStart, value); }, i18nKey: 'sdxl.refinerStart', LabelComponent: MetadataLabel, @@ -704,7 +705,7 @@ const VAEModel: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - store.dispatch(vaeSelected(value)); + paramsDispatch(store, vaeSelected, value); }, i18nKey: 'metadata.vae', LabelComponent: MetadataLabel, 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..beb6dd92b6f 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 { selectActiveParams } 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 } = selectActiveParams(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..b07fa3c6423 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 { selectActiveParams } 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, + } = selectActiveParams(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 352dfe35bd0..a48c89d3145 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 @@ -3,7 +3,7 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getDenoisingStartAndEnd, @@ -35,7 +35,7 @@ export const addFLUXFill = async ({ denoise.width = scaledSize.width; denoise.height = scaledSize.height; - const params = selectParamsSlice(state); + const params = selectActiveParams(state); const canvasSettings = buildSelectCanvasSettingsByCanvasId(manager.canvasId)(state); const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer'); 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 2b86c7763fb..e606b92b859 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 @@ -3,7 +3,7 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getDenoisingStartAndEnd, @@ -48,7 +48,7 @@ export const addInpaint = async ({ denoise.denoising_start = denoising_start; denoise.denoising_end = denoising_end; - const params = selectParamsSlice(state); + const params = selectActiveParams(state); const canvasSettings = buildSelectCanvasSettingsByCanvasId(manager.canvasId)(state); const { originalSize, scaledSize, rect } = getOriginalAndScaledSizesForOtherModes(state); 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 3ac047c0918..a5be085ec34 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 @@ -3,7 +3,7 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getDenoisingStartAndEnd, @@ -50,7 +50,7 @@ export const addOutpaint = async ({ denoise.denoising_start = denoising_start; denoise.denoising_end = denoising_end; - const params = selectParamsSlice(state); + const params = selectActiveParams(state); const canvasSettings = buildSelectCanvasSettingsByCanvasId(manager.canvasId)(state); const { originalSize, scaledSize, rect } = getOriginalAndScaledSizesForOtherModes(state); 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..0edb6e50f6b 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 { selectActiveParams } 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; + } = selectActiveParams(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..8e6fceabdb8 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 { selectActiveParams } 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 } = selectActiveParams(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/buildCogView4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts index 6adee057545..09cc5bf681f 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,6 +1,6 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage'; @@ -29,7 +29,7 @@ export const buildCogView4Graph = 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 720f4ddae2b..a18874962dc 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,6 +1,6 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectMainModelConfig, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; import { selectCanvasById, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; @@ -35,7 +35,7 @@ export const buildSD1Graph = 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 = selectActiveParams(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 3ef707bee77..0ea3e40bad6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -4,15 +4,15 @@ import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectSaveAllImagesToGallery } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { + selectActiveParams, selectImg2imgStrength, selectMainModelConfig, selectOptimizedDenoisingEnabled, - selectParamsSlice, selectRefinerModel, selectRefinerStart, } from 'features/controlLayers/store/paramsSlice'; import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; -import type { ParamsState } from 'features/controlLayers/store/types'; +import type { InstanceParamsState } 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'; @@ -81,7 +81,7 @@ export const selectCanvasDestination = (state: RootState, canvasId: string) => { * Gets the prompts, modified for the active style preset. */ export const selectPresetModifiedPrompts = createSelector( - selectParamsSlice, + selectActiveParams, selectStylePresetSlice, selectListStylePresetsRequestState, (params, stylePresetSlice, listStylePresetsRequestState) => { @@ -121,7 +121,7 @@ export const selectPresetModifiedPrompts = createSelector( export const getOriginalAndScaledSizesForTextToImage = (state: RootState) => { const tab = selectActiveTab(state); - const params = selectParamsSlice(state); + const params = selectActiveParams(state); if (tab === 'canvas') { const canvas = selectActiveCanvas(state); @@ -158,7 +158,7 @@ export const getOriginalAndScaledSizesForOtherModes = (state: RootState) => { export const getInfill = ( g: Graph, - params: ParamsState + params: InstanceParamsState ): Invocation<'infill_patchmatch' | 'infill_cv2' | 'infill_lama' | 'infill_rgba' | 'infill_tile'> => { const { infillMethod, infillColorValue, infillPatchmatchDownscaleSize, infillTileSize } = params; 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..1d10cd0e2dc 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx @@ -1,7 +1,11 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectCFGRescaleMultiplier, setCfgRescaleMultiplier } from 'features/controlLayers/store/paramsSlice'; +import { + selectCFGRescaleMultiplier, + setCfgRescaleMultiplier, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { selectCFGRescaleMultiplierConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,10 +14,10 @@ const ParamCFGRescaleMultiplier = () => { const cfgRescaleMultiplier = useAppSelector(selectCFGRescaleMultiplier); const config = useAppSelector(selectCFGRescaleMultiplierConfig); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); - const handleChange = useCallback((v: number) => dispatch(setCfgRescaleMultiplier(v)), [dispatch]); + const handleChange = useCallback((v: number) => dispatchParams(setCfgRescaleMultiplier, v), [dispatchParams]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx index b8480d6b2cf..13703a14931 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx @@ -1,7 +1,11 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { clipEmbedModelSelected, selectCLIPEmbedModel } from 'features/controlLayers/store/paramsSlice'; +import { + clipEmbedModelSelected, + selectCLIPEmbedModel, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +13,7 @@ import { useCLIPEmbedModels } from 'services/api/hooks/modelsByType'; import type { CLIPEmbedModelConfig } from 'services/api/types'; const ParamCLIPEmbedModelSelect = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const clipEmbedModel = useAppSelector(selectCLIPEmbedModel); const [modelConfigs, { isLoading }] = useCLIPEmbedModels(); @@ -17,10 +21,10 @@ const ParamCLIPEmbedModelSelect = () => { const _onChange = useCallback( (clipEmbedModel: CLIPEmbedModelConfig | null) => { if (clipEmbedModel) { - dispatch(clipEmbedModelSelected(zModelIdentifierField.parse(clipEmbedModel))); + dispatchParams(clipEmbedModelSelected, zModelIdentifierField.parse(clipEmbedModel)); } }, - [dispatch] + [dispatchParams] ); const { options, value, onChange, noOptionsMessage } = useModelCombobox({ diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx index 0d63512a410..af0122a2f1b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx @@ -1,7 +1,11 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { clipGEmbedModelSelected, selectCLIPGEmbedModel } from 'features/controlLayers/store/paramsSlice'; +import { + clipGEmbedModelSelected, + selectCLIPGEmbedModel, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +14,7 @@ import type { CLIPGEmbedModelConfig } from 'services/api/types'; import { isCLIPGEmbedModelConfig } from 'services/api/types'; const ParamCLIPEmbedModelSelect = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const clipEmbedModel = useAppSelector(selectCLIPGEmbedModel); const [modelConfigs, { isLoading }] = useCLIPEmbedModels(); @@ -18,10 +22,10 @@ const ParamCLIPEmbedModelSelect = () => { const _onChange = useCallback( (clipEmbedModel: CLIPGEmbedModelConfig | null) => { if (clipEmbedModel) { - dispatch(clipGEmbedModelSelected(zModelIdentifierField.parse(clipEmbedModel))); + dispatchParams(clipGEmbedModelSelected, zModelIdentifierField.parse(clipEmbedModel)); } }, - [dispatch] + [dispatchParams] ); const { options, value, onChange, noOptionsMessage } = useModelCombobox({ diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx index f0a038d510d..5626d7a954a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx @@ -1,7 +1,11 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { clipLEmbedModelSelected, selectCLIPLEmbedModel } from 'features/controlLayers/store/paramsSlice'; +import { + clipLEmbedModelSelected, + selectCLIPLEmbedModel, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +14,7 @@ import type { CLIPLEmbedModelConfig } from 'services/api/types'; import { isCLIPLEmbedModelConfig } from 'services/api/types'; const ParamCLIPEmbedModelSelect = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const clipEmbedModel = useAppSelector(selectCLIPLEmbedModel); const [modelConfigs, { isLoading }] = useCLIPEmbedModels(); @@ -18,10 +22,10 @@ const ParamCLIPEmbedModelSelect = () => { const _onChange = useCallback( (clipEmbedModel: CLIPLEmbedModelConfig | null) => { if (clipEmbedModel) { - dispatch(clipLEmbedModelSelected(zModelIdentifierField.parse(clipEmbedModel))); + dispatchParams(clipLEmbedModelSelected, zModelIdentifierField.parse(clipEmbedModel)); } }, - [dispatch] + [dispatchParams] ); const { options, value, onChange, noOptionsMessage } = useModelCombobox({ 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..4bdbf9b3a8e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectCLIPSkip, selectModel, setClipSkip } from 'features/controlLayers/store/paramsSlice'; +import { selectCLIPSkip, selectModel, setClipSkip, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; import { selectCLIPSkipConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; @@ -12,14 +12,14 @@ const ParamClipSkip = () => { const config = useAppSelector(selectCLIPSkipConfig); const model = useAppSelector(selectModel); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const handleClipSkipChange = useCallback( (v: number) => { - dispatch(setClipSkip(v)); + dispatchParams(setClipSkip, v); }, - [dispatch] + [dispatchParams] ); const max = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamOptimizedDenoisingToggle.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamOptimizedDenoisingToggle.tsx index 7bfb62fc159..99e60321aa7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamOptimizedDenoisingToggle.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamOptimizedDenoisingToggle.tsx @@ -1,9 +1,10 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { selectOptimizedDenoisingEnabled, setOptimizedDenoisingEnabled, + useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; @@ -11,13 +12,13 @@ import { useTranslation } from 'react-i18next'; export const ParamOptimizedDenoisingToggle = memo(() => { const optimizedDenoisingEnabled = useAppSelector(selectOptimizedDenoisingEnabled); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const onChange = useCallback( (event: ChangeEvent) => { - dispatch(setOptimizedDenoisingEnabled(event.target.checked)); + dispatchParams(setOptimizedDenoisingEnabled, event.target.checked); }, - [dispatch] + [dispatchParams] ); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx index 8501e77cd4b..da6309c2f2a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx @@ -1,7 +1,11 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { selectT5EncoderModel, t5EncoderModelSelected } from 'features/controlLayers/store/paramsSlice'; +import { + selectT5EncoderModel, + t5EncoderModelSelected, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +13,7 @@ import { useT5EncoderModels } from 'services/api/hooks/modelsByType'; import type { T5EncoderBnbQuantizedLlmInt8bModelConfig, T5EncoderModelConfig } from 'services/api/types'; const ParamT5EncoderModelSelect = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const t5EncoderModel = useAppSelector(selectT5EncoderModel); const [modelConfigs, { isLoading }] = useT5EncoderModels(); @@ -17,10 +21,10 @@ const ParamT5EncoderModelSelect = () => { const _onChange = useCallback( (t5EncoderModel: T5EncoderBnbQuantizedLlmInt8bModelConfig | T5EncoderModelConfig | null) => { if (t5EncoderModel) { - dispatch(t5EncoderModelSelected(zModelIdentifierField.parse(t5EncoderModel))); + dispatchParams(t5EncoderModelSelected, zModelIdentifierField.parse(t5EncoderModel)); } }, - [dispatch] + [dispatchParams] ); const { options, value, onChange, noOptionsMessage } = useModelCombobox({ 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/BboxScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Bbox/BboxScaledHeight.tsx index d4cc0c08b7b..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,7 +2,8 @@ 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 { selectActiveCanvas, 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'; 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 de08f1b4f73..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,7 +2,8 @@ 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 { selectActiveCanvas, 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'; 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 cc96408ea7b..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,7 +2,8 @@ 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 { selectActiveCanvas, 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'; 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/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx index 007b2b04887..499aa1255ae 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx @@ -1,13 +1,17 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectCanvasCoherenceEdgeSize, setCanvasCoherenceEdgeSize } from 'features/controlLayers/store/paramsSlice'; +import { + selectCanvasCoherenceEdgeSize, + setCanvasCoherenceEdgeSize, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { selectCanvasCoherenceEdgeSizeConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceEdgeSize = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const canvasCoherenceEdgeSize = useAppSelector(selectCanvasCoherenceEdgeSize); const config = useAppSelector(selectCanvasCoherenceEdgeSizeConfig); @@ -15,9 +19,9 @@ const ParamCanvasCoherenceEdgeSize = () => { const handleChange = useCallback( (v: number) => { - dispatch(setCanvasCoherenceEdgeSize(v)); + dispatchParams(setCanvasCoherenceEdgeSize, v); }, - [dispatch] + [dispatchParams] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx index eb9047fbf52..272c107c5ff 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx @@ -1,23 +1,24 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { selectCanvasCoherenceMinDenoise, setCanvasCoherenceMinDenoise, + useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceMinDenoise = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const canvasCoherenceMinDenoise = useAppSelector(selectCanvasCoherenceMinDenoise); const { t } = useTranslation(); const handleChange = useCallback( (v: number) => { - dispatch(setCanvasCoherenceMinDenoise(v)); + dispatchParams(setCanvasCoherenceMinDenoise, v); }, - [dispatch] + [dispatchParams] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx index c6b2084e3a3..5150342142d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx @@ -1,14 +1,18 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectCanvasCoherenceMode, setCanvasCoherenceMode } from 'features/controlLayers/store/paramsSlice'; +import { + selectCanvasCoherenceMode, + setCanvasCoherenceMode, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { isParameterCanvasCoherenceMode } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceMode = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const canvasCoherenceMode = useAppSelector(selectCanvasCoherenceMode); const { t } = useTranslation(); @@ -27,9 +31,9 @@ const ParamCanvasCoherenceMode = () => { return; } - dispatch(setCanvasCoherenceMode(v.value)); + dispatchParams(setCanvasCoherenceMode, v.value); }, - [dispatch] + [dispatchParams] ); const value = useMemo(() => options.find((o) => o.value === canvasCoherenceMode), [canvasCoherenceMode, options]); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx index a165388fdcd..045d3ef55f3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx @@ -1,22 +1,22 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectMaskBlur, setMaskBlur } from 'features/controlLayers/store/paramsSlice'; +import { selectMaskBlur, setMaskBlur, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { selectMaskBlurConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamMaskBlur = () => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const maskBlur = useAppSelector(selectMaskBlur); const config = useAppSelector(selectMaskBlurConfig); const handleChange = useCallback( (v: number) => { - dispatch(setMaskBlur(v)); + dispatchParams(setMaskBlur, v); }, - [dispatch] + [dispatchParams] ); return ( 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..0abb8067ed3 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 @@ -1,17 +1,18 @@ import { Box, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import RgbaColorPicker from 'common/components/ColorPicker/RgbaColorPicker'; import { selectInfillColorValue, selectInfillMethod, setInfillColorValue, + useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import type { RgbaColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; const ParamInfillColorOptions = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const infillColor = useAppSelector(selectInfillColorValue); const infillMethod = useAppSelector(selectInfillMethod); @@ -20,9 +21,9 @@ const ParamInfillColorOptions = () => { const handleInfillColor = useCallback( (v: RgbaColor) => { - dispatch(setInfillColorValue(v)); + dispatchParams(setInfillColorValue, v); }, - [dispatch] + [dispatchParams] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx index 2ae24fdb805..938ba56dee4 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx @@ -1,15 +1,15 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectInfillMethod, setInfillMethod } from 'features/controlLayers/store/paramsSlice'; +import { selectInfillMethod, setInfillMethod, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo'; const ParamInfillMethod = () => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const infillMethod = useAppSelector(selectInfillMethod); const { data: appConfigData } = useGetAppConfigQuery(); const options = useMemo( @@ -28,9 +28,9 @@ const ParamInfillMethod = () => { if (!v || !options.find((o) => o.value === v.value)) { return; } - dispatch(setInfillMethod(v.value)); + dispatchParams(setInfillMethod, v.value); }, - [dispatch, options] + [dispatchParams, options] ); const value = useMemo(() => options.find((o) => o.value === infillMethod), [options, infillMethod]); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx index f2998b9f84b..91330164b95 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx @@ -1,17 +1,18 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { selectInfillMethod, selectInfillPatchmatchDownscaleSize, setInfillPatchmatchDownscaleSize, + useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { selectInfillPatchmatchDownscaleSizeConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamInfillPatchmatchDownscaleSize = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const infillMethod = useAppSelector(selectInfillMethod); const infillPatchmatchDownscaleSize = useAppSelector(selectInfillPatchmatchDownscaleSize); const config = useAppSelector(selectInfillPatchmatchDownscaleSizeConfig); @@ -20,9 +21,9 @@ const ParamInfillPatchmatchDownscaleSize = () => { const handleChange = useCallback( (v: number) => { - dispatch(setInfillPatchmatchDownscaleSize(v)); + dispatchParams(setInfillPatchmatchDownscaleSize, v); }, - [dispatch] + [dispatchParams] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx index 3df4b3e9282..ac9000ed64c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx @@ -1,12 +1,17 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectInfillMethod, selectInfillTileSize, setInfillTileSize } from 'features/controlLayers/store/paramsSlice'; +import { useAppSelector } from 'app/store/storeHooks'; +import { + selectInfillMethod, + selectInfillTileSize, + setInfillTileSize, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { selectInfillTileSizeConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamInfillTileSize = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const infillTileSize = useAppSelector(selectInfillTileSize); const config = useAppSelector(selectInfillTileSizeConfig); const infillMethod = useAppSelector(selectInfillMethod); @@ -15,9 +20,9 @@ const ParamInfillTileSize = () => { const handleChange = useCallback( (v: number) => { - dispatch(setInfillTileSize(v)); + dispatchParams(setInfillTileSize, v); }, - [dispatch] + [dispatchParams] ); return ( 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..2419849d99d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx @@ -1,6 +1,10 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePromptChanged, selectHasNegativePrompt } from 'features/controlLayers/store/paramsSlice'; +import { useAppSelector } from 'app/store/storeHooks'; +import { + negativePromptChanged, + selectHasNegativePrompt, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusMinusBold } from 'react-icons/pi'; @@ -9,15 +13,15 @@ export const NegativePromptToggleButton = memo(() => { const { t } = useTranslation(); const hasNegativePrompt = useAppSelector(selectHasNegativePrompt); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const onClick = useCallback(() => { if (hasNegativePrompt) { - dispatch(negativePromptChanged(null)); + dispatchParams(negativePromptChanged, null); } else { - dispatch(negativePromptChanged('')); + dispatchParams(negativePromptChanged, ''); } - }, [dispatch, hasNegativePrompt]); + }, [dispatchParams, hasNegativePrompt]); const label = useMemo( () => (hasNegativePrompt ? t('common.removeNegativePrompt') : t('common.addNegativePrompt')), diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx index 145ca6f2da7..91f854baece 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectCFGScale, setCfgScale } from 'features/controlLayers/store/paramsSlice'; +import { selectCFGScale, setCfgScale, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { selectCFGScaleConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,13 +9,13 @@ import { useTranslation } from 'react-i18next'; const ParamCFGScale = () => { const cfgScale = useAppSelector(selectCFGScale); const config = useAppSelector(selectCFGScaleConfig); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const marks = useMemo( () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], [config.sliderMax, config.sliderMin] ); - const onChange = useCallback((v: number) => dispatch(setCfgScale(v)), [dispatch]); + const onChange = useCallback((v: number) => dispatchParams(setCfgScale, v), [dispatchParams]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx index 86740e0846d..4c80f0bcc3a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectGuidance, setGuidance } from 'features/controlLayers/store/paramsSlice'; +import { selectGuidance, setGuidance, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { selectGuidanceConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; const ParamGuidance = () => { const guidance = useAppSelector(selectGuidance); const config = useAppSelector(selectGuidanceConfig); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const marks = useMemo( () => [ @@ -19,7 +19,7 @@ const ParamGuidance = () => { ], [config.sliderMax, config.sliderMin] ); - const onChange = useCallback((v: number) => dispatch(setGuidance(v)), [dispatch]); + const onChange = useCallback((v: number) => dispatchParams(setGuidance, v), [dispatchParams]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index 1ba98fa774f..bff4a95c54e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -1,7 +1,11 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize'; -import { negativePromptChanged, selectNegativePromptWithFallback } from 'features/controlLayers/store/paramsSlice'; +import { + negativePromptChanged, + selectNegativePromptWithFallback, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; @@ -22,7 +26,7 @@ const persistOptions: Parameters[2] = { }; export const ParamNegativePrompt = memo(() => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const prompt = useAppSelector(selectNegativePromptWithFallback); const viewMode = useAppSelector(selectStylePresetViewMode); const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); @@ -43,9 +47,9 @@ export const ParamNegativePrompt = memo(() => { const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(negativePromptChanged(v)); + dispatchParams(negativePromptChanged, v); }, - [dispatch] + [dispatchParams] ); const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ prompt, 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..fb87a0a0c04 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -1,12 +1,13 @@ import { Box, Flex, Textarea } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize'; import { positivePromptChanged, selectModelSupportsNegativePrompt, selectPositivePrompt, selectPositivePromptHistory, + useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { promptGenerationFromImageDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; @@ -44,6 +45,7 @@ const persistOptions: Parameters[2] = { const usePromptHistory = () => { const store = useAppStore(); + const dispatchParams = useParamsDispatch(); const history = useAppSelector(selectPositivePromptHistory); /** @@ -79,8 +81,8 @@ const usePromptHistory = () => { // Shouldn't happen return; } - store.dispatch(positivePromptChanged(newPrompt)); - }, [history, store]); + dispatchParams(positivePromptChanged, newPrompt); + }, [dispatchParams, history, store]); const next = useCallback(() => { if (history.length === 0) { // No history, nothing to do @@ -94,7 +96,7 @@ const usePromptHistory = () => { state.historyIdx = state.historyIdx - 1; if (state.historyIdx < 0) { // Overshot to the "current" stashed prompt - store.dispatch(positivePromptChanged(state.stashedPrompt)); + dispatchParams(positivePromptChanged, state.stashedPrompt); // Clear state bc we're back to current prompt stateRef.current = null; return; @@ -105,8 +107,8 @@ const usePromptHistory = () => { // Shouldn't happen return; } - store.dispatch(positivePromptChanged(newPrompt)); - }, [history, store]); + dispatchParams(positivePromptChanged, newPrompt); + }, [dispatchParams, history]); const reset = useCallback(() => { // Clear stashed state - used when user clicks away or types in the prompt box stateRef.current = null; @@ -115,7 +117,7 @@ const usePromptHistory = () => { }; export const ParamPositivePrompt = memo(() => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const prompt = useAppSelector(selectPositivePrompt); const viewMode = useAppSelector(selectStylePresetViewMode); const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); @@ -142,12 +144,12 @@ export const ParamPositivePrompt = memo(() => { const { t } = useTranslation(); const handleChange = useCallback( (v: string) => { - dispatch(positivePromptChanged(v)); + dispatchParams(positivePromptChanged, v); // When the user changes the prompt, reset the prompt history state. This event is not fired when the prompt is // changed via the prompt history navigation. promptHistoryApi.reset(); }, - [dispatch, promptHistoryApi] + [dispatchParams, promptHistoryApi] ); const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({ prompt, diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx index d670de68b80..517a60d1218 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx @@ -1,15 +1,15 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectScheduler, setScheduler } from 'features/controlLayers/store/paramsSlice'; +import { selectScheduler, setScheduler, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamScheduler = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const scheduler = useAppSelector(selectScheduler); @@ -18,9 +18,9 @@ const ParamScheduler = () => { if (!isParameterScheduler(v?.value)) { return; } - dispatch(setScheduler(v.value)); + dispatchParams(setScheduler, v.value); }, - [dispatch] + [dispatchParams] ); const value = useMemo(() => SCHEDULER_OPTIONS.find((o) => o.value === scheduler), [scheduler]); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx index f7ef4660b58..60397c2870c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectSteps, setSteps } from 'features/controlLayers/store/paramsSlice'; +import { selectSteps, setSteps, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { selectStepsConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; const ParamSteps = () => { const steps = useAppSelector(selectSteps); const config = useAppSelector(selectStepsConfig); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const marks = useMemo( () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], @@ -17,9 +17,9 @@ const ParamSteps = () => { ); const onChange = useCallback( (v: number) => { - dispatch(setSteps(v)); + dispatchParams(setSteps, v); }, - [dispatch] + [dispatchParams] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/PositivePromptHistory.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/PositivePromptHistory.tsx index 628c895da22..e050c654dcb 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/PositivePromptHistory.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/PositivePromptHistory.tsx @@ -13,13 +13,14 @@ import { Text, useShiftModifier, } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { positivePromptChanged, promptHistoryCleared, promptRemovedFromHistory, selectPositivePromptHistory, + useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback, useMemo, useState } from 'react'; @@ -53,13 +54,13 @@ PositivePromptHistoryIconButton.displayName = 'PositivePromptHistoryIconButton'; const PromptHistoryContent = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const positivePromptHistory = useAppSelector(selectPositivePromptHistory); const [searchTerm, setSearchTerm] = useState(''); const onClickClearHistory = useCallback(() => { - dispatch(promptHistoryCleared()); - }, [dispatch]); + dispatchParams(promptHistoryCleared); + }, [dispatchParams]); const filteredPrompts = useMemo(() => { const trimmedSearchTerm = searchTerm.trim(); @@ -131,16 +132,16 @@ const PromptHistoryContent = memo(() => { PromptHistoryContent.displayName = 'PromptHistoryContent'; const PromptItem = memo(({ prompt }: { prompt: string }) => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const shiftKey = useShiftModifier(); const onClickUse = useCallback(() => { - dispatch(positivePromptChanged(prompt)); - }, [dispatch, prompt]); + dispatchParams(positivePromptChanged, prompt); + }, [dispatchParams, prompt]); const onClickDelete = useCallback(() => { - dispatch(promptRemovedFromHistory(prompt)); - }, [dispatch, prompt]); + dispatchParams(promptRemovedFromHistory, prompt); + }, [dispatchParams, prompt]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx index bd0c0d03a6b..77da433a13d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx @@ -1,5 +1,5 @@ import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { aspectRatioIdChanged, @@ -9,6 +9,7 @@ import { selectIsGemini2_5, selectIsImagen3, selectIsImagen4, + useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { isAspectRatioID, @@ -25,7 +26,7 @@ import { PiCaretDownBold } from 'react-icons/pi'; export const DimensionsAspectRatioSelect = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const id = useAppSelector(selectAspectRatioID); const isImagen3 = useAppSelector(selectIsImagen3); const isChatGPT4o = useAppSelector(selectIsChatGPT4o); @@ -56,9 +57,9 @@ export const DimensionsAspectRatioSelect = memo(() => { if (!isAspectRatioID(e.target.value)) { return; } - dispatch(aspectRatioIdChanged({ id: e.target.value })); + dispatchParams(aspectRatioIdChanged, { id: e.target.value }); }, - [dispatch] + [dispatchParams] ); return ( 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..3a12e603ca5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsHeight.tsx @@ -1,15 +1,21 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { 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, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { selectHeightConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const DimensionsHeight = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); const height = useAppSelector(selectHeight); const config = useAppSelector(selectHeightConfig); @@ -18,9 +24,9 @@ export const DimensionsHeight = memo(() => { const onChange = useCallback( (v: number) => { - dispatch(heightChanged({ height: v })); + dispatchParams(heightChanged, { height: v }); }, - [dispatch] + [dispatchParams] ); const marks = useMemo( diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx index 2de397cc784..3ce394cc4b3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx @@ -1,9 +1,10 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { aspectRatioLockToggled, selectAspectRatioIsLocked, selectIsApiBaseModel, + useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,13 +12,13 @@ import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi'; export const DimensionsLockAspectRatioButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const isLocked = useAppSelector(selectAspectRatioIsLocked); const isApiModel = useAppSelector(selectIsApiBaseModel); const onClick = useCallback(() => { - dispatch(aspectRatioLockToggled()); - }, [dispatch]); + dispatchParams(aspectRatioLockToggled); + }, [dispatchParams]); return ( { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const isApiModel = useAppSelector(selectIsApiBaseModel); const width = useAppSelector(selectWidth); const height = useAppSelector(selectHeight); @@ -28,8 +29,8 @@ export const DimensionsSetOptimalSizeButton = memo(() => { [height, width, optimalDimension] ); const onClick = useCallback(() => { - dispatch(sizeOptimized()); - }, [dispatch]); + dispatchParams(sizeOptimized); + }, [dispatchParams]); const tooltip = useMemo(() => { if (isSizeTooSmall) { return t('parameters.setToOptimalSizeTooSmall'); 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..ed85a8a3314 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx @@ -1,16 +1,15 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { dimensionsSwapped } from 'features/controlLayers/store/paramsSlice'; +import { dimensionsSwapped, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsDownUpBold } from 'react-icons/pi'; export const DimensionsSwapButton = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const onClick = useCallback(() => { - dispatch(dimensionsSwapped()); - }, [dispatch]); + dispatchParams(dimensionsSwapped); + }, [dispatchParams]); return ( { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const width = useAppSelector(selectWidth); const optimalDimension = useAppSelector(selectOptimalDimension); const config = useAppSelector(selectWidthConfig); @@ -20,9 +26,9 @@ export const DimensionsWidth = memo(() => { const onChange = useCallback( (v: number) => { - dispatch(widthChanged({ width: v })); + dispatchParams(widthChanged, { width: v }); }, - [dispatch] + [dispatchParams] ); const marks = useMemo( diff --git a/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx b/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx index 08b24de1034..3990f0edc5a 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 { selectActiveParams } 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(selectActiveParams, selectLoRAsSlice, (params, loras) => { const keys: string[] = []; const main = params.model; const vae = params.vae; 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..fd39e9a00a7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx @@ -1,7 +1,7 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectSeamlessXAxis, setSeamlessXAxis } from 'features/controlLayers/store/paramsSlice'; +import { selectSeamlessXAxis, setSeamlessXAxis, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,13 +10,13 @@ const ParamSeamlessXAxis = () => { const { t } = useTranslation(); const seamlessXAxis = useAppSelector(selectSeamlessXAxis); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const handleChange = useCallback( (e: ChangeEvent) => { - dispatch(setSeamlessXAxis(e.target.checked)); + dispatchParams(setSeamlessXAxis, e.target.checked); }, - [dispatch] + [dispatchParams] ); return ( 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..c7bfac96ddb 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx @@ -1,7 +1,7 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectSeamlessYAxis, setSeamlessYAxis } from 'features/controlLayers/store/paramsSlice'; +import { selectSeamlessYAxis, setSeamlessYAxis, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,12 +9,12 @@ import { useTranslation } from 'react-i18next'; const ParamSeamlessYAxis = () => { const { t } = useTranslation(); const seamlessYAxis = useAppSelector(selectSeamlessYAxis); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const handleChange = useCallback( (e: ChangeEvent) => { - dispatch(setSeamlessYAxis(e.target.checked)); + dispatchParams(setSeamlessYAxis, e.target.checked); }, - [dispatch] + [dispatchParams] ); return ( 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..14ecafa9367 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx @@ -1,8 +1,13 @@ import { CompositeNumberInput, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectSeed, selectShouldRandomizeSeed, setSeed } from 'features/controlLayers/store/paramsSlice'; +import { + selectSeed, + selectShouldRandomizeSeed, + setSeed, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,9 +17,9 @@ export const ParamSeedNumberInput = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); - const handleChangeSeed = useCallback((v: number) => dispatch(setSeed(v)), [dispatch]); + const handleChangeSeed = useCallback((v: number) => dispatchParams(setSeed, v), [dispatchParams]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx index ba41887e746..0e11a5fb6f8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx @@ -1,19 +1,23 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectShouldRandomizeSeed, setShouldRandomizeSeed } from 'features/controlLayers/store/paramsSlice'; +import { useAppSelector } from 'app/store/storeHooks'; +import { + selectShouldRandomizeSeed, + setShouldRandomizeSeed, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const ParamSeedRandomize = memo(() => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); const handleChangeShouldRandomizeSeed = useCallback( - (e: ChangeEvent) => dispatch(setShouldRandomizeSeed(e.target.checked)), - [dispatch] + (e: ChangeEvent) => dispatchParams(setShouldRandomizeSeed, e.target.checked), + [dispatchParams] ); return ( 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..4fb08721bc9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx @@ -1,21 +1,21 @@ import { Button } from '@invoke-ai/ui-library'; import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import randomInt from 'common/util/randomInt'; -import { selectShouldRandomizeSeed, setSeed } from 'features/controlLayers/store/paramsSlice'; +import { selectShouldRandomizeSeed, setSeed, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiShuffleBold } from 'react-icons/pi'; export const ParamSeedShuffle = memo(() => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); const { t } = useTranslation(); const handleClickRandomizeSeed = useCallback( - () => dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX))), - [dispatch] + () => dispatchParams(setSeed, randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)), + [dispatchParams] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleCFGScale.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleCFGScale.tsx index 9af368cc018..f628184ef7a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleCFGScale.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleCFGScale.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectUpscaleCfgScale, setUpscaleCfgScale } from 'features/controlLayers/store/paramsSlice'; +import { selectUpscaleCfgScale, setUpscaleCfgScale, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { selectCFGScaleConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,13 +9,13 @@ import { useTranslation } from 'react-i18next'; const ParamUpscaleCFGScale = () => { const cfgScale = useAppSelector(selectUpscaleCfgScale); const config = useAppSelector(selectCFGScaleConfig); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const marks = useMemo( () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], [config.sliderMax, config.sliderMin] ); - const onChange = useCallback((v: number) => dispatch(setUpscaleCfgScale(v)), [dispatch]); + const onChange = useCallback((v: number) => dispatchParams(setUpscaleCfgScale, v), [dispatchParams]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleScheduler.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleScheduler.tsx index e7cb1655988..1eee3140d6d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleScheduler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleScheduler.tsx @@ -1,15 +1,19 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectUpscaleScheduler, setUpscaleScheduler } from 'features/controlLayers/store/paramsSlice'; +import { + selectUpscaleScheduler, + setUpscaleScheduler, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamUpscaleScheduler = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const scheduler = useAppSelector(selectUpscaleScheduler); @@ -18,9 +22,9 @@ const ParamUpscaleScheduler = () => { if (!isParameterScheduler(v?.value)) { return; } - dispatch(setUpscaleScheduler(v.value)); + dispatchParams(setUpscaleScheduler, v.value); }, - [dispatch] + [dispatchParams] ); const value = useMemo(() => SCHEDULER_OPTIONS.find((o) => o.value === scheduler), [scheduler]); diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx index 49502ebc625..649605ef899 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx @@ -1,8 +1,8 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { fluxVAESelected, selectFLUXVAE } from 'features/controlLayers/store/paramsSlice'; +import { fluxVAESelected, selectFLUXVAE, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +10,7 @@ import { useFluxVAEModels } from 'services/api/hooks/modelsByType'; import type { VAEModelConfig } from 'services/api/types'; const ParamFLUXVAEModelSelect = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const vae = useAppSelector(selectFLUXVAE); const [modelConfigs, { isLoading }] = useFluxVAEModels(); @@ -18,10 +18,10 @@ const ParamFLUXVAEModelSelect = () => { const _onChange = useCallback( (vae: VAEModelConfig | null) => { if (vae) { - dispatch(fluxVAESelected(zModelIdentifierField.parse(vae))); + dispatchParams(fluxVAESelected, zModelIdentifierField.parse(vae)); } }, - [dispatch] + [dispatchParams] ); const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx index ea18c132ee7..d96414f3059 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx @@ -1,8 +1,8 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { selectBase, selectVAE, vaeSelected } from 'features/controlLayers/store/paramsSlice'; +import { selectBase, selectVAE, useParamsDispatch, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +10,7 @@ import { useVAEModels } from 'services/api/hooks/modelsByType'; import type { VAEModelConfig } from 'services/api/types'; const ParamVAEModelSelect = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const base = useAppSelector(selectBase); const vae = useAppSelector(selectVAE); @@ -25,9 +25,9 @@ const ParamVAEModelSelect = () => { ); const _onChange = useCallback( (vae: VAEModelConfig | null) => { - dispatch(vaeSelected(vae ? zModelIdentifierField.parse(vae) : null)); + dispatchParams(vaeSelected, vae ? zModelIdentifierField.parse(vae) : null); }, - [dispatch] + [dispatchParams] ); const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ modelConfigs, diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx index 26d2fa2888b..de242a1627a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx @@ -1,8 +1,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectVAEPrecision, vaePrecisionChanged } from 'features/controlLayers/store/paramsSlice'; +import { selectVAEPrecision, useParamsDispatch, vaePrecisionChanged } from 'features/controlLayers/store/paramsSlice'; import { isParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -14,7 +14,7 @@ const options = [ const ParamVAEPrecision = () => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const vaePrecision = useAppSelector(selectVAEPrecision); const onChange = useCallback( @@ -23,9 +23,9 @@ const ParamVAEPrecision = () => { return; } - dispatch(vaePrecisionChanged(v.value)); + dispatchParams(vaePrecisionChanged, v.value); }, - [dispatch] + [dispatchParams] ); const value = useMemo(() => options.find((o) => o.value === vaePrecision), [vaePrecision]); diff --git a/invokeai/frontend/web/src/features/prompt/PromptExpansion/PromptExpansionResultOverlay.tsx b/invokeai/frontend/web/src/features/prompt/PromptExpansion/PromptExpansionResultOverlay.tsx index 015bf5946d1..35ea26b1552 100644 --- a/invokeai/frontend/web/src/features/prompt/PromptExpansion/PromptExpansionResultOverlay.tsx +++ b/invokeai/frontend/web/src/features/prompt/PromptExpansion/PromptExpansionResultOverlay.tsx @@ -1,6 +1,10 @@ import { ButtonGroup, Flex, Icon, IconButton, Text, Tooltip } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { positivePromptChanged, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice'; +import { useAppSelector } from 'app/store/storeHooks'; +import { + positivePromptChanged, + selectPositivePrompt, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { useCallback } from 'react'; import { PiCheckBold, PiMagicWandBold, PiPlusBold, PiXBold } from 'react-icons/pi'; @@ -11,20 +15,20 @@ interface PromptExpansionResultOverlayProps { } export const PromptExpansionResultOverlay = ({ expandedText }: PromptExpansionResultOverlayProps) => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const positivePrompt = useAppSelector(selectPositivePrompt); const handleReplace = useCallback(() => { - dispatch(positivePromptChanged(expandedText)); + dispatchParams(positivePromptChanged, expandedText); promptExpansionApi.reset(); - }, [dispatch, expandedText]); + }, [dispatchParams, expandedText]); const handleInsert = useCallback(() => { const currentText = positivePrompt; const newText = currentText ? `${currentText}\n${expandedText}` : expandedText; - dispatch(positivePromptChanged(newText)); + dispatchParams(positivePromptChanged, newText); promptExpansionApi.reset(); - }, [dispatch, expandedText, positivePrompt]); + }, [dispatchParams, expandedText, positivePrompt]); const handleDiscard = useCallback(() => { promptExpansionApi.reset(); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx index 3c2bd72bcc0..59396201c75 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx @@ -1,19 +1,19 @@ import { CompositeNumberInput } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectIterations, setIterations } from 'features/controlLayers/store/paramsSlice'; +import { selectIterations, setIterations, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { selectIterationsConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; export const QueueIterationsNumberInput = memo(() => { const iterations = useAppSelector(selectIterations); const config = useAppSelector(selectIterationsConfig); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const handleChange = useCallback( (v: number) => { - dispatch(setIterations(v)); + dispatchParams(setIterations, v); }, - [dispatch] + [dispatchParams] ); return ( diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts index 64c3791f9e6..04a09c469e2 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts @@ -7,7 +7,12 @@ 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 { + paramsDispatch, + positivePromptAddedToHistory, + selectActiveParams, + 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,11 +42,10 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep dispatch(enqueueRequestedCanvas()); const state = getState(); - const destination = selectCanvasDestination(state, canvasManager.canvasId); - assert(destination, 'Destination must exist when CanvasManager has already been created'); + const params = selectActiveParams(state); - const model = state.params.model; + const model = params.model; if (!model) { log.error('No model found in state'); return; @@ -133,7 +137,7 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep const enqueueResult = await req.unwrap(); // Push to prompt history on successful enqueue - dispatch(positivePromptAddedToHistory(selectPositivePrompt(state))); + paramsDispatch(store, positivePromptAddedToHistory, selectPositivePrompt(state)); return { batchConfig, enqueueResult }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts index 1529a87cff9..44c394d56a1 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts @@ -5,7 +5,12 @@ 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 { + paramsDispatch, + positivePromptAddedToHistory, + selectActiveParams, + 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 +40,9 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => { dispatch(enqueueRequestedGenerate()); const state = getState(); + const params = selectActiveParams(state); - const model = state.params.model; + const model = params.model; if (!model) { log.error('No model found in state'); return; @@ -126,7 +132,7 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => { const enqueueResult = await req.unwrap(); // Push to prompt history on successful enqueue - dispatch(positivePromptAddedToHistory(selectPositivePrompt(state))); + paramsDispatch(store, positivePromptAddedToHistory, selectPositivePrompt(state)); return { batchConfig, enqueueResult }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts index 01f278d98db..21b5c76dfa1 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts @@ -2,7 +2,12 @@ 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 { + paramsDispatch, + positivePromptAddedToHistory, + selectActiveParams, + 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 +23,9 @@ const enqueueUpscaling = async (store: AppStore, prepend: boolean) => { dispatch(enqueueRequestedUpscaling()); const state = getState(); + const params = selectActiveParams(state); - const model = state.params.model; + const model = params.model; if (!model) { log.error('No model found in state'); return; @@ -45,7 +51,7 @@ const enqueueUpscaling = async (store: AppStore, prepend: boolean) => { const enqueueResult = await req.unwrap(); // Push to prompt history on successful enqueue - dispatch(positivePromptAddedToHistory(selectPositivePrompt(state))); + paramsDispatch(store, positivePromptAddedToHistory, selectPositivePrompt(state)); return { batchConfig, enqueueResult }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueVideo.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueVideo.ts index 183026c3632..862a031011c 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueVideo.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueVideo.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 { + paramsDispatch, + positivePromptAddedToHistory, + selectPositivePrompt, +} from 'features/controlLayers/store/paramsSlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildRunwayVideoGraph } from 'features/nodes/util/graph/generation/buildRunwayVideoGraph'; import { buildVeo3VideoGraph } from 'features/nodes/util/graph/generation/buildVeo3VideoGraph'; @@ -110,7 +114,7 @@ const enqueueVideo = async (store: AppStore, prepend: boolean) => { const enqueueResult = await req.unwrap(); // Push to prompt history on successful enqueue - dispatch(positivePromptAddedToHistory(selectPositivePrompt(state))); + paramsDispatch(store, positivePromptAddedToHistory, selectPositivePrompt(state)); return { batchConfig, enqueueResult }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueWorkflows.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueWorkflows.ts index 85272f4768a..9093423648c 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 { selectActiveParams } 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 = selectActiveParams(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/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index dc71ebfdde7..9cd30ca960f 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 { selectActiveParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; -import type { CanvasState, LoRA, ParamsState, RefImagesState } from 'features/controlLayers/store/types'; +import type { CanvasState, InstanceParamsState, LoRA, RefImagesState } from 'features/controlLayers/store/types'; import { getControlLayerWarnings, getGlobalReferenceImageWarnings, @@ -78,7 +78,7 @@ type UpdateReasonsArg = { tab: TabName; isConnected: boolean; canvas: CanvasState; - params: ParamsState; + params: InstanceParamsState; refImages: RefImagesState; dynamicPrompts: DynamicPromptsState; canvasIsFiltering: boolean; @@ -199,7 +199,7 @@ export const useReadinessWatcher = () => { const canvasManager = useCanvasManagerSafe(); const tab = useAppSelector(selectActiveTab); const canvas = useAppSelector(selectActiveCanvas); - const params = useAppSelector(selectParamsSlice); + const params = useAppSelector(selectActiveParams); const refImages = useAppSelector(selectRefImagesSlice); const dynamicPrompts = useAppSelector(selectDynamicPromptsSlice); const nodes = useAppSelector(selectNodesSlice); @@ -277,7 +277,7 @@ const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invok const getReasonsWhyCannotEnqueueVideoTab = (arg: { isConnected: boolean; video: VideoState; - params: ParamsState; + params: InstanceParamsState; dynamicPrompts: DynamicPromptsState; promptExpansionRequest: PromptExpansionRequestState; isVideoEnabled: boolean; @@ -319,7 +319,7 @@ const getReasonsWhyCannotEnqueueVideoTab = (arg: { const getReasonsWhyCannotEnqueueGenerateTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; - params: ParamsState; + params: InstanceParamsState; refImages: RefImagesState; loras: LoRA[]; dynamicPrompts: DynamicPromptsState; @@ -490,7 +490,7 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: { isConnected: boolean; upscale: UpscaleState; config: AppConfig; - params: ParamsState; + params: InstanceParamsState; loras: LoRA[]; promptExpansionRequest: PromptExpansionRequestState; }) => { @@ -553,7 +553,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; canvas: CanvasState; - params: ParamsState; + params: InstanceParamsState; refImages: RefImagesState; loras: LoRA[]; dynamicPrompts: DynamicPromptsState; @@ -821,7 +821,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: { }; export const selectPromptsCount = createSelector( - selectParamsSlice, + selectActiveParams, selectDynamicPromptsSlice, (params, dynamicPrompts) => (getShouldProcessPrompt(params.positivePrompt) ? dynamicPrompts.prompts.length : 1) ); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx index 3d0ad822bab..b2ab0175511 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectRefinerCFGScale, setRefinerCFGScale } from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerCFGScale, setRefinerCFGScale, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { selectCFGScaleConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerCFGScale = () => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const refinerCFGScale = useAppSelector(selectRefinerCFGScale); const config = useAppSelector(selectCFGScaleConfig); const marks = useMemo( @@ -16,7 +16,7 @@ const ParamSDXLRefinerCFGScale = () => { [config.sliderMax, config.sliderMin] ); - const onChange = useCallback((v: number) => dispatch(setRefinerCFGScale(v)), [dispatch]); + const onChange = useCallback((v: number) => dispatchParams(setRefinerCFGScale, v), [dispatchParams]); return ( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx index fa817193aff..3b302ecaa64 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx @@ -1,8 +1,8 @@ import { Combobox, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { refinerModelChanged, selectRefinerModel } from 'features/controlLayers/store/paramsSlice'; +import { refinerModelChanged, selectRefinerModel, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,19 +13,19 @@ import type { MainModelConfig } from 'services/api/types'; const optionsFilter = (model: MainModelConfig) => model.base === 'sdxl-refiner'; const ParamSDXLRefinerModelSelect = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const model = useAppSelector(selectRefinerModel); const { t } = useTranslation(); const [modelConfigs, { isLoading }] = useRefinerModels(); const _onChange = useCallback( (model: MainModelConfig | null) => { if (!model) { - dispatch(refinerModelChanged(null)); + dispatchParams(refinerModelChanged, null); return; } - dispatch(refinerModelChanged(zModelIdentifierField.parse(model))); + dispatchParams(refinerModelChanged, zModelIdentifierField.parse(model)); }, - [dispatch] + [dispatchParams] ); const { options, value, onChange, placeholder, noOptionsMessage } = useModelCombobox({ modelConfigs, diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx index 3729468bcdc..f0c8ee22b6b 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx @@ -1,9 +1,10 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { selectRefinerNegativeAestheticScore, setRefinerNegativeAestheticScore, + useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,10 +12,13 @@ import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerNegativeAestheticScore = () => { const refinerNegativeAestheticScore = useAppSelector(selectRefinerNegativeAestheticScore); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); - const handleChange = useCallback((v: number) => dispatch(setRefinerNegativeAestheticScore(v)), [dispatch]); + const handleChange = useCallback( + (v: number) => dispatchParams(setRefinerNegativeAestheticScore, v), + [dispatchParams] + ); return ( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx index 2661c5ce573..f51038658a9 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx @@ -1,19 +1,23 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { selectRefinerPositiveAestheticScore, setRefinerPositiveAestheticScore, + useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerPositiveAestheticScore = () => { const refinerPositiveAestheticScore = useAppSelector(selectRefinerPositiveAestheticScore); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); - const handleChange = useCallback((v: number) => dispatch(setRefinerPositiveAestheticScore(v)), [dispatch]); + const handleChange = useCallback( + (v: number) => dispatchParams(setRefinerPositiveAestheticScore, v), + [dispatchParams] + ); return ( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx index acce9dd8e9e..39280048387 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx @@ -1,15 +1,19 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectRefinerScheduler, setRefinerScheduler } from 'features/controlLayers/store/paramsSlice'; +import { + selectRefinerScheduler, + setRefinerScheduler, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerScheduler = () => { - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const refinerScheduler = useAppSelector(selectRefinerScheduler); @@ -18,9 +22,9 @@ const ParamSDXLRefinerScheduler = () => { if (!isParameterScheduler(v?.value)) { return; } - dispatch(setRefinerScheduler(v.value)); + dispatchParams(setRefinerScheduler, v.value); }, - [dispatch] + [dispatchParams] ); const value = useMemo(() => SCHEDULER_OPTIONS.find((o) => o.value === refinerScheduler), [refinerScheduler]); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx index 856ca391347..7ff89ebc80c 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectRefinerStart, setRefinerStart } from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerStart, setRefinerStart, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerStart = () => { const refinerStart = useAppSelector(selectRefinerStart); - const dispatch = useAppDispatch(); - const handleChange = useCallback((v: number) => dispatch(setRefinerStart(v)), [dispatch]); + const dispatchParams = useParamsDispatch(); + const handleChange = useCallback((v: number) => dispatchParams(setRefinerStart, v), [dispatchParams]); const { t } = useTranslation(); return ( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx index 3a3a2ce5c49..b8733ee15d4 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectRefinerSteps, setRefinerSteps } from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerSteps, setRefinerSteps, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; import { selectStepsConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerSteps = () => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const refinerSteps = useAppSelector(selectRefinerSteps); const config = useAppSelector(selectStepsConfig); @@ -19,9 +19,9 @@ const ParamSDXLRefinerSteps = () => { const onChange = useCallback( (v: number) => { - dispatch(setRefinerSteps(v)); + dispatchParams(setRefinerSteps, v); }, - [dispatch] + [dispatchParams] ); return ( 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..94dbf3793b0 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,7 @@ 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 { selectActiveParams, 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 +36,7 @@ export const AdvancedSettingsAccordion = memo(() => { const selectBadges = useMemo( () => - createMemoizedSelector([selectParamsSlice, selectIsFLUX], (params, isFLUX) => { + createMemoizedSelector([selectActiveParams, 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..9e5b7030180 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,7 @@ 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 { selectActiveParams, 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 +23,7 @@ export const UpscaleTabAdvancedSettingsAccordion = memo(() => { const selectBadges = useMemo( () => - createMemoizedSelector([selectParamsSlice, selectIsFLUX], (params, isFLUX) => { + createMemoizedSelector([selectActiveParams, 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..89cb41a975d 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 { selectActiveParams, 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(selectActiveParams, (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..c2575833f11 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx @@ -1,6 +1,10 @@ import { Badge, Flex, IconButton, Spacer, Text, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/paramsSlice'; +import { + negativePromptChanged, + positivePromptChanged, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; import { activeStylePresetIdChanged, @@ -31,6 +35,7 @@ export const ActiveStylePreset = () => { }); const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const { presetModifiedPositivePrompt, presetModifiedNegativePrompt } = usePresetModifiedPrompts(); @@ -47,12 +52,12 @@ export const ActiveStylePreset = () => { const handleFlattenPrompts = useCallback>( (e) => { e.stopPropagation(); - dispatch(positivePromptChanged(presetModifiedPositivePrompt)); - dispatch(negativePromptChanged(presetModifiedNegativePrompt)); + dispatchParams(positivePromptChanged, presetModifiedPositivePrompt); + dispatchParams(negativePromptChanged, presetModifiedNegativePrompt); dispatch(viewModeChanged(false)); dispatch(activeStylePresetIdChanged(null)); }, - [dispatch, presetModifiedPositivePrompt, presetModifiedNegativePrompt] + [dispatch, dispatchParams, presetModifiedPositivePrompt, presetModifiedNegativePrompt] ); const handleToggleViewMode = useCallback>( 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..ee7754bd1f8 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -19,7 +19,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { buildUseBoolean } from 'common/hooks/useBoolean'; -import { selectShouldUseCPUNoise, shouldUseCpuNoiseChanged } from 'features/controlLayers/store/paramsSlice'; +import { + selectShouldUseCPUNoise, + shouldUseCpuNoiseChanged, + useParamsDispatch, +} from 'features/controlLayers/store/paramsSlice'; import { useRefreshAfterResetModal } from 'features/system/components/SettingsModal/RefreshAfterResetModal'; import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled'; import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel'; @@ -81,6 +85,7 @@ const [useSettingsModal] = buildUseBoolean(false); const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps) => { const dispatch = useAppDispatch(); + const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const { isNSFWCheckerAvailable, isWatermarkerAvailable } = useGetAppConfigQuery(undefined, { @@ -171,9 +176,9 @@ const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps) ); const handleChangeShouldUseCpuNoise = useCallback( (e: ChangeEvent) => { - dispatch(shouldUseCpuNoiseChanged(e.target.checked)); + dispatchParams(shouldUseCpuNoiseChanged, e.target.checked); }, - [dispatch] + [dispatchParams] ); const handleChangeShouldShowInvocationProgressDetail = useCallback( From f7200e77aed6d1c58a2e0b382aba61193079c5b2 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Tue, 30 Sep 2025 11:50:06 +0200 Subject: [PATCH 09/16] canvas dependencies initialization fixed --- invokeai/frontend/web/src/app/store/store.ts | 12 +- .../store/canvasSettingsSlice.ts | 22 +++- .../controlLayers/store/canvasSlice.ts | 38 +++--- .../store/canvasStagingAreaSlice.ts | 115 +++++++++++------- .../controlLayers/store/paramsSlice.ts | 59 +++++---- .../src/features/controlLayers/store/types.ts | 18 +-- .../nodes/util/graph/graphBuilderUtils.ts | 4 +- .../web/src/features/queue/store/readiness.ts | 12 +- 8 files changed, 169 insertions(+), 111 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index fa3c31a5e51..0fe6d06426b 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -22,8 +22,13 @@ import { merge } from 'es-toolkit'; import { omit, pick } from 'es-toolkit/compat'; import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/slice'; import { canvasSettingsReducer, canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice'; -import { canvasSliceConfig, migrateCanvas, undoableCanvasesReducer } from 'features/controlLayers/store/canvasSlice'; -import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { + canvasSliceConfig, + initializeCanvasDependencies, + migrateCanvas, + undoableCanvasesReducer, +} from 'features/controlLayers/store/canvasSlice'; +import { canvasSessionReducer, canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; import { paramsSliceConfig, paramsSliceReducer } from 'features/controlLayers/store/paramsSlice'; import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice'; @@ -88,7 +93,7 @@ const SLICE_CONFIGS = { // Remember to wrap undoable reducers in `undoable()`! const ALL_REDUCERS = { [api.reducerPath]: api.reducer, - [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer, + [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionReducer, [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsReducer, [canvasSliceConfig.slice.reducerPath]: undoableCanvasesReducer, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer, @@ -222,6 +227,7 @@ export const createStore = (options?: { persist?: boolean; persistDebounce?: num effect: (action, { dispatch, unsubscribe }) => { unsubscribe(); dispatch(migrateCanvas()); + dispatch(initializeCanvasDependencies()); options?.onRehydrated?.(); }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index 20b13875bea..2ddc61b7571 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -11,6 +11,7 @@ import { z } from 'zod'; import { canvasAdding, canvasDeleted, + canvasInitialized, canvasMultiCanvasMigrated, MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, } from './canvasSlice'; @@ -154,7 +155,7 @@ type CanvasPayloadAction

= PayloadAction>; const canvasSettingsSlice = createSlice({ name: 'canvasSettings', - initialState: getInitialCanvasSettingsState(), + initialState: getInitialCanvasSettingsState, reducers: { settingsClipToBboxChanged: (state, action: PayloadAction<{ clipToBbox: boolean }>) => { const { clipToBbox } = action.payload; @@ -231,10 +232,16 @@ const canvasSettingsSlice = createSlice({ state.canvases[settings.canvasId] = settings; delete state.canvases[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; }); + builder.addCase(canvasInitialized, (state, action) => { + const canvasId = action.payload.canvasId; + if (!state.canvases[canvasId]) { + state.canvases[canvasId] = getInitialCanvasInstanceSettings(canvasId); + } + }); }, }); -const canvasInstanceSettingsSlice = createSlice({ +const canvasInstanceSettingsFragment = createSlice({ name: 'canvasSettings', initialState: {} as CanvasInstanceSettings, reducers: { @@ -294,11 +301,14 @@ export const { settingsBgColorChanged, settingsFgColorChanged, settingsColorsSetToDefault, -} = canvasInstanceSettingsSlice.actions; +} = canvasInstanceSettingsFragment.actions; -const isCanvasInstanceSettingsAction = isAnyOf(...Object.values(canvasInstanceSettingsSlice.actions)); +const isCanvasInstanceSettingsAction = isAnyOf(...Object.values(canvasInstanceSettingsFragment.actions)); -export const canvasSettingsReducer = (state: CanvasSettingsState, action: UnknownAction): CanvasSettingsState => { +export const canvasSettingsReducer = ( + state: CanvasSettingsState | undefined, + action: UnknownAction +): CanvasSettingsState => { state = canvasSettingsSlice.reducer(state, action); if (!isCanvasInstanceSettingsAction(action)) { @@ -311,7 +321,7 @@ export const canvasSettingsReducer = (state: CanvasSettingsState, action: Unknow ...state, canvases: { ...state.canvases, - [canvasId]: canvasInstanceSettingsSlice.reducer(state.canvases[canvasId], action), + [canvasId]: canvasInstanceSettingsFragment.reducer(state.canvases[canvasId], action), }, }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index ce22c26630d..6c2b2ba161d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -193,10 +193,6 @@ const canvasesSlice = createSlice({ }, }, canvasAdding: (_state, _action: CanvasPayloadAction) => {}, - canvasMigrated: (state) => { - delete state.migration; - }, - canvasMultiCanvasMigrated: (_state, _action: CanvasPayloadAction) => {}, canvasActivated: (state, action: CanvasPayloadAction) => { const { canvasId } = action.payload; @@ -227,10 +223,15 @@ const canvasesSlice = createSlice({ delete state.canvases[canvas.id]; }, canvasDeleted: (_state, _action: CanvasPayloadAction) => {}, + canvasMigrated: (state) => { + delete state.migration; + }, + canvasMultiCanvasMigrated: (_state, _action: CanvasPayloadAction) => {}, + canvasInitialized: (_state, _action: CanvasPayloadAction) => {}, }, }); -const canvasSlice = createSlice({ +const canvasFragment = createSlice({ name: 'canvas', initialState: {} as CanvasState, reducers: { @@ -1890,6 +1891,11 @@ export const addCanvas = (payload: { isSelected?: boolean }) => (dispatch: AppDi export const MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER = 'multi-canvas-id-placeholder'; +export const deleteCanvas = (payload: { canvasId: string }) => (dispatch: AppDispatch) => { + dispatch(canvasesSlice.actions.canvasDeleting(payload)); + dispatch(canvasesSlice.actions.canvasDeleted(payload)); +}; + export const migrateCanvas = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -1900,17 +1906,19 @@ export const migrateCanvas = () => (dispatch: AppDispatch, getState: () => RootS dispatch(canvasesSlice.actions.canvasMigrated()); }; -export const deleteCanvas = (payload: { canvasId: string }) => (dispatch: AppDispatch) => { - dispatch(canvasesSlice.actions.canvasDeleting(payload)); - dispatch(canvasesSlice.actions.canvasDeleted(payload)); +export const initializeCanvasDependencies = () => (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + + dispatch(canvasInitialized({ canvasId: state.canvas.activeCanvasId })); }; export const { // Canvas canvasAdding, - canvasMultiCanvasMigrated, canvasDeleted, canvasActivated, + canvasMultiCanvasMigrated, + canvasInitialized, } = canvasesSlice.actions; export const { @@ -2010,9 +2018,9 @@ export const { inpaintMaskDenoiseLimitChanged, inpaintMaskDenoiseLimitDeleted, // inpaintMaskRecalled, -} = canvasSlice.actions; +} = canvasFragment.actions; -const isCanvasSliceAction = isAnyOf(...Object.values(canvasSlice.actions)); +const isCanvasAction = isAnyOf(...Object.values(canvasFragment.actions)); let filter = true; const isActionFileterd = isAnyOf(canvasNameChanged, entitySelected); @@ -2024,7 +2032,7 @@ const canvasUndoableConfig: UndoableOptions = { clearHistoryType: canvasClearHistory.type, filter: (action, _state, _history) => { // Ignore both all actions from other slices and canvas management actions - if (!action.type.startsWith(canvasSlice.name) || isActionFileterd(action)) { + if (!action.type.startsWith(canvasFragment.name) || isActionFileterd(action)) { return false; } // Throttle rapid actions of the same type @@ -2035,15 +2043,15 @@ const canvasUndoableConfig: UndoableOptions = { // debug: import.meta.env.MODE === 'development', }; -const undoableCanvasReducer = undoable(canvasSlice.reducer, canvasUndoableConfig); +const undoableCanvasReducer = undoable(canvasFragment.reducer, canvasUndoableConfig); export const undoableCanvasesReducer = ( - state: CanvasesStateWithHistory, + state: CanvasesStateWithHistory | undefined, action: UnknownAction ): CanvasesStateWithHistory => { state = canvasesSlice.reducer(state, action); - if (!isCanvasSliceAction(action)) { + if (!isCanvasAction(action)) { return state; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 3004b71bfcc..e40972061c4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -1,4 +1,5 @@ -import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PayloadAction, UnknownAction } 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 type { SliceConfig } from 'app/store/types'; @@ -11,64 +12,85 @@ import z from 'zod'; import { canvasAdding, canvasDeleted, + canvasInitialized, canvasMultiCanvasMigrated, MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, } from './canvasSlice'; import { selectActiveCanvasId } from './selectors'; -const zCanvasSessionState = z.object({ +const zCanvasSession = z.object({ canvasId: z.string().min(1), canvasSessionId: z.string(), canvasDiscardedQueueItems: z.array(z.number().int()), }); -type CanvasSessionState = z.infer; +type CanvasSession = z.infer; const zCanvasStagingAreaState = z.object({ _version: z.literal(2), - sessions: z.record(z.string(), zCanvasSessionState), + sessions: z.record(z.string(), zCanvasSession), }); type CanvasStagingAreaState = z.infer; type CanvasPayload = { canvasId: string } & T; type CanvasPayloadAction = PayloadAction>; -const getInitialCanvasSessionState = (canvasId: string): CanvasSessionState => ({ +const getInitialCanvasSessionState = (canvasId: string): CanvasSession => ({ canvasId, canvasSessionId: getPrefixedId('canvas'), canvasDiscardedQueueItems: [], }); -const getInitialState = (): CanvasStagingAreaState => ({ +const getInitialCanvasStagingAreaState = (): CanvasStagingAreaState => ({ _version: 2, sessions: {}, }); const canvasStagingAreaSlice = createSlice({ name: 'canvasSession', - initialState: getInitialState(), - reducers: { - canvasQueueItemDiscarded: (state, action: CanvasPayloadAction<{ itemId: number }>) => { - const { canvasId, itemId } = action.payload; - - const session = state.sessions[canvasId]; + initialState: getInitialCanvasStagingAreaState, + reducers: {}, + extraReducers(builder) { + builder.addCase(canvasAdding, (state, action) => { + const session = getInitialCanvasSessionState(action.payload.canvasId); + state.sessions[session.canvasId] = session; + }); + builder.addCase(canvasDeleted, (state, action) => { + delete state.sessions[action.payload.canvasId]; + }); + builder.addCase(canvasMultiCanvasMigrated, (state, action) => { + const session = state.sessions[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; if (!session) { return; } + session.canvasId = action.payload.canvasId; + state.sessions[session.canvasId] = session; + delete state.sessions[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; + }); + builder.addCase(canvasInitialized, (state, action) => { + const canvasId = action.payload.canvasId; + if (!state.sessions[canvasId]) { + state.sessions[canvasId] = getInitialCanvasSessionState(canvasId); + } + }); + }, +}); + +const canvasSessionFragment = createSlice({ + name: 'canvasSession', + initialState: {} as CanvasSession, + reducers: { + canvasQueueItemDiscarded: (state, action: CanvasPayloadAction<{ itemId: number }>) => { + const { itemId } = action.payload; - if (!session.canvasDiscardedQueueItems.includes(itemId)) { - session.canvasDiscardedQueueItems.push(itemId); + if (!state.canvasDiscardedQueueItems.includes(itemId)) { + state.canvasDiscardedQueueItems.push(itemId); } }, canvasSessionReset: { reducer: (state, action: CanvasPayloadAction<{ canvasSessionId: string }>) => { - const { canvasId, canvasSessionId } = action.payload; - - const session = state.sessions[canvasId]; - if (!session) { - return; - } + const { canvasSessionId } = action.payload; - session.canvasSessionId = canvasSessionId; - session.canvasDiscardedQueueItems = []; + state.canvasSessionId = canvasSessionId; + state.canvasDiscardedQueueItems = []; }, prepare: (payload: CanvasPayload) => { return { @@ -80,32 +102,37 @@ const canvasStagingAreaSlice = createSlice({ }, }, }, - extraReducers(builder) { - builder.addCase(canvasAdding, (state, action) => { - const session = getInitialCanvasSessionState(action.payload.canvasId); - state.sessions[session.canvasId] = session; - }); - builder.addCase(canvasDeleted, (state, action) => { - delete state.sessions[action.payload.canvasId]; - }); - builder.addCase(canvasMultiCanvasMigrated, (state, action) => { - const session = state.sessions[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; - if (!session) { - return; - } - session.canvasId = action.payload.canvasId; - state.sessions[session.canvasId] = session; - delete state.sessions[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; - }); - }, }); -export const { canvasSessionReset, canvasQueueItemDiscarded } = canvasStagingAreaSlice.actions; +export const { canvasSessionReset, canvasQueueItemDiscarded } = canvasSessionFragment.actions; + +const isCanvasSessionAction = isAnyOf(...Object.values(canvasSessionFragment.actions)); + +export const canvasSessionReducer = ( + state: CanvasStagingAreaState | undefined, + action: UnknownAction +): CanvasStagingAreaState => { + state = canvasStagingAreaSlice.reducer(state, action); + + if (!isCanvasSessionAction(action)) { + return state; + } + + const canvasId = action.payload.canvasId; + + return { + ...state, + sessions: { + ...state.sessions, + [canvasId]: canvasSessionFragment.reducer(state.sessions[canvasId], action), + }, + }; +}; export const canvasSessionSliceConfig: SliceConfig = { slice: canvasStagingAreaSlice, schema: zCanvasStagingAreaState, - getInitialState, + getInitialState: getInitialCanvasStagingAreaState, persistConfig: { migrate: (state) => { assert(isPlainObject(state)); @@ -117,7 +144,7 @@ export const canvasSessionSliceConfig: SliceConfig, canvasId: string) => { +const findSessionByCanvasId = (sessions: Record, canvasId: string) => { const session = sessions[canvasId]; assert(session, 'Session must exist for a canvas once the canvas has been created'); return session; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index b51e275fd41..e23a795cd11 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -9,8 +9,8 @@ import { isPlainObject } from 'es-toolkit'; import { clamp } from 'es-toolkit/compat'; import type { AspectRatioID, - CanvasInstanceParamsState, - InstanceParamsState, + CanvasInstanceParams, + InstanceParams, ParamsPayloadAction, ParamsState, RgbaColor, @@ -31,7 +31,7 @@ import { isImagenAspectRatioID, isParamsTab, MAX_POSITIVE_PROMPT_HISTORY, - zInstanceParamsState, + zInstanceParams, zParamsState, } from 'features/controlLayers/store/types'; import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; @@ -75,6 +75,7 @@ import { modelChanged } from './actions'; import { canvasAdding, canvasDeleted, + canvasInitialized, canvasMultiCanvasMigrated, MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, } from './canvasSlice'; @@ -82,7 +83,7 @@ import { selectActiveCanvasId } from './selectors'; const paramsSlice = createSlice({ name: 'params', - initialState: getInitialParamsState(), + initialState: getInitialParamsState, reducers: {}, extraReducers(builder) { builder.addCase(canvasAdding, (state, action) => { @@ -101,12 +102,18 @@ const paramsSlice = createSlice({ state.canvases[canvasParams.canvasId] = canvasParams; delete state.canvases[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; }); + builder.addCase(canvasInitialized, (state, action) => { + const canvasId = action.payload.canvasId; + if (!state.canvases[canvasId]) { + state.canvases[canvasId] = getInitialCanvasInstanceParamsState(canvasId); + } + }); }, }); -const instanceParamsSlice = createSlice({ +const instanceParamsFragment = createSlice({ name: 'params', - initialState: getInitialInstanceParamsState(), + initialState: {} as InstanceParams, reducers: { setIterations: (state, action: ParamsPayloadAction) => { state.iterations = action.payload.value; @@ -154,49 +161,49 @@ const instanceParamsSlice = createSlice({ }, vaeSelected: (state, action: ParamsPayloadAction) => { // null is a valid VAE! - const result = zInstanceParamsState.shape.vae.safeParse(action.payload); + const result = zInstanceParams.shape.vae.safeParse(action.payload); if (!result.success) { return; } state.vae = result.data; }, fluxVAESelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.fluxVAE.safeParse(action.payload); + const result = zInstanceParams.shape.fluxVAE.safeParse(action.payload); if (!result.success) { return; } state.fluxVAE = result.data; }, t5EncoderModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.t5EncoderModel.safeParse(action.payload); + const result = zInstanceParams.shape.t5EncoderModel.safeParse(action.payload); if (!result.success) { return; } state.t5EncoderModel = result.data; }, controlLoRAModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.controlLora.safeParse(action.payload); + const result = zInstanceParams.shape.controlLora.safeParse(action.payload); if (!result.success) { return; } state.controlLora = result.data; }, clipEmbedModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.clipEmbedModel.safeParse(action.payload); + const result = zInstanceParams.shape.clipEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipEmbedModel = result.data; }, clipLEmbedModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.clipLEmbedModel.safeParse(action.payload); + const result = zInstanceParams.shape.clipLEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipLEmbedModel = result.data; }, clipGEmbedModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.clipGEmbedModel.safeParse(action.payload); + const result = zInstanceParams.shape.clipGEmbedModel.safeParse(action.payload); if (!result.success) { return; } @@ -236,7 +243,7 @@ const instanceParamsSlice = createSlice({ state.negativePrompt = action.payload.value; }, refinerModelChanged: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.refinerModel.safeParse(action.payload); + const result = zInstanceParams.shape.refinerModel.safeParse(action.payload); if (!result.success) { return; } @@ -433,7 +440,7 @@ const instanceParamsSlice = createSlice({ extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { const { previousModel } = action.payload.value; - const result = zInstanceParamsState.shape.model.safeParse(action.payload.value.model); + const result = zInstanceParams.shape.model.safeParse(action.payload.value.model); if (!result.success) { return; } @@ -485,7 +492,7 @@ const getModelMaxClipSkip = (model: ParameterModel) => { return CLIP_SKIP_MAP[model.base]?.maxClip; }; -const resetState = (state: InstanceParamsState): InstanceParamsState => { +const resetState = (state: InstanceParams): InstanceParams => { // When a new session is requested, we need to keep the current model selections, plus dependent state // like VAE precision. Everything else gets reset to default. const oldState = deepClone(state); @@ -557,11 +564,11 @@ export const { syncedToOptimalDimension, paramsReset, -} = instanceParamsSlice.actions; +} = instanceParamsFragment.actions; -const instanceParamsSliceActions = { ...instanceParamsSlice.actions, modelChanged }; +const instanceParamsActions = { ...instanceParamsFragment.actions, modelChanged }; -type InstanceParamsAction = typeof instanceParamsSliceActions; +type InstanceParamsAction = typeof instanceParamsActions; type InstanceParamsActionCreator = InstanceParamsAction[keyof InstanceParamsAction]; type PayloadOf = AC extends ActionCreatorWithPayload ? P : never; type ValueOf = PayloadOf extends { value: infer V } ? V : never; @@ -614,12 +621,12 @@ const dispatchParamsAction = ( } }; -const isInstanceParamsSliceAction = isAnyOf(...Object.values(instanceParamsSliceActions)); +const isInstanceParamsAction = isAnyOf(...Object.values(instanceParamsActions)); export const paramsSliceReducer = (state: ParamsState, action: UnknownAction): ParamsState => { state = paramsSlice.reducer(state, action); - if (!isInstanceParamsSliceAction(action)) { + if (!isInstanceParamsAction(action)) { return state; } @@ -629,20 +636,20 @@ export const paramsSliceReducer = (state: ParamsState, action: UnknownAction): P case 'generate': return { ...state, - generate: instanceParamsSlice.reducer(state.generate, action), + generate: instanceParamsFragment.reducer(state.generate, action), }; case 'canvas': return { ...state, canvases: { ...state.canvases, - [canvasId]: { canvasId, ...instanceParamsSlice.reducer(state.canvases[canvasId], action) }, + [canvasId]: { canvasId, ...instanceParamsFragment.reducer(state.canvases[canvasId], action) }, }, }; case 'upscaling': return { ...state, - upscaling: instanceParamsSlice.reducer(state.upscaling, action), + upscaling: instanceParamsFragment.reducer(state.upscaling, action), }; } }; @@ -673,7 +680,7 @@ export const paramsSliceConfig: SliceConfig = { const canvasParams = { canvasId: MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, ...state, - } as CanvasInstanceParamsState; + } as CanvasInstanceParams; state = { _version: 3, @@ -710,7 +717,7 @@ export const selectActiveParams = (state: RootState) => { }; const buildActiveParamsSelector = - (selector: Selector) => + (selector: Selector) => (state: RootState) => selector(selectActiveParams(state)); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 741ff162381..0728ea72f17 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -692,7 +692,7 @@ type ParamsTabName = 'generate' | 'canvas' | 'upscaling'; export type ParamsEnrichedPayload

= EnrichedPayload<{ tab: ParamsTabName; canvasId: string }, P>; export type ParamsPayloadAction

= PayloadAction>; -export const zInstanceParamsState = z.object({ +export const zInstanceParams = z.object({ maskBlur: z.number(), maskBlurMethod: zParameterMaskBlurMethod, canvasCoherenceMode: zParameterCanvasCoherenceMode, @@ -739,22 +739,22 @@ export const zInstanceParamsState = z.object({ controlLora: zParameterControlLoRAModel.nullable(), dimensions: zDimensionsState, }); -export type InstanceParamsState = z.infer; +export type InstanceParams = z.infer; -const zCanvasInstanceParamsState = zInstanceParamsState.extend({ +const zCanvasInstanceParams = zInstanceParams.extend({ canvasId: zId, }); -export type CanvasInstanceParamsState = z.infer; +export type CanvasInstanceParams = z.infer; export const zParamsState = z.object({ _version: z.literal(3), - generate: zInstanceParamsState, - canvases: z.record(z.string(), zCanvasInstanceParamsState), - upscaling: zInstanceParamsState, + generate: zInstanceParams, + canvases: z.record(z.string(), zCanvasInstanceParams), + upscaling: zInstanceParams, }); export type ParamsState = z.infer; -export const getInitialInstanceParamsState = (): InstanceParamsState => ({ +export const getInitialInstanceParamsState = (): InstanceParams => ({ maskBlur: 16, maskBlurMethod: 'box', canvasCoherenceMode: 'Gaussian Blur', @@ -806,7 +806,7 @@ export const getInitialInstanceParamsState = (): InstanceParamsState => ({ }, }); -export const getInitialCanvasInstanceParamsState = (canvasId: string): CanvasInstanceParamsState => ({ +export const getInitialCanvasInstanceParamsState = (canvasId: string): CanvasInstanceParams => ({ canvasId, ...getInitialInstanceParamsState(), }); 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 0ea3e40bad6..9f0d7a6d41a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -12,7 +12,7 @@ import { selectRefinerStart, } from 'features/controlLayers/store/paramsSlice'; import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; -import type { InstanceParamsState } from 'features/controlLayers/store/types'; +import type { InstanceParams } 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'; @@ -158,7 +158,7 @@ export const getOriginalAndScaledSizesForOtherModes = (state: RootState) => { export const getInfill = ( g: Graph, - params: InstanceParamsState + params: InstanceParams ): Invocation<'infill_patchmatch' | 'infill_cv2' | 'infill_lama' | 'infill_rgba' | 'infill_tile'> => { const { infillMethod, infillColorValue, infillPatchmatchDownscaleSize, infillTileSize } = params; diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index 9cd30ca960f..65d0c18474a 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -13,7 +13,7 @@ import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; import { selectActiveParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; -import type { CanvasState, InstanceParamsState, LoRA, RefImagesState } from 'features/controlLayers/store/types'; +import type { CanvasState, InstanceParams, LoRA, RefImagesState } from 'features/controlLayers/store/types'; import { getControlLayerWarnings, getGlobalReferenceImageWarnings, @@ -78,7 +78,7 @@ type UpdateReasonsArg = { tab: TabName; isConnected: boolean; canvas: CanvasState; - params: InstanceParamsState; + params: InstanceParams; refImages: RefImagesState; dynamicPrompts: DynamicPromptsState; canvasIsFiltering: boolean; @@ -277,7 +277,7 @@ const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invok const getReasonsWhyCannotEnqueueVideoTab = (arg: { isConnected: boolean; video: VideoState; - params: InstanceParamsState; + params: InstanceParams; dynamicPrompts: DynamicPromptsState; promptExpansionRequest: PromptExpansionRequestState; isVideoEnabled: boolean; @@ -319,7 +319,7 @@ const getReasonsWhyCannotEnqueueVideoTab = (arg: { const getReasonsWhyCannotEnqueueGenerateTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; - params: InstanceParamsState; + params: InstanceParams; refImages: RefImagesState; loras: LoRA[]; dynamicPrompts: DynamicPromptsState; @@ -490,7 +490,7 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: { isConnected: boolean; upscale: UpscaleState; config: AppConfig; - params: InstanceParamsState; + params: InstanceParams; loras: LoRA[]; promptExpansionRequest: PromptExpansionRequestState; }) => { @@ -553,7 +553,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; canvas: CanvasState; - params: InstanceParamsState; + params: InstanceParams; refImages: RefImagesState; loras: LoRA[]; dynamicPrompts: DynamicPromptsState; From 23bd8e6665a34c7ca11fd01ebadf0b9673c592c5 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Wed, 1 Oct 2025 08:50:44 +0200 Subject: [PATCH 10/16] rendering global controls in all tabs fixed --- .../features/controlLayers/store/paramsSlice.ts | 17 +++++++++++++---- .../src/features/controlLayers/store/types.ts | 7 +++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index e23a795cd11..31baf4c07ba 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -651,6 +651,11 @@ export const paramsSliceReducer = (state: ParamsState, action: UnknownAction): P ...state, upscaling: instanceParamsFragment.reducer(state.upscaling, action), }; + case 'video': + return { + ...state, + upscaling: instanceParamsFragment.reducer(state.video, action), + }; } }; @@ -687,6 +692,7 @@ export const paramsSliceConfig: SliceConfig = { generate: { ...state }, canvases: { [canvasParams.canvasId]: canvasParams }, upscaling: { ...state }, + video: { ...state }, }; } @@ -695,13 +701,11 @@ export const paramsSliceConfig: SliceConfig = { }, }; +const initialInstanceParamsState = getInitialInstanceParamsState(); + export const selectActiveParams = (state: RootState) => { const tab = selectActiveTab(state); const canvasId = selectActiveCanvasId(state); - assert( - tab === 'generate' || tab === 'canvas' || tab === 'upscaling', - `Unsupported tab ${tab} for params slice selector` - ); switch (tab) { case 'generate': @@ -713,7 +717,12 @@ export const selectActiveParams = (state: RootState) => { } case 'upscaling': return state.params.upscaling; + case 'video': + return state.params.video; } + + // Fallback for global controls + return initialInstanceParamsState; }; const buildActiveParamsSelector = diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 0728ea72f17..2cda5cc7e5e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -687,8 +687,9 @@ const zPositivePromptHistory = z type EnrichedPayload = P extends undefined ? T : T & { value: P }; -export const isParamsTab = (tab: TabName) => tab === 'generate' || tab === 'canvas' || tab === 'upscaling'; -type ParamsTabName = 'generate' | 'canvas' | 'upscaling'; +export const isParamsTab = (tab: TabName) => + tab === 'generate' || tab === 'canvas' || tab === 'upscaling' || tab === 'video'; +type ParamsTabName = 'generate' | 'canvas' | 'upscaling' | 'video'; export type ParamsEnrichedPayload

= EnrichedPayload<{ tab: ParamsTabName; canvasId: string }, P>; export type ParamsPayloadAction

= PayloadAction>; @@ -751,6 +752,7 @@ export const zParamsState = z.object({ generate: zInstanceParams, canvases: z.record(z.string(), zCanvasInstanceParams), upscaling: zInstanceParams, + video: zInstanceParams, }); export type ParamsState = z.infer; @@ -816,6 +818,7 @@ export const getInitialParamsState = (): ParamsState => ({ generate: getInitialInstanceParamsState(), canvases: {}, upscaling: getInitialInstanceParamsState(), + video: getInitialInstanceParamsState(), }); const zInpaintMasks = z.object({ From aa9c61d538ea4c37a1e30d4a883f8b289f4561b3 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Wed, 1 Oct 2025 10:19:25 +0200 Subject: [PATCH 11/16] slices merged --- .../listeners/modelSelected.ts | 4 +- .../listeners/setDefaultSettings.ts | 4 +- invokeai/frontend/web/src/app/store/store.ts | 23 +- .../CanvasSettingsClipToBboxCheckbox.tsx | 2 +- .../CanvasSettingsInvertScrollCheckbox.tsx | 2 +- .../StagingArea/QueueItemPreviewMini.tsx | 2 +- .../StagingAreaAutoSwitchButtons.tsx | 6 +- .../__mocks__/mockStagingAreaApp.ts | 2 +- .../components/StagingArea/context.tsx | 10 +- .../components/StagingArea/state.ts | 2 +- .../components/Tool/ToolFillColorPicker.tsx | 22 +- .../components/Tool/ToolWidthPicker.tsx | 12 +- .../common/CanvasEntityMenuItemsArrange.tsx | 4 +- .../controlLayers/hooks/useCanvasSessionId.ts | 6 +- .../konva/CanvasEntityRendererModule.ts | 4 +- .../konva/CanvasStateApiModule.ts | 20 +- .../konva/CanvasTool/CanvasToolModule.ts | 8 +- .../store/canvasSettingsSlice.ts | 413 ++++-------------- .../controlLayers/store/canvasSlice.ts | 232 +++++----- .../store/canvasStagingAreaSlice.ts | 171 ++------ .../controlLayers/store/paramsSlice.ts | 193 ++++---- .../features/controlLayers/store/selectors.ts | 36 +- .../src/features/controlLayers/store/types.ts | 221 ++++++---- .../features/deleteImageModal/store/state.ts | 4 +- .../web/src/features/imageActions/actions.ts | 12 +- .../util/graph/generation/addFLUXFill.ts | 4 +- .../nodes/util/graph/generation/addInpaint.ts | 4 +- .../util/graph/generation/addOutpaint.ts | 4 +- .../util/graph/generation/buildFLUXGraph.ts | 4 +- .../util/graph/generation/buildSD1Graph.ts | 4 +- .../util/graph/generation/buildSDXLGraph.ts | 4 +- .../nodes/util/graph/graphBuilderUtils.ts | 8 +- .../web/src/features/queue/store/readiness.ts | 16 +- .../src/features/ui/layouts/CanvasTabs.tsx | 6 +- 34 files changed, 571 insertions(+), 898 deletions(-) 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 9a7406efe1a..ca999025f7e 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 @@ -4,7 +4,7 @@ import { modelChanged } from 'features/controlLayers/store/actions'; import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice'; import { buildSelectIsStagingBySessionId, - selectActiveCanvasSessionId, + selectActiveCanvasStagingAreaSessionId, } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; import { @@ -170,7 +170,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = if (modelBase !== params.model?.base) { // Sync generate tab settings whenever the model base changes paramsDispatch(api, syncedToOptimalDimension); - const sessionId = selectActiveCanvasSessionId(state); + const sessionId = selectActiveCanvasStagingAreaSessionId(state); const selectIsStaging = buildSelectIsStagingBySessionId(sessionId); const isStaging = selectIsStaging(state); if (!isStaging) { 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 6671d0190e6..bc1723f3d19 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 @@ -3,7 +3,7 @@ import { isNil } from 'es-toolkit'; import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; import { buildSelectIsStagingBySessionId, - selectActiveCanvasSessionId, + selectActiveCanvasStagingAreaSessionId, } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { heightChanged, @@ -121,7 +121,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni } const setSizeOptions = { updateAspectRatio: true, clamp: true }; - const sessionId = selectActiveCanvasSessionId(state); + const sessionId = selectActiveCanvasStagingAreaSessionId(state); const selectIsStaging = buildSelectIsStagingBySessionId(sessionId); const isStaging = selectIsStaging(state); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 0fe6d06426b..6ac251c1f84 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -21,16 +21,9 @@ 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 { canvasSettingsReducer, canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice'; -import { - canvasSliceConfig, - initializeCanvasDependencies, - migrateCanvas, - undoableCanvasesReducer, -} from 'features/controlLayers/store/canvasSlice'; -import { canvasSessionReducer, canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice'; import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; -import { paramsSliceConfig, paramsSliceReducer } from 'features/controlLayers/store/paramsSlice'; +import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice'; import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice'; import { dynamicPromptsSliceConfig } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { gallerySliceConfig } from 'features/gallery/store/gallerySlice'; @@ -67,8 +60,6 @@ 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, @@ -93,9 +84,7 @@ const SLICE_CONFIGS = { // Remember to wrap undoable reducers in `undoable()`! const ALL_REDUCERS = { [api.reducerPath]: api.reducer, - [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionReducer, - [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsReducer, - [canvasSliceConfig.slice.reducerPath]: undoableCanvasesReducer, + [canvasSliceConfig.slice.reducerPath]: canvasSliceConfig.slice.reducer, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer, [configSliceConfig.slice.reducerPath]: configSliceConfig.slice.reducer, [dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer, @@ -103,7 +92,7 @@ const ALL_REDUCERS = { [lorasSliceConfig.slice.reducerPath]: lorasSliceConfig.slice.reducer, [modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig.slice.reducer, [nodesSliceConfig.slice.reducerPath]: undoableNodesSliceReducer, - [paramsSliceConfig.slice.reducerPath]: paramsSliceReducer, + [paramsSliceConfig.slice.reducerPath]: paramsSliceConfig.slice.reducer, [queueSliceConfig.slice.reducerPath]: queueSliceConfig.slice.reducer, [refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig.slice.reducer, [stylePresetSliceConfig.slice.reducerPath]: stylePresetSliceConfig.slice.reducer, @@ -224,10 +213,8 @@ export const createStore = (options?: { persist?: boolean; persistDebounce?: num // Once-off listener to support waiting for rehydration before rendering the app startAppListening({ actionCreator: createAction(REMEMBER_REHYDRATED), - effect: (action, { dispatch, unsubscribe }) => { + effect: (action, { unsubscribe }) => { unsubscribe(); - dispatch(migrateCanvas()); - dispatch(initializeCanvasDependencies()); options?.onRehydrated?.(); }, }); 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 d6f677dd19c..54a23d8859f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx @@ -10,7 +10,7 @@ export const CanvasSettingsClipToBboxCheckbox = memo(() => { const dispatch = useAppDispatch(); const clipToBbox = useAppSelector((state) => selectClipToBbox(state)); const onChange = useCallback( - (e: ChangeEvent) => dispatch(settingsClipToBboxChanged({ clipToBbox: e.target.checked })), + (e: ChangeEvent) => dispatch(settingsClipToBboxChanged(e.target.checked)), [dispatch] ); return ( 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 018bff88be5..8f474e801c9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx @@ -14,7 +14,7 @@ export const CanvasSettingsInvertScrollCheckbox = memo(() => { const invertScrollForToolWidth = useAppSelector((state) => selectInvertScrollForToolWidth(state)); const onChange = useCallback( (e: ChangeEvent) => { - dispatch(settingsInvertScrollForToolWidthChanged({ invertScrollForToolWidth: e.target.checked })); + dispatch(settingsInvertScrollForToolWidthChanged(e.target.checked)); }, [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 9a133183b68..b5e19f52fc4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemPreviewMini.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/QueueItemPreviewMini.tsx @@ -55,7 +55,7 @@ export const QueueItemPreviewMini = memo(({ item, index }: Props) => { const onDoubleClick = useCallback(() => { if (autoSwitch !== 'off') { - dispatch(settingsStagingAreaAutoSwitchChanged({ stagingAreaAutoSwitch: 'off' })); + dispatch(settingsStagingAreaAutoSwitchChanged('off')); toast({ title: 'Auto-Switch Disabled', }); 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 849ee82e51b..be985b83fd7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaAutoSwitchButtons.tsx @@ -16,13 +16,13 @@ export const StagingAreaAutoSwitchButtons = memo(() => { const dispatch = useAppDispatch(); const onClickOff = useCallback(() => { - dispatch(settingsStagingAreaAutoSwitchChanged({ stagingAreaAutoSwitch: 'off' })); + dispatch(settingsStagingAreaAutoSwitchChanged('off')); }, [dispatch]); const onClickSwitchOnStart = useCallback(() => { - dispatch(settingsStagingAreaAutoSwitchChanged({ stagingAreaAutoSwitch: 'switch_on_start' })); + dispatch(settingsStagingAreaAutoSwitchChanged('switch_on_start')); }, [dispatch]); const onClickSwitchOnFinished = useCallback(() => { - dispatch(settingsStagingAreaAutoSwitchChanged({ stagingAreaAutoSwitch: 'switch_on_finish' })); + dispatch(settingsStagingAreaAutoSwitchChanged('switch_on_finish')); }, [dispatch]); return ( 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 d5cb3fb2b0a..ef876f50fa1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx @@ -62,13 +62,13 @@ export const StagingAreaContextProvider = memo(({ canvasId, children }: PropsWit }); }, onDiscard: ({ item_id, status }) => { - store.dispatch(canvasQueueItemDiscarded({ canvasId, itemId: item_id })); + store.dispatch(canvasQueueItemDiscarded({ itemId: item_id })); if (status === 'in_progress' || status === 'pending') { store.dispatch(queueApi.endpoints.cancelQueueItem.initiate({ item_id }, { track: false })); } }, onDiscardAll: () => { - store.dispatch(canvasSessionReset({ canvasId })); + store.dispatch(canvasSessionReset()); if (sessionId) { store.dispatch( queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false }) @@ -86,7 +86,7 @@ export const StagingAreaContextProvider = memo(({ canvasId, children }: PropsWit }; store.dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' })); - store.dispatch(canvasSessionReset({ canvasId })); + store.dispatch(canvasSessionReset()); if (sessionId) { store.dispatch( queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false }) @@ -94,12 +94,12 @@ export const StagingAreaContextProvider = memo(({ canvasId, children }: PropsWit } }, onAutoSwitchChange: (mode) => { - store.dispatch(settingsStagingAreaAutoSwitchChanged({ stagingAreaAutoSwitch: mode })); + store.dispatch(settingsStagingAreaAutoSwitchChanged(mode)); }, }; return _stagingAreaAppApi; - }, [canvasId, sessionId, selectQueueItems, socket, store]); + }, [sessionId, selectQueueItems, socket, store]); const [stagingAreaApi] = useState(() => new StagingAreaApi()); 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 e60abfab74e..cb383975df2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolFillColorPicker.tsx @@ -12,7 +12,6 @@ import { import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbaColorPicker from 'common/components/ColorPicker/RgbaColorPicker'; import { rgbaColorToString } from 'common/util/colorCodeTransformers'; -import { useCanvasId } from 'features/controlLayers/hooks/useCanvasId'; import { selectActiveColor, selectBgColor, @@ -29,10 +28,9 @@ import { useTranslation } from 'react-i18next'; export const ToolFillColorPicker = memo(() => { const { t } = useTranslation(); - const canvasId = useCanvasId(); - const activeColorType = useAppSelector((state) => selectActiveColor(state, canvasId)); - const bgColor = useAppSelector((state) => selectBgColor(state, canvasId)); - const fgColor = useAppSelector((state) => selectFgColor(state, canvasId)); + 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 }; @@ -44,28 +42,28 @@ export const ToolFillColorPicker = memo(() => { const onColorChange = useCallback( (color: RgbaColor) => { if (activeColorType === 'bgColor') { - dispatch(settingsBgColorChanged({ canvasId, bgColor: color })); + dispatch(settingsBgColorChanged(color)); } else { - dispatch(settingsFgColorChanged({ canvasId, fgColor: color })); + dispatch(settingsFgColorChanged(color)); } }, - [activeColorType, canvasId, dispatch] + [activeColorType, dispatch] ); useRegisteredHotkeys({ id: 'setFillColorsToDefault', category: 'canvas', - callback: () => dispatch(settingsColorsSetToDefault({ canvasId })), + callback: () => dispatch(settingsColorsSetToDefault()), options: { preventDefault: true }, - dependencies: [canvasId, dispatch], + dependencies: [dispatch], }); useRegisteredHotkeys({ id: 'toggleFillColor', category: 'canvas', - callback: () => dispatch(settingsActiveColorToggled({ canvasId })), + callback: () => dispatch(settingsActiveColorToggled()), options: { preventDefault: true }, - dependencies: [canvasId, dispatch], + dependencies: [dispatch], }); return ( 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 e3da696f91f..2e39cb78986 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolWidthPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolWidthPicker.tsx @@ -16,7 +16,6 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { clamp } from 'es-toolkit/compat'; -import { useCanvasId } from 'features/controlLayers/hooks/useCanvasId'; import { selectBrushWidth, selectEraserWidth, @@ -184,14 +183,13 @@ SliderToolWidthPickerComponent.displayName = 'SliderToolWidthPickerComponent'; export const ToolWidthPicker = memo(() => { const ref = useRef(null); const dispatch = useAppDispatch(); - const canvasId = useCanvasId(); const isBrushSelected = useToolIsSelected('brush'); const isEraserSelected = useToolIsSelected('eraser'); const isToolSelected = useMemo(() => { return isBrushSelected || isEraserSelected; }, [isBrushSelected, isEraserSelected]); - const brushWidth = useAppSelector((state) => selectBrushWidth(state, canvasId)); - const eraserWidth = useAppSelector((state) => selectEraserWidth(state, canvasId)); + const brushWidth = useAppSelector(selectBrushWidth); + const eraserWidth = useAppSelector(selectEraserWidth); const width = useMemo(() => { if (isBrushSelected) { return brushWidth; @@ -228,12 +226,12 @@ export const ToolWidthPicker = memo(() => { const onValueChange = useCallback( (value: number) => { if (isBrushSelected) { - dispatch(settingsBrushWidthChanged({ canvasId, brushWidth: value })); + dispatch(settingsBrushWidthChanged(value)); } else if (isEraserSelected) { - dispatch(settingsEraserWidthChanged({ canvasId, eraserWidth: value })); + dispatch(settingsEraserWidthChanged(value)); } }, - [isBrushSelected, isEraserSelected, canvasId, dispatch] + [isBrushSelected, isEraserSelected, dispatch] ); const onChange = useCallback( 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 3346ee6a05c..f81d22981d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx @@ -10,13 +10,13 @@ import { entityArrangedToFront, } from 'features/controlLayers/store/canvasSlice'; import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; -import type { CanvasEntityIdentifier, CanvasState } from 'features/controlLayers/store/types'; +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') { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts index 30208347431..0985ea5cc28 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts @@ -1,14 +1,14 @@ import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectCanvasStagingAreaSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { useCanvasId } from './useCanvasId'; export const useCanvasSessionId = () => { const canvasId = useCanvasId(); - return useAppSelector((state) => selectCanvasSessionId(state, canvasId)); + return useAppSelector((state) => selectCanvasStagingAreaSessionId(state, canvasId)); }; export const useScopedCanvasSessionId = (canvasId: string) => { - return useAppSelector((state) => selectCanvasSessionId(state, canvasId)); + return useAppSelector((state) => selectCanvasStagingAreaSessionId(state, canvasId)); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts index 69357a24732..02f25a4406c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntityRendererModule.ts @@ -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'; @@ -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/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 84d75e188c0..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 { - buildSelectCanvasSettingsByCanvasId, + selectCanvasSettingsByCanvasId, settingsBgColorChanged, settingsBrushWidthChanged, settingsEraserWidthChanged, @@ -29,11 +29,11 @@ import { rasterLayerAdded, rgAdded, } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasSessionByCanvasId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +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, @@ -123,7 +123,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { * * The state is stored in redux. */ - getCanvasState = (): CanvasState => { + getCanvasState = (): CanvasEntity => { return this.runSelector(selectActiveCanvas); }; @@ -215,14 +215,14 @@ export class CanvasStateApiModule extends CanvasModuleBase { * Sets the brush width, pushing state to redux. */ setBrushWidth = (width: number) => { - this.store.dispatch(settingsBrushWidthChanged({ canvasId: this.manager.canvasId, brushWidth: width })); + this.store.dispatch(settingsBrushWidthChanged(width)); }; /** * Sets the eraser width, pushing state to redux. */ setEraserWidth = (width: number) => { - this.store.dispatch(settingsEraserWidthChanged({ canvasId: this.manager.canvasId, eraserWidth: width })); + this.store.dispatch(settingsEraserWidthChanged(width)); }; /** @@ -230,8 +230,8 @@ export class CanvasStateApiModule extends CanvasModuleBase { */ setColor = (color: Partial) => { return this.getSettings().activeColor === 'bgColor' - ? this.store.dispatch(settingsBgColorChanged({ canvasId: this.manager.canvasId, bgColor: color })) - : this.store.dispatch(settingsFgColorChanged({ canvasId: this.manager.canvasId, fgColor: color })); + ? this.store.dispatch(settingsBgColorChanged(color)) + : this.store.dispatch(settingsFgColorChanged(color)); }; /** @@ -308,7 +308,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { * Gets the canvas settings from redux. */ getSettings = () => { - return this.runSelector(buildSelectCanvasSettingsByCanvasId(this.manager.canvasId)); + return this.runSelector((state) => selectCanvasSettingsByCanvasId(state, this.manager.canvasId)); }; /** @@ -367,7 +367,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { * Gets the canvas staging area state from redux. */ getStagingArea = () => { - return this.runSelector((state) => selectCanvasSessionByCanvasId(state, this.manager.canvasId)); + 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 bcfa2e07136..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 { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectCanvasById } from 'features/controlLayers/store/selectors'; +import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectCanvasByCanvasId } from 'features/controlLayers/store/selectors'; import type { CanvasControlLayerState, CanvasInpaintMaskState, @@ -137,13 +137,13 @@ export class CanvasToolModule extends CanvasModuleBase { this.subscriptions.add(this.manager.$isBusy.listen(this.render)); this.subscriptions.add( this.manager.stateApi.createStoreSubscription( - (state) => selectCanvasById(state, this.manager.canvasId), + (state) => selectCanvasByCanvasId(state, this.manager.canvasId), this.render ) ); this.subscriptions.add( this.manager.stateApi.createStoreSubscription( - buildSelectCanvasSettingsByCanvasId(this.manager.canvasId), + (state) => selectCanvasSettingsByCanvasId(state, this.manager.canvasId), this.render ) ); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index 2ddc61b7571..f3fe19bd0dd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -1,128 +1,20 @@ -import type { PayloadAction, Selector, UnknownAction } from '@reduxjs/toolkit'; -import { createSelector, createSlice, isAnyOf } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; -import type { SliceConfig } from 'app/store/types'; -import { isPlainObject } from 'es-toolkit'; -import type { RgbaColor } from 'features/controlLayers/store/types'; -import { RGBA_BLACK, RGBA_WHITE, zRgbaColor } from 'features/controlLayers/store/types'; +import type { CanvasSettingsState, RgbaColor } from 'features/controlLayers/store/types'; +import { RGBA_BLACK, RGBA_WHITE } from 'features/controlLayers/store/types'; import { assert } from 'tsafe'; -import { z } from 'zod'; -import { - canvasAdding, - canvasDeleted, - canvasInitialized, - canvasMultiCanvasMigrated, - MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, -} from './canvasSlice'; - -const zAutoSwitchMode = z.enum(['off', 'switch_on_start', 'switch_on_finish']); -export type AutoSwitchMode = z.infer; - -const zCanvasSharedSettings = 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(), - /** - * 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 CanvasSharedSettings = z.infer; - -const zCanvasInstanceSettings = z.object({ - canvasId: z.string(), - /** - * 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, -}); -type CanvasInstanceSettings = z.infer; - -const zCanvasSettingsState = z.object({ - _version: z.literal(1), - shared: zCanvasSharedSettings, - canvases: z.record(z.string(), zCanvasInstanceSettings), -}); -type CanvasSettingsState = z.infer; - -const getInitialCanvasSharedSettings = (): CanvasSharedSettings => ({ +export const getInitialCanvasSettings = (): CanvasSettingsState => ({ showHUD: true, clipToBbox: false, dynamicGrid: false, invertScrollForToolWidth: false, + brushWidth: 50, + eraserWidth: 50, + activeColor: 'fgColor', + bgColor: RGBA_BLACK, + fgColor: RGBA_WHITE, outputOnlyMaskedRegions: true, autoProcess: true, snapToGrid: true, @@ -136,141 +28,83 @@ const getInitialCanvasSharedSettings = (): CanvasSharedSettings => ({ saveAllImagesToGallery: false, stagingAreaAutoSwitch: 'switch_on_start', }); -const getInitialCanvasInstanceSettings = (canvasId: string): CanvasInstanceSettings => ({ - canvasId, - brushWidth: 50, - eraserWidth: 50, - activeColor: 'fgColor', - bgColor: RGBA_BLACK, - fgColor: RGBA_WHITE, -}); -const getInitialCanvasSettingsState = (): CanvasSettingsState => ({ - _version: 1, - shared: getInitialCanvasSharedSettings(), - canvases: {}, -}); -type PayloadWithCanvasId

= P & { canvasId: string }; -type CanvasPayloadAction

= PayloadAction>; - -const canvasSettingsSlice = createSlice({ +export const canvasSettingsState = createSlice({ name: 'canvasSettings', - initialState: getInitialCanvasSettingsState, + initialState: {} as CanvasSettingsState, reducers: { - settingsClipToBboxChanged: (state, action: PayloadAction<{ clipToBbox: boolean }>) => { - const { clipToBbox } = action.payload; - - state.shared.clipToBbox = clipToBbox; + settingsClipToBboxChanged: (state, action: PayloadAction) => { + state.clipToBbox = action.payload; }, settingsDynamicGridToggled: (state) => { - state.shared.dynamicGrid = !state.shared.dynamicGrid; + state.dynamicGrid = !state.dynamicGrid; }, settingsShowHUDToggled: (state) => { - state.shared.showHUD = !state.shared.showHUD; + state.showHUD = !state.showHUD; }, - settingsInvertScrollForToolWidthChanged: (state, action: PayloadAction<{ invertScrollForToolWidth: boolean }>) => { - const { invertScrollForToolWidth } = action.payload; - - state.shared.invertScrollForToolWidth = invertScrollForToolWidth; + settingsBrushWidthChanged: (state, action: PayloadAction) => { + state.brushWidth = Math.round(action.payload); + }, + settingsEraserWidthChanged: (state, action: PayloadAction) => { + state.eraserWidth = Math.round(action.payload); + }, + settingsActiveColorToggled: (state) => { + state.activeColor = state.activeColor === 'bgColor' ? 'fgColor' : 'bgColor'; + }, + settingsBgColorChanged: (state, action: PayloadAction>) => { + state.bgColor = { ...state.bgColor, ...action.payload }; + }, + settingsFgColorChanged: (state, action: PayloadAction>) => { + state.fgColor = { ...state.fgColor, ...action.payload }; + }, + settingsColorsSetToDefault: (state) => { + state.bgColor = RGBA_BLACK; + state.fgColor = RGBA_WHITE; + }, + settingsInvertScrollForToolWidthChanged: ( + state, + action: PayloadAction + ) => { + state.invertScrollForToolWidth = action.payload; }, settingsOutputOnlyMaskedRegionsToggled: (state) => { - state.shared.outputOnlyMaskedRegions = !state.shared.outputOnlyMaskedRegions; + state.outputOnlyMaskedRegions = !state.outputOnlyMaskedRegions; }, settingsAutoProcessToggled: (state) => { - state.shared.autoProcess = !state.shared.autoProcess; + state.autoProcess = !state.autoProcess; }, settingsSnapToGridToggled: (state) => { - state.shared.snapToGrid = !state.shared.snapToGrid; + state.snapToGrid = !state.snapToGrid; }, settingsShowProgressOnCanvasToggled: (state) => { - state.shared.showProgressOnCanvas = !state.shared.showProgressOnCanvas; + state.showProgressOnCanvas = !state.showProgressOnCanvas; }, settingsBboxOverlayToggled: (state) => { - state.shared.bboxOverlay = !state.shared.bboxOverlay; + state.bboxOverlay = !state.bboxOverlay; }, settingsPreserveMaskToggled: (state) => { - state.shared.preserveMask = !state.shared.preserveMask; + state.preserveMask = !state.preserveMask; }, settingsIsolatedStagingPreviewToggled: (state) => { - state.shared.isolatedStagingPreview = !state.shared.isolatedStagingPreview; + state.isolatedStagingPreview = !state.isolatedStagingPreview; }, settingsIsolatedLayerPreviewToggled: (state) => { - state.shared.isolatedLayerPreview = !state.shared.isolatedLayerPreview; + state.isolatedLayerPreview = !state.isolatedLayerPreview; }, settingsPressureSensitivityToggled: (state) => { - state.shared.pressureSensitivity = !state.shared.pressureSensitivity; + state.pressureSensitivity = !state.pressureSensitivity; }, settingsRuleOfThirdsToggled: (state) => { - state.shared.ruleOfThirds = !state.shared.ruleOfThirds; + state.ruleOfThirds = !state.ruleOfThirds; }, settingsSaveAllImagesToGalleryToggled: (state) => { - state.shared.saveAllImagesToGallery = !state.shared.saveAllImagesToGallery; + state.saveAllImagesToGallery = !state.saveAllImagesToGallery; }, settingsStagingAreaAutoSwitchChanged: ( state, - action: PayloadAction<{ stagingAreaAutoSwitch: CanvasSharedSettings['stagingAreaAutoSwitch'] }> + action: PayloadAction ) => { - const { stagingAreaAutoSwitch } = action.payload; - - state.shared.stagingAreaAutoSwitch = stagingAreaAutoSwitch; - }, - }, - extraReducers(builder) { - builder.addCase(canvasAdding, (state, action) => { - const canvasSettings = getInitialCanvasInstanceSettings(action.payload.canvasId); - state.canvases[canvasSettings.canvasId] = canvasSettings; - }); - builder.addCase(canvasDeleted, (state, action) => { - delete state.canvases[action.payload.canvasId]; - }); - builder.addCase(canvasMultiCanvasMigrated, (state, action) => { - const settings = state.canvases[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; - if (!settings) { - return; - } - settings.canvasId = action.payload.canvasId; - state.canvases[settings.canvasId] = settings; - delete state.canvases[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; - }); - builder.addCase(canvasInitialized, (state, action) => { - const canvasId = action.payload.canvasId; - if (!state.canvases[canvasId]) { - state.canvases[canvasId] = getInitialCanvasInstanceSettings(canvasId); - } - }); - }, -}); - -const canvasInstanceSettingsFragment = createSlice({ - name: 'canvasSettings', - initialState: {} as CanvasInstanceSettings, - reducers: { - settingsBrushWidthChanged: (state, action: CanvasPayloadAction<{ brushWidth: number }>) => { - const { brushWidth } = action.payload; - - state.brushWidth = Math.round(brushWidth); - }, - settingsEraserWidthChanged: (state, action: CanvasPayloadAction<{ eraserWidth: number }>) => { - const { eraserWidth } = action.payload; - - state.eraserWidth = Math.round(eraserWidth); - }, - settingsActiveColorToggled: (state, _action: CanvasPayloadAction) => { - state.activeColor = state.activeColor === 'bgColor' ? 'fgColor' : 'bgColor'; - }, - settingsBgColorChanged: (state, action: CanvasPayloadAction<{ bgColor: Partial }>) => { - const { bgColor } = action.payload; - - state.bgColor = { ...state.bgColor, ...bgColor }; - }, - settingsFgColorChanged: (state, action: CanvasPayloadAction<{ fgColor: Partial }>) => { - const { fgColor } = action.payload; - - state.fgColor = { ...state.fgColor, ...fgColor }; - }, - settingsColorsSetToDefault: (state, _action: CanvasPayloadAction) => { - state.bgColor = RGBA_BLACK; - state.fgColor = RGBA_WHITE; + state.stagingAreaAutoSwitch = action.payload; }, }, }); @@ -279,6 +113,12 @@ export const { settingsClipToBboxChanged, settingsDynamicGridToggled, settingsShowHUDToggled, + settingsBrushWidthChanged, + settingsEraserWidthChanged, + settingsActiveColorToggled, + settingsBgColorChanged, + settingsFgColorChanged, + settingsColorsSetToDefault, settingsInvertScrollForToolWidthChanged, settingsOutputOnlyMaskedRegionsToggled, settingsAutoProcessToggled, @@ -292,117 +132,44 @@ export const { settingsRuleOfThirdsToggled, settingsSaveAllImagesToGalleryToggled, settingsStagingAreaAutoSwitchChanged, -} = canvasSettingsSlice.actions; +} = canvasSettingsState.actions; -export const { - settingsBrushWidthChanged, - settingsEraserWidthChanged, - settingsActiveColorToggled, - settingsBgColorChanged, - settingsFgColorChanged, - settingsColorsSetToDefault, -} = canvasInstanceSettingsFragment.actions; - -const isCanvasInstanceSettingsAction = isAnyOf(...Object.values(canvasInstanceSettingsFragment.actions)); - -export const canvasSettingsReducer = ( - state: CanvasSettingsState | undefined, - action: UnknownAction -): CanvasSettingsState => { - state = canvasSettingsSlice.reducer(state, action); - - if (!isCanvasInstanceSettingsAction(action)) { - return state; - } - - const canvasId = action.payload.canvasId; - - return { - ...state, - canvases: { - ...state.canvases, - [canvasId]: canvasInstanceSettingsFragment.reducer(state.canvases[canvasId], action), - }, - }; -}; - -export const canvasSettingsSliceConfig: SliceConfig = { - slice: canvasSettingsSlice, - schema: zCanvasSettingsState, - getInitialState: getInitialCanvasSettingsState, - persistConfig: { - migrate: (state) => { - assert(isPlainObject(state)); - if (!('_version' in state)) { - // Migrate from v1: slice represented a canvas settings instance -> slice represents multiple canvas settings instances - const settings = { - canvasId: MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, - ...state, - } as CanvasInstanceSettings; - - state = { - _version: 1, - shared: { - ...state, - }, - canvases: { [settings.canvasId]: settings }, - }; - } - - return 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; }; - -export const buildSelectCanvasSettingsByCanvasId = (canvasId: string) => - createSelector( - selectCanvasSharedSettings, - (state: RootState) => selectCanvasInstanceSettings(state, canvasId), - (sharedSettings, instanceSettings) => { - return { - ...sharedSettings, - ...instanceSettings, - }; - } - ); -const selectCanvasSharedSettings = (state: RootState) => state.canvasSettings.shared; -const selectCanvasInstanceSettings = (state: RootState, canvasId: string) => { - const settings = state.canvasSettings.canvases[canvasId]; - assert(settings, 'Settings must exist for a canvas once the canvas has been created'); - return settings; +const selectActiveCanvasSettings = (state: RootState) => { + return state.canvas.canvases[state.canvas.activeCanvasId]!.settings; }; -const buildCanvasSharedSettingsSelector = - (selector: Selector) => +const buildActiveCanvasSettingsSelector = + (selector: Selector) => (state: RootState) => - selector(selectCanvasSharedSettings(state)); -const buildCanvasInstanceSettingsSelector = - (selector: Selector) => - (state: RootState, canvasId: string) => - selector(selectCanvasInstanceSettings(state, canvasId)); + selector(selectActiveCanvasSettings(state)); -export const selectPreserveMask = buildCanvasSharedSettingsSelector((state) => state.preserveMask); -export const selectOutputOnlyMaskedRegions = buildCanvasSharedSettingsSelector( +export const selectPreserveMask = buildActiveCanvasSettingsSelector((state) => state.preserveMask); +export const selectOutputOnlyMaskedRegions = buildActiveCanvasSettingsSelector( (state) => state.outputOnlyMaskedRegions ); -export const selectDynamicGrid = buildCanvasSharedSettingsSelector((state) => state.dynamicGrid); -export const selectInvertScrollForToolWidth = buildCanvasSharedSettingsSelector( +export const selectDynamicGrid = buildActiveCanvasSettingsSelector((state) => state.dynamicGrid); +export const selectInvertScrollForToolWidth = buildActiveCanvasSettingsSelector( (state) => state.invertScrollForToolWidth ); -export const selectBboxOverlay = buildCanvasSharedSettingsSelector((state) => state.bboxOverlay); -export const selectShowHUD = buildCanvasSharedSettingsSelector((state) => state.showHUD); -export const selectClipToBbox = buildCanvasSharedSettingsSelector((state) => state.clipToBbox); -export const selectAutoProcess = buildCanvasSharedSettingsSelector((state) => state.autoProcess); -export const selectSnapToGrid = buildCanvasSharedSettingsSelector((state) => state.snapToGrid); -export const selectShowProgressOnCanvas = buildCanvasSharedSettingsSelector((state) => state.showProgressOnCanvas); -export const selectIsolatedStagingPreview = buildCanvasSharedSettingsSelector((state) => state.isolatedStagingPreview); -export const selectIsolatedLayerPreview = buildCanvasSharedSettingsSelector((state) => state.isolatedLayerPreview); -export const selectPressureSensitivity = buildCanvasSharedSettingsSelector((state) => state.pressureSensitivity); -export const selectRuleOfThirds = buildCanvasSharedSettingsSelector((state) => state.ruleOfThirds); -export const selectSaveAllImagesToGallery = buildCanvasSharedSettingsSelector((state) => state.saveAllImagesToGallery); -export const selectStagingAreaAutoSwitch = buildCanvasSharedSettingsSelector((state) => state.stagingAreaAutoSwitch); -export const selectActiveColor = buildCanvasInstanceSettingsSelector((state) => state.activeColor); -export const selectBgColor = buildCanvasInstanceSettingsSelector((state) => state.bgColor); -export const selectFgColor = buildCanvasInstanceSettingsSelector((state) => state.fgColor); -export const selectBrushWidth = buildCanvasInstanceSettingsSelector((state) => state.brushWidth); -export const selectEraserWidth = buildCanvasInstanceSettingsSelector((state) => state.eraserWidth); +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 6c2b2ba161d..d3718340beb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -1,6 +1,5 @@ import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; -import type { AppDispatch, RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; @@ -16,12 +15,14 @@ import { selectRegionalGuidanceReferenceImage, } from 'features/controlLayers/store/selectors'; import type { + CanvasEntity, CanvasEntityStateFromType, CanvasEntityType, - CanvasesStateWithHistory, - CanvasesStateWithoutHistory, CanvasInpaintMaskState, + CanvasInstanceStateBase, + CanvasInstanceStateWithHistory, CanvasMetadata, + CanvasState, CanvasStateWithHistory, ChannelName, ChannelPoints, @@ -43,7 +44,7 @@ import { isMainModelBase, zModelIdentifierField } from 'features/nodes/types/com 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 { StateWithHistory, UndoableOptions } from 'redux-undo'; +import type { UndoableOptions } from 'redux-undo'; import undoable, { newHistory } from 'redux-undo'; import { type ControlLoRAModelConfig, @@ -57,14 +58,17 @@ import { } from 'services/api/types'; import { assert } from 'tsafe'; +import { canvasSettingsState, getInitialCanvasSettings } from './canvasSettingsSlice'; +import { canvasStagingAreaState, getInitialCanvasStagingAreaState } from './canvasStagingAreaSlice'; +import { getInitialInstanceParamsState, instanceParamsState, isInstanceParamsAction } from './paramsSlice'; import type { AspectRatioID, BoundingBoxScaleMethod, CanvasControlLayerState, CanvasEntityIdentifier, + CanvasInstanceState, CanvasRasterLayerState, CanvasRegionalGuidanceState, - CanvasState, CLIPVisionModelV2, ControlModeV2, ControlNetConfig, @@ -91,8 +95,8 @@ import { isImagenAspectRatioID, isRegionalGuidanceFLUXReduxConfig, isRegionalGuidanceIPAdapterConfig, - zCanvasesStateWithHistory, - zCanvasesStateWithoutHistory, + zCanvasStateWithHistory, + zCanvasStateWithoutHistory, } from './types'; import { converters, @@ -110,9 +114,7 @@ import { makeDefaultRasterLayerAdjustments, } from './util'; -const getInitialCanvasState = (id: string, name: string): CanvasState => ({ - id, - name, +const getInitialCanvasEntity = (): CanvasEntity => ({ selectedEntityIdentifier: null, bookmarkedEntityIdentifier: null, inpaintMasks: { isHidden: false, entities: [] }, @@ -128,16 +130,28 @@ const getInitialCanvasState = (id: string, name: string): CanvasState => ({ }, }); -const getInitialCanvasHistoryState = (id: string, name: string): StateWithHistory => { - const canvas = getInitialCanvasState(id, name); +const getInitialCanvasInstanceState = (id: string, name: string): CanvasInstanceState => ({ + id, + name, + canvas: getInitialCanvasEntity(), + params: getInitialInstanceParamsState(), + settings: getInitialCanvasSettings(), + staging: getInitialCanvasStagingAreaState(), +}); + +const getInitialCanvasInstanceHistoryState = (id: string, name: string): CanvasInstanceStateWithHistory => { + const instance = getInitialCanvasInstanceState(id, name); - return newHistory([], canvas, []); + return { + ...instance, + canvas: newHistory([], instance.canvas, []), + }; }; -const getInitialCanvasesState = (): CanvasesStateWithoutHistory => { +const getInitialCanvasState = (): CanvasState => { const canvasId = getPrefixedId('canvas'); const canvasName = getNextCanvasName([]); - const canvas = getInitialCanvasState(canvasId, canvasName); + const canvas = getInitialCanvasInstanceState(canvasId, canvasName); return { _version: 4, @@ -146,21 +160,24 @@ const getInitialCanvasesState = (): CanvasesStateWithoutHistory => { }; }; -const getInitialCanvasesHistoryState = (): CanvasesStateWithHistory => { - const state = getInitialCanvasesState(); +const getInitialCanvasHistoryState = (): CanvasStateWithHistory => { + const state = getInitialCanvasState(); return { ...state, canvases: Object.fromEntries( - Object.entries(state.canvases).map(([canvasId, canvas]) => [canvasId, newHistory([], canvas, [])]) + Object.entries(state.canvases).map(([canvasId, instance]) => [ + canvasId, + { ...instance, canvas: newHistory([], instance.canvas, []) }, + ]) ), }; }; -const getNextCanvasName = (canvases: CanvasStateWithHistory[]): string => { +const getNextCanvasName = (canvases: CanvasInstanceStateBase[]): string => { for (let i = 1; ; i++) { const name = `Canvas-${i}`; - if (!canvases.some((c) => c.present.name === name)) { + if (!canvases.some((c) => c.name === name)) { return name; } } @@ -169,16 +186,16 @@ const getNextCanvasName = (canvases: CanvasStateWithHistory[]): string => { type PayloadWithCanvasId

= P & { canvasId: string }; type CanvasPayloadAction

= PayloadAction>; -const canvasesSlice = createSlice({ +const canvasSlice = createSlice({ name: 'canvas', - initialState: getInitialCanvasesHistoryState(), + initialState: getInitialCanvasHistoryState(), reducers: { canvasAdded: { reducer: (state, action: CanvasPayloadAction<{ isSelected?: boolean }>) => { const { canvasId, isSelected } = action.payload; const name = getNextCanvasName(Object.values(state.canvases)); - const canvas = getInitialCanvasHistoryState(canvasId, name); + const canvas = getInitialCanvasInstanceHistoryState(canvasId, name); state.canvases[canvasId] = canvas; @@ -192,22 +209,21 @@ const canvasesSlice = createSlice({ }; }, }, - canvasAdding: (_state, _action: CanvasPayloadAction) => {}, canvasActivated: (state, action: CanvasPayloadAction) => { const { canvasId } = action.payload; - const canvas = state.canvases[canvasId]?.present; + const canvas = state.canvases[canvasId]; if (!canvas) { return; } state.activeCanvasId = canvas.id; }, - canvasDeleting: (state, action: CanvasPayloadAction) => { + canvasDeleted: (state, action: CanvasPayloadAction) => { const { canvasId } = action.payload; const canvasIds = Object.keys(state.canvases); - const canvas = state.canvases[canvasId]?.present; + const canvas = state.canvases[canvasId]; if (!canvas) { return; } @@ -222,24 +238,47 @@ const canvasesSlice = createSlice({ state.activeCanvasId = canvasIds[nextIndex]!; delete state.canvases[canvas.id]; }, - canvasDeleted: (_state, _action: CanvasPayloadAction) => {}, - canvasMigrated: (state) => { - delete state.migration; - }, - canvasMultiCanvasMigrated: (_state, _action: CanvasPayloadAction) => {}, - canvasInitialized: (_state, _action: CanvasPayloadAction) => {}, + }, + extraReducers(builder) { + builder.addDefaultCase((state, action) => { + const canvasId = isCanvasPayloadAction(action) ? action.payload.canvasId : state.activeCanvasId; + + const canvasInstance = state.canvases[canvasId]; + if (!canvasInstance) { + return; + } + + state.canvases[canvasId] = canvasInstanceState.reducer(canvasInstance, action); + }); }, }); -const canvasFragment = createSlice({ - name: 'canvas', - initialState: {} as CanvasState, +const canvasInstanceState = createSlice({ + name: 'canvasInstance', + initialState: {} as CanvasInstanceStateWithHistory, reducers: { canvasNameChanged: (state, action: CanvasPayloadAction<{ name: string }>) => { const { name } = action.payload; state.name = name; }, + }, + extraReducers(builder) { + builder.addDefaultCase((state, action) => { + const tab = isInstanceParamsAction(action) ? action.payload.tab : undefined; + + state.canvas = undoableCanvasEntityReducer(state.canvas, action); + state.params = tab === 'canvas' ? instanceParamsState.reducer(state.params, action) : state.params; + state.settings = canvasSettingsState.reducer(state.settings, action); + state.staging = canvasStagingAreaState.reducer(state.staging, action); + }); + }, +}); + +const canvasEntityState = createSlice({ + name: 'canvasEntity', + initialState: {} as CanvasEntity, + reducers: { //#region Raster layers rasterLayerAdjustmentsSet: ( state, @@ -1652,7 +1691,7 @@ const canvasFragment = 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; @@ -1715,7 +1754,7 @@ const canvasFragment = createSlice({ moveToStart(selectAllEntitiesOfType(state, entity.type), entity); }, entitiesReordered: ( - state: CanvasState, + state: CanvasEntity, action: PayloadAction<{ type: T; entityIdentifiers: CanvasEntityIdentifier[] }> ) => { const { type, entityIdentifiers } = action.payload; @@ -1786,7 +1825,7 @@ const canvasFragment = createSlice({ }, allEntitiesDeleted: (state) => { // Deleting all entities is equivalent to resetting the state for each entity type - const initialState = getInitialCanvasState('dummyId', 'dummyName'); + const initialState = getInitialCanvasEntity(); state.rasterLayers = initialState.rasterLayers; state.controlLayers = initialState.controlLayers; state.inpaintMasks = initialState.inpaintMasks; @@ -1845,8 +1884,8 @@ const canvasFragment = createSlice({ }, }); -const resetCanvasState = (state: CanvasState) => { - const newState = getInitialCanvasState(state.id, state.name); +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. @@ -1863,7 +1902,7 @@ const resetCanvasState = (state: CanvasState) => { syncScaledSize(newState); }; -const syncScaledSize = (state: CanvasState) => { +const syncScaledSize = (state: CanvasEntity) => { if (API_BASE_MODELS.includes(state.bbox.modelBase)) { // Imagen3 has fixed sizes. Scaled bbox is not supported. return; @@ -1882,47 +1921,19 @@ const syncScaledSize = (state: CanvasState) => { } }; -export const addCanvas = (payload: { isSelected?: boolean }) => (dispatch: AppDispatch) => { - const canvasAdded = canvasesSlice.actions.canvasAdded(payload); - - dispatch(canvasesSlice.actions.canvasAdding({ canvasId: canvasAdded.payload.canvasId })); - dispatch(canvasAdded); -}; - -export const MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER = 'multi-canvas-id-placeholder'; - -export const deleteCanvas = (payload: { canvasId: string }) => (dispatch: AppDispatch) => { - dispatch(canvasesSlice.actions.canvasDeleting(payload)); - dispatch(canvasesSlice.actions.canvasDeleted(payload)); -}; - -export const migrateCanvas = () => (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - - if (state.canvas.migration?.isMultiCanvasMigrationPending) { - dispatch(canvasesSlice.actions.canvasMultiCanvasMigrated({ canvasId: Object.keys(state.canvas.canvases)[0]! })); - } - - dispatch(canvasesSlice.actions.canvasMigrated()); -}; - -export const initializeCanvasDependencies = () => (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - - dispatch(canvasInitialized({ canvasId: state.canvas.activeCanvasId })); -}; - export const { // Canvas - canvasAdding, + canvasAdded, canvasDeleted, canvasActivated, - canvasMultiCanvasMigrated, - canvasInitialized, -} = canvasesSlice.actions; +} = canvasSlice.actions; export const { canvasNameChanged, + // inpaintMaskRecalled, +} = canvasInstanceState.actions; + +export const { canvasMetadataRecalled, canvasUndo, canvasRedo, @@ -2018,21 +2029,18 @@ export const { inpaintMaskDenoiseLimitChanged, inpaintMaskDenoiseLimitDeleted, // inpaintMaskRecalled, -} = canvasFragment.actions; - -const isCanvasAction = isAnyOf(...Object.values(canvasFragment.actions)); +} = canvasEntityState.actions; let filter = true; -const isActionFileterd = isAnyOf(canvasNameChanged, entitySelected); -const canvasUndoableConfig: UndoableOptions = { +const canvasEntityUndoableConfig: UndoableOptions = { limit: 64, undoType: canvasUndo.type, redoType: canvasRedo.type, clearHistoryType: canvasClearHistory.type, filter: (action, _state, _history) => { // Ignore both all actions from other slices and canvas management actions - if (!action.type.startsWith(canvasFragment.name) || isActionFileterd(action)) { + if (!action.type.startsWith(canvasInstanceState.name)) { return false; } // Throttle rapid actions of the same type @@ -2043,28 +2051,7 @@ const canvasUndoableConfig: UndoableOptions = { // debug: import.meta.env.MODE === 'development', }; -const undoableCanvasReducer = undoable(canvasFragment.reducer, canvasUndoableConfig); - -export const undoableCanvasesReducer = ( - state: CanvasesStateWithHistory | undefined, - action: UnknownAction -): CanvasesStateWithHistory => { - state = canvasesSlice.reducer(state, action); - - if (!isCanvasAction(action)) { - return state; - } - - const canvasId = isCanvasPayloadAction(action) ? action.payload.canvasId : state.activeCanvasId; - - return { - ...state, - canvases: { - ...state.canvases, - [canvasId]: undoableCanvasReducer(state.canvases[canvasId], action), - }, - }; -}; +const undoableCanvasEntityReducer = undoable(canvasEntityState.reducer, canvasEntityUndoableConfig); const isCanvasPayloadAction = (action: UnknownAction): action is CanvasPayloadAction => { return ( @@ -2075,14 +2062,10 @@ const isCanvasPayloadAction = (action: UnknownAction): action is CanvasPayloadAc ); }; -export const canvasSliceConfig: SliceConfig< - typeof canvasesSlice, - CanvasesStateWithHistory, - CanvasesStateWithoutHistory -> = { - slice: canvasesSlice, - getInitialState: getInitialCanvasesState, - schema: zCanvasesStateWithHistory, +export const canvasSliceConfig: SliceConfig = { + slice: canvasSlice, + getInitialState: getInitialCanvasState, + schema: zCanvasStateWithHistory, persistConfig: { migrate: (state) => { assert(isPlainObject(state)); @@ -2094,8 +2077,9 @@ export const canvasSliceConfig: SliceConfig< const canvas = { id: canvasId, name: canvasName, - ...state, - } as CanvasState; + canvas: { ...state }, + settings: getInitialCanvasSettings(), + } as CanvasInstanceState; state = { _version: 4, @@ -2106,18 +2090,20 @@ export const canvasSliceConfig: SliceConfig< }, }; } - return zCanvasesStateWithoutHistory.parse(state); + return zCanvasStateWithoutHistory.parse(state); }, wrapState: (state) => { - const canvasesState = state as CanvasesStateWithoutHistory; + const canvasState = state as CanvasState; return { - _version: canvasesState._version, - activeCanvasId: canvasesState.activeCanvasId, + _version: canvasState._version, + activeCanvasId: canvasState.activeCanvasId, canvases: Object.fromEntries( - Object.entries(canvasesState.canvases).map(([canvasId, canvas]) => [canvasId, newHistory([], canvas, [])]) + Object.entries(canvasState.canvases).map(([canvasId, instance]) => [ + canvasId, + { ...instance, canvas: newHistory([], instance.canvas, []) }, + ]) ), - migration: canvasesState.migration, }; }, unwrapState: (state) => { @@ -2125,9 +2111,11 @@ export const canvasSliceConfig: SliceConfig< _version: state._version, activeCanvasId: state.activeCanvasId, canvases: Object.fromEntries( - Object.entries(state.canvases).map(([canvasId, canvas]) => [canvasId, canvas.present]) + Object.entries(state.canvases).map(([canvasId, instance]) => [ + canvasId, + { ...instance, canvas: instance.canvas.present }, + ]) ), - migration: state.migration, }; }, }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index e40972061c4..c415e7e312b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -1,84 +1,25 @@ -import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; -import { createSelector, createSlice, isAnyOf } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; import { EMPTY_ARRAY } from 'app/store/constants'; import type { RootState } from 'app/store/store'; -import type { SliceConfig } from 'app/store/types'; -import { isPlainObject } from 'es-toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { queueApi } from 'services/api/endpoints/queue'; import { assert } from 'tsafe'; -import z from 'zod'; -import { - canvasAdding, - canvasDeleted, - canvasInitialized, - canvasMultiCanvasMigrated, - MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, -} from './canvasSlice'; import { selectActiveCanvasId } from './selectors'; +import type { CanvasStagingAreaState } from './types'; -const zCanvasSession = z.object({ - canvasId: z.string().min(1), - canvasSessionId: z.string(), - canvasDiscardedQueueItems: z.array(z.number().int()), -}); -type CanvasSession = z.infer; -const zCanvasStagingAreaState = z.object({ - _version: z.literal(2), - sessions: z.record(z.string(), zCanvasSession), -}); -type CanvasStagingAreaState = z.infer; - -type CanvasPayload = { canvasId: string } & T; -type CanvasPayloadAction = PayloadAction>; - -const getInitialCanvasSessionState = (canvasId: string): CanvasSession => ({ - canvasId, +export const getInitialCanvasStagingAreaState = (): CanvasStagingAreaState => ({ + _version: 1, canvasSessionId: getPrefixedId('canvas'), canvasDiscardedQueueItems: [], }); -const getInitialCanvasStagingAreaState = (): CanvasStagingAreaState => ({ - _version: 2, - sessions: {}, -}); - -const canvasStagingAreaSlice = createSlice({ - name: 'canvasSession', - initialState: getInitialCanvasStagingAreaState, - reducers: {}, - extraReducers(builder) { - builder.addCase(canvasAdding, (state, action) => { - const session = getInitialCanvasSessionState(action.payload.canvasId); - state.sessions[session.canvasId] = session; - }); - builder.addCase(canvasDeleted, (state, action) => { - delete state.sessions[action.payload.canvasId]; - }); - builder.addCase(canvasMultiCanvasMigrated, (state, action) => { - const session = state.sessions[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; - if (!session) { - return; - } - session.canvasId = action.payload.canvasId; - state.sessions[session.canvasId] = session; - delete state.sessions[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; - }); - builder.addCase(canvasInitialized, (state, action) => { - const canvasId = action.payload.canvasId; - if (!state.sessions[canvasId]) { - state.sessions[canvasId] = getInitialCanvasSessionState(canvasId); - } - }); - }, -}); - -const canvasSessionFragment = createSlice({ +export const canvasStagingAreaState = createSlice({ name: 'canvasSession', - initialState: {} as CanvasSession, + initialState: {} as CanvasStagingAreaState, reducers: { - canvasQueueItemDiscarded: (state, action: CanvasPayloadAction<{ itemId: number }>) => { + canvasQueueItemDiscarded: (state, action: PayloadAction<{ itemId: number }>) => { const { itemId } = action.payload; if (!state.canvasDiscardedQueueItems.includes(itemId)) { @@ -86,16 +27,15 @@ const canvasSessionFragment = createSlice({ } }, canvasSessionReset: { - reducer: (state, action: CanvasPayloadAction<{ canvasSessionId: string }>) => { + reducer: (state, action: PayloadAction<{ canvasSessionId: string }>) => { const { canvasSessionId } = action.payload; state.canvasSessionId = canvasSessionId; state.canvasDiscardedQueueItems = []; }, - prepare: (payload: CanvasPayload) => { + prepare: () => { return { payload: { - ...payload, canvasSessionId: getPrefixedId('canvas'), }, }; @@ -104,91 +44,40 @@ const canvasSessionFragment = createSlice({ }, }); -export const { canvasSessionReset, canvasQueueItemDiscarded } = canvasSessionFragment.actions; - -const isCanvasSessionAction = isAnyOf(...Object.values(canvasSessionFragment.actions)); - -export const canvasSessionReducer = ( - state: CanvasStagingAreaState | undefined, - action: UnknownAction -): CanvasStagingAreaState => { - state = canvasStagingAreaSlice.reducer(state, action); - - if (!isCanvasSessionAction(action)) { - return state; - } - - const canvasId = action.payload.canvasId; - - return { - ...state, - sessions: { - ...state.sessions, - [canvasId]: canvasSessionFragment.reducer(state.sessions[canvasId], action), - }, - }; -}; - -export const canvasSessionSliceConfig: SliceConfig = { - slice: canvasStagingAreaSlice, - schema: zCanvasStagingAreaState, - getInitialState: getInitialCanvasStagingAreaState, - persistConfig: { - migrate: (state) => { - assert(isPlainObject(state)); - if (!('_version' in state)) { - state._version = 1; - state.canvasSessionId = state.canvasSessionId ?? getPrefixedId('canvas'); - } else if (state._version === 1) { - // Migrate from v1 to v2: slice represented a canvas session instance -> slice represents multiple canvas session instances - const session = { - canvasId: MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, - ...state, - } as CanvasSession; - - state = { - _version: 2, - sessions: { [session.canvasId]: session }, - }; - } - - return zCanvasStagingAreaState.parse(state); - }, - }, -}; +export const { canvasSessionReset, canvasQueueItemDiscarded } = canvasStagingAreaState.actions; -const findSessionByCanvasId = (sessions: Record, canvasId: string) => { - const session = sessions[canvasId]; - assert(session, 'Session must exist for a canvas once the canvas has been created'); - return session; +const findCanvasStagingAreaByCanvasId = (state: RootState, canvasId: string) => { + const instance = state.canvas.canvases[canvasId]; + assert(instance, 'Canvas does not exist'); + return instance.staging; }; -export const selectCanvasSessionByCanvasId = (state: RootState, canvasId: string) => - findSessionByCanvasId(state.canvasSession.sessions, canvasId); -const selectActiveCanvasSession = (state: RootState) => { +export const selectCanvasStagingAreaByCanvasId = (state: RootState, canvasId: string) => + findCanvasStagingAreaByCanvasId(state, canvasId); +const selectActiveCanvasStagingArea = (state: RootState) => { const canvasId = selectActiveCanvasId(state); - return findSessionByCanvasId(state.canvasSession.sessions, canvasId); + return findCanvasStagingAreaByCanvasId(state, canvasId); }; -const selectCanvasSessionBySessionId = (state: RootState, sessionId: string) => { - const session = Object.values(state.canvasSession.sessions).find((s) => s.canvasSessionId === sessionId); - assert(session, 'Session does not exist'); - return session; +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 selectCanvasSessionId = (state: RootState, canvasId: string) => { - const session = selectCanvasSessionByCanvasId(state, canvasId); +export const selectCanvasStagingAreaSessionId = (state: RootState, canvasId: string) => { + const session = selectCanvasStagingAreaByCanvasId(state, canvasId); return session.canvasSessionId; }; -export const selectActiveCanvasSessionId = (state: RootState) => { - const session = selectActiveCanvasSession(state); +export const selectActiveCanvasStagingAreaSessionId = (state: RootState) => { + const session = selectActiveCanvasStagingArea(state); return session.canvasSessionId; }; -const selectCanvasSessionDiscardedItemsBySessionId = (state: RootState, sessionId: string) => { - const session = selectCanvasSessionBySessionId(state, sessionId); +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 }), - (state: RootState) => selectCanvasSessionDiscardedItemsBySessionId(state, sessionId), + (state: RootState) => selectCanvasStagingAreaDiscardedItemsBySessionId(state, sessionId), ({ data }, discardedItems) => { if (!data) { return EMPTY_ARRAY; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index 31baf4c07ba..daac485cca4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -1,4 +1,4 @@ -import type { ActionCreatorWithPayload, Selector, UnknownAction } from '@reduxjs/toolkit'; +import type { ActionCreatorWithPayload, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { AppDispatch, RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; @@ -9,8 +9,7 @@ import { isPlainObject } from 'es-toolkit'; import { clamp } from 'es-toolkit/compat'; import type { AspectRatioID, - CanvasInstanceParams, - InstanceParams, + InstanceParamsState, ParamsPayloadAction, ParamsState, RgbaColor, @@ -21,9 +20,6 @@ import { DEFAULT_ASPECT_RATIO_CONFIG, FLUX_KONTEXT_ASPECT_RATIOS, GEMINI_2_5_ASPECT_RATIOS, - getInitialCanvasInstanceParamsState, - getInitialInstanceParamsState, - getInitialParamsState, IMAGEN_ASPECT_RATIOS, isChatGPT4oAspectRatioID, isFluxKontextAspectRatioID, @@ -31,7 +27,7 @@ import { isImagenAspectRatioID, isParamsTab, MAX_POSITIVE_PROMPT_HISTORY, - zInstanceParams, + zInstanceParamsState, zParamsState, } from 'features/controlLayers/store/types'; import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; @@ -72,48 +68,103 @@ import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { modelChanged } from './actions'; -import { - canvasAdding, - canvasDeleted, - canvasInitialized, - canvasMultiCanvasMigrated, - MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, -} from './canvasSlice'; import { selectActiveCanvasId } from './selectors'; +export const getInitialInstanceParamsState = (): InstanceParamsState => ({ + 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 getInitialParamsState = (): ParamsState => ({ + _version: 3, + generate: getInitialInstanceParamsState(), + upscaling: getInitialInstanceParamsState(), + video: getInitialInstanceParamsState(), +}); + const paramsSlice = createSlice({ name: 'params', initialState: getInitialParamsState, reducers: {}, extraReducers(builder) { - builder.addCase(canvasAdding, (state, action) => { - const canvasParams = getInitialCanvasInstanceParamsState(action.payload.canvasId); - state.canvases[canvasParams.canvasId] = canvasParams; - }); - builder.addCase(canvasDeleted, (state, action) => { - delete state.canvases[action.payload.canvasId]; - }); - builder.addCase(canvasMultiCanvasMigrated, (state, action) => { - const canvasParams = state.canvases[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; - if (!canvasParams) { - return; + builder.addDefaultCase((state, action) => { + if (!isInstanceParamsAction(action)) { + return state; } - canvasParams.canvasId = action.payload.canvasId; - state.canvases[canvasParams.canvasId] = canvasParams; - delete state.canvases[MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER]; - }); - builder.addCase(canvasInitialized, (state, action) => { - const canvasId = action.payload.canvasId; - if (!state.canvases[canvasId]) { - state.canvases[canvasId] = getInitialCanvasInstanceParamsState(canvasId); + + const { tab } = action.payload; + + switch (tab) { + case 'generate': + return { + ...state, + generate: instanceParamsState.reducer(state.generate, action), + }; + case 'upscaling': + return { + ...state, + upscaling: instanceParamsState.reducer(state.upscaling, action), + }; + case 'video': + return { + ...state, + upscaling: instanceParamsState.reducer(state.video, action), + }; } }); }, }); -const instanceParamsFragment = createSlice({ +export const instanceParamsState = createSlice({ name: 'params', - initialState: {} as InstanceParams, + initialState: {} as InstanceParamsState, reducers: { setIterations: (state, action: ParamsPayloadAction) => { state.iterations = action.payload.value; @@ -161,49 +212,49 @@ const instanceParamsFragment = createSlice({ }, vaeSelected: (state, action: ParamsPayloadAction) => { // null is a valid VAE! - const result = zInstanceParams.shape.vae.safeParse(action.payload); + const result = zInstanceParamsState.shape.vae.safeParse(action.payload); if (!result.success) { return; } state.vae = result.data; }, fluxVAESelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParams.shape.fluxVAE.safeParse(action.payload); + const result = zInstanceParamsState.shape.fluxVAE.safeParse(action.payload); if (!result.success) { return; } state.fluxVAE = result.data; }, t5EncoderModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParams.shape.t5EncoderModel.safeParse(action.payload); + const result = zInstanceParamsState.shape.t5EncoderModel.safeParse(action.payload); if (!result.success) { return; } state.t5EncoderModel = result.data; }, controlLoRAModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParams.shape.controlLora.safeParse(action.payload); + const result = zInstanceParamsState.shape.controlLora.safeParse(action.payload); if (!result.success) { return; } state.controlLora = result.data; }, clipEmbedModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParams.shape.clipEmbedModel.safeParse(action.payload); + const result = zInstanceParamsState.shape.clipEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipEmbedModel = result.data; }, clipLEmbedModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParams.shape.clipLEmbedModel.safeParse(action.payload); + const result = zInstanceParamsState.shape.clipLEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipLEmbedModel = result.data; }, clipGEmbedModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParams.shape.clipGEmbedModel.safeParse(action.payload); + const result = zInstanceParamsState.shape.clipGEmbedModel.safeParse(action.payload); if (!result.success) { return; } @@ -243,7 +294,7 @@ const instanceParamsFragment = createSlice({ state.negativePrompt = action.payload.value; }, refinerModelChanged: (state, action: ParamsPayloadAction) => { - const result = zInstanceParams.shape.refinerModel.safeParse(action.payload); + const result = zInstanceParamsState.shape.refinerModel.safeParse(action.payload); if (!result.success) { return; } @@ -440,7 +491,7 @@ const instanceParamsFragment = createSlice({ extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { const { previousModel } = action.payload.value; - const result = zInstanceParams.shape.model.safeParse(action.payload.value.model); + const result = zInstanceParamsState.shape.model.safeParse(action.payload.value.model); if (!result.success) { return; } @@ -492,7 +543,7 @@ const getModelMaxClipSkip = (model: ParameterModel) => { return CLIP_SKIP_MAP[model.base]?.maxClip; }; -const resetState = (state: InstanceParams): InstanceParams => { +const resetState = (state: InstanceParamsState): InstanceParamsState => { // When a new session is requested, we need to keep the current model selections, plus dependent state // like VAE precision. Everything else gets reset to default. const oldState = deepClone(state); @@ -564,9 +615,9 @@ export const { syncedToOptimalDimension, paramsReset, -} = instanceParamsFragment.actions; +} = instanceParamsState.actions; -const instanceParamsActions = { ...instanceParamsFragment.actions, modelChanged }; +const instanceParamsActions = { ...instanceParamsState.actions, modelChanged }; type InstanceParamsAction = typeof instanceParamsActions; type InstanceParamsActionCreator = InstanceParamsAction[keyof InstanceParamsAction]; @@ -621,43 +672,7 @@ const dispatchParamsAction = ( } }; -const isInstanceParamsAction = isAnyOf(...Object.values(instanceParamsActions)); - -export const paramsSliceReducer = (state: ParamsState, action: UnknownAction): ParamsState => { - state = paramsSlice.reducer(state, action); - - if (!isInstanceParamsAction(action)) { - return state; - } - - const { tab, canvasId } = action.payload; - - switch (tab) { - case 'generate': - return { - ...state, - generate: instanceParamsFragment.reducer(state.generate, action), - }; - case 'canvas': - return { - ...state, - canvases: { - ...state.canvases, - [canvasId]: { canvasId, ...instanceParamsFragment.reducer(state.canvases[canvasId], action) }, - }, - }; - case 'upscaling': - return { - ...state, - upscaling: instanceParamsFragment.reducer(state.upscaling, action), - }; - case 'video': - return { - ...state, - upscaling: instanceParamsFragment.reducer(state.video, action), - }; - } -}; +export const isInstanceParamsAction = isAnyOf(...Object.values(instanceParamsActions)); export const paramsSliceConfig: SliceConfig = { slice: paramsSlice, @@ -682,15 +697,9 @@ export const paramsSliceConfig: SliceConfig = { if (state._version === 2) { // Migrate from v2 to v3: slice represented shared params -> slice represents multiple tabs/canvases params - const canvasParams = { - canvasId: MIGRATION_MULTI_CANVAS_ID_PLACEHOLDER, - ...state, - } as CanvasInstanceParams; - state = { _version: 3, generate: { ...state }, - canvases: { [canvasParams.canvasId]: canvasParams }, upscaling: { ...state }, video: { ...state }, }; @@ -711,7 +720,7 @@ export const selectActiveParams = (state: RootState) => { case 'generate': return state.params.generate; case 'canvas': { - const params = state.params.canvases[canvasId]; + const params = state.canvas.canvases[canvasId]?.params; assert(params, 'Params must exist for a canvas once the canvas has been created'); return params; } @@ -726,7 +735,7 @@ export const selectActiveParams = (state: RootState) => { }; const buildActiveParamsSelector = - (selector: Selector) => + (selector: Selector) => (state: RootState) => selector(selectActiveParams(state)); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts index 96933ac6ab3..eda52dae38a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -2,6 +2,7 @@ import { createSelector } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import type { CanvasControlLayerState, + CanvasEntity, CanvasEntityIdentifier, CanvasEntityState, CanvasEntityType, @@ -9,7 +10,6 @@ import type { CanvasMetadata, CanvasRasterLayerState, CanvasRegionalGuidanceState, - CanvasState, } from 'features/controlLayers/store/types'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; @@ -23,9 +23,11 @@ const selectCanvasSlice = (state: RootState) => state.canvas; * Selects the canvases */ export const selectCanvases = createSelector(selectCanvasSlice, (state) => - Object.values(state.canvases).map(({ present: canvas }) => ({ - ...canvas, - isActive: canvas.id === state.activeCanvasId, + 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, })) ); @@ -35,16 +37,16 @@ export const selectCanvases = createSelector(selectCanvasSlice, (state) => */ const selectActiveCanvasWithHistory = createSelector( selectCanvasSlice, - (state) => state.canvases[state.activeCanvasId]! + (state) => state.canvases[state.activeCanvasId]!.canvas ); export const selectActiveCanvas = createSelector(selectActiveCanvasWithHistory, (canvas) => canvas.present); -export const selectActiveCanvasId = createSelector(selectActiveCanvas, (canvas) => canvas.id); +export const selectActiveCanvasId = createSelector(selectCanvasSlice, (state) => state.activeCanvasId); -export const selectCanvasById = (state: RootState, canvasId: string) => { - const canvas = selectCanvasSlice(state).canvases[canvasId]; - assert(canvas, 'Canvas does not exist'); - return canvas.present; +export const selectCanvasByCanvasId = (state: RootState, canvasId: string) => { + const instance = selectCanvasSlice(state).canvases[canvasId]; + assert(instance, 'Canvas does not exist'); + return instance.canvas.present; }; /** @@ -101,7 +103,7 @@ export const selectHasEntities = createSelector(selectEntityCountAll, (count) => * return type will be narrowed as well. */ export function selectEntity( - state: CanvasState, + state: CanvasEntity, entityIdentifier: T ): Extract | undefined { const { id, type } = entityIdentifier; @@ -131,7 +133,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; @@ -174,7 +176,7 @@ export function selectEntityIdentifierBelowThisOne( - state: CanvasState, + state: CanvasEntity, entityIdentifier: T, caller: string ): Extract { @@ -191,7 +193,7 @@ export const selectEntityExists = (entityIdent * Selects all entities of the given type. */ export function selectAllEntitiesOfType( - state: CanvasState, + state: CanvasEntity, type: T ): Extract[] { let entities: CanvasEntityState[] = []; @@ -218,7 +220,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(), @@ -236,7 +238,7 @@ export function selectAllEntities(state: CanvasState): CanvasEntityState[] { * - Regional guidance */ export function selectAllRenderableEntities( - state: CanvasState + state: CanvasEntity ): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] { return [ ...state.rasterLayers.entities, @@ -250,7 +252,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 ) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 2cda5cc7e5e..359a3cdc879 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -118,7 +118,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; @@ -693,7 +693,7 @@ type ParamsTabName = 'generate' | 'canvas' | 'upscaling' | 'video'; export type ParamsEnrichedPayload

= EnrichedPayload<{ tab: ParamsTabName; canvasId: string }, P>; export type ParamsPayloadAction

= PayloadAction>; -export const zInstanceParams = z.object({ +export const zInstanceParamsState = z.object({ maskBlur: z.number(), maskBlurMethod: zParameterMaskBlurMethod, canvasCoherenceMode: zParameterCanvasCoherenceMode, @@ -740,87 +740,15 @@ export const zInstanceParams = z.object({ controlLora: zParameterControlLoRAModel.nullable(), dimensions: zDimensionsState, }); -export type InstanceParams = z.infer; - -const zCanvasInstanceParams = zInstanceParams.extend({ - canvasId: zId, -}); -export type CanvasInstanceParams = z.infer; - +export type InstanceParamsState = z.infer; export const zParamsState = z.object({ _version: z.literal(3), - generate: zInstanceParams, - canvases: z.record(z.string(), zCanvasInstanceParams), - upscaling: zInstanceParams, - video: zInstanceParams, + generate: zInstanceParamsState, + upscaling: zInstanceParamsState, + video: zInstanceParamsState, }); export type ParamsState = z.infer; -export const getInitialInstanceParamsState = (): InstanceParams => ({ - 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 getInitialCanvasInstanceParamsState = (canvasId: string): CanvasInstanceParams => ({ - canvasId, - ...getInitialInstanceParamsState(), -}); - -export const getInitialParamsState = (): ParamsState => ({ - _version: 3, - generate: getInitialInstanceParamsState(), - canvases: {}, - upscaling: getInitialInstanceParamsState(), - video: getInitialInstanceParamsState(), -}); - const zInpaintMasks = z.object({ isHidden: z.boolean(), entities: z.array(zCanvasInpaintMaskState), @@ -837,6 +765,104 @@ const zRegionalGuidance = z.object({ isHidden: z.boolean(), entities: z.array(zCanvasRegionalGuidanceState), }); + +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), @@ -847,9 +873,7 @@ const zStateWithHistory = (stateSchema: T) => index: z.number().optional(), limit: z.number().optional(), }); -const zCanvasState = z.object({ - id: zId, - name: z.string().min(1), +const zCanvasEntity = z.object({ selectedEntityIdentifier: zCanvasEntityIdentifer.nullable(), bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable(), inpaintMasks: zInpaintMasks, @@ -858,23 +882,34 @@ const zCanvasState = z.object({ regionalGuidance: zRegionalGuidance, bbox: zBboxState, }); -export type CanvasState = z.infer; -const zCanvasStateWithHistory = zStateWithHistory(zCanvasState); -export type CanvasStateWithHistory = z.infer; -const zCanvasesStateMigration = z.object({ - isMultiCanvasMigrationPending: z.boolean().optional(), +export type CanvasEntity = z.infer; +const zCanvasInstanceStateBase = z.object({ + id: zId, + name: z.string().min(1), }); -const zCanvasesState = (canvasStateSchema: T) => +const zCanvasInstanceState = (canvasEntitySchema: T) => + zCanvasInstanceStateBase.extend({ + canvas: canvasEntitySchema, + params: zInstanceParamsState, + 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, canvasStateSchema), - migration: zCanvasesStateMigration.optional(), + canvases: z.record(zId, canvasInstanceSchema), }); -export const zCanvasesStateWithHistory = zCanvasesState(zCanvasStateWithHistory); -export type CanvasesStateWithHistory = z.infer; -export const zCanvasesStateWithoutHistory = zCanvasesState(zCanvasState); -export type CanvasesStateWithoutHistory = z.infer; +export const zCanvasStateWithoutHistory = zCanvasState(zCanvasInstanceStateWithoutHistory); +export const zCanvasStateWithHistory = zCanvasState(zCanvasInstanceStateWithHistory); +export type CanvasState = z.infer; +export type CanvasStateWithHistory = z.infer; + export const zRefImagesState = z.object({ selectedEntityId: z.string().nullable(), isPanelOpen: z.boolean(), diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts index 0aafe2769f3..1fd137136d4 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/state.ts @@ -9,7 +9,7 @@ import { selectRefImagesSlice, } from 'features/controlLayers/store/refImagesSlice'; import { selectCanvases } from 'features/controlLayers/store/selectors'; -import type { CanvasState, RefImagesState } from 'features/controlLayers/store/types'; +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'; @@ -268,7 +268,7 @@ const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, image_ export const getImageUsage = ( nodes: NodesState, - canvases: CanvasState[], + canvases: CanvasEntity[], upscale: UpscaleState, refImages: RefImagesState, image_name: string diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts index 85940ed725a..1181080d67c 100644 --- a/invokeai/frontend/web/src/features/imageActions/actions.ts +++ b/invokeai/frontend/web/src/features/imageActions/actions.ts @@ -4,8 +4,8 @@ import { getDefaultRegionalGuidanceRefImageConfig } from 'features/controlLayers import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { - addCanvas, bboxChangedFromCanvas, + canvasAdded, canvasClearHistory, controlLayerAdded, entityRasterized, @@ -213,7 +213,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(addCanvas({ isSelected: true })); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(rasterLayerAdded({ overrides, isSelected: true })); @@ -230,7 +230,7 @@ export const newCanvasFromImage = async (arg: { controlAdapter: deepClone(initialControlNet), } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(addCanvas({ isSelected: true })); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(controlLayerAdded({ overrides, isSelected: true })); @@ -246,7 +246,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(addCanvas({ isSelected: true })); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(inpaintMaskAdded({ overrides, isSelected: true })); @@ -262,7 +262,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(addCanvas({ isSelected: true })); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(rgAdded({ overrides, isSelected: true })); @@ -276,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(addCanvas({ isSelected: true })); + 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/nodes/util/graph/generation/addFLUXFill.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addFLUXFill.ts index a48c89d3145..1b185bb84b7 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,7 +2,7 @@ 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 { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { @@ -36,7 +36,7 @@ export const addFLUXFill = async ({ denoise.height = scaledSize.height; const params = selectActiveParams(state); - const canvasSettings = buildSelectCanvasSettingsByCanvasId(manager.canvasId)(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/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index e606b92b859..c26df3a3b0a 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,7 +2,7 @@ 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 { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { @@ -49,7 +49,7 @@ export const addInpaint = async ({ denoise.denoising_end = denoising_end; const params = selectActiveParams(state); - const canvasSettings = buildSelectCanvasSettingsByCanvasId(manager.canvasId)(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/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index a5be085ec34..d4b66439dae 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,7 +2,7 @@ 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 { buildSelectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { @@ -51,7 +51,7 @@ export const addOutpaint = async ({ denoise.denoising_end = denoising_end; const params = selectActiveParams(state); - const canvasSettings = buildSelectCanvasSettingsByCanvasId(manager.canvasId)(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/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts index 3d2939b2048..6efd1b9b3af 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectActiveParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasById, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; +import { selectCanvasByCanvasId, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { isFluxKontextReferenceImageConfig } from 'features/controlLayers/store/types'; import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators'; import { zImageField } from 'features/nodes/types/common'; @@ -40,7 +40,7 @@ export const buildFLUXGraph = async (arg: GraphBuilderArg): Promise { return 'canvas'; } - return selectCanvasSessionId(state, canvasId); + return selectCanvasStagingAreaSessionId(state, canvasId); }; /** @@ -158,7 +158,7 @@ export const getOriginalAndScaledSizesForOtherModes = (state: RootState) => { export const getInfill = ( g: Graph, - params: InstanceParams + params: InstanceParamsState ): Invocation<'infill_patchmatch' | 'infill_cv2' | 'infill_lama' | 'infill_rgba' | 'infill_tile'> => { const { infillMethod, infillColorValue, infillPatchmatchDownscaleSize, infillTileSize } = params; diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index 65d0c18474a..bee69c4b188 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -13,7 +13,7 @@ import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; import { selectActiveParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; -import type { CanvasState, InstanceParams, LoRA, RefImagesState } from 'features/controlLayers/store/types'; +import type { CanvasEntity, InstanceParamsState, LoRA, RefImagesState } from 'features/controlLayers/store/types'; import { getControlLayerWarnings, getGlobalReferenceImageWarnings, @@ -77,8 +77,8 @@ export const $isReadyToEnqueue = computed($reasonsWhyCannotEnqueue, (reasons) => type UpdateReasonsArg = { tab: TabName; isConnected: boolean; - canvas: CanvasState; - params: InstanceParams; + canvas: CanvasEntity; + params: InstanceParamsState; refImages: RefImagesState; dynamicPrompts: DynamicPromptsState; canvasIsFiltering: boolean; @@ -277,7 +277,7 @@ const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invok const getReasonsWhyCannotEnqueueVideoTab = (arg: { isConnected: boolean; video: VideoState; - params: InstanceParams; + params: InstanceParamsState; dynamicPrompts: DynamicPromptsState; promptExpansionRequest: PromptExpansionRequestState; isVideoEnabled: boolean; @@ -319,7 +319,7 @@ const getReasonsWhyCannotEnqueueVideoTab = (arg: { const getReasonsWhyCannotEnqueueGenerateTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; - params: InstanceParams; + params: InstanceParamsState; refImages: RefImagesState; loras: LoRA[]; dynamicPrompts: DynamicPromptsState; @@ -490,7 +490,7 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: { isConnected: boolean; upscale: UpscaleState; config: AppConfig; - params: InstanceParams; + params: InstanceParamsState; loras: LoRA[]; promptExpansionRequest: PromptExpansionRequestState; }) => { @@ -552,8 +552,8 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: { const getReasonsWhyCannotEnqueueCanvasTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; - canvas: CanvasState; - params: InstanceParams; + canvas: CanvasEntity; + params: InstanceParamsState; refImages: RefImagesState; loras: LoRA[]; dynamicPrompts: DynamicPromptsState; diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx index 8a98f769350..d6011e9310d 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx @@ -1,7 +1,7 @@ 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 { addCanvas, canvasActivated, deleteCanvas } from 'features/controlLayers/store/canvasSlice'; +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'; @@ -18,7 +18,7 @@ const AddCanvasButton = memo(() => { const dispatch = useAppDispatch(); const onClick = useCallback(() => { - dispatch(addCanvas({ isSelected: true })); + dispatch(canvasAdded({ isSelected: true })); }, [dispatch]); return ( @@ -46,7 +46,7 @@ const CloseCanvasButton = memo(({ canvasId, canDelete }: CloseCanvasButtonProps) const dispatch = useAppDispatch(); const onClick = useCallback(() => { - dispatch(deleteCanvas({ canvasId })); + dispatch(canvasDeleted({ canvasId })); }, [dispatch, canvasId]); return ( From 67a9f5d9ec3f97bad7d4483f02b720ffbe0515e1 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Fri, 3 Oct 2025 10:44:49 +0200 Subject: [PATCH 12/16] TabActionContext implemented --- .../frontend/web/.storybook/ReduxInit.tsx | 11 +- .../middleware/actionContextMiddleware.ts | 24 ++ .../listeners/appConfigReceived.ts | 4 +- .../listeners/modelSelected.ts | 13 +- .../listeners/modelsLoaded.ts | 20 +- .../listeners/setDefaultSettings.ts | 23 +- invokeai/frontend/web/src/app/store/store.ts | 2 + invokeai/frontend/web/src/app/store/util.ts | 29 ++ .../common/components/SessionMenuItems.tsx | 7 +- .../components/ParamDenoisingStrength.tsx | 10 +- .../components/RefImage/RefImageImage.tsx | 12 +- .../RegionalGuidanceRefImageImage.tsx | 12 +- .../features/controlLayers/store/actions.ts | 9 +- .../store/canvasSettingsSlice.ts | 4 +- .../controlLayers/store/canvasSlice.ts | 74 ++-- .../store/canvasStagingAreaSlice.ts | 4 +- .../controlLayers/store/paramsSlice.ts | 342 +++++++----------- .../src/features/controlLayers/store/types.ts | 22 +- .../web/src/features/metadata/parsing.tsx | 45 ++- .../nodes/util/graph/graphBuilderUtils.ts | 4 +- .../Advanced/ParamCFGRescaleMultiplier.tsx | 12 +- .../Advanced/ParamCLIPEmbedModelSelect.tsx | 14 +- .../Advanced/ParamCLIPGEmbedModelSelect.tsx | 14 +- .../Advanced/ParamCLIPLEmbedModelSelect.tsx | 14 +- .../components/Advanced/ParamClipSkip.tsx | 10 +- .../ParamOptimizedDenoisingToggle.tsx | 9 +- .../Advanced/ParamT5EncoderModelSelect.tsx | 14 +- .../ParamCanvasCoherenceEdgeSize.tsx | 14 +- .../ParamCanvasCoherenceMinDenoise.tsx | 9 +- .../ParamCanvasCoherenceMode.tsx | 14 +- .../MaskAdjustment/ParamMaskBlur.tsx | 10 +- .../ParamInfillColorOptions.tsx | 10 +- .../InfillAndScaling/ParamInfillMethod.tsx | 10 +- .../ParamInfillPatchmatchDownscaleSize.tsx | 9 +- .../InfillAndScaling/ParamInfillTilesize.tsx | 15 +- .../Core/NegativePromptToggleButton.tsx | 17 +- .../components/Core/ParamCFGScale.tsx | 8 +- .../components/Core/ParamGuidance.tsx | 8 +- .../components/Core/ParamNegativePrompt.tsx | 14 +- .../components/Core/ParamPositivePrompt.tsx | 21 +- .../components/Core/ParamScheduler.tsx | 10 +- .../parameters/components/Core/ParamSteps.tsx | 10 +- .../components/Core/PositivePromptHistory.tsx | 19 +- .../DimensionsAspectRatioSelect.tsx | 9 +- .../Dimensions/DimensionsHeight.tsx | 9 +- .../DimensionsLockAspectRatioButton.tsx | 9 +- .../DimensionsSetOptimalSizeButton.tsx | 9 +- .../Dimensions/DimensionsSwapButton.tsx | 10 +- .../components/Dimensions/DimensionsWidth.tsx | 9 +- .../Seamless/ParamSeamlessXAxis.tsx | 11 +- .../Seamless/ParamSeamlessYAxis.tsx | 11 +- .../components/Seed/ParamSeedNumberInput.tsx | 15 +- .../components/Seed/ParamSeedRandomize.tsx | 14 +- .../components/Seed/ParamSeedShuffle.tsx | 11 +- .../Upscale/ParamUpscaleCFGScale.tsx | 8 +- .../Upscale/ParamUpscaleScheduler.tsx | 14 +- .../VAEModel/ParamFLUXVAEModelSelect.tsx | 10 +- .../VAEModel/ParamVAEModelSelect.tsx | 10 +- .../components/VAEModel/ParamVAEPrecision.tsx | 10 +- .../PromptExpansionResultOverlay.tsx | 18 +- .../components/QueueIterationsNumberInput.tsx | 11 +- .../features/queue/hooks/useEnqueueCanvas.ts | 3 +- .../queue/hooks/useEnqueueGenerate.ts | 3 +- .../queue/hooks/useEnqueueUpscaling.ts | 3 +- .../features/queue/hooks/useEnqueueVideo.ts | 8 +- .../web/src/features/queue/store/readiness.ts | 12 +- .../SDXLRefiner/ParamSDXLRefinerCFGScale.tsx | 8 +- .../ParamSDXLRefinerModelSelect.tsx | 12 +- ...ParamSDXLRefinerNegativeAestheticScore.tsx | 10 +- ...ParamSDXLRefinerPositiveAestheticScore.tsx | 10 +- .../SDXLRefiner/ParamSDXLRefinerScheduler.tsx | 14 +- .../SDXLRefiner/ParamSDXLRefinerStart.tsx | 8 +- .../SDXLRefiner/ParamSDXLRefinerSteps.tsx | 10 +- .../components/ActiveStylePreset.tsx | 14 +- .../SettingsModal/SettingsModal.tsx | 12 +- 75 files changed, 577 insertions(+), 704 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/actionContextMiddleware.ts create mode 100644 invokeai/frontend/web/src/app/store/util.ts diff --git a/invokeai/frontend/web/.storybook/ReduxInit.tsx b/invokeai/frontend/web/.storybook/ReduxInit.tsx index 6a7c2c8593d..b284c6dff9c 100644 --- a/invokeai/frontend/web/.storybook/ReduxInit.tsx +++ b/invokeai/frontend/web/.storybook/ReduxInit.tsx @@ -4,18 +4,19 @@ import { memo, useEffect } from 'react'; import { useAppDispatch } from '../src/app/store/storeHooks'; import { modelChanged } from 'features/controlLayers/store/actions'; -import { useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; /** * Initializes some state for storybook. Must be in a different component * so that it is run inside the redux context. */ export const ReduxInit = memo(({ children }: PropsWithChildren) => { - const dispatch = useParamsDispatch(); + const dispatch = useAppDispatch(); useGlobalModifiersInit(); useEffect(() => { - dispatch(modelChanged, { - model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' }, - }); + dispatch( + modelChanged({ + model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' }, + }) + ); }, [dispatch]); return children; 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..3cab63aad2e --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/actionContextMiddleware.ts @@ -0,0 +1,24 @@ +import type { Middleware, UnknownAction } from '@reduxjs/toolkit'; +import { injectTabActionContext } from 'app/store/util'; +import { isCanvasInstanceAction } from 'features/controlLayers/store/canvasSlice'; +import { isTabParamsStateAction } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; + +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 isTabParamsStateAction(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 933f47d1401..2459137cf75 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,5 +1,5 @@ import type { AppStartListening } from 'app/store/store'; -import { paramsDispatch, selectActiveParams, setInfillMethod } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveParams, setInfillMethod } from 'features/controlLayers/store/paramsSlice'; import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged } from 'features/system/store/systemSlice'; import { appInfoApi } from 'services/api/endpoints/appInfo'; @@ -15,7 +15,7 @@ export const addAppConfigReceivedListener = (startAppListening: AppStartListenin // If the selected infill method does not exist, prefer 'lama' if it's in the list, otherwise 'tile'. // TODO(psyche): lama _should_ always be in the list, but the API doesn't guarantee it... const infillMethod = infill_methods.includes('lama') ? 'lama' : 'tile'; - paramsDispatch(api, setInfillMethod, infillMethod); + dispatch(setInfillMethod(infillMethod)); } if (!nsfw_methods.includes('nsfw_checker')) { 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 ca999025f7e..a848ff678e4 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 @@ -7,12 +7,7 @@ import { selectActiveCanvasStagingAreaSessionId, } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; -import { - paramsDispatch, - selectActiveParams, - syncedToOptimalDimension, - vaeSelected, -} from 'features/controlLayers/store/paramsSlice'; +import { selectActiveParams, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice'; import { selectActiveCanvas, @@ -70,7 +65,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = // handle incompatible vae const { vae } = params; if (vae && vae.base !== newBase) { - paramsDispatch(api, vaeSelected, null); + dispatch(vaeSelected(null)); modelsUpdatedDisabledOrCleared += 1; } @@ -163,13 +158,13 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } } - paramsDispatch(api, modelChanged, { model: newModel, previousModel: params.model }); + dispatch(modelChanged({ model: newModel, previousModel: params.model })); const modelBase = selectBboxModelBase(state); if (modelBase !== params.model?.base) { // Sync generate tab settings whenever the model base changes - paramsDispatch(api, syncedToOptimalDimension); + dispatch(syncedToOptimalDimension()); const sessionId = selectActiveCanvasStagingAreaSessionId(state); const selectIsStaging = buildSelectIsStagingBySessionId(sessionId); const isStaging = selectIsStaging(state); 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 1113686b7e1..5a379012988 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 @@ -6,11 +6,9 @@ import { loraDeleted } from 'features/controlLayers/store/lorasSlice'; import { clipEmbedModelSelected, fluxVAESelected, - paramsDispatch, refinerModelChanged, selectActiveParams, t5EncoderModelSelected, - toStore, vaeSelected, } from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; @@ -116,7 +114,7 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { // Only clear the model if we have one currently selected if (selectedMainModel !== null) { log.debug({ selectedMainModel }, 'No main models available, clearing'); - paramsDispatch(toStore(state, dispatch), modelChanged, { model: null }); + dispatch(modelChanged({ model: null })); } return; } @@ -166,7 +164,7 @@ const handleRefinerModels: ModelHandler = (models, state, dispatch, log) => { // Else, we need to clear the refiner model log.debug({ selectedRefinerModel }, 'Selected refiner model is not available, clearing'); - paramsDispatch(toStore(state, dispatch), refinerModelChanged, null); + dispatch(refinerModelChanged(null)); return; }; @@ -190,7 +188,7 @@ const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { // Else, we need to clear the VAE model log.debug({ selectedVAEModel }, 'Selected VAE model is not available, clearing'); - paramsDispatch(toStore(state, dispatch), vaeSelected, null); + dispatch(vaeSelected(null)); return; }; @@ -435,14 +433,14 @@ const handleT5EncoderModels: ModelHandler = (models, state, dispatch, log) => { { selectedT5EncoderModel, firstModel }, 'No selected T5 encoder model or selected T5 encoder model is not available, selecting first available model' ); - paramsDispatch(toStore(state, dispatch), t5EncoderModelSelected, zParameterT5EncoderModel.parse(firstModel)); + dispatch(t5EncoderModelSelected(zParameterT5EncoderModel.parse(firstModel))); return; } // No available models, we should clear the selected model - but only if we have one selected if (selectedT5EncoderModel) { log.debug({ selectedT5EncoderModel }, 'Selected T5 encoder model is not available, clearing'); - paramsDispatch(toStore(state, dispatch), t5EncoderModelSelected, null); + dispatch(t5EncoderModelSelected(null)); return; } }; @@ -463,14 +461,14 @@ const handleCLIPEmbedModels: ModelHandler = (models, state, dispatch, log) => { { selectedCLIPEmbedModel, firstModel }, 'No selected CLIP embed model or selected CLIP embed model is not available, selecting first available model' ); - paramsDispatch(toStore(state, dispatch), clipEmbedModelSelected, zParameterCLIPEmbedModel.parse(firstModel)); + dispatch(clipEmbedModelSelected(zParameterCLIPEmbedModel.parse(firstModel))); return; } // No available models, we should clear the selected model - but only if we have one selected if (selectedCLIPEmbedModel) { log.debug({ selectedCLIPEmbedModel }, 'Selected CLIP embed model is not available, clearing'); - paramsDispatch(toStore(state, dispatch), clipEmbedModelSelected, null); + dispatch(clipEmbedModelSelected(null)); return; } }; @@ -491,14 +489,14 @@ const handleFLUXVAEModels: ModelHandler = (models, state, dispatch, log) => { { selectedFLUXVAEModel, firstModel }, 'No selected FLUX VAE model or selected FLUX VAE model is not available, selecting first available model' ); - paramsDispatch(toStore(state, dispatch), fluxVAESelected, zParameterVAEModel.parse(firstModel)); + dispatch(fluxVAESelected(zParameterVAEModel.parse(firstModel))); return; } // No available models, we should clear the selected model - but only if we have one selected if (selectedFLUXVAEModel) { log.debug({ selectedFLUXVAEModel }, 'Selected FLUX VAE model is not available, clearing'); - paramsDispatch(toStore(state, dispatch), fluxVAESelected, null); + dispatch(fluxVAESelected(null)); return; } }; 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 bc1723f3d19..56262d310ca 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 @@ -7,7 +7,6 @@ import { } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { heightChanged, - paramsDispatch, selectActiveParams, setCfgRescaleMultiplier, setCfgScale, @@ -67,56 +66,56 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni // we store this as "default" within default settings // to distinguish it from no default set if (vae === 'default') { - paramsDispatch(api, vaeSelected, null); + dispatch(vaeSelected(null)); } else { const vaeModel = models.find((model) => model.key === vae); const result = zParameterVAEModel.safeParse(vaeModel); if (!result.success) { return; } - paramsDispatch(api, vaeSelected, result.data); + dispatch(vaeSelected(result.data)); } } if (vae_precision) { if (isParameterPrecision(vae_precision)) { - paramsDispatch(api, vaePrecisionChanged, vae_precision); + dispatch(vaePrecisionChanged(vae_precision)); } } if (guidance) { if (isParameterGuidance(guidance)) { - paramsDispatch(api, setGuidance, guidance); + dispatch(setGuidance(guidance)); } } if (cfg_scale) { if (isParameterCFGScale(cfg_scale)) { - paramsDispatch(api, setCfgScale, cfg_scale); + dispatch(setCfgScale(cfg_scale)); } } if (!isNil(cfg_rescale_multiplier)) { if (isParameterCFGRescaleMultiplier(cfg_rescale_multiplier)) { - paramsDispatch(api, setCfgRescaleMultiplier, cfg_rescale_multiplier); + dispatch(setCfgRescaleMultiplier(cfg_rescale_multiplier)); } } else { // Set this to 0 if it doesn't have a default. This value is // easy to miss in the UI when users are resetting defaults // and leaving it non-zero could lead to detrimental // effects. - paramsDispatch(api, setCfgRescaleMultiplier, 0); + dispatch(setCfgRescaleMultiplier(0)); } if (steps) { if (isParameterSteps(steps)) { - paramsDispatch(api, setSteps, steps); + dispatch(setSteps(steps)); } } if (scheduler) { if (isParameterScheduler(scheduler)) { - paramsDispatch(api, setScheduler, scheduler); + dispatch(setScheduler(scheduler)); } } const setSizeOptions = { updateAspectRatio: true, clamp: true }; @@ -128,10 +127,10 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni const activeTab = selectActiveTab(getState()); if (activeTab === 'generate') { if (isParameterWidth(width)) { - paramsDispatch(api, widthChanged, { width, ...setSizeOptions }); + dispatch(widthChanged({ width, ...setSizeOptions })); } if (isParameterHeight(height)) { - paramsDispatch(api, heightChanged, { height, ...setSizeOptions }); + dispatch(heightChanged({ height, ...setSizeOptions })); } } diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 6ac251c1f84..8fe794039aa 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -48,6 +48,7 @@ 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'; @@ -179,6 +180,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/util.ts b/invokeai/frontend/web/src/app/store/util.ts new file mode 100644 index 00000000000..742188fa09f --- /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/ui/store/uiTypes'; + +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/common/components/SessionMenuItems.tsx b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx index 6bee5eb632f..952fa4a1f0b 100644 --- a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx +++ b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx @@ -2,7 +2,7 @@ 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 { paramsReset, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { paramsReset } from 'features/controlLayers/store/paramsSlice'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,7 +11,6 @@ import { PiArrowsCounterClockwiseBold } from 'react-icons/pi'; export const SessionMenuItems = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const paramsDispatch = useParamsDispatch(); const tab = useAppSelector(selectActiveTab); const canvasManager = useCanvasManagerSafe(); @@ -21,8 +20,8 @@ export const SessionMenuItems = memo(() => { canvasManager?.stage.fitBboxToStage(); }, [dispatch, canvasManager]); const resetGenerationSettings = useCallback(() => { - paramsDispatch(paramsReset); - }, [paramsDispatch]); + dispatch(paramsReset()); + }, [dispatch]); return ( <> {tab === 'canvas' && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx index 6ead251dcab..bf4464bd5bd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ParamDenoisingStrength.tsx @@ -8,10 +8,10 @@ import { useToken, } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import WavyLine from 'common/components/WavyLine'; -import { selectImg2imgStrength, setImg2imgStrength, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice'; import { selectActiveRasterLayerEntities } from 'features/controlLayers/store/selectors'; import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; @@ -25,15 +25,15 @@ const selectHasRasterLayersWithContent = createSelector( export const ParamDenoisingStrength = memo(() => { const img2imgStrength = useAppSelector(selectImg2imgStrength); - const paramsDispatch = useParamsDispatch(); + const dispatch = useAppDispatch(); const hasRasterLayersWithContent = useAppSelector(selectHasRasterLayersWithContent); const selectedModelConfig = useSelectedModelConfig(); const onChange = useCallback( (v: number) => { - paramsDispatch(setImg2imgStrength, v); + dispatch(setImg2imgStrength(v)); }, - [paramsDispatch] + [dispatch] ); const config = useAppSelector(selectImg2imgStrengthConfig); 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 bef83fb4885..e0ac13d4571 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,11 @@ 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 { sizeOptimized, sizeRecalled, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice'; import type { CroppableImageWithDims } from 'features/controlLayers/store/types'; import { imageDTOToCroppableImage, imageDTOToImageWithDims } from 'features/controlLayers/store/util'; import { Editor } from 'features/cropper/lib/editor'; @@ -39,7 +39,7 @@ export const RefImageImage = memo( }: Props) => { const { t } = useTranslation(); const store = useAppStore(); - const paramsDispatch = useParamsDispatch(); + const dispatch = useAppDispatch(); const isConnected = useStore($isConnected); const tab = useAppSelector(selectActiveTab); const isStaging = useCanvasIsStaging(); @@ -78,10 +78,10 @@ export const RefImageImage = memo( store.dispatch(bboxSizeRecalled({ width, height })); store.dispatch(bboxSizeOptimized()); } else if (tab === 'generate') { - paramsDispatch(sizeRecalled, { width, height }); - paramsDispatch(sizeOptimized); + dispatch(sizeRecalled({ width, height })); + dispatch(sizeOptimized()); } - }, [paramsDispatch, imageDTO, isStaging, store, tab]); + }, [dispatch, imageDTO, isStaging, store, tab]); const edit = useCallback(() => { if (!originalImageDTO) { 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 8b6d567f365..2e4ac5693e2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx @@ -1,11 +1,11 @@ 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 { sizeOptimized, sizeRecalled, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice'; import type { ImageWithDims } from 'features/controlLayers/store/types'; import type { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; @@ -29,7 +29,7 @@ type Props = { export const RegionalGuidanceRefImageImage = memo(({ image, onChangeImage, dndTarget, dndTargetData }: Props) => { const { t } = useTranslation(); const store = useAppStore(); - const paramsDispatch = useParamsDispatch(); + const dispatch = useAppDispatch(); const isConnected = useStore($isConnected); const tab = useAppSelector(selectActiveTab); const isStaging = useCanvasIsStaging(); @@ -60,10 +60,10 @@ export const RegionalGuidanceRefImageImage = memo(({ image, onChangeImage, dndTa store.dispatch(bboxSizeRecalled({ width, height })); store.dispatch(bboxSizeOptimized()); } else if (tab === 'generate') { - paramsDispatch(sizeRecalled, { width, height }); - paramsDispatch(sizeOptimized); + dispatch(sizeRecalled({ width, height })); + dispatch(sizeOptimized()); } - }, [paramsDispatch, imageDTO, isStaging, store, tab]); + }, [dispatch, imageDTO, isStaging, store, tab]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/store/actions.ts b/invokeai/frontend/web/src/features/controlLayers/store/actions.ts index 86f1eee5cb6..2198a9cf4c9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/actions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/actions.ts @@ -1,13 +1,10 @@ import { createAction } from '@reduxjs/toolkit'; import type { ParameterModel } from 'features/parameters/types/parameterSchemas'; -import type { ParamsEnrichedPayload } from './types'; - // 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>( - 'params/modelChanged' - ); +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 f3fe19bd0dd..24103b7ca26 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -1,5 +1,5 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; +import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import type { CanvasSettingsState, RgbaColor } from 'features/controlLayers/store/types'; import { RGBA_BLACK, RGBA_WHITE } from 'features/controlLayers/store/types'; @@ -109,6 +109,8 @@ export const canvasSettingsState = createSlice({ }, }); +export const isCanvasSettingsStateAction = isAnyOf(...Object.values(canvasSettingsState.actions)); + export const { settingsClipToBboxChanged, settingsDynamicGridToggled, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index d3718340beb..825daf875e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -1,6 +1,7 @@ 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'; @@ -58,9 +59,13 @@ import { } from 'services/api/types'; import { assert } from 'tsafe'; -import { canvasSettingsState, getInitialCanvasSettings } from './canvasSettingsSlice'; -import { canvasStagingAreaState, getInitialCanvasStagingAreaState } from './canvasStagingAreaSlice'; -import { getInitialInstanceParamsState, instanceParamsState, isInstanceParamsAction } from './paramsSlice'; +import { canvasSettingsState, getInitialCanvasSettings, isCanvasSettingsStateAction } from './canvasSettingsSlice'; +import { + canvasStagingAreaState, + getInitialCanvasStagingAreaState, + isCanvasStagingAreaStateAction, +} from './canvasStagingAreaSlice'; +import { getInitialTabParamsState, isTabParamsStateAction, tabParamsState } from './paramsSlice'; import type { AspectRatioID, BoundingBoxScaleMethod, @@ -134,7 +139,7 @@ const getInitialCanvasInstanceState = (id: string, name: string): CanvasInstance id, name, canvas: getInitialCanvasEntity(), - params: getInitialInstanceParamsState(), + params: getInitialTabParamsState(), settings: getInitialCanvasSettings(), staging: getInitialCanvasStagingAreaState(), }); @@ -183,15 +188,12 @@ const getNextCanvasName = (canvases: CanvasInstanceStateBase[]): string => { } }; -type PayloadWithCanvasId

= P & { canvasId: string }; -type CanvasPayloadAction

= PayloadAction>; - const canvasSlice = createSlice({ name: 'canvas', initialState: getInitialCanvasHistoryState(), reducers: { canvasAdded: { - reducer: (state, action: CanvasPayloadAction<{ isSelected?: boolean }>) => { + reducer: (state, action: PayloadAction<{ canvasId: string; isSelected?: boolean }>) => { const { canvasId, isSelected } = action.payload; const name = getNextCanvasName(Object.values(state.canvases)); @@ -209,7 +211,7 @@ const canvasSlice = createSlice({ }; }, }, - canvasActivated: (state, action: CanvasPayloadAction) => { + canvasActivated: (state, action: PayloadAction<{ canvasId: string }>) => { const { canvasId } = action.payload; const canvas = state.canvases[canvasId]; @@ -219,7 +221,7 @@ const canvasSlice = createSlice({ state.activeCanvasId = canvas.id; }, - canvasDeleted: (state, action: CanvasPayloadAction) => { + canvasDeleted: (state, action: PayloadAction<{ canvasId: string }>) => { const { canvasId } = action.payload; const canvasIds = Object.keys(state.canvases); @@ -241,14 +243,18 @@ const canvasSlice = createSlice({ }, extraReducers(builder) { builder.addDefaultCase((state, action) => { - const canvasId = isCanvasPayloadAction(action) ? action.payload.canvasId : state.activeCanvasId; + const context = extractTabActionContext(action); + + if (!context || context.tab !== 'canvas' || !context.canvasId) { + return; + } - const canvasInstance = state.canvases[canvasId]; + const canvasInstance = state.canvases[context.canvasId]; if (!canvasInstance) { return; } - state.canvases[canvasId] = canvasInstanceState.reducer(canvasInstance, action); + state.canvases[context.canvasId] = canvasInstanceState.reducer(canvasInstance, action); }); }, }); @@ -257,20 +263,26 @@ const canvasInstanceState = createSlice({ name: 'canvasInstance', initialState: {} as CanvasInstanceStateWithHistory, reducers: { - canvasNameChanged: (state, action: CanvasPayloadAction<{ name: string }>) => { + canvasNameChanged: (state, action: PayloadAction<{ canvasId: string; name: string }>) => { const { name } = action.payload; state.name = name; }, }, extraReducers(builder) { - builder.addDefaultCase((state, action) => { - const tab = isInstanceParamsAction(action) ? action.payload.tab : undefined; - - state.canvas = undoableCanvasEntityReducer(state.canvas, action); - state.params = tab === 'canvas' ? instanceParamsState.reducer(state.params, action) : state.params; - state.settings = canvasSettingsState.reducer(state.settings, action); - state.staging = canvasStagingAreaState.reducer(state.staging, action); + builder.addMatcher(isCanvasInstanceAction, (state, action) => { + if (isCanvasEntityStateAction(action)) { + state.canvas = undoableCanvasEntityReducer(state.canvas, action); + } + if (isTabParamsStateAction(action)) { + state.params = tabParamsState.reducer(state.params, action); + } + if (isCanvasSettingsStateAction(action)) { + state.settings = canvasSettingsState.reducer(state.settings, action); + } + if (isCanvasStagingAreaStateAction(action)) { + state.staging = canvasStagingAreaState.reducer(state.staging, action); + } }); }, }); @@ -1848,7 +1860,7 @@ const canvasEntityState = createSlice({ return resetCanvasState(state); }); builder.addCase(modelChanged, (state, action) => { - const { model } = action.payload.value; + const { model } = action.payload; /** * Because the bbox depends in part on the model, it needs to be in sync with the model. However, due to * complications with managing undo/redo history, we need to store the model in a separate slice from the canvas @@ -1921,6 +1933,13 @@ const syncScaledSize = (state: CanvasEntity) => { } }; +export const isCanvasInstanceAction = (action: UnknownAction) => + isCanvasEntityStateAction(action) || + isTabParamsStateAction(action) || + isCanvasSettingsStateAction(action) || + isCanvasStagingAreaStateAction(action); +export const isCanvasEntityStateAction = isAnyOf(...Object.values(canvasEntityState.actions), canvasReset); + export const { // Canvas canvasAdded, @@ -2040,7 +2059,7 @@ const canvasEntityUndoableConfig: UndoableOptions = clearHistoryType: canvasClearHistory.type, filter: (action, _state, _history) => { // Ignore both all actions from other slices and canvas management actions - if (!action.type.startsWith(canvasInstanceState.name)) { + if (!action.type.startsWith(canvasEntityState.name)) { return false; } // Throttle rapid actions of the same type @@ -2053,15 +2072,6 @@ const canvasEntityUndoableConfig: UndoableOptions = const undoableCanvasEntityReducer = undoable(canvasEntityState.reducer, canvasEntityUndoableConfig); -const isCanvasPayloadAction = (action: UnknownAction): action is CanvasPayloadAction => { - return ( - typeof action.payload === 'object' && - action.payload !== null && - 'canvasId' in action.payload && - typeof (action.payload as { canvasId: unknown }).canvasId === 'string' - ); -}; - export const canvasSliceConfig: SliceConfig = { slice: canvasSlice, getInitialState: getInitialCanvasState, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index c415e7e312b..f7843003b51 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -1,5 +1,5 @@ import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSelector, createSlice } 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 { getPrefixedId } from 'features/controlLayers/konva/util'; @@ -44,6 +44,8 @@ export const canvasStagingAreaState = createSlice({ }, }); +export const isCanvasStagingAreaStateAction = isAnyOf(...Object.values(canvasStagingAreaState.actions)); + export const { canvasSessionReset, canvasQueueItemDiscarded } = canvasStagingAreaState.actions; const findCanvasStagingAreaByCanvasId = (state: RootState, canvasId: string) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index daac485cca4..d46b7b7e7b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -1,19 +1,13 @@ -import type { ActionCreatorWithPayload, Selector } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice, isAnyOf } from '@reduxjs/toolkit'; -import type { AppDispatch, RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import type { RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; +import { extractTabActionContext } from 'app/store/util'; 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, - InstanceParamsState, - ParamsPayloadAction, - ParamsState, - RgbaColor, -} from 'features/controlLayers/store/types'; +import type { AspectRatioID, ParamsState, RgbaColor, TabParamsState } from 'features/controlLayers/store/types'; import { ASPECT_RATIO_MAP, CHATGPT_ASPECT_RATIOS, @@ -25,10 +19,9 @@ import { isFluxKontextAspectRatioID, isGemini2_5AspectRatioID, isImagenAspectRatioID, - isParamsTab, MAX_POSITIVE_PROMPT_HISTORY, - zInstanceParamsState, zParamsState, + zTabParamsState, } from 'features/controlLayers/store/types'; import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { @@ -61,8 +54,6 @@ import type { } from 'features/parameters/types/parameterSchemas'; import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import type { TabName } from 'features/ui/store/uiTypes'; -import { useCallback } from 'react'; import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models'; import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; @@ -70,7 +61,7 @@ import { assert } from 'tsafe'; import { modelChanged } from './actions'; import { selectActiveCanvasId } from './selectors'; -export const getInitialInstanceParamsState = (): InstanceParamsState => ({ +export const getInitialTabParamsState = (): TabParamsState => ({ maskBlur: 16, maskBlurMethod: 'box', canvasCoherenceMode: 'Gaussian Blur', @@ -124,9 +115,9 @@ export const getInitialInstanceParamsState = (): InstanceParamsState => ({ const getInitialParamsState = (): ParamsState => ({ _version: 3, - generate: getInitialInstanceParamsState(), - upscaling: getInitialInstanceParamsState(), - video: getInitialInstanceParamsState(), + generate: getInitialTabParamsState(), + upscaling: getInitialTabParamsState(), + video: getInitialTabParamsState(), }); const paramsSlice = createSlice({ @@ -135,145 +126,139 @@ const paramsSlice = createSlice({ reducers: {}, extraReducers(builder) { builder.addDefaultCase((state, action) => { - if (!isInstanceParamsAction(action)) { - return state; - } + const context = extractTabActionContext(action); - const { tab } = action.payload; + if (!context) { + return; + } - switch (tab) { + switch (context.tab) { case 'generate': - return { - ...state, - generate: instanceParamsState.reducer(state.generate, action), - }; + state.generate = tabParamsState.reducer(state.generate, action); + break; case 'upscaling': - return { - ...state, - upscaling: instanceParamsState.reducer(state.upscaling, action), - }; + state.upscaling = tabParamsState.reducer(state.upscaling, action); + break; case 'video': - return { - ...state, - upscaling: instanceParamsState.reducer(state.video, action), - }; + state.video = tabParamsState.reducer(state.video, action); + break; } }); }, }); -export const instanceParamsState = createSlice({ - name: 'params', - initialState: {} as InstanceParamsState, +export const tabParamsState = createSlice({ + name: 'tabParams', + initialState: {} as TabParamsState, reducers: { - setIterations: (state, action: ParamsPayloadAction) => { - state.iterations = action.payload.value; + setIterations: (state, action: PayloadAction) => { + state.iterations = action.payload; }, - setSteps: (state, action: ParamsPayloadAction) => { - state.steps = action.payload.value; + setSteps: (state, action: PayloadAction) => { + state.steps = action.payload; }, - setCfgScale: (state, action: ParamsPayloadAction) => { - state.cfgScale = action.payload.value; + setCfgScale: (state, action: PayloadAction) => { + state.cfgScale = action.payload; }, - setUpscaleCfgScale: (state, action: ParamsPayloadAction) => { - state.upscaleCfgScale = action.payload.value; + setUpscaleCfgScale: (state, action: PayloadAction) => { + state.upscaleCfgScale = action.payload; }, - setGuidance: (state, action: ParamsPayloadAction) => { - state.guidance = action.payload.value; + setGuidance: (state, action: PayloadAction) => { + state.guidance = action.payload; }, - setCfgRescaleMultiplier: (state, action: ParamsPayloadAction) => { - state.cfgRescaleMultiplier = action.payload.value; + setCfgRescaleMultiplier: (state, action: PayloadAction) => { + state.cfgRescaleMultiplier = action.payload; }, - setScheduler: (state, action: ParamsPayloadAction) => { - state.scheduler = action.payload.value; + setScheduler: (state, action: PayloadAction) => { + state.scheduler = action.payload; }, - setUpscaleScheduler: (state, action: ParamsPayloadAction) => { - state.upscaleScheduler = action.payload.value; + setUpscaleScheduler: (state, action: PayloadAction) => { + state.upscaleScheduler = action.payload; }, - setSeed: (state, action: ParamsPayloadAction) => { - state.seed = action.payload.value; + setSeed: (state, action: PayloadAction) => { + state.seed = action.payload; state.shouldRandomizeSeed = false; }, - setImg2imgStrength: (state, action: ParamsPayloadAction) => { - state.img2imgStrength = action.payload.value; + setImg2imgStrength: (state, action: PayloadAction) => { + state.img2imgStrength = action.payload; }, - setOptimizedDenoisingEnabled: (state, action: ParamsPayloadAction) => { - state.optimizedDenoisingEnabled = action.payload.value; + setOptimizedDenoisingEnabled: (state, action: PayloadAction) => { + state.optimizedDenoisingEnabled = action.payload; }, - setSeamlessXAxis: (state, action: ParamsPayloadAction) => { - state.seamlessXAxis = action.payload.value; + setSeamlessXAxis: (state, action: PayloadAction) => { + state.seamlessXAxis = action.payload; }, - setSeamlessYAxis: (state, action: ParamsPayloadAction) => { - state.seamlessYAxis = action.payload.value; + setSeamlessYAxis: (state, action: PayloadAction) => { + state.seamlessYAxis = action.payload; }, - setShouldRandomizeSeed: (state, action: ParamsPayloadAction) => { - state.shouldRandomizeSeed = action.payload.value; + setShouldRandomizeSeed: (state, action: PayloadAction) => { + state.shouldRandomizeSeed = action.payload; }, - vaeSelected: (state, action: ParamsPayloadAction) => { + vaeSelected: (state, action: PayloadAction) => { // null is a valid VAE! - const result = zInstanceParamsState.shape.vae.safeParse(action.payload); + const result = zTabParamsState.shape.vae.safeParse(action.payload); if (!result.success) { return; } state.vae = result.data; }, - fluxVAESelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.fluxVAE.safeParse(action.payload); + fluxVAESelected: (state, action: PayloadAction) => { + const result = zTabParamsState.shape.fluxVAE.safeParse(action.payload); if (!result.success) { return; } state.fluxVAE = result.data; }, - t5EncoderModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.t5EncoderModel.safeParse(action.payload); + t5EncoderModelSelected: (state, action: PayloadAction) => { + const result = zTabParamsState.shape.t5EncoderModel.safeParse(action.payload); if (!result.success) { return; } state.t5EncoderModel = result.data; }, - controlLoRAModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.controlLora.safeParse(action.payload); + controlLoRAModelSelected: (state, action: PayloadAction) => { + const result = zTabParamsState.shape.controlLora.safeParse(action.payload); if (!result.success) { return; } state.controlLora = result.data; }, - clipEmbedModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.clipEmbedModel.safeParse(action.payload); + clipEmbedModelSelected: (state, action: PayloadAction) => { + const result = zTabParamsState.shape.clipEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipEmbedModel = result.data; }, - clipLEmbedModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.clipLEmbedModel.safeParse(action.payload); + clipLEmbedModelSelected: (state, action: PayloadAction) => { + const result = zTabParamsState.shape.clipLEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipLEmbedModel = result.data; }, - clipGEmbedModelSelected: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.clipGEmbedModel.safeParse(action.payload); + clipGEmbedModelSelected: (state, action: PayloadAction) => { + const result = zTabParamsState.shape.clipGEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipGEmbedModel = result.data; }, - vaePrecisionChanged: (state, action: ParamsPayloadAction) => { - state.vaePrecision = action.payload.value; + vaePrecisionChanged: (state, action: PayloadAction) => { + state.vaePrecision = action.payload; }, - setClipSkip: (state, action: ParamsPayloadAction) => { - applyClipSkip(state, state.model, action.payload.value); + setClipSkip: (state, action: PayloadAction) => { + applyClipSkip(state, state.model, action.payload); }, - shouldUseCpuNoiseChanged: (state, action: ParamsPayloadAction) => { - state.shouldUseCpuNoise = action.payload.value; + shouldUseCpuNoiseChanged: (state, action: PayloadAction) => { + state.shouldUseCpuNoise = action.payload; }, - positivePromptChanged: (state, action: ParamsPayloadAction) => { - state.positivePrompt = action.payload.value; + positivePromptChanged: (state, action: PayloadAction) => { + state.positivePrompt = action.payload; }, - positivePromptAddedToHistory: (state, action: ParamsPayloadAction) => { - const prompt = action.payload.value.trim(); + positivePromptAddedToHistory: (state, action: PayloadAction) => { + const prompt = action.payload.trim(); if (prompt.length === 0) { return; } @@ -284,68 +269,68 @@ export const instanceParamsState = createSlice({ state.positivePromptHistory = state.positivePromptHistory.slice(0, MAX_POSITIVE_PROMPT_HISTORY); } }, - promptRemovedFromHistory: (state, action: ParamsPayloadAction) => { - state.positivePromptHistory = state.positivePromptHistory.filter((p) => p !== action.payload.value); + promptRemovedFromHistory: (state, action: PayloadAction) => { + state.positivePromptHistory = state.positivePromptHistory.filter((p) => p !== action.payload); }, - promptHistoryCleared: (state, _action: ParamsPayloadAction) => { + promptHistoryCleared: (state) => { state.positivePromptHistory = []; }, - negativePromptChanged: (state, action: ParamsPayloadAction) => { - state.negativePrompt = action.payload.value; + negativePromptChanged: (state, action: PayloadAction) => { + state.negativePrompt = action.payload; }, - refinerModelChanged: (state, action: ParamsPayloadAction) => { - const result = zInstanceParamsState.shape.refinerModel.safeParse(action.payload); + refinerModelChanged: (state, action: PayloadAction) => { + const result = zTabParamsState.shape.refinerModel.safeParse(action.payload); if (!result.success) { return; } state.refinerModel = result.data; }, - setRefinerSteps: (state, action: ParamsPayloadAction) => { - state.refinerSteps = action.payload.value; + setRefinerSteps: (state, action: PayloadAction) => { + state.refinerSteps = action.payload; }, - setRefinerCFGScale: (state, action: ParamsPayloadAction) => { - state.refinerCFGScale = action.payload.value; + setRefinerCFGScale: (state, action: PayloadAction) => { + state.refinerCFGScale = action.payload; }, - setRefinerScheduler: (state, action: ParamsPayloadAction) => { - state.refinerScheduler = action.payload.value; + setRefinerScheduler: (state, action: PayloadAction) => { + state.refinerScheduler = action.payload; }, - setRefinerPositiveAestheticScore: (state, action: ParamsPayloadAction) => { - state.refinerPositiveAestheticScore = action.payload.value; + setRefinerPositiveAestheticScore: (state, action: PayloadAction) => { + state.refinerPositiveAestheticScore = action.payload; }, - setRefinerNegativeAestheticScore: (state, action: ParamsPayloadAction) => { - state.refinerNegativeAestheticScore = action.payload.value; + setRefinerNegativeAestheticScore: (state, action: PayloadAction) => { + state.refinerNegativeAestheticScore = action.payload; }, - setRefinerStart: (state, action: ParamsPayloadAction) => { - state.refinerStart = action.payload.value; + setRefinerStart: (state, action: PayloadAction) => { + state.refinerStart = action.payload; }, - setInfillMethod: (state, action: ParamsPayloadAction) => { - state.infillMethod = action.payload.value; + setInfillMethod: (state, action: PayloadAction) => { + state.infillMethod = action.payload; }, - setInfillTileSize: (state, action: ParamsPayloadAction) => { - state.infillTileSize = action.payload.value; + setInfillTileSize: (state, action: PayloadAction) => { + state.infillTileSize = action.payload; }, - setInfillPatchmatchDownscaleSize: (state, action: ParamsPayloadAction) => { - state.infillPatchmatchDownscaleSize = action.payload.value; + setInfillPatchmatchDownscaleSize: (state, action: PayloadAction) => { + state.infillPatchmatchDownscaleSize = action.payload; }, - setInfillColorValue: (state, action: ParamsPayloadAction) => { - state.infillColorValue = action.payload.value; + setInfillColorValue: (state, action: PayloadAction) => { + state.infillColorValue = action.payload; }, - setMaskBlur: (state, action: ParamsPayloadAction) => { - state.maskBlur = action.payload.value; + setMaskBlur: (state, action: PayloadAction) => { + state.maskBlur = action.payload; }, - setCanvasCoherenceMode: (state, action: ParamsPayloadAction) => { - state.canvasCoherenceMode = action.payload.value; + setCanvasCoherenceMode: (state, action: PayloadAction) => { + state.canvasCoherenceMode = action.payload; }, - setCanvasCoherenceEdgeSize: (state, action: ParamsPayloadAction) => { - state.canvasCoherenceEdgeSize = action.payload.value; + setCanvasCoherenceEdgeSize: (state, action: PayloadAction) => { + state.canvasCoherenceEdgeSize = action.payload; }, - setCanvasCoherenceMinDenoise: (state, action: ParamsPayloadAction) => { - state.canvasCoherenceMinDenoise = action.payload.value; + setCanvasCoherenceMinDenoise: (state, action: PayloadAction) => { + state.canvasCoherenceMinDenoise = action.payload; }, //#region Dimensions - sizeRecalled: (state, action: ParamsPayloadAction<{ width: number; height: number }>) => { - const { width, height } = action.payload.value; + sizeRecalled: (state, action: PayloadAction<{ width: number; height: number }>) => { + const { width, height } = action.payload; const gridSize = getGridSize(state.model?.base); state.dimensions.width = Math.max(roundDownToMultiple(width, gridSize), 64); state.dimensions.height = Math.max(roundDownToMultiple(height, gridSize), 64); @@ -353,11 +338,8 @@ export const instanceParamsState = createSlice({ state.dimensions.aspectRatio.id = 'Free'; state.dimensions.aspectRatio.isLocked = true; }, - widthChanged: ( - state, - action: ParamsPayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }> - ) => { - const { width, updateAspectRatio, clamp } = action.payload.value; + widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { + const { width, updateAspectRatio, clamp } = action.payload; const gridSize = getGridSize(state.model?.base); state.dimensions.width = clamp ? Math.max(roundDownToMultiple(width, gridSize), 64) : width; @@ -374,11 +356,8 @@ export const instanceParamsState = createSlice({ state.dimensions.aspectRatio.isLocked = false; } }, - heightChanged: ( - state, - action: ParamsPayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }> - ) => { - const { height, updateAspectRatio, clamp } = action.payload.value; + heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { + const { height, updateAspectRatio, clamp } = action.payload; const gridSize = getGridSize(state.model?.base); state.dimensions.height = clamp ? Math.max(roundDownToMultiple(height, gridSize), 64) : height; @@ -395,11 +374,11 @@ export const instanceParamsState = createSlice({ state.dimensions.aspectRatio.isLocked = false; } }, - aspectRatioLockToggled: (state, _action: ParamsPayloadAction) => { + aspectRatioLockToggled: (state) => { state.dimensions.aspectRatio.isLocked = !state.dimensions.aspectRatio.isLocked; }, - aspectRatioIdChanged: (state, action: ParamsPayloadAction<{ id: AspectRatioID }>) => { - const { id } = action.payload.value; + aspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => { + const { id } = action.payload; state.dimensions.aspectRatio.id = id; if (id === 'Free') { state.dimensions.aspectRatio.isLocked = false; @@ -439,7 +418,7 @@ export const instanceParamsState = createSlice({ state.dimensions.height = height; } }, - dimensionsSwapped: (state, _action: ParamsPayloadAction) => { + dimensionsSwapped: (state) => { state.dimensions.aspectRatio.value = 1 / state.dimensions.aspectRatio.value; if (state.dimensions.aspectRatio.id === 'Free') { const newWidth = state.dimensions.height; @@ -457,7 +436,7 @@ export const instanceParamsState = createSlice({ state.dimensions.aspectRatio.id = ASPECT_RATIO_MAP[state.dimensions.aspectRatio.id].inverseID; } }, - sizeOptimized: (state, _action: ParamsPayloadAction) => { + sizeOptimized: (state) => { const optimalDimension = getOptimalDimension(state.model?.base); if (state.dimensions.aspectRatio.isLocked) { const { width, height } = calculateNewSize( @@ -473,7 +452,7 @@ export const instanceParamsState = createSlice({ state.dimensions.height = optimalDimension; } }, - syncedToOptimalDimension: (state, _action: ParamsPayloadAction) => { + syncedToOptimalDimension: (state) => { const optimalDimension = getOptimalDimension(state.model?.base); if (!getIsSizeOptimal(state.dimensions.width, state.dimensions.height, state.model?.base)) { @@ -486,12 +465,12 @@ export const instanceParamsState = createSlice({ state.dimensions.height = bboxDims.height; } }, - paramsReset: (state, _action: ParamsPayloadAction) => resetState(state), + paramsReset: (state) => resetState(state), }, extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { - const { previousModel } = action.payload.value; - const result = zInstanceParamsState.shape.model.safeParse(action.payload.value.model); + const { previousModel } = action.payload; + const result = zTabParamsState.shape.model.safeParse(action.payload.model); if (!result.success) { return; } @@ -543,11 +522,11 @@ const getModelMaxClipSkip = (model: ParameterModel) => { return CLIP_SKIP_MAP[model.base]?.maxClip; }; -const resetState = (state: InstanceParamsState): InstanceParamsState => { +const resetState = (state: TabParamsState): TabParamsState => { // When a new session is requested, we need to keep the current model selections, plus dependent state // like VAE precision. Everything else gets reset to default. const oldState = deepClone(state); - const newState = getInitialInstanceParamsState(); + const newState = getInitialTabParamsState(); newState.dimensions = oldState.dimensions; newState.model = oldState.model; newState.vae = oldState.vae; @@ -559,6 +538,8 @@ const resetState = (state: InstanceParamsState): InstanceParamsState => { return newState; }; +export const isTabParamsStateAction = isAnyOf(...Object.values(tabParamsState.actions), modelChanged); + export const { setInfillMethod, setInfillTileSize, @@ -615,64 +596,7 @@ export const { syncedToOptimalDimension, paramsReset, -} = instanceParamsState.actions; - -const instanceParamsActions = { ...instanceParamsState.actions, modelChanged }; - -type InstanceParamsAction = typeof instanceParamsActions; -type InstanceParamsActionCreator = InstanceParamsAction[keyof InstanceParamsAction]; -type PayloadOf = AC extends ActionCreatorWithPayload ? P : never; -type ValueOf = PayloadOf extends { value: infer V } ? V : never; - -export const useParamsDispatch = () => { - const dispatch = useAppDispatch(); - const activeTab = useAppSelector(selectActiveTab); - const activeCanvasId = useAppSelector(selectActiveCanvasId); - - const paramsDispatch = ( - actionCreator: AC, - ...rest: ValueOf extends never ? [] : [value: ValueOf] - ): void => { - const value = rest[0]; - - dispatchParamsAction(actionCreator, activeTab, activeCanvasId, value, dispatch); - }; - - return useCallback(paramsDispatch, [activeTab, activeCanvasId, dispatch]); -}; - -export const toStore = (state: RootState, dispatch: AppDispatch) => ({ getState: () => state, dispatch }); - -export const paramsDispatch = ( - store: { getState: () => RootState; dispatch: AppDispatch }, - actionCreator: AC, - ...rest: ValueOf extends never ? [] : [value: ValueOf] -): void => { - const state = store.getState(); - const activeTab = selectActiveTab(state); - const activeCanvasId = selectActiveCanvasId(state); - const value = rest[0]; - - dispatchParamsAction(actionCreator, activeTab, activeCanvasId, value, store.dispatch); -}; - -const dispatchParamsAction = ( - actionCreator: AC, - tab: TabName, - canvasId: string, - value: ValueOf | undefined, - dispatch: AppDispatch -) => { - if (isParamsTab(tab)) { - // Type information simplified to help TS compiler cope with too complex type - const payload = value !== undefined ? { tab, canvasId, value } : ({ tab, canvasId } as unknown); - const action = (actionCreator as ActionCreatorWithPayload)(payload); - - dispatch(action); - } -}; - -export const isInstanceParamsAction = isAnyOf(...Object.values(instanceParamsActions)); +} = tabParamsState.actions; export const paramsSliceConfig: SliceConfig = { slice: paramsSlice, @@ -710,7 +634,7 @@ export const paramsSliceConfig: SliceConfig = { }, }; -const initialInstanceParamsState = getInitialInstanceParamsState(); +const initialTabParamsState = getInitialTabParamsState(); export const selectActiveParams = (state: RootState) => { const tab = selectActiveTab(state); @@ -731,11 +655,11 @@ export const selectActiveParams = (state: RootState) => { } // Fallback for global controls - return initialInstanceParamsState; + return initialTabParamsState; }; const buildActiveParamsSelector = - (selector: Selector) => + (selector: Selector) => (state: RootState) => selector(selectActiveParams(state)); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 359a3cdc879..69b612ec3b9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,4 +1,3 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common'; @@ -25,7 +24,6 @@ import { zParameterT5EncoderModel, zParameterVAEModel, } from 'features/parameters/types/parameterSchemas'; -import type { TabName } from 'features/ui/store/uiTypes'; import type { JsonObject } from 'type-fest'; import { z } from 'zod'; @@ -685,15 +683,7 @@ const zPositivePromptHistory = z .array(zParameterPositivePrompt) .transform((arr) => arr.slice(0, MAX_POSITIVE_PROMPT_HISTORY)); -type EnrichedPayload = P extends undefined ? T : T & { value: P }; - -export const isParamsTab = (tab: TabName) => - tab === 'generate' || tab === 'canvas' || tab === 'upscaling' || tab === 'video'; -type ParamsTabName = 'generate' | 'canvas' | 'upscaling' | 'video'; -export type ParamsEnrichedPayload

= EnrichedPayload<{ tab: ParamsTabName; canvasId: string }, P>; -export type ParamsPayloadAction

= PayloadAction>; - -export const zInstanceParamsState = z.object({ +export const zTabParamsState = z.object({ maskBlur: z.number(), maskBlurMethod: zParameterMaskBlurMethod, canvasCoherenceMode: zParameterCanvasCoherenceMode, @@ -740,12 +730,12 @@ export const zInstanceParamsState = z.object({ controlLora: zParameterControlLoRAModel.nullable(), dimensions: zDimensionsState, }); -export type InstanceParamsState = z.infer; +export type TabParamsState = z.infer; export const zParamsState = z.object({ _version: z.literal(3), - generate: zInstanceParamsState, - upscaling: zInstanceParamsState, - video: zInstanceParamsState, + generate: zTabParamsState, + upscaling: zTabParamsState, + video: zTabParamsState, }); export type ParamsState = z.infer; @@ -890,7 +880,7 @@ const zCanvasInstanceStateBase = z.object({ const zCanvasInstanceState = (canvasEntitySchema: T) => zCanvasInstanceStateBase.extend({ canvas: canvasEntitySchema, - params: zInstanceParamsState, + params: zTabParamsState, settings: zCanvasSettingsState, staging: zCanvasStagingAreaState, }); diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index c3c7f45e07a..c1d9ecb138b 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -10,7 +10,6 @@ import { loraAllDeleted, loraRecalled } from 'features/controlLayers/store/loras import { heightChanged, negativePromptChanged, - paramsDispatch, positivePromptChanged, refinerModelChanged, selectBase, @@ -277,7 +276,7 @@ const PositivePrompt: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, positivePromptChanged, value); + store.dispatch(positivePromptChanged(value)); }, i18nKey: 'metadata.positivePrompt', LabelComponent: MetadataLabel, @@ -297,7 +296,7 @@ const NegativePrompt: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, negativePromptChanged, value); + store.dispatch(negativePromptChanged(value)); }, i18nKey: 'metadata.negativePrompt', LabelComponent: MetadataLabel, @@ -317,7 +316,7 @@ const CFGScale: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setCfgScale, value); + store.dispatch(setCfgScale(value)); }, i18nKey: 'metadata.cfgScale', LabelComponent: MetadataLabel, @@ -335,7 +334,7 @@ const CFGRescaleMultiplier: SingleMetadataHandler return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setCfgRescaleMultiplier, value); + store.dispatch(setCfgRescaleMultiplier(value)); }, i18nKey: 'metadata.cfgRescaleMultiplier', LabelComponent: MetadataLabel, @@ -355,7 +354,7 @@ const CLIPSkip: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setClipSkip, value); + store.dispatch(setClipSkip(value)); }, i18nKey: 'metadata.clipSkip', LabelComponent: MetadataLabel, @@ -373,7 +372,7 @@ const Guidance: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setGuidance, value); + store.dispatch(setGuidance(value)); }, i18nKey: 'metadata.guidance', LabelComponent: MetadataLabel, @@ -391,7 +390,7 @@ const Scheduler: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setScheduler, value); + store.dispatch(setScheduler(value)); }, i18nKey: 'metadata.scheduler', LabelComponent: MetadataLabel, @@ -413,7 +412,7 @@ const Width: SingleMetadataHandler = { if (activeTab === 'canvas') { store.dispatch(bboxWidthChanged({ width: value, updateAspectRatio: true, clamp: true })); } else if (activeTab === 'generate') { - paramsDispatch(store, widthChanged, { width: value, updateAspectRatio: true, clamp: true }); + store.dispatch(widthChanged({ width: value, updateAspectRatio: true, clamp: true })); } }, i18nKey: 'metadata.width', @@ -436,7 +435,7 @@ const Height: SingleMetadataHandler = { if (activeTab === 'canvas') { store.dispatch(bboxHeightChanged({ height: value, updateAspectRatio: true, clamp: true })); } else if (activeTab === 'generate') { - paramsDispatch(store, heightChanged, { height: value, updateAspectRatio: true, clamp: true }); + store.dispatch(heightChanged({ height: value, updateAspectRatio: true, clamp: true })); } }, i18nKey: 'metadata.height', @@ -455,7 +454,7 @@ const Seed: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setSeed, value); + store.dispatch(setSeed(value)); }, i18nKey: 'metadata.seed', LabelComponent: MetadataLabel, @@ -473,7 +472,7 @@ const Steps: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setSteps, value); + store.dispatch(setSteps(value)); }, i18nKey: 'metadata.steps', LabelComponent: MetadataLabel, @@ -491,7 +490,7 @@ const DenoisingStrength: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setImg2imgStrength, value); + store.dispatch(setImg2imgStrength(value)); }, i18nKey: 'metadata.strength', LabelComponent: MetadataLabel, @@ -509,7 +508,7 @@ const SeamlessX: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setSeamlessXAxis, value); + store.dispatch(setSeamlessXAxis(value)); }, i18nKey: 'metadata.seamlessXAxis', LabelComponent: MetadataLabel, @@ -527,7 +526,7 @@ const SeamlessY: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setSeamlessYAxis, value); + store.dispatch(setSeamlessYAxis(value)); }, i18nKey: 'metadata.seamlessYAxis', LabelComponent: MetadataLabel, @@ -548,7 +547,7 @@ const RefinerModel: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, refinerModelChanged, value); + store.dispatch(refinerModelChanged(value)); }, i18nKey: 'sdxl.refinermodel', LabelComponent: MetadataLabel, @@ -568,7 +567,7 @@ const RefinerSteps: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setRefinerSteps, value); + store.dispatch(setRefinerSteps(value)); }, i18nKey: 'sdxl.refinerSteps', LabelComponent: MetadataLabel, @@ -586,7 +585,7 @@ const RefinerCFGScale: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setRefinerCFGScale, value); + store.dispatch(setRefinerCFGScale(value)); }, i18nKey: 'sdxl.cfgScale', LabelComponent: MetadataLabel, @@ -604,7 +603,7 @@ const RefinerScheduler: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setRefinerScheduler, value); + store.dispatch(setRefinerScheduler(value)); }, i18nKey: 'sdxl.scheduler', LabelComponent: MetadataLabel, @@ -622,7 +621,7 @@ const RefinerPositiveAestheticScore: SingleMetadataHandler { - paramsDispatch(store, setRefinerPositiveAestheticScore, value); + store.dispatch(setRefinerPositiveAestheticScore(value)); }, i18nKey: 'sdxl.posAestheticScore', LabelComponent: MetadataLabel, @@ -642,7 +641,7 @@ const RefinerNegativeAestheticScore: SingleMetadataHandler { - paramsDispatch(store, setRefinerNegativeAestheticScore, value); + store.dispatch(setRefinerNegativeAestheticScore(value)); }, i18nKey: 'sdxl.negAestheticScore', LabelComponent: MetadataLabel, @@ -662,7 +661,7 @@ const RefinerDenoisingStart: SingleMetadataHandler = return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, setRefinerStart, value); + store.dispatch(setRefinerStart(value)); }, i18nKey: 'sdxl.refinerStart', LabelComponent: MetadataLabel, @@ -705,7 +704,7 @@ const VAEModel: SingleMetadataHandler = { return Promise.resolve(parsed); }, recall: (value, store) => { - paramsDispatch(store, vaeSelected, value); + store.dispatch(vaeSelected(value)); }, i18nKey: 'metadata.vae', LabelComponent: MetadataLabel, 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 290e2c39a62..ab38dbfd398 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -12,7 +12,7 @@ import { selectRefinerStart, } from 'features/controlLayers/store/paramsSlice'; import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; -import type { InstanceParamsState } from 'features/controlLayers/store/types'; +import type { TabParamsState } 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'; @@ -158,7 +158,7 @@ export const getOriginalAndScaledSizesForOtherModes = (state: RootState) => { export const getInfill = ( g: Graph, - params: InstanceParamsState + params: TabParamsState ): Invocation<'infill_patchmatch' | 'infill_cv2' | 'infill_lama' | 'infill_rgba' | 'infill_tile'> => { const { infillMethod, infillColorValue, infillPatchmatchDownscaleSize, infillTileSize } = params; 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 1d10cd0e2dc..519898f6fb5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx @@ -1,11 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { - selectCFGRescaleMultiplier, - setCfgRescaleMultiplier, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { selectCFGRescaleMultiplier, setCfgRescaleMultiplier } from 'features/controlLayers/store/paramsSlice'; import { selectCFGRescaleMultiplierConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,11 +9,11 @@ import { useTranslation } from 'react-i18next'; const ParamCFGRescaleMultiplier = () => { const cfgRescaleMultiplier = useAppSelector(selectCFGRescaleMultiplier); const config = useAppSelector(selectCFGRescaleMultiplierConfig); + const dispatch = useAppDispatch(); - const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); - const handleChange = useCallback((v: number) => dispatchParams(setCfgRescaleMultiplier, v), [dispatchParams]); + const handleChange = useCallback((v: number) => dispatch(setCfgRescaleMultiplier(v)), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx index 13703a14931..b8480d6b2cf 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPEmbedModelSelect.tsx @@ -1,11 +1,7 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { - clipEmbedModelSelected, - selectCLIPEmbedModel, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { clipEmbedModelSelected, selectCLIPEmbedModel } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,7 +9,7 @@ import { useCLIPEmbedModels } from 'services/api/hooks/modelsByType'; import type { CLIPEmbedModelConfig } from 'services/api/types'; const ParamCLIPEmbedModelSelect = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const clipEmbedModel = useAppSelector(selectCLIPEmbedModel); const [modelConfigs, { isLoading }] = useCLIPEmbedModels(); @@ -21,10 +17,10 @@ const ParamCLIPEmbedModelSelect = () => { const _onChange = useCallback( (clipEmbedModel: CLIPEmbedModelConfig | null) => { if (clipEmbedModel) { - dispatchParams(clipEmbedModelSelected, zModelIdentifierField.parse(clipEmbedModel)); + dispatch(clipEmbedModelSelected(zModelIdentifierField.parse(clipEmbedModel))); } }, - [dispatchParams] + [dispatch] ); const { options, value, onChange, noOptionsMessage } = useModelCombobox({ diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx index af0122a2f1b..0d63512a410 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPGEmbedModelSelect.tsx @@ -1,11 +1,7 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { - clipGEmbedModelSelected, - selectCLIPGEmbedModel, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { clipGEmbedModelSelected, selectCLIPGEmbedModel } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -14,7 +10,7 @@ import type { CLIPGEmbedModelConfig } from 'services/api/types'; import { isCLIPGEmbedModelConfig } from 'services/api/types'; const ParamCLIPEmbedModelSelect = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const clipEmbedModel = useAppSelector(selectCLIPGEmbedModel); const [modelConfigs, { isLoading }] = useCLIPEmbedModels(); @@ -22,10 +18,10 @@ const ParamCLIPEmbedModelSelect = () => { const _onChange = useCallback( (clipEmbedModel: CLIPGEmbedModelConfig | null) => { if (clipEmbedModel) { - dispatchParams(clipGEmbedModelSelected, zModelIdentifierField.parse(clipEmbedModel)); + dispatch(clipGEmbedModelSelected(zModelIdentifierField.parse(clipEmbedModel))); } }, - [dispatchParams] + [dispatch] ); const { options, value, onChange, noOptionsMessage } = useModelCombobox({ diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx index 5626d7a954a..f0a038d510d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCLIPLEmbedModelSelect.tsx @@ -1,11 +1,7 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { - clipLEmbedModelSelected, - selectCLIPLEmbedModel, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { clipLEmbedModelSelected, selectCLIPLEmbedModel } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -14,7 +10,7 @@ import type { CLIPLEmbedModelConfig } from 'services/api/types'; import { isCLIPLEmbedModelConfig } from 'services/api/types'; const ParamCLIPEmbedModelSelect = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const clipEmbedModel = useAppSelector(selectCLIPLEmbedModel); const [modelConfigs, { isLoading }] = useCLIPEmbedModels(); @@ -22,10 +18,10 @@ const ParamCLIPEmbedModelSelect = () => { const _onChange = useCallback( (clipEmbedModel: CLIPLEmbedModelConfig | null) => { if (clipEmbedModel) { - dispatchParams(clipLEmbedModelSelected, zModelIdentifierField.parse(clipEmbedModel)); + dispatch(clipLEmbedModelSelected(zModelIdentifierField.parse(clipEmbedModel))); } }, - [dispatchParams] + [dispatch] ); const { options, value, onChange, noOptionsMessage } = useModelCombobox({ 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 4bdbf9b3a8e..e53d7c34b1a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectCLIPSkip, selectModel, setClipSkip, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectCLIPSkip, selectModel, setClipSkip } from 'features/controlLayers/store/paramsSlice'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; import { selectCLIPSkipConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; @@ -11,15 +11,15 @@ const ParamClipSkip = () => { const clipSkip = useAppSelector(selectCLIPSkip); const config = useAppSelector(selectCLIPSkipConfig); const model = useAppSelector(selectModel); + const dispatch = useAppDispatch(); - const dispatchParams = useParamsDispatch(); const { t } = useTranslation(); const handleClipSkipChange = useCallback( (v: number) => { - dispatchParams(setClipSkip, v); + dispatch(setClipSkip(v)); }, - [dispatchParams] + [dispatch] ); const max = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamOptimizedDenoisingToggle.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamOptimizedDenoisingToggle.tsx index 99e60321aa7..7bfb62fc159 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamOptimizedDenoisingToggle.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamOptimizedDenoisingToggle.tsx @@ -1,10 +1,9 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { selectOptimizedDenoisingEnabled, setOptimizedDenoisingEnabled, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; @@ -12,13 +11,13 @@ import { useTranslation } from 'react-i18next'; export const ParamOptimizedDenoisingToggle = memo(() => { const optimizedDenoisingEnabled = useAppSelector(selectOptimizedDenoisingEnabled); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const onChange = useCallback( (event: ChangeEvent) => { - dispatchParams(setOptimizedDenoisingEnabled, event.target.checked); + dispatch(setOptimizedDenoisingEnabled(event.target.checked)); }, - [dispatchParams] + [dispatch] ); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx index da6309c2f2a..8501e77cd4b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamT5EncoderModelSelect.tsx @@ -1,11 +1,7 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { - selectT5EncoderModel, - t5EncoderModelSelected, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { selectT5EncoderModel, t5EncoderModelSelected } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,7 +9,7 @@ import { useT5EncoderModels } from 'services/api/hooks/modelsByType'; import type { T5EncoderBnbQuantizedLlmInt8bModelConfig, T5EncoderModelConfig } from 'services/api/types'; const ParamT5EncoderModelSelect = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const t5EncoderModel = useAppSelector(selectT5EncoderModel); const [modelConfigs, { isLoading }] = useT5EncoderModels(); @@ -21,10 +17,10 @@ const ParamT5EncoderModelSelect = () => { const _onChange = useCallback( (t5EncoderModel: T5EncoderBnbQuantizedLlmInt8bModelConfig | T5EncoderModelConfig | null) => { if (t5EncoderModel) { - dispatchParams(t5EncoderModelSelected, zModelIdentifierField.parse(t5EncoderModel)); + dispatch(t5EncoderModelSelected(zModelIdentifierField.parse(t5EncoderModel))); } }, - [dispatchParams] + [dispatch] ); const { options, value, onChange, noOptionsMessage } = useModelCombobox({ diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx index 499aa1255ae..007b2b04887 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx @@ -1,17 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { - selectCanvasCoherenceEdgeSize, - setCanvasCoherenceEdgeSize, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasCoherenceEdgeSize, setCanvasCoherenceEdgeSize } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasCoherenceEdgeSizeConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceEdgeSize = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const canvasCoherenceEdgeSize = useAppSelector(selectCanvasCoherenceEdgeSize); const config = useAppSelector(selectCanvasCoherenceEdgeSizeConfig); @@ -19,9 +15,9 @@ const ParamCanvasCoherenceEdgeSize = () => { const handleChange = useCallback( (v: number) => { - dispatchParams(setCanvasCoherenceEdgeSize, v); + dispatch(setCanvasCoherenceEdgeSize(v)); }, - [dispatchParams] + [dispatch] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx index 272c107c5ff..eb9047fbf52 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx @@ -1,24 +1,23 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { selectCanvasCoherenceMinDenoise, setCanvasCoherenceMinDenoise, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceMinDenoise = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const canvasCoherenceMinDenoise = useAppSelector(selectCanvasCoherenceMinDenoise); const { t } = useTranslation(); const handleChange = useCallback( (v: number) => { - dispatchParams(setCanvasCoherenceMinDenoise, v); + dispatch(setCanvasCoherenceMinDenoise(v)); }, - [dispatchParams] + [dispatch] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx index 5150342142d..c6b2084e3a3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx @@ -1,18 +1,14 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { - selectCanvasCoherenceMode, - setCanvasCoherenceMode, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasCoherenceMode, setCanvasCoherenceMode } from 'features/controlLayers/store/paramsSlice'; import { isParameterCanvasCoherenceMode } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceMode = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const canvasCoherenceMode = useAppSelector(selectCanvasCoherenceMode); const { t } = useTranslation(); @@ -31,9 +27,9 @@ const ParamCanvasCoherenceMode = () => { return; } - dispatchParams(setCanvasCoherenceMode, v.value); + dispatch(setCanvasCoherenceMode(v.value)); }, - [dispatchParams] + [dispatch] ); const value = useMemo(() => options.find((o) => o.value === canvasCoherenceMode), [canvasCoherenceMode, options]); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx index 045d3ef55f3..a165388fdcd 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx @@ -1,22 +1,22 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectMaskBlur, setMaskBlur, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectMaskBlur, setMaskBlur } from 'features/controlLayers/store/paramsSlice'; import { selectMaskBlurConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamMaskBlur = () => { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const maskBlur = useAppSelector(selectMaskBlur); const config = useAppSelector(selectMaskBlurConfig); const handleChange = useCallback( (v: number) => { - dispatchParams(setMaskBlur, v); + dispatch(setMaskBlur(v)); }, - [dispatchParams] + [dispatch] ); return ( 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 0abb8067ed3..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 @@ -1,19 +1,17 @@ import { Box, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbaColorPicker from 'common/components/ColorPicker/RgbaColorPicker'; import { selectInfillColorValue, selectInfillMethod, setInfillColorValue, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import type { RgbaColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; const ParamInfillColorOptions = () => { - const dispatchParams = useParamsDispatch(); - + const dispatch = useAppDispatch(); const infillColor = useAppSelector(selectInfillColorValue); const infillMethod = useAppSelector(selectInfillMethod); @@ -21,9 +19,9 @@ const ParamInfillColorOptions = () => { const handleInfillColor = useCallback( (v: RgbaColor) => { - dispatchParams(setInfillColorValue, v); + dispatch(setInfillColorValue(v)); }, - [dispatchParams] + [dispatch] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx index 938ba56dee4..2ae24fdb805 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx @@ -1,15 +1,15 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectInfillMethod, setInfillMethod, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectInfillMethod, setInfillMethod } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo'; const ParamInfillMethod = () => { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const infillMethod = useAppSelector(selectInfillMethod); const { data: appConfigData } = useGetAppConfigQuery(); const options = useMemo( @@ -28,9 +28,9 @@ const ParamInfillMethod = () => { if (!v || !options.find((o) => o.value === v.value)) { return; } - dispatchParams(setInfillMethod, v.value); + dispatch(setInfillMethod(v.value)); }, - [dispatchParams, options] + [dispatch, options] ); const value = useMemo(() => options.find((o) => o.value === infillMethod), [options, infillMethod]); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx index 91330164b95..f2998b9f84b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx @@ -1,18 +1,17 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { selectInfillMethod, selectInfillPatchmatchDownscaleSize, setInfillPatchmatchDownscaleSize, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { selectInfillPatchmatchDownscaleSizeConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamInfillPatchmatchDownscaleSize = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const infillMethod = useAppSelector(selectInfillMethod); const infillPatchmatchDownscaleSize = useAppSelector(selectInfillPatchmatchDownscaleSize); const config = useAppSelector(selectInfillPatchmatchDownscaleSizeConfig); @@ -21,9 +20,9 @@ const ParamInfillPatchmatchDownscaleSize = () => { const handleChange = useCallback( (v: number) => { - dispatchParams(setInfillPatchmatchDownscaleSize, v); + dispatch(setInfillPatchmatchDownscaleSize(v)); }, - [dispatchParams] + [dispatch] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx index ac9000ed64c..3df4b3e9282 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx @@ -1,17 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectInfillMethod, - selectInfillTileSize, - setInfillTileSize, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectInfillMethod, selectInfillTileSize, setInfillTileSize } from 'features/controlLayers/store/paramsSlice'; import { selectInfillTileSizeConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamInfillTileSize = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const infillTileSize = useAppSelector(selectInfillTileSize); const config = useAppSelector(selectInfillTileSizeConfig); const infillMethod = useAppSelector(selectInfillMethod); @@ -20,9 +15,9 @@ const ParamInfillTileSize = () => { const handleChange = useCallback( (v: number) => { - dispatchParams(setInfillTileSize, v); + dispatch(setInfillTileSize(v)); }, - [dispatchParams] + [dispatch] ); return ( 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 2419849d99d..9f03b4e79ac 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/NegativePromptToggleButton.tsx @@ -1,10 +1,6 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - negativePromptChanged, - selectHasNegativePrompt, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { negativePromptChanged, selectHasNegativePrompt } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusMinusBold } from 'react-icons/pi'; @@ -12,16 +8,15 @@ import { PiPlusMinusBold } from 'react-icons/pi'; export const NegativePromptToggleButton = memo(() => { const { t } = useTranslation(); const hasNegativePrompt = useAppSelector(selectHasNegativePrompt); - - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const onClick = useCallback(() => { if (hasNegativePrompt) { - dispatchParams(negativePromptChanged, null); + dispatch(negativePromptChanged(null)); } else { - dispatchParams(negativePromptChanged, ''); + dispatch(negativePromptChanged('')); } - }, [dispatchParams, hasNegativePrompt]); + }, [dispatch, hasNegativePrompt]); const label = useMemo( () => (hasNegativePrompt ? t('common.removeNegativePrompt') : t('common.addNegativePrompt')), diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx index 91f854baece..145ca6f2da7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectCFGScale, setCfgScale, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectCFGScale, setCfgScale } from 'features/controlLayers/store/paramsSlice'; import { selectCFGScaleConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,13 +9,13 @@ import { useTranslation } from 'react-i18next'; const ParamCFGScale = () => { const cfgScale = useAppSelector(selectCFGScale); const config = useAppSelector(selectCFGScaleConfig); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const marks = useMemo( () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], [config.sliderMax, config.sliderMin] ); - const onChange = useCallback((v: number) => dispatchParams(setCfgScale, v), [dispatchParams]); + const onChange = useCallback((v: number) => dispatch(setCfgScale(v)), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx index 4c80f0bcc3a..86740e0846d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamGuidance.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectGuidance, setGuidance, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectGuidance, setGuidance } from 'features/controlLayers/store/paramsSlice'; import { selectGuidanceConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; const ParamGuidance = () => { const guidance = useAppSelector(selectGuidance); const config = useAppSelector(selectGuidanceConfig); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const marks = useMemo( () => [ @@ -19,7 +19,7 @@ const ParamGuidance = () => { ], [config.sliderMax, config.sliderMin] ); - const onChange = useCallback((v: number) => dispatchParams(setGuidance, v), [dispatchParams]); + const onChange = useCallback((v: number) => dispatch(setGuidance(v)), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index bff4a95c54e..1ba98fa774f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -1,11 +1,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize'; -import { - negativePromptChanged, - selectNegativePromptWithFallback, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { negativePromptChanged, selectNegativePromptWithFallback } from 'features/controlLayers/store/paramsSlice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; @@ -26,7 +22,7 @@ const persistOptions: Parameters[2] = { }; export const ParamNegativePrompt = memo(() => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const prompt = useAppSelector(selectNegativePromptWithFallback); const viewMode = useAppSelector(selectStylePresetViewMode); const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); @@ -47,9 +43,9 @@ export const ParamNegativePrompt = memo(() => { const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatchParams(negativePromptChanged, v); + dispatch(negativePromptChanged(v)); }, - [dispatchParams] + [dispatch] ); const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ prompt, 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 fb87a0a0c04..e4553d89147 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -1,13 +1,12 @@ import { Box, Flex, Textarea } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks'; import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize'; import { positivePromptChanged, selectModelSupportsNegativePrompt, selectPositivePrompt, selectPositivePromptHistory, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { promptGenerationFromImageDndTarget } from 'features/dnd/dnd'; import { DndDropTarget } from 'features/dnd/DndDropTarget'; @@ -45,7 +44,7 @@ const persistOptions: Parameters[2] = { const usePromptHistory = () => { const store = useAppStore(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const history = useAppSelector(selectPositivePromptHistory); /** @@ -81,8 +80,8 @@ const usePromptHistory = () => { // Shouldn't happen return; } - dispatchParams(positivePromptChanged, newPrompt); - }, [dispatchParams, history, store]); + dispatch(positivePromptChanged(newPrompt)); + }, [dispatch, history, store]); const next = useCallback(() => { if (history.length === 0) { // No history, nothing to do @@ -96,7 +95,7 @@ const usePromptHistory = () => { state.historyIdx = state.historyIdx - 1; if (state.historyIdx < 0) { // Overshot to the "current" stashed prompt - dispatchParams(positivePromptChanged, state.stashedPrompt); + dispatch(positivePromptChanged(state.stashedPrompt)); // Clear state bc we're back to current prompt stateRef.current = null; return; @@ -107,8 +106,8 @@ const usePromptHistory = () => { // Shouldn't happen return; } - dispatchParams(positivePromptChanged, newPrompt); - }, [dispatchParams, history]); + 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; @@ -117,7 +116,7 @@ const usePromptHistory = () => { }; export const ParamPositivePrompt = memo(() => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const prompt = useAppSelector(selectPositivePrompt); const viewMode = useAppSelector(selectStylePresetViewMode); const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); @@ -144,12 +143,12 @@ export const ParamPositivePrompt = memo(() => { const { t } = useTranslation(); const handleChange = useCallback( (v: string) => { - dispatchParams(positivePromptChanged, v); + dispatch(positivePromptChanged(v)); // When the user changes the prompt, reset the prompt history state. This event is not fired when the prompt is // changed via the prompt history navigation. promptHistoryApi.reset(); }, - [dispatchParams, promptHistoryApi] + [dispatch, promptHistoryApi] ); const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({ prompt, diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx index 517a60d1218..d670de68b80 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx @@ -1,15 +1,15 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectScheduler, setScheduler, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectScheduler, setScheduler } from 'features/controlLayers/store/paramsSlice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamScheduler = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const scheduler = useAppSelector(selectScheduler); @@ -18,9 +18,9 @@ const ParamScheduler = () => { if (!isParameterScheduler(v?.value)) { return; } - dispatchParams(setScheduler, v.value); + dispatch(setScheduler(v.value)); }, - [dispatchParams] + [dispatch] ); const value = useMemo(() => SCHEDULER_OPTIONS.find((o) => o.value === scheduler), [scheduler]); diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx index 60397c2870c..f7ef4660b58 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectSteps, setSteps, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectSteps, setSteps } from 'features/controlLayers/store/paramsSlice'; import { selectStepsConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; const ParamSteps = () => { const steps = useAppSelector(selectSteps); const config = useAppSelector(selectStepsConfig); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const marks = useMemo( () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], @@ -17,9 +17,9 @@ const ParamSteps = () => { ); const onChange = useCallback( (v: number) => { - dispatchParams(setSteps, v); + dispatch(setSteps(v)); }, - [dispatchParams] + [dispatch] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/PositivePromptHistory.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/PositivePromptHistory.tsx index e050c654dcb..628c895da22 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/PositivePromptHistory.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/PositivePromptHistory.tsx @@ -13,14 +13,13 @@ import { Text, useShiftModifier, } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { positivePromptChanged, promptHistoryCleared, promptRemovedFromHistory, selectPositivePromptHistory, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback, useMemo, useState } from 'react'; @@ -54,13 +53,13 @@ PositivePromptHistoryIconButton.displayName = 'PositivePromptHistoryIconButton'; const PromptHistoryContent = memo(() => { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const positivePromptHistory = useAppSelector(selectPositivePromptHistory); const [searchTerm, setSearchTerm] = useState(''); const onClickClearHistory = useCallback(() => { - dispatchParams(promptHistoryCleared); - }, [dispatchParams]); + dispatch(promptHistoryCleared()); + }, [dispatch]); const filteredPrompts = useMemo(() => { const trimmedSearchTerm = searchTerm.trim(); @@ -132,16 +131,16 @@ const PromptHistoryContent = memo(() => { PromptHistoryContent.displayName = 'PromptHistoryContent'; const PromptItem = memo(({ prompt }: { prompt: string }) => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const shiftKey = useShiftModifier(); const onClickUse = useCallback(() => { - dispatchParams(positivePromptChanged, prompt); - }, [dispatchParams, prompt]); + dispatch(positivePromptChanged(prompt)); + }, [dispatch, prompt]); const onClickDelete = useCallback(() => { - dispatchParams(promptRemovedFromHistory, prompt); - }, [dispatchParams, prompt]); + dispatch(promptRemovedFromHistory(prompt)); + }, [dispatch, prompt]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx index 77da433a13d..bd0c0d03a6b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsAspectRatioSelect.tsx @@ -1,5 +1,5 @@ import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { aspectRatioIdChanged, @@ -9,7 +9,6 @@ import { selectIsGemini2_5, selectIsImagen3, selectIsImagen4, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { isAspectRatioID, @@ -26,7 +25,7 @@ import { PiCaretDownBold } from 'react-icons/pi'; export const DimensionsAspectRatioSelect = memo(() => { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const id = useAppSelector(selectAspectRatioID); const isImagen3 = useAppSelector(selectIsImagen3); const isChatGPT4o = useAppSelector(selectIsChatGPT4o); @@ -57,9 +56,9 @@ export const DimensionsAspectRatioSelect = memo(() => { if (!isAspectRatioID(e.target.value)) { return; } - dispatchParams(aspectRatioIdChanged, { id: e.target.value }); + dispatch(aspectRatioIdChanged({ id: e.target.value })); }, - [dispatchParams] + [dispatch] ); return ( 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 3a12e603ca5..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,5 +1,5 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { heightChanged, @@ -7,7 +7,6 @@ import { selectHeight, selectIsApiBaseModel, selectOptimalDimension, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { selectHeightConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; @@ -15,7 +14,7 @@ import { useTranslation } from 'react-i18next'; export const DimensionsHeight = memo(() => { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const optimalDimension = useAppSelector(selectOptimalDimension); const height = useAppSelector(selectHeight); const config = useAppSelector(selectHeightConfig); @@ -24,9 +23,9 @@ export const DimensionsHeight = memo(() => { const onChange = useCallback( (v: number) => { - dispatchParams(heightChanged, { height: v }); + dispatch(heightChanged({ height: v })); }, - [dispatchParams] + [dispatch] ); const marks = useMemo( diff --git a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx index 3ce394cc4b3..2de397cc784 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsLockAspectRatioButton.tsx @@ -1,10 +1,9 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { aspectRatioLockToggled, selectAspectRatioIsLocked, selectIsApiBaseModel, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,13 +11,13 @@ import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi'; export const DimensionsLockAspectRatioButton = memo(() => { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const isLocked = useAppSelector(selectAspectRatioIsLocked); const isApiModel = useAppSelector(selectIsApiBaseModel); const onClick = useCallback(() => { - dispatchParams(aspectRatioLockToggled); - }, [dispatchParams]); + dispatch(aspectRatioLockToggled()); + }, [dispatch]); return ( { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const isApiModel = useAppSelector(selectIsApiBaseModel); const width = useAppSelector(selectWidth); const height = useAppSelector(selectHeight); @@ -29,8 +28,8 @@ export const DimensionsSetOptimalSizeButton = memo(() => { [height, width, optimalDimension] ); const onClick = useCallback(() => { - dispatchParams(sizeOptimized); - }, [dispatchParams]); + dispatch(sizeOptimized()); + }, [dispatch]); const tooltip = useMemo(() => { if (isSizeTooSmall) { return t('parameters.setToOptimalSizeTooSmall'); 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 ed85a8a3314..4f7fe86eb03 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsSwapButton.tsx @@ -1,15 +1,17 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { dimensionsSwapped, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { dimensionsSwapped } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsDownUpBold } from 'react-icons/pi'; export const DimensionsSwapButton = memo(() => { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { - dispatchParams(dimensionsSwapped); - }, [dispatchParams]); + dispatch(dimensionsSwapped()); + }, [dispatch]); return ( { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const width = useAppSelector(selectWidth); const optimalDimension = useAppSelector(selectOptimalDimension); const config = useAppSelector(selectWidthConfig); @@ -26,9 +25,9 @@ export const DimensionsWidth = memo(() => { const onChange = useCallback( (v: number) => { - dispatchParams(widthChanged, { width: v }); + dispatch(widthChanged({ width: v })); }, - [dispatchParams] + [dispatch] ); const marks = useMemo( 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 fd39e9a00a7..85b058f8022 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx @@ -1,7 +1,7 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectSeamlessXAxis, setSeamlessXAxis, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectSeamlessXAxis, setSeamlessXAxis } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,14 +9,13 @@ import { useTranslation } from 'react-i18next'; const ParamSeamlessXAxis = () => { const { t } = useTranslation(); const seamlessXAxis = useAppSelector(selectSeamlessXAxis); - - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const handleChange = useCallback( (e: ChangeEvent) => { - dispatchParams(setSeamlessXAxis, e.target.checked); + dispatch(setSeamlessXAxis(e.target.checked)); }, - [dispatchParams] + [dispatch] ); return ( 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 c7bfac96ddb..6cfa8c5d346 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx @@ -1,20 +1,21 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectSeamlessYAxis, setSeamlessYAxis, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectSeamlessYAxis, setSeamlessYAxis } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSeamlessYAxis = () => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); const seamlessYAxis = useAppSelector(selectSeamlessYAxis); - const dispatchParams = useParamsDispatch(); + const handleChange = useCallback( (e: ChangeEvent) => { - dispatchParams(setSeamlessYAxis, e.target.checked); + dispatch(setSeamlessYAxis(e.target.checked)); }, - [dispatchParams] + [dispatch] ); return ( 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 14ecafa9367..a57078c2f39 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx @@ -1,25 +1,18 @@ import { CompositeNumberInput, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { - selectSeed, - selectShouldRandomizeSeed, - setSeed, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { selectSeed, selectShouldRandomizeSeed, setSeed } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const ParamSeedNumberInput = memo(() => { const seed = useAppSelector(selectSeed); const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); - + const dispatch = useAppDispatch(); const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); - - const handleChangeSeed = useCallback((v: number) => dispatchParams(setSeed, v), [dispatchParams]); + const handleChangeSeed = useCallback((v: number) => dispatch(setSeed(v)), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx index 0e11a5fb6f8..ba41887e746 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx @@ -1,23 +1,19 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectShouldRandomizeSeed, - setShouldRandomizeSeed, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectShouldRandomizeSeed, setShouldRandomizeSeed } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const ParamSeedRandomize = memo(() => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); const handleChangeShouldRandomizeSeed = useCallback( - (e: ChangeEvent) => dispatchParams(setShouldRandomizeSeed, e.target.checked), - [dispatchParams] + (e: ChangeEvent) => dispatch(setShouldRandomizeSeed(e.target.checked)), + [dispatch] ); return ( 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 4fb08721bc9..b5280ee2cfb 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx @@ -1,21 +1,20 @@ import { Button } from '@invoke-ai/ui-library'; import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import randomInt from 'common/util/randomInt'; -import { selectShouldRandomizeSeed, setSeed, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectShouldRandomizeSeed, setSeed } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiShuffleBold } from 'react-icons/pi'; export const ParamSeedShuffle = memo(() => { - const dispatchParams = useParamsDispatch(); const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); - + const dispatch = useAppDispatch(); const { t } = useTranslation(); const handleClickRandomizeSeed = useCallback( - () => dispatchParams(setSeed, randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)), - [dispatchParams] + () => dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX))), + [dispatch] ); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleCFGScale.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleCFGScale.tsx index f628184ef7a..9af368cc018 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleCFGScale.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleCFGScale.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectUpscaleCfgScale, setUpscaleCfgScale, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectUpscaleCfgScale, setUpscaleCfgScale } from 'features/controlLayers/store/paramsSlice'; import { selectCFGScaleConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,13 +9,13 @@ import { useTranslation } from 'react-i18next'; const ParamUpscaleCFGScale = () => { const cfgScale = useAppSelector(selectUpscaleCfgScale); const config = useAppSelector(selectCFGScaleConfig); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const marks = useMemo( () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], [config.sliderMax, config.sliderMin] ); - const onChange = useCallback((v: number) => dispatchParams(setUpscaleCfgScale, v), [dispatchParams]); + const onChange = useCallback((v: number) => dispatch(setUpscaleCfgScale(v)), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleScheduler.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleScheduler.tsx index 1eee3140d6d..e7cb1655988 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleScheduler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamUpscaleScheduler.tsx @@ -1,19 +1,15 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { - selectUpscaleScheduler, - setUpscaleScheduler, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { selectUpscaleScheduler, setUpscaleScheduler } from 'features/controlLayers/store/paramsSlice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamUpscaleScheduler = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const scheduler = useAppSelector(selectUpscaleScheduler); @@ -22,9 +18,9 @@ const ParamUpscaleScheduler = () => { if (!isParameterScheduler(v?.value)) { return; } - dispatchParams(setUpscaleScheduler, v.value); + dispatch(setUpscaleScheduler(v.value)); }, - [dispatchParams] + [dispatch] ); const value = useMemo(() => SCHEDULER_OPTIONS.find((o) => o.value === scheduler), [scheduler]); diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx index 649605ef899..49502ebc625 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamFLUXVAEModelSelect.tsx @@ -1,8 +1,8 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { fluxVAESelected, selectFLUXVAE, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { fluxVAESelected, selectFLUXVAE } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +10,7 @@ import { useFluxVAEModels } from 'services/api/hooks/modelsByType'; import type { VAEModelConfig } from 'services/api/types'; const ParamFLUXVAEModelSelect = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const vae = useAppSelector(selectFLUXVAE); const [modelConfigs, { isLoading }] = useFluxVAEModels(); @@ -18,10 +18,10 @@ const ParamFLUXVAEModelSelect = () => { const _onChange = useCallback( (vae: VAEModelConfig | null) => { if (vae) { - dispatchParams(fluxVAESelected, zModelIdentifierField.parse(vae)); + dispatch(fluxVAESelected(zModelIdentifierField.parse(vae))); } }, - [dispatchParams] + [dispatch] ); const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx index d96414f3059..ea18c132ee7 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx @@ -1,8 +1,8 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { selectBase, selectVAE, useParamsDispatch, vaeSelected } from 'features/controlLayers/store/paramsSlice'; +import { selectBase, selectVAE, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +10,7 @@ import { useVAEModels } from 'services/api/hooks/modelsByType'; import type { VAEModelConfig } from 'services/api/types'; const ParamVAEModelSelect = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const base = useAppSelector(selectBase); const vae = useAppSelector(selectVAE); @@ -25,9 +25,9 @@ const ParamVAEModelSelect = () => { ); const _onChange = useCallback( (vae: VAEModelConfig | null) => { - dispatchParams(vaeSelected, vae ? zModelIdentifierField.parse(vae) : null); + dispatch(vaeSelected(vae ? zModelIdentifierField.parse(vae) : null)); }, - [dispatchParams] + [dispatch] ); const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ modelConfigs, diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx index de242a1627a..26d2fa2888b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx @@ -1,8 +1,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectVAEPrecision, useParamsDispatch, vaePrecisionChanged } from 'features/controlLayers/store/paramsSlice'; +import { selectVAEPrecision, vaePrecisionChanged } from 'features/controlLayers/store/paramsSlice'; import { isParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -14,7 +14,7 @@ const options = [ const ParamVAEPrecision = () => { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const vaePrecision = useAppSelector(selectVAEPrecision); const onChange = useCallback( @@ -23,9 +23,9 @@ const ParamVAEPrecision = () => { return; } - dispatchParams(vaePrecisionChanged, v.value); + dispatch(vaePrecisionChanged(v.value)); }, - [dispatchParams] + [dispatch] ); const value = useMemo(() => options.find((o) => o.value === vaePrecision), [vaePrecision]); diff --git a/invokeai/frontend/web/src/features/prompt/PromptExpansion/PromptExpansionResultOverlay.tsx b/invokeai/frontend/web/src/features/prompt/PromptExpansion/PromptExpansionResultOverlay.tsx index 35ea26b1552..015bf5946d1 100644 --- a/invokeai/frontend/web/src/features/prompt/PromptExpansion/PromptExpansionResultOverlay.tsx +++ b/invokeai/frontend/web/src/features/prompt/PromptExpansion/PromptExpansionResultOverlay.tsx @@ -1,10 +1,6 @@ import { ButtonGroup, Flex, Icon, IconButton, Text, Tooltip } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - positivePromptChanged, - selectPositivePrompt, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { positivePromptChanged, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice'; import { useCallback } from 'react'; import { PiCheckBold, PiMagicWandBold, PiPlusBold, PiXBold } from 'react-icons/pi'; @@ -15,20 +11,20 @@ interface PromptExpansionResultOverlayProps { } export const PromptExpansionResultOverlay = ({ expandedText }: PromptExpansionResultOverlayProps) => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const positivePrompt = useAppSelector(selectPositivePrompt); const handleReplace = useCallback(() => { - dispatchParams(positivePromptChanged, expandedText); + dispatch(positivePromptChanged(expandedText)); promptExpansionApi.reset(); - }, [dispatchParams, expandedText]); + }, [dispatch, expandedText]); const handleInsert = useCallback(() => { const currentText = positivePrompt; const newText = currentText ? `${currentText}\n${expandedText}` : expandedText; - dispatchParams(positivePromptChanged, newText); + dispatch(positivePromptChanged(newText)); promptExpansionApi.reset(); - }, [dispatchParams, expandedText, positivePrompt]); + }, [dispatch, expandedText, positivePrompt]); const handleDiscard = useCallback(() => { promptExpansionApi.reset(); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx index 59396201c75..8ff94197678 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx @@ -1,19 +1,20 @@ import { CompositeNumberInput } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectIterations, setIterations, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectIterations, setIterations } from 'features/controlLayers/store/paramsSlice'; 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 dispatchParams = useParamsDispatch(); + const handleChange = useCallback( (v: number) => { - dispatchParams(setIterations, v); + dispatch(setIterations(v)); }, - [dispatchParams] + [dispatch] ); return ( diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts index 04a09c469e2..79b301ab059 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts @@ -8,7 +8,6 @@ import { withResult, withResultAsync } from 'common/util/result'; import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { - paramsDispatch, positivePromptAddedToHistory, selectActiveParams, selectPositivePrompt, @@ -137,7 +136,7 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep const enqueueResult = await req.unwrap(); // Push to prompt history on successful enqueue - paramsDispatch(store, positivePromptAddedToHistory, selectPositivePrompt(state)); + dispatch(positivePromptAddedToHistory(selectPositivePrompt(state))); return { batchConfig, enqueueResult }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts index 44c394d56a1..ab59dca57aa 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts @@ -6,7 +6,6 @@ import { useAppStore } from 'app/store/storeHooks'; import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError'; import { withResult, withResultAsync } from 'common/util/result'; import { - paramsDispatch, positivePromptAddedToHistory, selectActiveParams, selectPositivePrompt, @@ -132,7 +131,7 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => { const enqueueResult = await req.unwrap(); // Push to prompt history on successful enqueue - paramsDispatch(store, positivePromptAddedToHistory, selectPositivePrompt(state)); + dispatch(positivePromptAddedToHistory(selectPositivePrompt(state))); return { batchConfig, enqueueResult }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts index 21b5c76dfa1..bfb1906c49a 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts @@ -3,7 +3,6 @@ import { logger } from 'app/logging/logger'; import type { AppStore } from 'app/store/store'; import { useAppStore } from 'app/store/storeHooks'; import { - paramsDispatch, positivePromptAddedToHistory, selectActiveParams, selectPositivePrompt, @@ -51,7 +50,7 @@ const enqueueUpscaling = async (store: AppStore, prepend: boolean) => { const enqueueResult = await req.unwrap(); // Push to prompt history on successful enqueue - paramsDispatch(store, positivePromptAddedToHistory, selectPositivePrompt(state)); + dispatch(positivePromptAddedToHistory(selectPositivePrompt(state))); return { batchConfig, enqueueResult }; }; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueVideo.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueVideo.ts index 862a031011c..183026c3632 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueVideo.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueVideo.ts @@ -5,11 +5,7 @@ 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 { - paramsDispatch, - positivePromptAddedToHistory, - selectPositivePrompt, -} from 'features/controlLayers/store/paramsSlice'; +import { positivePromptAddedToHistory, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildRunwayVideoGraph } from 'features/nodes/util/graph/generation/buildRunwayVideoGraph'; import { buildVeo3VideoGraph } from 'features/nodes/util/graph/generation/buildVeo3VideoGraph'; @@ -114,7 +110,7 @@ const enqueueVideo = async (store: AppStore, prepend: boolean) => { const enqueueResult = await req.unwrap(); // Push to prompt history on successful enqueue - paramsDispatch(store, positivePromptAddedToHistory, selectPositivePrompt(state)); + dispatch(positivePromptAddedToHistory(selectPositivePrompt(state))); return { batchConfig, enqueueResult }; }; diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index bee69c4b188..d99136a57a5 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -13,7 +13,7 @@ import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; import { selectActiveParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; -import type { CanvasEntity, InstanceParamsState, LoRA, RefImagesState } from 'features/controlLayers/store/types'; +import type { CanvasEntity, LoRA, RefImagesState, TabParamsState } from 'features/controlLayers/store/types'; import { getControlLayerWarnings, getGlobalReferenceImageWarnings, @@ -78,7 +78,7 @@ type UpdateReasonsArg = { tab: TabName; isConnected: boolean; canvas: CanvasEntity; - params: InstanceParamsState; + params: TabParamsState; refImages: RefImagesState; dynamicPrompts: DynamicPromptsState; canvasIsFiltering: boolean; @@ -277,7 +277,7 @@ const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invok const getReasonsWhyCannotEnqueueVideoTab = (arg: { isConnected: boolean; video: VideoState; - params: InstanceParamsState; + params: TabParamsState; dynamicPrompts: DynamicPromptsState; promptExpansionRequest: PromptExpansionRequestState; isVideoEnabled: boolean; @@ -319,7 +319,7 @@ const getReasonsWhyCannotEnqueueVideoTab = (arg: { const getReasonsWhyCannotEnqueueGenerateTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; - params: InstanceParamsState; + params: TabParamsState; refImages: RefImagesState; loras: LoRA[]; dynamicPrompts: DynamicPromptsState; @@ -490,7 +490,7 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: { isConnected: boolean; upscale: UpscaleState; config: AppConfig; - params: InstanceParamsState; + params: TabParamsState; loras: LoRA[]; promptExpansionRequest: PromptExpansionRequestState; }) => { @@ -553,7 +553,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; canvas: CanvasEntity; - params: InstanceParamsState; + params: TabParamsState; refImages: RefImagesState; loras: LoRA[]; dynamicPrompts: DynamicPromptsState; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx index b2ab0175511..3d0ad822bab 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectRefinerCFGScale, setRefinerCFGScale, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerCFGScale, setRefinerCFGScale } from 'features/controlLayers/store/paramsSlice'; import { selectCFGScaleConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerCFGScale = () => { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const refinerCFGScale = useAppSelector(selectRefinerCFGScale); const config = useAppSelector(selectCFGScaleConfig); const marks = useMemo( @@ -16,7 +16,7 @@ const ParamSDXLRefinerCFGScale = () => { [config.sliderMax, config.sliderMin] ); - const onChange = useCallback((v: number) => dispatchParams(setRefinerCFGScale, v), [dispatchParams]); + const onChange = useCallback((v: number) => dispatch(setRefinerCFGScale(v)), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx index 3b302ecaa64..fa817193aff 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx @@ -1,8 +1,8 @@ import { Combobox, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { refinerModelChanged, selectRefinerModel, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { refinerModelChanged, selectRefinerModel } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,19 +13,19 @@ import type { MainModelConfig } from 'services/api/types'; const optionsFilter = (model: MainModelConfig) => model.base === 'sdxl-refiner'; const ParamSDXLRefinerModelSelect = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const model = useAppSelector(selectRefinerModel); const { t } = useTranslation(); const [modelConfigs, { isLoading }] = useRefinerModels(); const _onChange = useCallback( (model: MainModelConfig | null) => { if (!model) { - dispatchParams(refinerModelChanged, null); + dispatch(refinerModelChanged(null)); return; } - dispatchParams(refinerModelChanged, zModelIdentifierField.parse(model)); + dispatch(refinerModelChanged(zModelIdentifierField.parse(model))); }, - [dispatchParams] + [dispatch] ); const { options, value, onChange, placeholder, noOptionsMessage } = useModelCombobox({ modelConfigs, diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx index f0c8ee22b6b..3729468bcdc 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx @@ -1,10 +1,9 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { selectRefinerNegativeAestheticScore, setRefinerNegativeAestheticScore, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,13 +11,10 @@ import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerNegativeAestheticScore = () => { const refinerNegativeAestheticScore = useAppSelector(selectRefinerNegativeAestheticScore); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); - const handleChange = useCallback( - (v: number) => dispatchParams(setRefinerNegativeAestheticScore, v), - [dispatchParams] - ); + const handleChange = useCallback((v: number) => dispatch(setRefinerNegativeAestheticScore(v)), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx index f51038658a9..2661c5ce573 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx @@ -1,23 +1,19 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { selectRefinerPositiveAestheticScore, setRefinerPositiveAestheticScore, - useParamsDispatch, } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerPositiveAestheticScore = () => { const refinerPositiveAestheticScore = useAppSelector(selectRefinerPositiveAestheticScore); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); - const handleChange = useCallback( - (v: number) => dispatchParams(setRefinerPositiveAestheticScore, v), - [dispatchParams] - ); + const handleChange = useCallback((v: number) => dispatch(setRefinerPositiveAestheticScore(v)), [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx index 39280048387..acce9dd8e9e 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx @@ -1,19 +1,15 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { - selectRefinerScheduler, - setRefinerScheduler, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerScheduler, setRefinerScheduler } from 'features/controlLayers/store/paramsSlice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerScheduler = () => { - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const { t } = useTranslation(); const refinerScheduler = useAppSelector(selectRefinerScheduler); @@ -22,9 +18,9 @@ const ParamSDXLRefinerScheduler = () => { if (!isParameterScheduler(v?.value)) { return; } - dispatchParams(setRefinerScheduler, v.value); + dispatch(setRefinerScheduler(v.value)); }, - [dispatchParams] + [dispatch] ); const value = useMemo(() => SCHEDULER_OPTIONS.find((o) => o.value === refinerScheduler), [refinerScheduler]); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx index 7ff89ebc80c..856ca391347 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectRefinerStart, setRefinerStart, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerStart, setRefinerStart } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerStart = () => { const refinerStart = useAppSelector(selectRefinerStart); - const dispatchParams = useParamsDispatch(); - const handleChange = useCallback((v: number) => dispatchParams(setRefinerStart, v), [dispatchParams]); + const dispatch = useAppDispatch(); + const handleChange = useCallback((v: number) => dispatch(setRefinerStart(v)), [dispatch]); const { t } = useTranslation(); return ( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx index b8733ee15d4..3a3a2ce5c49 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { selectRefinerSteps, setRefinerSteps, useParamsDispatch } from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerSteps, setRefinerSteps } from 'features/controlLayers/store/paramsSlice'; import { selectStepsConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerSteps = () => { const { t } = useTranslation(); - const dispatchParams = useParamsDispatch(); + const dispatch = useAppDispatch(); const refinerSteps = useAppSelector(selectRefinerSteps); const config = useAppSelector(selectStepsConfig); @@ -19,9 +19,9 @@ const ParamSDXLRefinerSteps = () => { const onChange = useCallback( (v: number) => { - dispatchParams(setRefinerSteps, v); + dispatch(setRefinerSteps(v)); }, - [dispatchParams] + [dispatch] ); return ( diff --git a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx index c2575833f11..c17149cd5fd 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx @@ -1,10 +1,6 @@ import { Badge, Flex, IconButton, Spacer, Text, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - negativePromptChanged, - positivePromptChanged, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/paramsSlice'; import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; import { activeStylePresetIdChanged, @@ -35,7 +31,7 @@ export const ActiveStylePreset = () => { }); const dispatch = useAppDispatch(); - const dispatchParams = useParamsDispatch(); + const { t } = useTranslation(); const { presetModifiedPositivePrompt, presetModifiedNegativePrompt } = usePresetModifiedPrompts(); @@ -52,12 +48,12 @@ export const ActiveStylePreset = () => { const handleFlattenPrompts = useCallback>( (e) => { e.stopPropagation(); - dispatchParams(positivePromptChanged, presetModifiedPositivePrompt); - dispatchParams(negativePromptChanged, presetModifiedNegativePrompt); + dispatch(positivePromptChanged(presetModifiedPositivePrompt)); + dispatch(negativePromptChanged(presetModifiedNegativePrompt)); dispatch(viewModeChanged(false)); dispatch(activeStylePresetIdChanged(null)); }, - [dispatch, dispatchParams, presetModifiedPositivePrompt, presetModifiedNegativePrompt] + [dispatch, presetModifiedPositivePrompt, presetModifiedNegativePrompt] ); const handleToggleViewMode = useCallback>( 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 ee7754bd1f8..ca650ec2984 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -19,11 +19,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { buildUseBoolean } from 'common/hooks/useBoolean'; -import { - selectShouldUseCPUNoise, - shouldUseCpuNoiseChanged, - useParamsDispatch, -} from 'features/controlLayers/store/paramsSlice'; +import { selectShouldUseCPUNoise, shouldUseCpuNoiseChanged } from 'features/controlLayers/store/paramsSlice'; import { useRefreshAfterResetModal } from 'features/system/components/SettingsModal/RefreshAfterResetModal'; import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled'; import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel'; @@ -85,7 +81,7 @@ const [useSettingsModal] = buildUseBoolean(false); const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps) => { const dispatch = useAppDispatch(); - const dispatchParams = useParamsDispatch(); + const { t } = useTranslation(); const { isNSFWCheckerAvailable, isWatermarkerAvailable } = useGetAppConfigQuery(undefined, { @@ -176,9 +172,9 @@ const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps) ); const handleChangeShouldUseCpuNoise = useCallback( (e: ChangeEvent) => { - dispatchParams(shouldUseCpuNoiseChanged, e.target.checked); + dispatch(shouldUseCpuNoiseChanged(e.target.checked)); }, - [dispatchParams] + [dispatch] ); const handleChangeShouldShowInvocationProgressDetail = useCallback( From a51c74d267588bb73611210bcb9edf08f49fb855 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Fri, 3 Oct 2025 13:42:39 +0200 Subject: [PATCH 13/16] SliceConfig refactored --- invokeai/frontend/web/src/app/store/store.ts | 25 +++--------- invokeai/frontend/web/src/app/store/types.ts | 21 +++++----- .../controlLayers/store/canvasSlice.ts | 32 +++++++++------ .../controlLayers/store/refImagesSlice.ts | 16 ++++++-- .../store/dynamicPromptsSlice.ts | 40 ++++++++++++------- .../features/gallery/store/gallerySlice.ts | 15 +++++-- .../store/modelManagerV2Slice.ts | 15 +++++-- .../src/features/nodes/store/nodesSlice.ts | 4 +- .../web/src/features/ui/store/uiSlice.ts | 15 +++++-- 9 files changed, 109 insertions(+), 74 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 8fe794039aa..77595a69a37 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -17,9 +17,6 @@ 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 { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice'; import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; @@ -117,21 +114,14 @@ const unserialize: UnserializeFunction = (data, key) => { 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, }, @@ -146,7 +136,7 @@ const unserialize: UnserializeFunction = (data, key) => { state = getInitialState(); } - return persistConfig.wrapState ? persistConfig.wrapState(state) : state; + return persistConfig.deserialize ? persistConfig.deserialize(state) : state; }; const serialize: SerializeFunction = (data, key) => { @@ -155,12 +145,9 @@ const serialize: SerializeFunction = (data, key) => { throw new Error(`No persist config for slice "${key}"`); } - const result = omit( - sliceConfig.persistConfig.unwrapState ? sliceConfig.persistConfig.unwrapState(data) : 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) diff --git a/invokeai/frontend/web/src/app/store/types.ts b/invokeai/frontend/web/src/app/store/types.ts index cdb203ab4fe..adf2d7480f7 100644 --- a/invokeai/frontend/web/src/app/store/types.ts +++ b/invokeai/frontend/web/src/app/store/types.ts @@ -2,6 +2,7 @@ import type { Slice } from '@reduxjs/toolkit'; import type { ZodType } from 'zod'; type StateFromSlice = T extends Slice ? U : never; +export type SerializedStateFromDenyList = Omit; export type SliceConfig, TSerializedState = StateFromSlice> = { /** @@ -29,22 +30,18 @@ export type SliceConfig, TSe */ migrate: (state: unknown) => TSerializedState; /** - * Keys to omit from the persisted state. - */ - persistDenylist?: (keyof StateFromSlice)[]; - /** - * Wraps state into state with history + * Serializes the state * - * @param state The state without history - * @returns The state with history + * @param state The internal state + * @returns The serialized state */ - wrapState?: (state: unknown) => TInternalState; + serialize?: (state: TInternalState) => TSerializedState; /** - * Unwraps state with history + * Deserializes the state * - * @param state The state with history - * @returns The state without history + * @param state The serialized state + * @returns The internal state */ - unwrapState?: (state: TInternalState) => TSerializedState; + deserialize?: (state: unknown) => TInternalState; }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 825daf875e1..ba38e9b02d1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -1938,7 +1938,7 @@ export const isCanvasInstanceAction = (action: UnknownAction) => isTabParamsStateAction(action) || isCanvasSettingsStateAction(action) || isCanvasStagingAreaStateAction(action); -export const isCanvasEntityStateAction = isAnyOf(...Object.values(canvasEntityState.actions), canvasReset); +const isCanvasEntityStateAction = isAnyOf(...Object.values(canvasEntityState.actions), canvasReset); export const { // Canvas @@ -2102,28 +2102,34 @@ export const canvasSliceConfig: SliceConfig { - const canvasState = state as CanvasState; - + serialize: (state) => { return { - _version: canvasState._version, - activeCanvasId: canvasState.activeCanvasId, + _version: state._version, + activeCanvasId: state.activeCanvasId, canvases: Object.fromEntries( - Object.entries(canvasState.canvases).map(([canvasId, instance]) => [ + Object.entries(state.canvases).map(([canvasId, instance]) => [ canvasId, - { ...instance, canvas: newHistory([], instance.canvas, []) }, + { + ...instance, + canvas: instance.canvas.present, + }, ]) ), }; }, - unwrapState: (state) => { + deserialize: (state) => { + const canvasState = state as CanvasState; + return { - _version: state._version, - activeCanvasId: state.activeCanvasId, + _version: canvasState._version, + activeCanvasId: canvasState.activeCanvasId, canvases: Object.fromEntries( - Object.entries(state.canvases).map(([canvasId, instance]) => [ + Object.entries(canvasState.canvases).map(([canvasId, instance]) => [ canvasId, - { ...instance, canvas: instance.canvas.present }, + { + ...instance, + canvas: newHistory([], instance.canvas, []), + }, ]) ), }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts index e787d08fca0..9949c4fe2db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts @@ -3,8 +3,8 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSelector, createSlice } 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 type { SerializedStateFromDenyList, SliceConfig } from 'app/store/types'; +import { clamp, merge, omit } from 'es-toolkit/compat'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import type { CroppableImageWithDims, @@ -279,13 +279,21 @@ export const { refImagesRecalled, } = slice.actions; -export const refImagesSliceConfig: SliceConfig = { +const denyList = ['selectedEntityId', 'isPanelOpen'] as const; +type SerializedRefImagesState = SerializedStateFromDenyList; + +export const refImagesSliceConfig: SliceConfig = { slice, schema: zRefImagesState, getInitialState: getInitialRefImagesState, persistConfig: { migrate: (state) => zRefImagesState.parse(state), - persistDenylist: ['selectedEntityId', 'isPanelOpen'], + serialize: (state) => omit(state, denyList), + deserialize: (state) => { + const refImagesState = state as SerializedRefImagesState; + + return merge(refImagesState, getInitialRefImagesState()); + }, }, }; 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/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 1f99fc7a6d3..7272243d210 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,11 @@ export const gallerySliceConfig: SliceConfig = { } return zGalleryState.parse(state); }, - persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'imageToCompare'], + serialize: (state) => omit(state, denyList), + deserialize: (state) => { + const galleryState = state as SerializedGalleryState; + + return merge(galleryState, getInitialState()); + }, }, }; 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/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 8ac1f64d7e5..2f17d88c912 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -819,12 +819,12 @@ export const nodesSliceConfig: SliceConfig { + serialize: (state) => state.present, + deserialize: (state) => { const nodesState = state as NodesState; return newHistory([], nodesState, []); }, - unwrapState: (state) => state.present, }, }; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index b1f3fe9fae6..30a9f1417d1 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'; @@ -87,7 +88,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 +115,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()); + }, }, }; From df4754a0111d5978bb75bcbeaaf550f2eb6d4fe4 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Sat, 4 Oct 2025 07:50:06 +0200 Subject: [PATCH 14/16] CanvasInstanceContext removed --- .../components/InvokeCanvasComponent.tsx | 9 +- .../components/StagingArea/context.tsx | 8 +- .../CanvasInstanceContextProvider.tsx | 13 --- .../contexts/CanvasManagerProviderGate.tsx | 3 +- .../controlLayers/hooks/useCanvasId.ts | 10 -- .../controlLayers/hooks/useCanvasIsStaging.ts | 9 +- .../controlLayers/hooks/useCanvasSessionId.ts | 14 --- .../ui/layouts/CanvasWorkspacePanel.tsx | 106 ++++++++---------- .../ui/layouts/DockviewTabCanvasWorkspace.tsx | 5 +- 9 files changed, 65 insertions(+), 112 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts delete mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx index d425ebe7770..3a44edfe372 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx @@ -1,12 +1,11 @@ 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'; -interface InvokeCanvasComponent { - canvasId: string; -} - -export const InvokeCanvasComponent = memo(({ canvasId }: InvokeCanvasComponent) => { +export const InvokeCanvasComponent = memo(() => { + const canvasId = useAppSelector(selectActiveCanvasId); const ref = useInvokeCanvas(canvasId); return ( 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 ef876f50fa1..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,6 +1,5 @@ import { useStore } from '@nanostores/react'; -import { useAppStore } from 'app/store/storeHooks'; -import { useScopedCanvasSessionId } from 'features/controlLayers/hooks/useCanvasSessionId'; +import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { selectStagingAreaAutoSwitch, settingsStagingAreaAutoSwitchChanged, @@ -10,6 +9,7 @@ import { 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'; @@ -27,10 +27,10 @@ import { getInitialProgressData, StagingAreaApi } from './state'; const StagingAreaContext = createContext(null); -export const StagingAreaContextProvider = memo(({ canvasId, children }: PropsWithChildren<{ canvasId: string }>) => { +export const StagingAreaContextProvider = memo(({ children }: PropsWithChildren) => { const store = useAppStore(); const socket = useStore($socket); - const sessionId = useScopedCanvasSessionId(canvasId); + const sessionId = useAppSelector(selectActiveCanvasStagingAreaSessionId); const selectQueueItems = useMemo(() => buildSelectCanvasQueueItemsBySessionId(sessionId), [sessionId]); const stagingAreaAppApi = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx deleted file mode 100644 index 4b3a6ecd8e8..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { PropsWithChildren } from 'react'; -import { createContext, memo, useContext } from 'react'; - -const CanvasInstanceContext = createContext(null); - -export const CanvasInstanceContextProvider = memo(({ canvasId, children }: PropsWithChildren<{ canvasId: string }>) => { - return {children}; -}); -CanvasInstanceContextProvider.displayName = 'CanvasInstanceContextProvider'; - -export const useScopedCanvasIdSafe = () => { - return useContext(CanvasInstanceContext); -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx index f7c19300fe7..a264ed784ba 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx @@ -1,6 +1,5 @@ import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; -import { useCanvasId } from 'features/controlLayers/hooks/useCanvasId'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { $canvasManagers } from 'features/controlLayers/store/ephemeral'; import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; @@ -38,7 +37,7 @@ export const useCanvasManager = (): CanvasManager => { */ export const useCanvasManagerSafe = (): CanvasManager | null => { const canvasManagers = useStore($canvasManagers); - const canvasId = useCanvasId(); + const canvasId = useAppSelector(selectActiveCanvasId); return canvasManagers[canvasId] ?? null; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts deleted file mode 100644 index e524a60faba..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasId.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { useScopedCanvasIdSafe } from 'features/controlLayers/contexts/CanvasInstanceContextProvider'; -import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; - -export const useCanvasId = () => { - const scopedCanvasId = useScopedCanvasIdSafe(); - const activeCanvasId = useAppSelector(selectActiveCanvasId); - - return scopedCanvasId ?? activeCanvasId; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsStaging.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsStaging.ts index c1c2e47a2ad..ca84d1f4ee6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsStaging.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsStaging.ts @@ -1,11 +1,12 @@ import { useAppSelector } from 'app/store/storeHooks'; -import { buildSelectIsStagingBySessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { + buildSelectIsStagingBySessionId, + selectActiveCanvasStagingAreaSessionId, +} from 'features/controlLayers/store/canvasStagingAreaSlice'; import { useMemo } from 'react'; -import { useCanvasSessionId } from './useCanvasSessionId'; - export const useCanvasIsStaging = () => { - const sessionId = useCanvasSessionId(); + const sessionId = useAppSelector(selectActiveCanvasStagingAreaSessionId); const selectIsStagingBySessionIdSelector = useMemo(() => buildSelectIsStagingBySessionId(sessionId), [sessionId]); return useAppSelector(selectIsStagingBySessionIdSelector); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts deleted file mode 100644 index 0985ea5cc28..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasSessionId.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasStagingAreaSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; - -import { useCanvasId } from './useCanvasId'; - -export const useCanvasSessionId = () => { - const canvasId = useCanvasId(); - - return useAppSelector((state) => selectCanvasStagingAreaSessionId(state, canvasId)); -}; - -export const useScopedCanvasSessionId = (canvasId: string) => { - return useAppSelector((state) => selectCanvasStagingAreaSessionId(state, canvasId)); -}; diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx index 22be4e3e4db..3dbbf50e7b7 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx @@ -16,10 +16,8 @@ import { SelectObject } from 'features/controlLayers/components/SelectObject/Sel import { StagingAreaContextProvider } from 'features/controlLayers/components/StagingArea/context'; import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar'; import { Transform } from 'features/controlLayers/components/Transform/Transform'; -import { CanvasInstanceContextProvider } from 'features/controlLayers/contexts/CanvasInstanceContextProvider'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; @@ -50,11 +48,7 @@ const canvasBgSx = { }, }; -interface CanvasProps { - canvasId: string; -} - -const Canvas = memo(({ canvasId }: CanvasProps) => { +const ActiveCanvas = memo(() => { const dynamicGrid = useAppSelector((state) => selectDynamicGrid(state)); const showHUD = useAppSelector((state) => selectShowHUD(state)); @@ -64,63 +58,59 @@ const Canvas = memo(({ canvasId }: CanvasProps) => { return ( - - - renderMenu={renderMenu} withLongPress={false}> - {(ref) => ( - - - - - {showHUD && } - - - - - - - -

- } colorScheme="base" /> - - - - - - - )} - - - - - - - - - - - + + renderMenu={renderMenu} withLongPress={false}> + {(ref) => ( + + + + + {showHUD && } + + + + + + + + + } colorScheme="base" /> + + + + + + + )} + + + + + - + + + - - + + + + + ); }); -Canvas.displayName = 'Canvas'; +ActiveCanvas.displayName = 'ActiveCanvas'; export const CanvasWorkspacePanel = memo(() => { - const canvasId = useAppSelector(selectActiveCanvasId); - return ( { - + ); }); diff --git a/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx index 11a664f6585..dddbf9d887a 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/DockviewTabCanvasWorkspace.tsx @@ -1,8 +1,9 @@ import { Flex, Text } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; import { setFocusedRegion } from 'common/hooks/focus'; import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter'; import type { IDockviewPanelHeaderProps } from 'dockview'; -import { useCanvasSessionId } from 'features/controlLayers/hooks/useCanvasSessionId'; +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'; @@ -14,7 +15,7 @@ import type { DockviewPanelParameters } from './auto-layout-context'; export const DockviewTabCanvasWorkspace = memo((props: IDockviewPanelHeaderProps) => { const { t } = useTranslation(); const isGenerationInProgress = useIsGenerationInProgress(); - const canvasSessionId = useCanvasSessionId(); + const canvasSessionId = useAppSelector(selectActiveCanvasStagingAreaSessionId); const currentQueueItemDestination = useCurrentQueueItemDestination(); const ref = useRef(null); From db15c546b4334adb2c013931065f75bd23729fb2 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Sat, 4 Oct 2025 08:09:40 +0200 Subject: [PATCH 15/16] selectors refined --- .../listeners/appConfigReceived.ts | 4 +- .../listeners/modelSelected.ts | 4 +- .../listeners/modelsLoaded.ts | 14 +- .../listeners/setDefaultSettings.ts | 4 +- .../common/hooks/useGroupedModelCombobox.ts | 4 +- .../controlLayers/store/paramsSlice.ts | 156 +++++++++--------- .../util/graph/buildLinearBatchConfig.ts | 4 +- .../graph/buildMultidiffusionUpscaleGraph.ts | 4 +- .../util/graph/generation/addFLUXFill.ts | 4 +- .../nodes/util/graph/generation/addInpaint.ts | 4 +- .../util/graph/generation/addOutpaint.ts | 4 +- .../util/graph/generation/addSDXLRefiner.ts | 4 +- .../util/graph/generation/addSeamless.ts | 4 +- .../graph/generation/buildCogView4Graph.ts | 4 +- .../util/graph/generation/buildFLUXGraph.ts | 4 +- .../graph/generation/buildRunwayVideoGraph.ts | 4 +- .../util/graph/generation/buildSD1Graph.ts | 4 +- .../util/graph/generation/buildSD3Graph.ts | 4 +- .../util/graph/generation/buildSDXLGraph.ts | 4 +- .../graph/generation/buildVeo3VideoGraph.ts | 4 +- .../nodes/util/graph/graphBuilderUtils.ts | 6 +- .../parameters/components/ModelPicker.tsx | 4 +- .../features/queue/hooks/useEnqueueCanvas.ts | 4 +- .../queue/hooks/useEnqueueGenerate.ts | 4 +- .../queue/hooks/useEnqueueUpscaling.ts | 4 +- .../queue/hooks/useEnqueueWorkflows.ts | 4 +- .../web/src/features/queue/store/readiness.ts | 6 +- .../AdvancedSettingsAccordion.tsx | 4 +- .../UpscaleTabAdvancedSettingsAccordion.tsx | 4 +- .../RefinerSettingsAccordion.tsx | 4 +- 30 files changed, 142 insertions(+), 144 deletions(-) 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 2459137cf75..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,5 +1,5 @@ import type { AppStartListening } from 'app/store/store'; -import { selectActiveParams, 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'; @@ -9,7 +9,7 @@ export const addAppConfigReceivedListener = (startAppListening: AppStartListenin effect: (action, api) => { const { getState, dispatch } = api; const { infill_methods = [], nsfw_methods = [], watermarking_methods = [] } = action.payload; - const infillMethod = selectActiveParams(getState()).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/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index a848ff678e4..565bc4bf26b 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 @@ -7,7 +7,7 @@ import { selectActiveCanvasStagingAreaSessionId, } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; -import { selectActiveParams, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice'; import { selectActiveCanvas, @@ -47,7 +47,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = const newModel = result.data; const newBase = newModel.base; - const params = selectActiveParams(state); + const params = selectActiveTabParams(state); const didBaseModelChange = params.model?.base !== newBase; if (didBaseModelChange) { 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 5a379012988..b89a2aec086 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 @@ -7,7 +7,7 @@ import { clipEmbedModelSelected, fluxVAESelected, refinerModelChanged, - selectActiveParams, + selectActiveTabParams, t5EncoderModelSelected, vaeSelected, } from 'features/controlLayers/store/paramsSlice'; @@ -104,7 +104,7 @@ type ModelHandler = ( ) => undefined; const handleMainModels: ModelHandler = (models, state, dispatch, log) => { - const selectedMainModel = selectActiveParams(state).model; + const selectedMainModel = selectActiveTabParams(state).model; const allMainModels = models.filter(isNonRefinerMainModelConfig).sort((a) => (a.base === 'sdxl' ? -1 : 1)); const firstModel = allMainModels[0]; @@ -145,7 +145,7 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => { }; const handleRefinerModels: ModelHandler = (models, state, dispatch, log) => { - const selectedRefinerModel = selectActiveParams(state).refinerModel; + const selectedRefinerModel = selectActiveTabParams(state).refinerModel; // `null` is a valid refiner model - no need to do anything. if (selectedRefinerModel === null) { @@ -169,7 +169,7 @@ const handleRefinerModels: ModelHandler = (models, state, dispatch, log) => { }; const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { - const selectedVAEModel = selectActiveParams(state).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) { @@ -418,7 +418,7 @@ const handleTileControlNetModel: ModelHandler = (models, state, dispatch, log) = }; const handleT5EncoderModels: ModelHandler = (models, state, dispatch, log) => { - const selectedT5EncoderModel = selectActiveParams(state).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 @@ -446,7 +446,7 @@ const handleT5EncoderModels: ModelHandler = (models, state, dispatch, log) => { }; const handleCLIPEmbedModels: ModelHandler = (models, state, dispatch, log) => { - const selectedCLIPEmbedModel = selectActiveParams(state).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 @@ -474,7 +474,7 @@ const handleCLIPEmbedModels: ModelHandler = (models, state, dispatch, log) => { }; const handleFLUXVAEModels: ModelHandler = (models, state, dispatch, log) => { - const selectedFLUXVAEModel = selectActiveParams(state).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 56262d310ca..457e67c0cc6 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 @@ -7,7 +7,7 @@ import { } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { heightChanged, - selectActiveParams, + selectActiveTabParams, setCfgRescaleMultiplier, setCfgScale, setGuidance, @@ -42,7 +42,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni const { dispatch, getState } = api; const state = getState(); - const currentModel = selectActiveParams(state).model; + const currentModel = selectActiveTabParams(state).model; if (!currentModel) { return; diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index e806a7b7f54..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 { selectActiveParams } 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(selectActiveParams, (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/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index d46b7b7e7b2..30cde805ccc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -636,7 +636,7 @@ export const paramsSliceConfig: SliceConfig = { const initialTabParamsState = getInitialTabParamsState(); -export const selectActiveParams = (state: RootState) => { +export const selectActiveTabParams = (state: RootState) => { const tab = selectActiveTab(state); const canvasId = selectActiveCanvasId(state); @@ -644,9 +644,7 @@ export const selectActiveParams = (state: RootState) => { case 'generate': return state.params.generate; case 'canvas': { - const params = state.canvas.canvases[canvasId]?.params; - assert(params, 'Params must exist for a canvas once the canvas has been created'); - return params; + return state.canvas.canvases[canvasId]!.params; } case 'upscaling': return state.params.upscaling; @@ -658,19 +656,19 @@ export const selectActiveParams = (state: RootState) => { return initialTabParamsState; }; -const buildActiveParamsSelector = +const buildActiveTabParamsSelector = (selector: Selector) => (state: RootState) => - selector(selectActiveParams(state)); - -export const selectBase = buildActiveParamsSelector((params) => params.model?.base); -export const selectIsSDXL = buildActiveParamsSelector((params) => params.model?.base === 'sdxl'); -export const selectIsFLUX = buildActiveParamsSelector((params) => params.model?.base === 'flux'); -export const selectIsSD3 = buildActiveParamsSelector((params) => params.model?.base === 'sd-3'); -export const selectIsCogView4 = buildActiveParamsSelector((params) => params.model?.base === 'cogview4'); -export const selectIsImagen3 = buildActiveParamsSelector((params) => params.model?.base === 'imagen3'); -export const selectIsImagen4 = buildActiveParamsSelector((params) => params.model?.base === 'imagen4'); -export const selectIsFluxKontext = buildActiveParamsSelector((params) => { + 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; } @@ -679,42 +677,42 @@ export const selectIsFluxKontext = buildActiveParamsSelector((params) => { } return false; }); -export const selectIsChatGPT4o = buildActiveParamsSelector((params) => params.model?.base === 'chatgpt-4o'); -export const selectIsGemini2_5 = buildActiveParamsSelector((params) => params.model?.base === 'gemini-2.5'); - -export const selectModel = buildActiveParamsSelector((params) => params.model); -export const selectModelKey = buildActiveParamsSelector((params) => params.model?.key); -export const selectVAE = buildActiveParamsSelector((params) => params.vae); -export const selectFLUXVAE = buildActiveParamsSelector((params) => params.fluxVAE); -export const selectVAEKey = buildActiveParamsSelector((params) => params.vae?.key); -export const selectT5EncoderModel = buildActiveParamsSelector((params) => params.t5EncoderModel); -export const selectCLIPEmbedModel = buildActiveParamsSelector((params) => params.clipEmbedModel); -export const selectCLIPLEmbedModel = buildActiveParamsSelector((params) => params.clipLEmbedModel); - -export const selectCLIPGEmbedModel = buildActiveParamsSelector((params) => params.clipGEmbedModel); - -export const selectCFGScale = buildActiveParamsSelector((params) => params.cfgScale); -export const selectGuidance = buildActiveParamsSelector((params) => params.guidance); -export const selectSteps = buildActiveParamsSelector((params) => params.steps); -export const selectCFGRescaleMultiplier = buildActiveParamsSelector((params) => params.cfgRescaleMultiplier); -export const selectCLIPSkip = buildActiveParamsSelector((params) => params.clipSkip); -export const selectHasModelCLIPSkip = buildActiveParamsSelector((params) => hasModelClipSkip(params.model)); -export const selectCanvasCoherenceEdgeSize = buildActiveParamsSelector((params) => params.canvasCoherenceEdgeSize); -export const selectCanvasCoherenceMinDenoise = buildActiveParamsSelector((params) => params.canvasCoherenceMinDenoise); -export const selectCanvasCoherenceMode = buildActiveParamsSelector((params) => params.canvasCoherenceMode); -export const selectMaskBlur = buildActiveParamsSelector((params) => params.maskBlur); -export const selectInfillMethod = buildActiveParamsSelector((params) => params.infillMethod); -export const selectInfillTileSize = buildActiveParamsSelector((params) => params.infillTileSize); -export const selectInfillPatchmatchDownscaleSize = buildActiveParamsSelector( +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 = buildActiveParamsSelector((params) => params.infillColorValue); -export const selectImg2imgStrength = buildActiveParamsSelector((params) => params.img2imgStrength); -export const selectOptimizedDenoisingEnabled = buildActiveParamsSelector((params) => params.optimizedDenoisingEnabled); -export const selectPositivePrompt = buildActiveParamsSelector((params) => params.positivePrompt); -export const selectNegativePrompt = buildActiveParamsSelector((params) => params.negativePrompt); -export const selectNegativePromptWithFallback = buildActiveParamsSelector((params) => params.negativePrompt ?? ''); -export const selectHasNegativePrompt = buildActiveParamsSelector((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) @@ -743,45 +741,45 @@ export const selectModelSupportsOptimizedDenoising = createSelector( selectModel, (model) => !!model && SUPPORTS_OPTIMIZED_DENOISING_BASE_MODELS.includes(model.base) ); -export const selectScheduler = buildActiveParamsSelector((params) => params.scheduler); -export const selectSeamlessXAxis = buildActiveParamsSelector((params) => params.seamlessXAxis); -export const selectSeamlessYAxis = buildActiveParamsSelector((params) => params.seamlessYAxis); -export const selectSeed = buildActiveParamsSelector((params) => params.seed); -export const selectShouldRandomizeSeed = buildActiveParamsSelector((params) => params.shouldRandomizeSeed); -export const selectVAEPrecision = buildActiveParamsSelector((params) => params.vaePrecision); -export const selectIterations = buildActiveParamsSelector((params) => params.iterations); -export const selectShouldUseCPUNoise = buildActiveParamsSelector((params) => params.shouldUseCpuNoise); - -export const selectUpscaleScheduler = buildActiveParamsSelector((params) => params.upscaleScheduler); -export const selectUpscaleCfgScale = buildActiveParamsSelector((params) => params.upscaleCfgScale); - -export const selectPositivePromptHistory = buildActiveParamsSelector((params) => params.positivePromptHistory); -export const selectRefinerCFGScale = buildActiveParamsSelector((params) => params.refinerCFGScale); -export const selectRefinerModel = buildActiveParamsSelector((params) => params.refinerModel); -export const selectIsRefinerModelSelected = buildActiveParamsSelector((params) => Boolean(params.refinerModel)); -export const selectRefinerPositiveAestheticScore = buildActiveParamsSelector( +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 = buildActiveParamsSelector( +export const selectRefinerNegativeAestheticScore = buildActiveTabParamsSelector( (params) => params.refinerNegativeAestheticScore ); -export const selectRefinerScheduler = buildActiveParamsSelector((params) => params.refinerScheduler); -export const selectRefinerStart = buildActiveParamsSelector((params) => params.refinerStart); -export const selectRefinerSteps = buildActiveParamsSelector((params) => params.refinerSteps); - -export const selectWidth = buildActiveParamsSelector((params) => params.dimensions.width); -export const selectHeight = buildActiveParamsSelector((params) => params.dimensions.height); -export const selectAspectRatioID = buildActiveParamsSelector((params) => params.dimensions.aspectRatio.id); -export const selectAspectRatioValue = buildActiveParamsSelector((params) => params.dimensions.aspectRatio.value); -export const selectAspectRatioIsLocked = buildActiveParamsSelector((params) => params.dimensions.aspectRatio.isLocked); -export const selectOptimalDimension = buildActiveParamsSelector((params) => +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 = buildActiveParamsSelector((params) => getGridSize(params.model?.base ?? null)); +export const selectGridSize = buildActiveTabParamsSelector((params) => getGridSize(params.model?.base ?? null)); export const selectMainModelConfig = createSelector( selectModelConfigsQuery, - selectActiveParams, + selectActiveTabParams, (modelConfigs, { model }) => { if (!modelConfigs.data) { return null; 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 beb6dd92b6f..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,7 +1,7 @@ import type { RootState } from 'app/store/store'; import { generateSeeds } from 'common/util/generateSeeds'; import { range } from 'es-toolkit/compat'; -import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; +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'; @@ -35,7 +35,7 @@ export const prepareLinearUIBatch = (arg: { destination: string; }): EnqueueBatchArg => { const { state, g, base, prepend, positivePromptNode, seedNode, origin, destination } = arg; - const { iterations, shouldRandomizeSeed, seed } = selectActiveParams(state); + 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 b07fa3c6423..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,6 +1,6 @@ import type { RootState } from 'app/store/store'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; +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'; @@ -19,7 +19,7 @@ export const buildMultidiffusionUpscaleGraph = async (state: RootState): Promise steps, vaePrecision, vae, - } = selectActiveParams(state); + } = 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 1b185bb84b7..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 @@ -3,7 +3,7 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getDenoisingStartAndEnd, @@ -35,7 +35,7 @@ export const addFLUXFill = async ({ denoise.width = scaledSize.width; denoise.height = scaledSize.height; - const params = selectActiveParams(state); + const params = selectActiveTabParams(state); const canvasSettings = selectCanvasSettingsByCanvasId(state, manager.canvasId); const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer'); 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 c26df3a3b0a..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 @@ -3,7 +3,7 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getDenoisingStartAndEnd, @@ -48,7 +48,7 @@ export const addInpaint = async ({ denoise.denoising_start = denoising_start; denoise.denoising_end = denoising_end; - const params = selectActiveParams(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/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index d4b66439dae..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 @@ -3,7 +3,7 @@ import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectCanvasSettingsByCanvasId } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { getDenoisingStartAndEnd, @@ -50,7 +50,7 @@ export const addOutpaint = async ({ denoise.denoising_start = denoising_start; denoise.denoising_end = denoising_end; - const params = selectActiveParams(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/addSDXLRefiner.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addSDXLRefiner.ts index 0edb6e50f6b..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,6 +1,6 @@ import type { RootState } from 'app/store/store'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; +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'; @@ -24,7 +24,7 @@ export const addSDXLRefiner = async ( refinerScheduler, refinerCFGScale, refinerStart, - } = selectActiveParams(state); + } = 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 8e6fceabdb8..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,6 +1,6 @@ import type { RootState } from 'app/store/store'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Invocation } from 'services/api/types'; @@ -22,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 } = selectActiveParams(state); + 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/buildCogView4Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildCogView4Graph.ts index 09cc5bf681f..cae0a13f09e 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,6 +1,6 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectActiveParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage'; @@ -29,7 +29,7 @@ export const buildCogView4Graph = 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 4d7e6f08d4f..6a60c8a7ae1 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,6 +1,6 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { selectActiveParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; import { selectCanvasByCanvasId, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { addControlNets, addT2IAdapters } from 'features/nodes/util/graph/generation/addControlAdapters'; @@ -35,7 +35,7 @@ export const buildSD1Graph = async (arg: GraphBuilderArg): Promise assert(model, 'No model selected'); assert(model.base === 'veo3', 'Selected model is not a Veo3 model'); - const params = selectActiveParams(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 ab38dbfd398..a3936282444 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -4,7 +4,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectSaveAllImagesToGallery } from 'features/controlLayers/store/canvasSettingsSlice'; import { selectCanvasStagingAreaSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { - selectActiveParams, + selectActiveTabParams, selectImg2imgStrength, selectMainModelConfig, selectOptimizedDenoisingEnabled, @@ -81,7 +81,7 @@ export const selectCanvasDestination = (state: RootState, canvasId: string) => { * Gets the prompts, modified for the active style preset. */ export const selectPresetModifiedPrompts = createSelector( - selectActiveParams, + selectActiveTabParams, selectStylePresetSlice, selectListStylePresetsRequestState, (params, stylePresetSlice, listStylePresetsRequestState) => { @@ -121,7 +121,7 @@ export const selectPresetModifiedPrompts = createSelector( export const getOriginalAndScaledSizesForTextToImage = (state: RootState) => { const tab = selectActiveTab(state); - const params = selectActiveParams(state); + const params = selectActiveTabParams(state); if (tab === 'canvas') { const canvas = selectActiveCanvas(state); diff --git a/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx b/invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx index 3990f0edc5a..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 { selectActiveParams } 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(selectActiveParams, 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/queue/hooks/useEnqueueCanvas.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts index 79b301ab059..595aae81e9d 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts @@ -9,7 +9,7 @@ import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasMana import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { positivePromptAddedToHistory, - selectActiveParams, + selectActiveTabParams, selectPositivePrompt, } from 'features/controlLayers/store/paramsSlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; @@ -42,7 +42,7 @@ const enqueueCanvas = async (store: AppStore, canvasManager: CanvasManager, prep const state = getState(); const destination = selectCanvasDestination(state, canvasManager.canvasId); - const params = selectActiveParams(state); + const params = selectActiveTabParams(state); const model = params.model; if (!model) { diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts index ab59dca57aa..10c55dbbca1 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueGenerate.ts @@ -7,7 +7,7 @@ import { extractMessageFromAssertionError } from 'common/util/extractMessageFrom import { withResult, withResultAsync } from 'common/util/result'; import { positivePromptAddedToHistory, - selectActiveParams, + selectActiveTabParams, selectPositivePrompt, } from 'features/controlLayers/store/paramsSlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; @@ -39,7 +39,7 @@ const enqueueGenerate = async (store: AppStore, prepend: boolean) => { dispatch(enqueueRequestedGenerate()); const state = getState(); - const params = selectActiveParams(state); + const params = selectActiveTabParams(state); const model = params.model; if (!model) { diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts index bfb1906c49a..e551f037064 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueUpscaling.ts @@ -4,7 +4,7 @@ import type { AppStore } from 'app/store/store'; import { useAppStore } from 'app/store/storeHooks'; import { positivePromptAddedToHistory, - selectActiveParams, + selectActiveTabParams, selectPositivePrompt, } from 'features/controlLayers/store/paramsSlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; @@ -22,7 +22,7 @@ const enqueueUpscaling = async (store: AppStore, prepend: boolean) => { dispatch(enqueueRequestedUpscaling()); const state = getState(); - const params = selectActiveParams(state); + const params = selectActiveTabParams(state); const model = params.model; if (!model) { diff --git a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueWorkflows.ts b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueWorkflows.ts index 9093423648c..146401d65f9 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useEnqueueWorkflows.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useEnqueueWorkflows.ts @@ -2,7 +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 { selectActiveParams } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams } from 'features/controlLayers/store/paramsSlice'; import { $outputNodeId, getPublishInputs, @@ -133,7 +133,7 @@ const enqueueWorkflows = async ( const nodesState = selectNodesSlice(state); const graph = buildNodesGraph(state, templates); const workflow = buildWorkflowWithValidation(nodesState); - const params = selectActiveParams(state); + const params = selectActiveTabParams(state); if (workflow) { // embedded workflows don't have an id diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index d99136a57a5..9abfa547cc2 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -10,7 +10,7 @@ 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 { selectActiveParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; import type { CanvasEntity, LoRA, RefImagesState, TabParamsState } from 'features/controlLayers/store/types'; @@ -199,7 +199,7 @@ export const useReadinessWatcher = () => { const canvasManager = useCanvasManagerSafe(); const tab = useAppSelector(selectActiveTab); const canvas = useAppSelector(selectActiveCanvas); - const params = useAppSelector(selectActiveParams); + const params = useAppSelector(selectActiveTabParams); const refImages = useAppSelector(selectRefImagesSlice); const dynamicPrompts = useAppSelector(selectDynamicPromptsSlice); const nodes = useAppSelector(selectNodesSlice); @@ -821,7 +821,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: { }; export const selectPromptsCount = createSelector( - selectActiveParams, + 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 94dbf3793b0..f0d9dd9f1a8 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,7 @@ 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 { selectActiveParams, selectIsFLUX, selectIsSD3, 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 +36,7 @@ export const AdvancedSettingsAccordion = memo(() => { const selectBadges = useMemo( () => - createMemoizedSelector([selectActiveParams, 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 9e5b7030180..6f035625a84 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,7 @@ 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 { selectActiveParams, selectIsFLUX, selectIsSD3, 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 +23,7 @@ export const UpscaleTabAdvancedSettingsAccordion = memo(() => { const selectBadges = useMemo( () => - createMemoizedSelector([selectActiveParams, 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 89cb41a975d..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 { selectActiveParams, selectIsRefinerModelSelected } 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(selectActiveParams, (params) => +const selectBadges = createMemoizedSelector(selectActiveTabParams, (params) => params.refinerModel ? ['Enabled'] : undefined ); From 8811a13bb79030cb695db84eafe0544edcbe126f Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Sat, 4 Oct 2025 12:31:27 +0200 Subject: [PATCH 16/16] tabSlice created --- .../middleware/actionContextMiddleware.ts | 7 +- .../listeners/modelSelected.ts | 4 +- .../listeners/modelsLoaded.ts | 4 +- .../listeners/setDefaultSettings.ts | 2 +- invokeai/frontend/web/src/app/store/store.ts | 12 +- invokeai/frontend/web/src/app/store/util.ts | 2 +- .../frontend/web/src/app/types/invokeai.ts | 2 +- .../common/components/SessionMenuItems.tsx | 2 +- .../components/RefImage/RefImageImage.tsx | 2 +- .../components/RefImage/RefImageList.tsx | 2 +- .../components/RefImage/RefImageSettings.tsx | 2 +- .../RegionalGuidanceRefImageImage.tsx | 2 +- .../controlLayers/store/canvasSlice.ts | 20 +-- .../controlLayers/store/lorasSlice.ts | 71 +++++---- .../controlLayers/store/paramsSlice.ts | 145 +++++------------- .../controlLayers/store/refImagesSlice.ts | 56 ++++--- .../features/controlLayers/store/selectors.ts | 5 + .../features/controlLayers/store/tabSlice.ts | 123 +++++++++++++++ .../src/features/controlLayers/store/types.ts | 53 ++++--- .../src/features/dnd/FullscreenDropzone.tsx | 2 +- .../ContextMenuItemLocateInGalery.tsx | 2 +- .../ContextMenu/SingleSelectionMenuItems.tsx | 2 +- .../ImageViewer/CurrentImageButtons.tsx | 2 +- .../ImageViewer/CurrentVideoButtons.tsx | 2 +- .../ImageViewer/NoContentForViewer.tsx | 2 +- .../hooks/useRecallAllImageMetadata.ts | 2 +- .../gallery/hooks/useRecallCLIPSkip.ts | 4 +- .../gallery/hooks/useRecallDimensions.ts | 2 +- .../gallery/hooks/useRecallPrompts.ts | 4 +- .../features/gallery/hooks/useRecallRemix.ts | 2 +- .../features/gallery/hooks/useRecallSeed.ts | 4 +- .../features/gallery/store/gallerySlice.ts | 5 +- .../src/features/lora/components/LoRACard.tsx | 7 +- .../web/src/features/metadata/parsing.tsx | 2 +- .../flow/AddNodeCmdk/AddNodeCmdk.tsx | 2 +- .../util/graph/generation/addFLUXLoRAs.ts | 3 +- .../nodes/util/graph/generation/addLoRAs.ts | 3 +- .../util/graph/generation/addSDXLLoRAs.ts | 3 +- .../graph/generation/buildChatGPT4oGraph.ts | 3 +- .../graph/generation/buildCogView4Graph.ts | 3 +- .../util/graph/generation/buildFLUXGraph.ts | 3 +- .../util/graph/generation/buildSD1Graph.ts | 3 +- .../util/graph/generation/buildSD3Graph.ts | 3 +- .../util/graph/generation/buildSDXLGraph.ts | 3 +- .../nodes/util/graph/graphBuilderUtils.ts | 7 +- .../components/Core/ParamPositivePrompt.tsx | 2 +- .../components/Dimensions/DimensionsWidth.tsx | 2 +- .../parameters/components/Prompts/Prompts.tsx | 2 +- .../InvokeButtonTooltip.tsx | 2 +- .../components/QueueActionsMenuButton.tsx | 2 +- .../web/src/features/queue/hooks/useInvoke.ts | 2 +- .../web/src/features/queue/store/readiness.ts | 16 +- .../AdvancedSettingsAccordion.tsx | 7 +- .../UpscaleTabAdvancedSettingsAccordion.tsx | 7 +- .../features/system/hooks/useFeatureStatus.ts | 2 +- .../src/features/ui/components/AppContent.tsx | 2 +- .../src/features/ui/components/TabButton.tsx | 4 +- .../ui/layouts/CanvasTabEditableTitle.tsx | 7 +- .../src/features/ui/layouts/CanvasTabs.tsx | 2 +- .../ui/layouts/DockviewTabLaunchpad.tsx | 4 +- .../LaunchpadGenerateFromTextButton.tsx | 2 +- .../ui/layouts/auto-layout-context.tsx | 2 +- .../ui/layouts/canvas-tab-auto-layout.tsx | 2 +- .../ui/layouts/generate-tab-auto-layout.tsx | 2 +- .../ui/layouts/models-tab-auto-layout.tsx | 2 +- .../src/features/ui/layouts/navigation-api.ts | 3 +- .../ui/layouts/queue-tab-auto-layout.tsx | 2 +- .../ui/layouts/upscaling-tab-auto-layout.tsx | 2 +- .../layouts/use-collapsible-gridview-panel.ts | 2 +- .../features/ui/layouts/use-gallery-panel.ts | 2 +- .../ui/layouts/use-navigation-api.tsx | 7 +- .../ui/layouts/video-tab-auto-layout.tsx | 2 +- .../ui/layouts/workflows-tab-auto-layout.tsx | 2 +- .../web/src/features/ui/store/uiSelectors.ts | 1 - .../web/src/features/ui/store/uiSlice.ts | 4 - .../web/src/features/ui/store/uiTypes.ts | 5 - 76 files changed, 391 insertions(+), 310 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/tabSlice.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/actionContextMiddleware.ts b/invokeai/frontend/web/src/app/store/middleware/actionContextMiddleware.ts index 3cab63aad2e..6bad65e5893 100644 --- a/invokeai/frontend/web/src/app/store/middleware/actionContextMiddleware.ts +++ b/invokeai/frontend/web/src/app/store/middleware/actionContextMiddleware.ts @@ -1,9 +1,8 @@ import type { Middleware, UnknownAction } from '@reduxjs/toolkit'; import { injectTabActionContext } from 'app/store/util'; import { isCanvasInstanceAction } from 'features/controlLayers/store/canvasSlice'; -import { isTabParamsStateAction } from 'features/controlLayers/store/paramsSlice'; -import { selectActiveCanvasId } from 'features/controlLayers/store/selectors'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; +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; @@ -20,5 +19,5 @@ export const actionContextMiddleware: Middleware = (store) => (next) => (action) }; const isTabActionContextRequired = (action: UnknownAction) => { - return isTabParamsStateAction(action) || isCanvasInstanceAction(action); + return isTabInstanceParamsAction(action) || isCanvasInstanceAction(action); }; 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 565bc4bf26b..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 @@ -6,7 +6,7 @@ import { buildSelectIsStagingBySessionId, selectActiveCanvasStagingAreaSessionId, } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; +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 { @@ -55,7 +55,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = 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; 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 b89a2aec086..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 @@ -2,7 +2,7 @@ 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, @@ -194,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; 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 457e67c0cc6..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 @@ -17,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, @@ -30,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'; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 77595a69a37..492a0565afa 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -19,9 +19,7 @@ import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMidd import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected'; import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/slice'; import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice'; -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'; @@ -63,12 +61,10 @@ const SLICE_CONFIGS = { [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, @@ -87,12 +83,10 @@ const ALL_REDUCERS = { [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, [nodesSliceConfig.slice.reducerPath]: undoableNodesSliceReducer, - [paramsSliceConfig.slice.reducerPath]: paramsSliceConfig.slice.reducer, [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, diff --git a/invokeai/frontend/web/src/app/store/util.ts b/invokeai/frontend/web/src/app/store/util.ts index 742188fa09f..8c7df447672 100644 --- a/invokeai/frontend/web/src/app/store/util.ts +++ b/invokeai/frontend/web/src/app/store/util.ts @@ -1,5 +1,5 @@ import type { UnknownAction } from '@reduxjs/toolkit'; -import type { TabName } from 'features/ui/store/uiTypes'; +import type { TabName } from 'features/controlLayers/store/types'; const TAB_KEY = Symbol('tab'); const CANVAS_ID_KEY = Symbol('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 952fa4a1f0b..20f4fa6bc10 100644 --- a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx +++ b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { allEntitiesDeleted, inpaintMaskAdded } from 'features/controlLayers/store/canvasSlice'; 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'; 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 e0ac13d4571..b65facc276f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx @@ -7,6 +7,7 @@ import { UploadImageIconButton } from 'common/hooks/useImageUploadButton'; import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasSlice'; 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'; 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 0a337f43ae5..64fd77cb6fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageList.tsx @@ -13,10 +13,10 @@ import { 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'; 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/RegionalGuidanceRefImageImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx index 2e4ac5693e2..b56ac232415 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx @@ -6,12 +6,12 @@ import { UploadImageIconButton } from 'common/hooks/useImageUploadButton'; import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging'; import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasSlice'; 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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index ba38e9b02d1..6fb10768d8a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -65,7 +65,7 @@ import { getInitialCanvasStagingAreaState, isCanvasStagingAreaStateAction, } from './canvasStagingAreaSlice'; -import { getInitialTabParamsState, isTabParamsStateAction, tabParamsState } from './paramsSlice'; +import { getInitialTabInstanceParamsState, isTabInstanceParamsAction, tabInstanceParamsSlice } from './tabSlice'; import type { AspectRatioID, BoundingBoxScaleMethod, @@ -139,7 +139,7 @@ const getInitialCanvasInstanceState = (id: string, name: string): CanvasInstance id, name, canvas: getInitialCanvasEntity(), - params: getInitialTabParamsState(), + params: getInitialTabInstanceParamsState(), settings: getInitialCanvasSettings(), staging: getInitialCanvasStagingAreaState(), }); @@ -242,7 +242,7 @@ const canvasSlice = createSlice({ }, }, extraReducers(builder) { - builder.addDefaultCase((state, action) => { + builder.addMatcher(isCanvasInstanceAction, (state, action) => { const context = extractTabActionContext(action); if (!context || context.tab !== 'canvas' || !context.canvasId) { @@ -263,19 +263,19 @@ const canvasInstanceState = createSlice({ name: 'canvasInstance', initialState: {} as CanvasInstanceStateWithHistory, reducers: { - canvasNameChanged: (state, action: PayloadAction<{ canvasId: string; name: string }>) => { + canvasNameChanged: (state, action: PayloadAction<{ name: string }>) => { const { name } = action.payload; state.name = name; }, }, extraReducers(builder) { - builder.addMatcher(isCanvasInstanceAction, (state, action) => { + builder.addDefaultCase((state, action) => { if (isCanvasEntityStateAction(action)) { state.canvas = undoableCanvasEntityReducer(state.canvas, action); } - if (isTabParamsStateAction(action)) { - state.params = tabParamsState.reducer(state.params, action); + if (isTabInstanceParamsAction(action)) { + state.params = tabInstanceParamsSlice.reducer(state.params, action); } if (isCanvasSettingsStateAction(action)) { state.settings = canvasSettingsState.reducer(state.settings, action); @@ -1934,11 +1934,13 @@ const syncScaledSize = (state: CanvasEntity) => { }; export const isCanvasInstanceAction = (action: UnknownAction) => + isCanvasInstanceStateAction(action) || isCanvasEntityStateAction(action) || - isTabParamsStateAction(action) || + isTabInstanceParamsAction(action) || isCanvasSettingsStateAction(action) || isCanvasStagingAreaStateAction(action); -const isCanvasEntityStateAction = isAnyOf(...Object.values(canvasEntityState.actions), canvasReset); +const isCanvasInstanceStateAction = isAnyOf(...Object.values(canvasInstanceState.actions)); +const isCanvasEntityStateAction = isAnyOf(...Object.values(canvasEntityState.actions), modelChanged, canvasReset); export const { // Canvas 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 30cde805ccc..f1d528f1f83 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -1,13 +1,10 @@ import type { PayloadAction, Selector } 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 { extractTabActionContext } from 'app/store/util'; 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, TabParamsState } from 'features/controlLayers/store/types'; +import type { AspectRatioID, ParamsState, RgbaColor } from 'features/controlLayers/store/types'; import { ASPECT_RATIO_MAP, CHATGPT_ASPECT_RATIOS, @@ -21,7 +18,6 @@ import { isImagenAspectRatioID, MAX_POSITIVE_PROMPT_HISTORY, zParamsState, - zTabParamsState, } from 'features/controlLayers/store/types'; import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { @@ -53,15 +49,13 @@ import type { ParameterVAEModel, } from 'features/parameters/types/parameterSchemas'; import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models'; import { isNonRefinerMainModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; import { modelChanged } from './actions'; -import { selectActiveCanvasId } from './selectors'; +import { selectActiveCanvasId, selectActiveTab } from './selectors'; -export const getInitialTabParamsState = (): TabParamsState => ({ +export const getInitialParamsState = (): ParamsState => ({ maskBlur: 16, maskBlurMethod: 'box', canvasCoherenceMode: 'Gaussian Blur', @@ -113,43 +107,9 @@ export const getInitialTabParamsState = (): TabParamsState => ({ }, }); -const getInitialParamsState = (): ParamsState => ({ - _version: 3, - generate: getInitialTabParamsState(), - upscaling: getInitialTabParamsState(), - video: getInitialTabParamsState(), -}); - -const paramsSlice = createSlice({ +export const paramsState = createSlice({ name: 'params', - initialState: getInitialParamsState, - reducers: {}, - extraReducers(builder) { - builder.addDefaultCase((state, action) => { - const context = extractTabActionContext(action); - - if (!context) { - return; - } - - switch (context.tab) { - case 'generate': - state.generate = tabParamsState.reducer(state.generate, action); - break; - case 'upscaling': - state.upscaling = tabParamsState.reducer(state.upscaling, action); - break; - case 'video': - state.video = tabParamsState.reducer(state.video, action); - break; - } - }); - }, -}); - -export const tabParamsState = createSlice({ - name: 'tabParams', - initialState: {} as TabParamsState, + initialState: {} as ParamsState, reducers: { setIterations: (state, action: PayloadAction) => { state.iterations = action.payload; @@ -197,49 +157,49 @@ export const tabParamsState = createSlice({ }, vaeSelected: (state, action: PayloadAction) => { // null is a valid VAE! - const result = zTabParamsState.shape.vae.safeParse(action.payload); + const result = zParamsState.shape.vae.safeParse(action.payload); if (!result.success) { return; } state.vae = result.data; }, fluxVAESelected: (state, action: PayloadAction) => { - const result = zTabParamsState.shape.fluxVAE.safeParse(action.payload); + const result = zParamsState.shape.fluxVAE.safeParse(action.payload); if (!result.success) { return; } state.fluxVAE = result.data; }, t5EncoderModelSelected: (state, action: PayloadAction) => { - const result = zTabParamsState.shape.t5EncoderModel.safeParse(action.payload); + const result = zParamsState.shape.t5EncoderModel.safeParse(action.payload); if (!result.success) { return; } state.t5EncoderModel = result.data; }, controlLoRAModelSelected: (state, action: PayloadAction) => { - const result = zTabParamsState.shape.controlLora.safeParse(action.payload); + const result = zParamsState.shape.controlLora.safeParse(action.payload); if (!result.success) { return; } state.controlLora = result.data; }, clipEmbedModelSelected: (state, action: PayloadAction) => { - const result = zTabParamsState.shape.clipEmbedModel.safeParse(action.payload); + const result = zParamsState.shape.clipEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipEmbedModel = result.data; }, clipLEmbedModelSelected: (state, action: PayloadAction) => { - const result = zTabParamsState.shape.clipLEmbedModel.safeParse(action.payload); + const result = zParamsState.shape.clipLEmbedModel.safeParse(action.payload); if (!result.success) { return; } state.clipLEmbedModel = result.data; }, clipGEmbedModelSelected: (state, action: PayloadAction) => { - const result = zTabParamsState.shape.clipGEmbedModel.safeParse(action.payload); + const result = zParamsState.shape.clipGEmbedModel.safeParse(action.payload); if (!result.success) { return; } @@ -279,7 +239,7 @@ export const tabParamsState = createSlice({ state.negativePrompt = action.payload; }, refinerModelChanged: (state, action: PayloadAction) => { - const result = zTabParamsState.shape.refinerModel.safeParse(action.payload); + const result = zParamsState.shape.refinerModel.safeParse(action.payload); if (!result.success) { return; } @@ -470,7 +430,7 @@ export const tabParamsState = createSlice({ extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { const { previousModel } = action.payload; - const result = zTabParamsState.shape.model.safeParse(action.payload.model); + const result = zParamsState.shape.model.safeParse(action.payload.model); if (!result.success) { return; } @@ -522,11 +482,11 @@ const getModelMaxClipSkip = (model: ParameterModel) => { return CLIP_SKIP_MAP[model.base]?.maxClip; }; -const resetState = (state: TabParamsState): TabParamsState => { +const resetState = (state: ParamsState): ParamsState => { // When a new session is requested, we need to keep the current model selections, plus dependent state // like VAE precision. Everything else gets reset to default. const oldState = deepClone(state); - const newState = getInitialTabParamsState(); + const newState = getInitialParamsState(); newState.dimensions = oldState.dimensions; newState.model = oldState.model; newState.vae = oldState.vae; @@ -538,7 +498,7 @@ const resetState = (state: TabParamsState): TabParamsState => { return newState; }; -export const isTabParamsStateAction = isAnyOf(...Object.values(tabParamsState.actions), modelChanged); +export const isTabParamsStateAction = isAnyOf(...Object.values(paramsState.actions), modelChanged); export const { setInfillMethod, @@ -596,45 +556,9 @@ export const { syncedToOptimalDimension, paramsReset, -} = tabParamsState.actions; - -export const paramsSliceConfig: SliceConfig = { - slice: paramsSlice, - 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; - } +} = paramsState.actions; - if (state._version === 1) { - // v1 -> v2, add positive prompt history - state._version = 2; - state.positivePromptHistory = []; - } - - if (state._version === 2) { - // Migrate from v2 to v3: slice represented shared params -> slice represents multiple tabs/canvases params - state = { - _version: 3, - generate: { ...state }, - upscaling: { ...state }, - video: { ...state }, - }; - } - - return zParamsState.parse(state); - }, - }, -}; - -const initialTabParamsState = getInitialTabParamsState(); +const initialTabParamsState = getInitialParamsState(); export const selectActiveTabParams = (state: RootState) => { const tab = selectActiveTab(state); @@ -642,22 +566,21 @@ export const selectActiveTabParams = (state: RootState) => { switch (tab) { case 'generate': - return state.params.generate; - case 'canvas': { - return state.canvas.canvases[canvasId]!.params; - } + return state.tab.generate.params; + case 'canvas': + return state.canvas.canvases[canvasId]!.params.params; case 'upscaling': - return state.params.upscaling; + return state.tab.upscaling.params; case 'video': - return state.params.video; + return state.tab.video.params; + default: + // Fallback for global controls in other tabs + return initialTabParamsState; } - - // Fallback for global controls - return initialTabParamsState; }; const buildActiveTabParamsSelector = - (selector: Selector) => + (selector: Selector) => (state: RootState) => selector(selectActiveTabParams(state)); @@ -698,7 +621,9 @@ export const selectCFGRescaleMultiplier = buildActiveTabParamsSelector((params) 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 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); @@ -708,7 +633,9 @@ export const selectInfillPatchmatchDownscaleSize = buildActiveTabParamsSelector( ); export const selectInfillColorValue = buildActiveTabParamsSelector((params) => params.infillColorValue); export const selectImg2imgStrength = buildActiveTabParamsSelector((params) => params.img2imgStrength); -export const selectOptimizedDenoisingEnabled = buildActiveTabParamsSelector((params) => params.optimizedDenoisingEnabled); +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 ?? ''); @@ -771,7 +698,9 @@ export const selectWidth = buildActiveTabParamsSelector((params) => params.dimen 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 selectAspectRatioIsLocked = buildActiveTabParamsSelector( + (params) => params.dimensions.aspectRatio.isLocked +); export const selectOptimalDimension = buildActiveTabParamsSelector((params) => getOptimalDimension(params.model?.base ?? null) ); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts index 9949c4fe2db..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 { SerializedStateFromDenyList, SliceConfig } from 'app/store/types'; -import { clamp, merge, omit } from 'es-toolkit/compat'; +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,27 +286,32 @@ export const { refImageFLUXReduxImageInfluenceChanged, refImageIsEnabledToggled, refImagesRecalled, -} = slice.actions; +} = refImagesSlice.actions; -const denyList = ['selectedEntityId', 'isPanelOpen'] as const; -type SerializedRefImagesState = SerializedStateFromDenyList; +export const refImagesDenyList = ['selectedEntityId', 'isPanelOpen'] as const; -export const refImagesSliceConfig: SliceConfig = { - slice, - schema: zRefImagesState, - getInitialState: getInitialRefImagesState, - persistConfig: { - migrate: (state) => zRefImagesState.parse(state), - serialize: (state) => omit(state, denyList), - deserialize: (state) => { - const refImagesState = state as SerializedRefImagesState; +const initialRefImages = getInitialRefImagesState(); - return merge(refImagesState, 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 eda52dae38a..12c000de424 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts @@ -14,6 +14,11 @@ import type { 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 */ 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 69b612ec3b9..106ff7c6e95 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -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,7 +687,17 @@ const zPositivePromptHistory = z .array(zParameterPositivePrompt) .transform((arr) => arr.slice(0, MAX_POSITIVE_PROMPT_HISTORY)); -export const zTabParamsState = z.object({ +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({ maskBlur: z.number(), maskBlurMethod: zParameterMaskBlurMethod, canvasCoherenceMode: zParameterCanvasCoherenceMode, @@ -730,15 +744,22 @@ export const zTabParamsState = z.object({ controlLora: zParameterControlLoRAModel.nullable(), dimensions: zDimensionsState, }); -export type TabParamsState = z.infer; -export const zParamsState = z.object({ - _version: z.literal(3), - generate: zTabParamsState, - upscaling: zTabParamsState, - video: zTabParamsState, -}); export type ParamsState = z.infer; +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(), entities: z.array(zCanvasInpaintMaskState), @@ -880,7 +901,7 @@ const zCanvasInstanceStateBase = z.object({ const zCanvasInstanceState = (canvasEntitySchema: T) => zCanvasInstanceStateBase.extend({ canvas: canvasEntitySchema, - params: zTabParamsState, + params: zTabInstanceParamsState, settings: zCanvasSettingsState, staging: zCanvasStagingAreaState, }); @@ -900,18 +921,6 @@ export const zCanvasStateWithHistory = zCanvasState(zCanvasInstanceStateWithHist export type CanvasState = z.infer; export type CanvasStateWithHistory = z.infer; -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 const zCanvasReferenceImageState_OLD = zCanvasEntityBase.extend({ type: z.literal('reference_image'), ipAdapter: z.discriminatedUnion('type', [ 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/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/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/useRecallAllImageMetadata.ts b/invokeai/frontend/web/src/features/gallery/hooks/useRecallAllImageMetadata.ts index 7dd64b77e4f..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/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 0b189c6414b..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/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 d839154d907..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/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 7272243d210..4da9e5fd7e7 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -192,7 +192,10 @@ export const gallerySliceConfig: SliceConfig omit(state, denyList), + serialize: (state) => { + const a = omit(state, denyList); + return a; + }, deserialize: (state) => { const galleryState = state as SerializedGalleryState; 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 c1d9ecb138b..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'; 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/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/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/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/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 cae0a13f09e..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 { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; -import { selectCanvasMetadata } from 'features/controlLayers/store/selectors'; +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'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts index e4eb95f10e7..965f8d68b4a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasByCanvasId, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; +import { selectActiveTab, selectCanvasByCanvasId, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { isFluxKontextReferenceImageConfig } from 'features/controlLayers/store/types'; import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators'; import { zImageField } from 'features/nodes/types/common'; @@ -20,7 +20,6 @@ import { Graph } from 'features/nodes/util/graph/generation/Graph'; import { selectCanvasOutputFields } from 'features/nodes/util/graph/graphBuilderUtils'; import type { GraphBuilderArg, GraphBuilderReturn, ImageOutputNodes } 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 { Invocation } from 'services/api/types'; import type { Equals } from 'tsafe'; 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 6a60c8a7ae1..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 @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasByCanvasId, selectCanvasMetadata } 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'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts index d7215f51f6b..4c2878466b8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD3Graph.ts @@ -1,7 +1,7 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; -import { selectCanvasMetadata } from 'features/controlLayers/store/selectors'; +import { selectActiveTab, selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import { addImageToImage } from 'features/nodes/util/graph/generation/addImageToImage'; import { addInpaint } from 'features/nodes/util/graph/generation/addInpaint'; import { addNSFWChecker } from 'features/nodes/util/graph/generation/addNSFWChecker'; @@ -11,7 +11,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'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index ee1aa69f9ad..7b650cc6352 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectCanvasByCanvasId, selectCanvasMetadata } 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'; 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 a3936282444..0487a8deffd 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -11,13 +11,12 @@ import { selectRefinerModel, selectRefinerStart, } from 'features/controlLayers/store/paramsSlice'; -import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; -import type { TabParamsState } from 'features/controlLayers/store/types'; +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'; @@ -158,7 +157,7 @@ export const getOriginalAndScaledSizesForOtherModes = (state: RootState) => { export const getInfill = ( g: Graph, - params: TabParamsState + params: ParamsState ): Invocation<'infill_patchmatch' | 'infill_cv2' | 'infill_lama' | 'infill_rgba' | 'infill_tile'> => { const { infillMethod, infillColorValue, infillPatchmatchDownscaleSize, infillTileSize } = params; 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 e4553d89147..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'; 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 0c49a4081ff..4af35abd917 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Dimensions/DimensionsWidth.tsx @@ -8,8 +8,8 @@ import { 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/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/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/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 9abfa547cc2..63b5be289aa 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -12,8 +12,8 @@ import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasMana import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; import { selectActiveTabParams, selectMainModelConfig } from 'features/controlLayers/store/paramsSlice'; import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; -import { selectActiveCanvas } from 'features/controlLayers/store/selectors'; -import type { CanvasEntity, LoRA, RefImagesState, TabParamsState } 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'; @@ -78,7 +76,7 @@ type UpdateReasonsArg = { tab: TabName; isConnected: boolean; canvas: CanvasEntity; - params: TabParamsState; + params: ParamsState; refImages: RefImagesState; dynamicPrompts: DynamicPromptsState; canvasIsFiltering: boolean; @@ -277,7 +275,7 @@ const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invok const getReasonsWhyCannotEnqueueVideoTab = (arg: { isConnected: boolean; video: VideoState; - params: TabParamsState; + params: ParamsState; dynamicPrompts: DynamicPromptsState; promptExpansionRequest: PromptExpansionRequestState; isVideoEnabled: boolean; @@ -319,7 +317,7 @@ const getReasonsWhyCannotEnqueueVideoTab = (arg: { const getReasonsWhyCannotEnqueueGenerateTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; - params: TabParamsState; + params: ParamsState; refImages: RefImagesState; loras: LoRA[]; dynamicPrompts: DynamicPromptsState; @@ -490,7 +488,7 @@ const getReasonsWhyCannotEnqueueUpscaleTab = (arg: { isConnected: boolean; upscale: UpscaleState; config: AppConfig; - params: TabParamsState; + params: ParamsState; loras: LoRA[]; promptExpansionRequest: PromptExpansionRequestState; }) => { @@ -553,7 +551,7 @@ const getReasonsWhyCannotEnqueueCanvasTab = (arg: { isConnected: boolean; model: MainModelConfig | null | undefined; canvas: CanvasEntity; - params: TabParamsState; + params: ParamsState; refImages: RefImagesState; loras: LoRA[]; dynamicPrompts: DynamicPromptsState; 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 f0d9dd9f1a8..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 { selectActiveTabParams, selectIsFLUX, selectIsSD3, 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'; 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 6f035625a84..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 { selectActiveTabParams, selectIsFLUX, selectIsSD3, 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'; 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 index d115de7bd6e..fbf0ba979e7 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx @@ -7,21 +7,20 @@ import { memo, useCallback, useRef } from 'react'; import { PiPencilBold } from 'react-icons/pi'; interface CanvasTabEditableTitleProps { - canvasId: string; name: string; isActive: boolean; } -export const CanvasTabEditableTitle = memo(({ canvasId, name, isActive }: CanvasTabEditableTitleProps) => { +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({ canvasId, name: value })); + dispatch(canvasNameChanged({ name: value })); }, - [dispatch, canvasId] + [dispatch] ); const editable = useEditable({ diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx index d6011e9310d..0e8f9c319f3 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabs.tsx @@ -98,7 +98,7 @@ const CanvasTab = memo(({ id, name, isActive, canDelete }: CanvasTabProps) => { h="full" > - + 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/LaunchpadGenerateFromTextButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/LaunchpadGenerateFromTextButton.tsx index 98e77dac8a8..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/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/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 30a9f1417d1..5f79741a67a 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -12,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; }, @@ -75,7 +72,6 @@ const slice = createSlice({ }); export const { - setActiveTab, setShouldShowItemDetails, setShouldShowProgressInViewer, accordionStateChanged, 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: {},