From 34db96aea3828928b8d1aea90769ec40ad4122c5 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Wed, 10 Sep 2025 13:44:50 +0200 Subject: [PATCH 1/9] 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 dcce85f6edd66a8cd1584b78d23f68025cc1a0f8 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Thu, 11 Sep 2025 10:40:47 +0200 Subject: [PATCH 2/9] 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 4582c411349895447845cd38791d673f57b313ef Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Thu, 11 Sep 2025 11:06:07 +0200 Subject: [PATCH 3/9] build error fixed --- .../RasterLayerAdjustmentsPanel.tsx | 12 ++++++---- .../RasterLayerCurvesAdjustmentsEditor.tsx | 6 ++--- .../RasterLayerMenuItemsAdjustments.tsx | 13 ++++++---- .../RasterLayerSimpleAdjustmentsEditor.tsx | 6 ++--- .../controlLayers/store/canvasSlice.ts | 24 ++++++++++++------- 5 files changed, 38 insertions(+), 23 deletions(-) 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..f2132edd595 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -197,7 +197,8 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, adjustments } = action.payload; - const layer = selectEntity(state, entityIdentifier); + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -212,7 +213,8 @@ const slice = createSlice({ }, rasterLayerAdjustmentsReset: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - const layer = selectEntity(state, entityIdentifier); + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer?.adjustments) { return; } @@ -221,7 +223,8 @@ const slice = createSlice({ }, rasterLayerAdjustmentsCancel: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - const layer = selectEntity(state, entityIdentifier); + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer) { return; } @@ -232,7 +235,8 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, mode } = action.payload; - const layer = selectEntity(state, entityIdentifier); + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer?.adjustments) { return; } @@ -243,7 +247,8 @@ const slice = createSlice({ action: PayloadAction }, 'raster_layer'>> ) => { const { entityIdentifier, simple } = action.payload; - const layer = selectEntity(state, entityIdentifier); + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer?.adjustments) { return; } @@ -254,7 +259,8 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, channel, points } = action.payload; - const layer = selectEntity(state, entityIdentifier); + const canvas = getSelectedCanvas(state); + const layer = selectEntity(canvas, entityIdentifier); if (!layer?.adjustments) { return; } @@ -265,7 +271,8 @@ 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?.adjustments) { return; } @@ -276,7 +283,8 @@ 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?.adjustments) { return; } From aa79950c20910aa425550ee764089ef8dae7ef41 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Fri, 12 Sep 2025 12:26:12 +0200 Subject: [PATCH 4/9] nonUndoableActions filtered --- .../src/features/controlLayers/store/canvasSlice.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index f2132edd595..83235b054c2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -2097,14 +2097,21 @@ const syncScaledSize = (canvas: CanvasState) => { let filter = true; +const nonUndoableActions: string[] = [ + canvasAdded.type, + canvasSelected.type, + canvasNameChanged.type, + canvasDeleted.type, +]; + 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(slice.name) || (nonUndoableActions.includes(action.type))) { return false; } // Throttle rapid actions of the same type From e5933183fd791a1439508802ef1be609f58d443d Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Mon, 15 Sep 2025 13:44:41 +0200 Subject: [PATCH 5/9] undo/redo handles multiple canvases --- invokeai/frontend/web/src/app/store/store.ts | 21 +- invokeai/frontend/web/src/app/store/types.ts | 21 +- .../controlLayers/store/canvasSlice.ts | 917 ++++++++---------- .../features/controlLayers/store/selectors.ts | 17 +- .../src/features/controlLayers/store/types.ts | 27 +- .../src/features/nodes/store/nodesSlice.ts | 14 +- .../ui/layouts/CanvasTabEditableTitle.tsx | 6 +- 7 files changed, 485 insertions(+), 538 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 12fcfa5a406..c14fd5ed4de 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, undoableCanvasSliceReducer } 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]: undoableCanvasSliceReducer, [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, @@ -162,7 +153,7 @@ const unserialize: UnserializeFunction = (data, key) => { // Undoable slices must be wrapped in a history! if (undoableConfig) { - return newHistory([], state, []); + return undoableConfig.wrapState(state); } else { return state; } @@ -175,7 +166,7 @@ const serialize: SerializeFunction = (data, key) => { } const result = omit( - sliceConfig.undoableConfig ? data.present : data, + sliceConfig.undoableConfig ? sliceConfig.undoableConfig.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..9e740acdf89 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,7 +27,7 @@ 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. */ @@ -39,8 +38,18 @@ export type SliceConfig = { */ 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/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 83235b054c2..11856577052 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -5,6 +5,7 @@ import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/uti import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; import { merge } from 'es-toolkit/compat'; +import { isPlainObject, omit } from 'es-toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { canvasReset } from 'features/controlLayers/store/actions'; import { modelChanged } from 'features/controlLayers/store/paramsSlice'; @@ -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, @@ -86,7 +89,8 @@ import { isImagenAspectRatioID, isRegionalGuidanceFLUXReduxConfig, isRegionalGuidanceIPAdapterConfig, - zCanvasesState, + zCanvasesStateWithHistory, + zCanvasesStateWithoutHistory, } from './types'; import { converters, @@ -103,20 +107,12 @@ import { initialT2IAdapter, makeDefaultRasterLayerAdjustments, } from './util'; +import { assert } from 'tsafe'; +import { Canvas } from 'konva/lib/Canvas'; -const getInitialCanvasesState = (): CanvasesState => { - const canvasId = getPrefixedId('canvas'); - const canvasName = 'default'; - const canvas = getCanvasState(canvasId, canvasName); +type CanvasDeletedPayloadAction = PayloadAction<{ id: string }>; - 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 +130,44 @@ const getCanvasState = (id: string, name: string): CanvasState => ({ }, }); +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 slice = 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; @@ -178,27 +199,33 @@ const slice = createSlice({ canvas.name = name; }, - canvasDeleted: (state, action: PayloadAction<{ id: string }>) => { + canvasDeleted: (state, action: CanvasDeletedPayloadAction) => { 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 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, action: PayloadAction> ) => { const { entityIdentifier, adjustments } = action.payload; - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -213,8 +240,7 @@ const slice = createSlice({ }, rasterLayerAdjustmentsReset: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer?.adjustments) { return; } @@ -223,8 +249,7 @@ const slice = createSlice({ }, rasterLayerAdjustmentsCancel: (state, action: PayloadAction>) => { const { entityIdentifier } = action.payload; - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer) { return; } @@ -235,8 +260,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, mode } = action.payload; - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer?.adjustments) { return; } @@ -247,8 +271,7 @@ const slice = createSlice({ action: PayloadAction }, 'raster_layer'>> ) => { const { entityIdentifier, simple } = action.payload; - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer?.adjustments) { return; } @@ -259,8 +282,7 @@ const slice = createSlice({ action: PayloadAction> ) => { const { entityIdentifier, channel, points } = action.payload; - const canvas = getSelectedCanvas(state); - const layer = selectEntity(canvas, entityIdentifier); + const layer = selectEntity(state, entityIdentifier); if (!layer?.adjustments) { return; } @@ -271,8 +293,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?.adjustments) { return; } @@ -283,8 +304,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?.adjustments) { return; } @@ -303,16 +323,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) ); } @@ -320,11 +339,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: { @@ -339,10 +358,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: ( @@ -355,8 +372,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; } @@ -366,15 +382,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< @@ -396,8 +410,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; } @@ -407,15 +420,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< @@ -437,8 +448,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; } @@ -448,15 +458,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< @@ -482,27 +490,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: { @@ -517,10 +524,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: ( @@ -533,9 +538,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; } @@ -545,15 +548,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< @@ -575,9 +578,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; } @@ -587,15 +588,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< @@ -617,9 +618,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; } @@ -629,15 +628,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< @@ -660,9 +659,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; } @@ -742,9 +739,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; } @@ -755,9 +750,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; } @@ -768,9 +761,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; } @@ -781,9 +772,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; } @@ -804,27 +793,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?: { @@ -839,10 +827,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: ( @@ -855,9 +841,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; } @@ -867,15 +851,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< @@ -891,9 +875,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; } @@ -904,9 +886,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; } @@ -914,9 +894,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; } @@ -933,9 +911,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; } @@ -954,9 +930,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; } @@ -969,9 +943,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; } @@ -982,9 +954,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; } @@ -1001,9 +971,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; } @@ -1019,9 +987,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; } @@ -1040,9 +1006,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; } @@ -1065,9 +1029,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; } @@ -1116,9 +1078,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; } @@ -1142,27 +1102,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?: { @@ -1177,16 +1136,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 } @@ -1196,18 +1151,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; } @@ -1223,9 +1174,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; } @@ -1235,15 +1184,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< @@ -1256,9 +1203,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 } @@ -1268,66 +1213,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) { @@ -1337,206 +1274,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; } @@ -1544,9 +1463,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; } @@ -1556,9 +1473,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; } @@ -1570,14 +1485,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': { @@ -1585,25 +1500,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; } @@ -1611,9 +1524,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; } @@ -1624,9 +1535,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; } @@ -1637,9 +1546,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; } @@ -1647,9 +1554,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; } @@ -1658,9 +1563,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; } @@ -1670,9 +1573,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; } @@ -1683,14 +1584,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; } @@ -1705,9 +1604,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; } @@ -1722,9 +1619,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; } @@ -1737,8 +1632,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) { @@ -1750,96 +1644,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; @@ -1847,9 +1729,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; } @@ -1858,51 +1738,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: () => {}, @@ -1911,11 +1785,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 @@ -1936,24 +1808,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. @@ -1970,19 +1842,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, +} = slice.actions; + +export const { + canvasMetadataRecalled, + canvasUndo, + canvasRedo, + canvasClearHistory, // All entities entitySelected, bookmarkedEntityChanged, @@ -2074,44 +1968,18 @@ export const { inpaintMaskDenoiseLimitChanged, inpaintMaskDenoiseLimitDeleted, // inpaintMaskRecalled, -} = slice.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 - ); - } -}; +} = canvasSlice.actions; let filter = true; -const nonUndoableActions: string[] = [ - canvasAdded.type, - canvasSelected.type, - canvasNameChanged.type, - canvasDeleted.type, -]; - -const canvasUndoableConfig: UndoableOptions = { +const canvasUndoableConfig: 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(slice.name) || (nonUndoableActions.includes(action.type))) { + if (!action.type.startsWith(slice.name)) { return false; } // Throttle rapid actions of the same type @@ -2122,15 +1990,66 @@ const canvasUndoableConfig: UndoableOptions = { // debug: import.meta.env.MODE === 'development', }; -export const canvasSliceConfig: SliceConfig = { +const undoableCanvasReducer = undoable(canvasSlice.reducer, canvasUndoableConfig); + +export const undoableCanvasSliceReducer = ( + state: CanvasesStateWithHistory, + action: UnknownAction +): CanvasesStateWithHistory => { + state = slice.reducer(state, action); + + return { + ...state, + canvases: state.canvases.map((c) => + c.present.id === state.selectedCanvasId ? undoableCanvasReducer(c, action) : c + ), + }; +}; + +export const canvasSliceConfig: SliceConfig = { slice, getInitialState: getInitialCanvasesState, - schema: zCanvasesState, - persistConfig: { - migrate: (state) => zCanvasesState.parse(state), - }, + schema: zCanvasesStateWithHistory, undoableConfig: { - reduxUndoOptions: canvasUndoableConfig, + unwrapState: (state) => { + return { + _version: state._version, + selectedCanvasId: state.selectedCanvasId, + canvases: state.canvases.map((canvas) => canvas.present), + }; + }, + wrapState: (state) => { + const canvasesState = state as CanvasesStateWithoutHistory; + + return { + _version: canvasesState._version, + selectedCanvasId: canvasesState.selectedCanvasId, + canvases: canvasesState.canvases.map((canvas) => newHistory([], canvas, [])), + }; + }, + }, + persistConfig: { + 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); + } }, }; 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..3cfdc19dd31 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); +export 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..2ff095eda35 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, @@ -818,7 +821,12 @@ export const nodesSliceConfig: SliceConfig = { }, }, undoableConfig: { - reduxUndoOptions, + unwrapState: (state) => state.present, + wrapState: (state) => { + const nodesState = state as NodesState; + + return newHistory([], nodesState, []); + }, }, }; diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx index ace7f38f35c..84b9d68a9dc 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx @@ -17,9 +17,9 @@ 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 edd34a9b88b454f1c75792a4eaba36604b2a9d91 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Mon, 15 Sep 2025 13:59:54 +0200 Subject: [PATCH 6/9] UI build errors fixed --- .../features/controlLayers/store/canvasSlice.ts | 15 +++++++-------- .../web/src/features/controlLayers/store/types.ts | 2 +- .../ui/layouts/CanvasTabEditableTitle.tsx | 9 ++++++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 11856577052..cff59e60abe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -4,8 +4,8 @@ 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 { isPlainObject, omit } from 'es-toolkit'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { canvasReset } from 'features/controlLayers/store/actions'; import { modelChanged } from 'features/controlLayers/store/paramsSlice'; @@ -54,6 +54,7 @@ import { isIPAdapterModelConfig, type T2IAdapterModelConfig, } from 'services/api/types'; +import { assert } from 'tsafe'; import type { AspectRatioID, @@ -107,8 +108,6 @@ import { initialT2IAdapter, makeDefaultRasterLayerAdjustments, } from './util'; -import { assert } from 'tsafe'; -import { Canvas } from 'konva/lib/Canvas'; type CanvasDeletedPayloadAction = PayloadAction<{ id: string }>; @@ -146,14 +145,14 @@ const getInitialCanvasesState = (): CanvasesStateWithoutHistory => { selectedCanvasId: canvasId, canvases: [canvas], }; -} +}; const getInitialCanvasesHistoryState = (): CanvasesStateWithHistory => { const state = getInitialCanvasesState(); return { ...state, - canvases: state.canvases.map((canvas) => newHistory([], canvas, [])) + canvases: state.canvases.map((canvas) => newHistory([], canvas, [])), }; }; @@ -2039,17 +2038,17 @@ export const canvasSliceConfig: SliceConfig; const zCanvasStateWithHistory = zStateWithHistory(zCanvasState); -export const zCanvasesState = (canvasStateSchema: T) => +const zCanvasesState = (canvasStateSchema: T) => z.object({ _version: z.literal(4), selectedCanvasId: zId, diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasTabEditableTitle.tsx index 84b9d68a9dc..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((value: string) => { - dispatch(canvasNameChanged({ id, name: value })); - }, [dispatch, id]); + const onChange = useCallback( + (value: string) => { + dispatch(canvasNameChanged({ id, name: value })); + }, + [dispatch, id] + ); const editable = useEditable({ value: name, From 19ba409c39a7b3306a9dcecf01cd7d838f1590d2 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Tue, 16 Sep 2025 11:05:11 +0200 Subject: [PATCH 7/9] undo/redo refinements --- invokeai/frontend/web/src/app/store/store.ts | 15 ++--- invokeai/frontend/web/src/app/store/types.ts | 9 +-- .../controlLayers/store/canvasSlice.ts | 62 ++++++++++--------- .../src/features/nodes/store/nodesSlice.ts | 4 +- 4 files changed, 42 insertions(+), 48 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index c14fd5ed4de..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, undoableCanvasSliceReducer } 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'; @@ -90,7 +90,7 @@ const ALL_REDUCERS = { [api.reducerPath]: api.reducer, [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer, [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer, - [canvasSliceConfig.slice.reducerPath]: undoableCanvasSliceReducer, + [canvasSliceConfig.slice.reducerPath]: undoableCanvasesReducer, [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer, [configSliceConfig.slice.reducerPath]: configSliceConfig.slice.reducer, [dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer, @@ -119,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(); @@ -151,12 +151,7 @@ const unserialize: UnserializeFunction = (data, key) => { state = getInitialState(); } - // Undoable slices must be wrapped in a history! - if (undoableConfig) { - return undoableConfig.wrapState(state); - } else { - return state; - } + return persistConfig.wrapState ? persistConfig.wrapState(state) : state; }; const serialize: SerializeFunction = (data, key) => { @@ -166,7 +161,7 @@ const serialize: SerializeFunction = (data, key) => { } const result = omit( - sliceConfig.undoableConfig ? sliceConfig.undoableConfig.unwrapState(data) : 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 9e740acdf89..cdb203ab4fe 100644 --- a/invokeai/frontend/web/src/app/store/types.ts +++ b/invokeai/frontend/web/src/app/store/types.ts @@ -32,24 +32,19 @@ export type SliceConfig, TSe * 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?: { /** * Wraps state into state with history * * @param state The state without history * @returns The state with history */ - wrapState: (state: unknown) => TInternalState; + wrapState?: (state: unknown) => TInternalState; /** * Unwraps state with history * * @param state The state with history * @returns The state without history */ - unwrapState: (state: TInternalState) => TSerializedState; + unwrapState?: (state: TInternalState) => TSerializedState; }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index cff59e60abe..5a8a421daa2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -109,8 +109,6 @@ import { makeDefaultRasterLayerAdjustments, } from './util'; -type CanvasDeletedPayloadAction = PayloadAction<{ id: string }>; - const getInitialCanvasState = (id: string, name: string): CanvasState => ({ id, name, @@ -156,7 +154,7 @@ const getInitialCanvasesHistoryState = (): CanvasesStateWithHistory => { }; }; -const slice = createSlice({ +const canvasesSlice = createSlice({ name: 'canvas', initialState: getInitialCanvasesHistoryState(), reducers: { @@ -198,7 +196,7 @@ const slice = createSlice({ canvas.name = name; }, - canvasDeleted: (state, action: CanvasDeletedPayloadAction) => { + canvasDeleted: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; if (state.canvases.length === 1) { @@ -1869,7 +1867,7 @@ export const { canvasSelected, canvasNameChanged, canvasDeleted, -} = slice.actions; +} = canvasesSlice.actions; export const { canvasMetadataRecalled, @@ -1969,6 +1967,8 @@ export const { // inpaintMaskRecalled, } = canvasSlice.actions; +const isCanvasSliceAction = isAnyOf(...Object.values(canvasSlice.actions)); + let filter = true; const canvasUndoableConfig: UndoableOptions = { @@ -1978,7 +1978,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(slice.name)) { + if (!action.type.startsWith(canvasSlice.name)) { return false; } // Throttle rapid actions of the same type @@ -1991,11 +1991,15 @@ const canvasUndoableConfig: UndoableOptions = { const undoableCanvasReducer = undoable(canvasSlice.reducer, canvasUndoableConfig); -export const undoableCanvasSliceReducer = ( +export const undoableCanvasesReducer = ( state: CanvasesStateWithHistory, action: UnknownAction ): CanvasesStateWithHistory => { - state = slice.reducer(state, action); + state = canvasesSlice.reducer(state, action); + + if (!isCanvasSliceAction(action)) { + return state; + } return { ...state, @@ -2005,28 +2009,14 @@ export const undoableCanvasSliceReducer = ( }; }; -export const canvasSliceConfig: SliceConfig = { - slice, +export const canvasSliceConfig: SliceConfig< + typeof canvasesSlice, + CanvasesStateWithHistory, + CanvasesStateWithoutHistory +> = { + slice: canvasesSlice, getInitialState: getInitialCanvasesState, schema: zCanvasesStateWithHistory, - undoableConfig: { - unwrapState: (state) => { - return { - _version: state._version, - selectedCanvasId: state.selectedCanvasId, - canvases: state.canvases.map((canvas) => canvas.present), - }; - }, - wrapState: (state) => { - const canvasesState = state as CanvasesStateWithoutHistory; - - return { - _version: canvasesState._version, - selectedCanvasId: canvasesState.selectedCanvasId, - canvases: canvasesState.canvases.map((canvas) => newHistory([], canvas, [])), - }; - }, - }, persistConfig: { migrate: (state) => { assert(isPlainObject(state)); @@ -2049,6 +2039,22 @@ export const canvasSliceConfig: SliceConfig { + 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/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 2ff095eda35..8ac1f64d7e5 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -819,14 +819,12 @@ export const nodesSliceConfig: SliceConfig state.present, wrapState: (state) => { const nodesState = state as NodesState; return newHistory([], nodesState, []); }, + unwrapState: (state) => state.present, }, }; From b1ea45d789a4715ed17cdd74c4fbfd5a861e2f42 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Thu, 18 Sep 2025 10:06:19 +0200 Subject: [PATCH 8/9] Multiple canvases displayed --- .../common/components/SessionMenuItems.tsx | 7 +- .../components/InvokeCanvasComponent.tsx | 8 +- .../components/RefImage/RefImageList.tsx | 4 +- .../components/RefImage/RefImageSettings.tsx | 5 +- .../CanvasInstanceContextProvider.tsx | 26 +++ .../contexts/CanvasManagerProviderGate.tsx | 96 +++++++++-- .../controlLayers/hooks/useCanvasIsBusy.ts | 74 ++++++++- .../controlLayers/hooks/useInvokeCanvas.ts | 19 ++- .../controlLayers/konva/CanvasManager.ts | 15 +- .../features/controlLayers/store/ephemeral.ts | 4 +- ...ntextMenuItemNewCanvasFromImageSubMenu.tsx | 28 +--- ...ontextMenuItemNewLayerFromImageSubMenu.tsx | 4 +- .../web/src/features/imageActions/actions.ts | 13 +- .../ui/layouts/CanvasWorkspacePanel.tsx | 150 ++++++++++-------- 14 files changed, 311 insertions(+), 142 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx diff --git a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx index 0018a78622c..8740c3b0869 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 { useSelectedCanvasManagerSafe } 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 = useSelectedCanvasManagerSafe(); 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/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/RefImage/RefImageSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx index 0c9153f16fd..c7fd7867256 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx @@ -10,10 +10,7 @@ import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAd import { RefImageModel } from 'features/controlLayers/components/RefImage/RefImageModel'; import { RefImageNoImageState } from 'features/controlLayers/components/RefImage/RefImageNoImageState'; import { RefImageNoImageStateWithCanvasOptions } from 'features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions'; -import { - CanvasManagerProviderGate, - useCanvasManagerSafe, -} from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { CanvasManagerProviderGate, useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext'; import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice'; import { 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..e22261c578d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx @@ -0,0 +1,26 @@ +import type { PropsWithChildren } from 'react'; +import { createContext, memo, useContext } from 'react'; +import { assert } from 'tsafe'; + +const CanvasInstanceContext = createContext(null); + +export const CanvasInstanceContextProvider = memo(({ canvasId, children }: PropsWithChildren<{ canvasId: string }>) => { + return {children}; +}); +CanvasInstanceContextProvider.displayName = 'CanvasInstanceContextProvider'; + +export const useScopedCanvas = () => { + const canvasId = useContext(CanvasInstanceContext); + assert(canvasId, 'useCanvasInstanceContext must be used within a CanvasInstanceContext'); + return canvasId; +}; + +export const useScopedCanvasSafe = () => { + return useContext(CanvasInstanceContext); +}; + +export const useHasScopedCanvas = () => { + const canvasId = useContext(CanvasInstanceContext); + + return !!canvasId; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx index ca3528c7a0e..d5fc6597b4a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx @@ -1,31 +1,98 @@ import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { $canvasManager } from 'features/controlLayers/store/ephemeral'; +import { $canvasManagers } from 'features/controlLayers/store/ephemeral'; +import { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; import type { PropsWithChildren } from 'react'; -import { createContext, memo, useContext } from 'react'; +import { createContext, memo, useContext, useMemo } from 'react'; import { assert } from 'tsafe'; -const CanvasManagerContext = createContext(null); +import { useScopedCanvas, useScopedCanvasSafe } from './CanvasInstanceContextProvider'; + +const CanvasManagerContext = createContext<{ [canvasId: string]: CanvasManager } | null>(null); export const CanvasManagerProviderGate = memo(({ children }: PropsWithChildren) => { - const canvasManager = useStore($canvasManager); + const canvasManagers = useStore($canvasManagers); + const selectedCanvas = useAppSelector(selectSelectedCanvas); - if (!canvasManager) { + if((Object.keys(canvasManagers).length === 0) || !canvasManagers[selectedCanvas.id]) { return null; } - return {children}; + return {children}; }); CanvasManagerProviderGate.displayName = 'CanvasManagerProviderGate'; /** - * Consumes the CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise + * Consumes the scoped CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise + * it will throw an error. + */ +export const useScopedCanvasManager = (): CanvasManager => { + const canvasManagers = useContext(CanvasManagerContext); + assert(canvasManagers, 'useScopedCanvasManager must be used within a CanvasManagerProviderGate'); + + const scopedCanvasId = useScopedCanvas(); + const canvasManager = useMemo(() => { + return canvasManagers[scopedCanvasId]; + }, [canvasManagers, scopedCanvasId]); + assert(canvasManager, 'Scoped canvas manager not initialised'); + + return canvasManager; +}; + +/** + * Consumes the selected CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise * it will throw an error. */ +export const useSelectedCanvasManager = (): CanvasManager => { + const canvasManagers = useContext(CanvasManagerContext); + assert(canvasManagers, 'useScopedCanvasManager must be used within a CanvasManagerProviderGate'); + + const selectedCanvas = useAppSelector(selectSelectedCanvas); + const canvasManager = useMemo(() => { + return canvasManagers[selectedCanvas.id]; + }, [canvasManagers, selectedCanvas]); + assert(canvasManager, 'Selected canvas manager not initialised'); + + return canvasManager; +}; + +/** + * Consumes the scoped CanvasManager from the context. If the CanvasManager is not available, it will return null. + */ +export const useScopedCanvasManagerSafe = (): CanvasManager | null => { + const canvasManagers = useStore($canvasManagers); + const scopedCanvasId = useScopedCanvasSafe(); + + const canvasManager = useMemo(() => { + return scopedCanvasId ? canvasManagers[scopedCanvasId] : undefined; + }, [canvasManagers, scopedCanvasId]); + + return canvasManager ?? null; +}; + +/** + * Consumes the selected CanvasManager from the context. If the CanvasManager is not available, it will return null. + */ +export const useSelectedCanvasManagerSafe = (): CanvasManager | null => { + const canvasManagers = useStore($canvasManagers); + const selectedCanvas = useAppSelector(selectSelectedCanvas); + + const canvasManager = useMemo(() => { + return canvasManagers[selectedCanvas.id]; + }, [canvasManagers, selectedCanvas]); + + return canvasManager ?? null; +}; + +/** + * 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, 'Selected canvas manager not initialised'); + return canvasManager; }; @@ -33,6 +100,13 @@ 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 scopedCanvasId = useScopedCanvasSafe(); + const selectedCanvas = useAppSelector(selectSelectedCanvas); + + const canvasManager = useMemo(() => { + return scopedCanvasId ? canvasManagers[scopedCanvasId] : canvasManagers[selectedCanvas.id]; + }, [canvasManagers, selectedCanvas, scopedCanvasId]); + + return canvasManager ?? null; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts index 02eec59b702..278f024affb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts @@ -1,9 +1,16 @@ import { useStore } from '@nanostores/react'; import { $false } from 'app/store/nanostores/util'; -import { useCanvasManager, useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { useHasScopedCanvas } from 'features/controlLayers/contexts/CanvasInstanceContextProvider'; +import { + useScopedCanvasManager, + useScopedCanvasManagerSafe, + useSelectedCanvasManager, + useSelectedCanvasManagerSafe, +} from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { useMemo } from 'react'; /** - * Returns a boolena indicating whether the canvas is busy: + * Returns a boolena indicating whether the scoped canvas is busy: * - While staging * - While an entity is transforming * - While an entity is filtering @@ -11,15 +18,47 @@ import { useCanvasManager, useCanvasManagerSafe } from 'features/controlLayers/c * * This hook will throw an error if the canvas manager is not initialized. */ -export const useCanvasIsBusy = () => { - const canvasManager = useCanvasManager(); +export const useScopedCanvasIsBusy = () => { + const canvasManager = useScopedCanvasManager(); const isBusy = useStore(canvasManager.$isBusy); return isBusy; }; /** - * Returns a boolena indicating whether the canvas is busy: + * Returns a boolena indicating whether the selected 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 useSelectedCanvasIsBusy = () => { + const canvasManager = useSelectedCanvasManager(); + const isBusy = useStore(canvasManager.$isBusy); + + return isBusy; +}; + +/** + * Returns a boolena indicating whether the scoped 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 fall back to false if the canvas manager is not initialized. + */ +export const useScopedCanvasIsBusySafe = () => { + const canvasManager = useScopedCanvasManagerSafe(); + const isBusy = useStore(canvasManager?.$isBusy ?? $false); + + return isBusy; +}; + +/** + * Returns a boolena indicating whether the selected canvas is busy: * - While staging * - While an entity is transforming * - While an entity is filtering @@ -27,9 +66,30 @@ export const useCanvasIsBusy = () => { * * This hook will fall back to false if the canvas manager is not initialized. */ -export const useCanvasIsBusySafe = () => { - const canvasManager = useCanvasManagerSafe(); +export const useSelectedCanvasIsBusySafe = () => { + const canvasManager = useSelectedCanvasManagerSafe(); const isBusy = useStore(canvasManager?.$isBusy ?? $false); return isBusy; }; + +/** + * 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 fall back to false if the canvas manager is not initialized. + */ +export const useCanvasIsBusy = () => { + const hasScopedCanvas = useHasScopedCanvas(); + const isScopedBusy = useScopedCanvasIsBusySafe(); + const isSelectedBusy = useSelectedCanvasIsBusySafe(); + + const isBusy = useMemo(() => { + return hasScopedCanvas ? isScopedBusy : isSelectedBusy; + }, [hasScopedCanvas, isScopedBusy, isSelectedBusy]); + + return isBusy; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts index 76d997c705b..3313e9dfab3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts @@ -3,19 +3,19 @@ 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 Konva from 'konva'; import { useLayoutEffect, useState } from 'react'; import { $socket } from 'services/events/stores'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; +import { $canvasManagers } from '../store/ephemeral'; 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/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/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/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts index 14d27e900c1..1181080d67c 100644 --- a/invokeai/frontend/web/src/features/imageActions/actions.ts +++ b/invokeai/frontend/web/src/features/imageActions/actions.ts @@ -3,9 +3,9 @@ import { deepClone } from 'common/util/deepClone'; import { getDefaultRegionalGuidanceRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks'; import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import { canvasReset } from 'features/controlLayers/store/actions'; import { bboxChangedFromCanvas, + canvasAdded, canvasClearHistory, controlLayerAdded, entityRasterized, @@ -155,7 +155,6 @@ export const createNewCanvasEntityFromImage = async (arg: { /** * Creates a new canvas with the given image as the only layer: - * - Reset the canvas * - Resize the bbox to the image's aspect ratio at the optimal size for the selected model * - Add the image as a layer of the given type * - If `withResize`: Resizes the layer to fit the bbox using the 'fill' strategy @@ -214,7 +213,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasReset()); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(rasterLayerAdded({ overrides, isSelected: true })); @@ -231,7 +230,7 @@ export const newCanvasFromImage = async (arg: { controlAdapter: deepClone(initialControlNet), } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasReset()); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(controlLayerAdded({ overrides, isSelected: true })); @@ -247,7 +246,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasReset()); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(inpaintMaskAdded({ overrides, isSelected: true })); @@ -263,7 +262,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasReset()); + dispatch(canvasAdded({ isSelected: true })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(rgAdded({ overrides, isSelected: true })); @@ -277,7 +276,7 @@ export const newCanvasFromImage = async (arg: { const config = getDefaultRegionalGuidanceRefImageConfig(getState); config.image = imageDTOToImageWithDims(imageDTO); const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), config }]; - dispatch(canvasReset()); + dispatch(canvasAdded({ isSelected: true })); dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true })); if (withInpaintMask) { dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true })); diff --git a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx index 9c17dd1ba63..6e962be053c 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx @@ -16,9 +16,11 @@ 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 { selectCanvases } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; @@ -49,80 +51,100 @@ const canvasBgSx = { }, }; -export const CanvasWorkspacePanel = memo(() => { +interface CanvasProps { + canvasId: string; + isSelected: boolean +} + +const Canvas = memo(({ canvasId, isSelected }: CanvasProps) => { + const sessionId = useAppSelector(selectCanvasSessionId); const dynamicGrid = useAppSelector(selectDynamicGrid); const showHUD = useAppSelector(selectShowHUD); - const sessionId = useAppSelector(selectCanvasSessionId); const renderMenu = useCallback(() => { return ; }, []); return ( - - - - - - - - renderMenu={renderMenu} withLongPress={false}> - {(ref) => ( - - - - - {showHUD && } - - - - - - - - - } colorScheme="base" /> - - - - - - - )} - - - - - + - - - - - + + + + ); +}); +Canvas.displayName = 'Canvas'; + +export const CanvasWorkspacePanel = memo(() => { + const canvases = useAppSelector(selectCanvases); + + return ( + + + + + + + {canvases.map(({ id, isSelected }) => ( + + ))} + ); }); CanvasWorkspacePanel.displayName = 'CanvasWorkspacePanel'; From 03b85daed84ae9f63df11d2794752ca7837987c3 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Sun, 21 Sep 2025 10:30:50 +0200 Subject: [PATCH 9/9] 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 | 4 +- .../CanvasAlerts/CanvasAlertsPreserveMask.tsx | 2 +- .../CanvasAlertsSaveAllImagesToGallery.tsx | 2 +- .../components/CanvasAutoProcessSwitch.tsx | 2 +- ...vasOperationIsolatedLayerPreviewSwitch.tsx | 2 +- .../components/Filters/Filter.tsx | 2 +- .../components/RefImage/RefImageImage.tsx | 2 +- .../components/RefImage/RefImageSettings.tsx | 5 +- .../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 | 15 +- .../contexts/CanvasManagerProviderGate.tsx | 84 +---- .../controlLayers/hooks/useCanvasId.ts | 10 + .../controlLayers/hooks/useCanvasIsBusy.ts | 82 +---- .../controlLayers/hooks/useCanvasIsStaging.ts | 12 + .../controlLayers/hooks/useCanvasSessionId.ts | 14 + .../controlLayers/hooks/useInvokeCanvas.ts | 2 +- .../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/selectors.ts | 7 + .../src/features/controlLayers/store/types.ts | 5 + .../features/deleteImageModal/store/state.ts | 78 +++-- .../components/Boards/DeleteBoardModal.tsx | 8 +- .../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 | 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 | 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 | 23 +- .../ui/layouts/DockviewTabCanvasWorkspace.tsx | 5 +- .../ui/layouts/LaunchpadEditImageButton.tsx | 2 +- .../LaunchpadGenerateFromTextButton.tsx | 2 +- .../LaunchpadUseALayoutImageButton.tsx | 2 +- .../src/features/ui/layouts/StagingArea.tsx | 2 +- 72 files changed, 698 insertions(+), 508 deletions(-) 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 8740c3b0869..952fa4a1f0b 100644 --- a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx +++ b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx @@ -1,6 +1,6 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useSelectedCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +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'; @@ -12,7 +12,7 @@ export const SessionMenuItems = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const tab = useAppSelector(selectActiveTab); - const canvasManager = useSelectedCanvasManagerSafe(); + const canvasManager = useCanvasManagerSafe(); const resetCanvasLayers = useCallback(() => { dispatch(allEntitiesDeleted()); 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/RefImage/RefImageImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx index a754f0e4da4..11768a06f01 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx @@ -4,8 +4,8 @@ import { objectEquals } from '@observ33r/object-equals'; 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 { CroppableImageWithDims } from 'features/controlLayers/store/types'; import { imageDTOToCroppableImage, imageDTOToImageWithDims } from 'features/controlLayers/store/util'; 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 c7fd7867256..0c9153f16fd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx @@ -10,7 +10,10 @@ import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAd import { RefImageModel } from 'features/controlLayers/components/RefImage/RefImageModel'; import { RefImageNoImageState } from 'features/controlLayers/components/RefImage/RefImageNoImageState'; import { RefImageNoImageStateWithCanvasOptions } from 'features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions'; -import { CanvasManagerProviderGate, useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { + CanvasManagerProviderGate, + useCanvasManagerSafe, +} from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext'; import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice'; import { 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 index e22261c578d..4b3a6ecd8e8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasInstanceContextProvider.tsx @@ -1,6 +1,5 @@ import type { PropsWithChildren } from 'react'; import { createContext, memo, useContext } from 'react'; -import { assert } from 'tsafe'; const CanvasInstanceContext = createContext(null); @@ -9,18 +8,6 @@ export const CanvasInstanceContextProvider = memo(({ canvasId, children }: Props }); CanvasInstanceContextProvider.displayName = 'CanvasInstanceContextProvider'; -export const useScopedCanvas = () => { - const canvasId = useContext(CanvasInstanceContext); - assert(canvasId, 'useCanvasInstanceContext must be used within a CanvasInstanceContext'); - return canvasId; -}; - -export const useScopedCanvasSafe = () => { +export const useScopedCanvasIdSafe = () => { return useContext(CanvasInstanceContext); }; - -export const useHasScopedCanvas = () => { - const canvasId = useContext(CanvasInstanceContext); - - return !!canvasId; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx index d5fc6597b4a..3dd30284ae3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/CanvasManagerProviderGate.tsx @@ -1,21 +1,20 @@ 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 { selectSelectedCanvas } from 'features/controlLayers/store/selectors'; +import { selectSelectedCanvasId } from 'features/controlLayers/store/selectors'; import type { PropsWithChildren } from 'react'; -import { createContext, memo, useContext, useMemo } from 'react'; +import { createContext, memo } from 'react'; import { assert } from 'tsafe'; -import { useScopedCanvas, useScopedCanvasSafe } from './CanvasInstanceContextProvider'; - const CanvasManagerContext = createContext<{ [canvasId: string]: CanvasManager } | null>(null); export const CanvasManagerProviderGate = memo(({ children }: PropsWithChildren) => { const canvasManagers = useStore($canvasManagers); - const selectedCanvas = useAppSelector(selectSelectedCanvas); + const selectedCanvasId = useAppSelector(selectSelectedCanvasId); - if((Object.keys(canvasManagers).length === 0) || !canvasManagers[selectedCanvas.id]) { + if (Object.keys(canvasManagers).length === 0 || !canvasManagers[selectedCanvasId]) { return null; } @@ -24,74 +23,12 @@ export const CanvasManagerProviderGate = memo(({ children }: PropsWithChildren) CanvasManagerProviderGate.displayName = 'CanvasManagerProviderGate'; -/** - * Consumes the scoped CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise - * it will throw an error. - */ -export const useScopedCanvasManager = (): CanvasManager => { - const canvasManagers = useContext(CanvasManagerContext); - assert(canvasManagers, 'useScopedCanvasManager must be used within a CanvasManagerProviderGate'); - - const scopedCanvasId = useScopedCanvas(); - const canvasManager = useMemo(() => { - return canvasManagers[scopedCanvasId]; - }, [canvasManagers, scopedCanvasId]); - assert(canvasManager, 'Scoped canvas manager not initialised'); - - return canvasManager; -}; - -/** - * Consumes the selected CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise - * it will throw an error. - */ -export const useSelectedCanvasManager = (): CanvasManager => { - const canvasManagers = useContext(CanvasManagerContext); - assert(canvasManagers, 'useScopedCanvasManager must be used within a CanvasManagerProviderGate'); - - const selectedCanvas = useAppSelector(selectSelectedCanvas); - const canvasManager = useMemo(() => { - return canvasManagers[selectedCanvas.id]; - }, [canvasManagers, selectedCanvas]); - assert(canvasManager, 'Selected canvas manager not initialised'); - - return canvasManager; -}; - -/** - * Consumes the scoped CanvasManager from the context. If the CanvasManager is not available, it will return null. - */ -export const useScopedCanvasManagerSafe = (): CanvasManager | null => { - const canvasManagers = useStore($canvasManagers); - const scopedCanvasId = useScopedCanvasSafe(); - - const canvasManager = useMemo(() => { - return scopedCanvasId ? canvasManagers[scopedCanvasId] : undefined; - }, [canvasManagers, scopedCanvasId]); - - return canvasManager ?? null; -}; - -/** - * Consumes the selected CanvasManager from the context. If the CanvasManager is not available, it will return null. - */ -export const useSelectedCanvasManagerSafe = (): CanvasManager | null => { - const canvasManagers = useStore($canvasManagers); - const selectedCanvas = useAppSelector(selectSelectedCanvas); - - const canvasManager = useMemo(() => { - return canvasManagers[selectedCanvas.id]; - }, [canvasManagers, selectedCanvas]); - - return canvasManager ?? null; -}; - /** * Consumes the CanvasManager from the context. If the CanvasManager is not available, it will throw an error. */ export const useCanvasManager = (): CanvasManager => { const canvasManager = useCanvasManagerSafe(); - assert(canvasManager, 'Selected canvas manager not initialised'); + assert(canvasManager, 'Canvas manager does not exist'); return canvasManager; }; @@ -101,12 +38,7 @@ export const useCanvasManager = (): CanvasManager => { */ export const useCanvasManagerSafe = (): CanvasManager | null => { const canvasManagers = useStore($canvasManagers); - const scopedCanvasId = useScopedCanvasSafe(); - const selectedCanvas = useAppSelector(selectSelectedCanvas); - - const canvasManager = useMemo(() => { - return scopedCanvasId ? canvasManagers[scopedCanvasId] : canvasManagers[selectedCanvas.id]; - }, [canvasManagers, selectedCanvas, scopedCanvasId]); + const canvasId = useCanvasId(); - return canvasManager ?? null; + 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 278f024affb..ee806515165 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasIsBusy.ts @@ -1,77 +1,6 @@ import { useStore } from '@nanostores/react'; import { $false } from 'app/store/nanostores/util'; -import { useHasScopedCanvas } from 'features/controlLayers/contexts/CanvasInstanceContextProvider'; -import { - useScopedCanvasManager, - useScopedCanvasManagerSafe, - useSelectedCanvasManager, - useSelectedCanvasManagerSafe, -} from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import { useMemo } from 'react'; - -/** - * Returns a boolena indicating whether the scoped 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 useScopedCanvasIsBusy = () => { - const canvasManager = useScopedCanvasManager(); - const isBusy = useStore(canvasManager.$isBusy); - - return isBusy; -}; - -/** - * Returns a boolena indicating whether the selected 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 useSelectedCanvasIsBusy = () => { - const canvasManager = useSelectedCanvasManager(); - const isBusy = useStore(canvasManager.$isBusy); - - return isBusy; -}; - -/** - * Returns a boolena indicating whether the scoped 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 fall back to false if the canvas manager is not initialized. - */ -export const useScopedCanvasIsBusySafe = () => { - const canvasManager = useScopedCanvasManagerSafe(); - const isBusy = useStore(canvasManager?.$isBusy ?? $false); - - return isBusy; -}; - -/** - * Returns a boolena indicating whether the selected 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 fall back to false if the canvas manager is not initialized. - */ -export const useSelectedCanvasIsBusySafe = () => { - const canvasManager = useSelectedCanvasManagerSafe(); - const isBusy = useStore(canvasManager?.$isBusy ?? $false); - - return isBusy; -}; +import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; /** * Returns a boolena indicating whether the canvas is busy: @@ -83,13 +12,8 @@ export const useSelectedCanvasIsBusySafe = () => { * This hook will fall back to false if the canvas manager is not initialized. */ export const useCanvasIsBusy = () => { - const hasScopedCanvas = useHasScopedCanvas(); - const isScopedBusy = useScopedCanvasIsBusySafe(); - const isSelectedBusy = useSelectedCanvasIsBusySafe(); - - const isBusy = useMemo(() => { - return hasScopedCanvas ? isScopedBusy : isSelectedBusy; - }, [hasScopedCanvas, isScopedBusy, isSelectedBusy]); + const canvasManager = useCanvasManagerSafe(); + const isBusy = useStore(canvasManager?.$isBusy ?? $false); return isBusy; }; 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 3313e9dfab3..32be4f9dfc0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts @@ -3,11 +3,11 @@ 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 { $canvasManagers } from 'features/controlLayers/store/ephemeral'; import Konva from 'konva'; import { useLayoutEffect, useState } from 'react'; import { $socket } from 'services/events/stores'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; -import { $canvasManagers } from '../store/ephemeral'; const log = logger('canvas'); 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..126f0d26c28 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 zCanvasGlobalSettingsState = 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 CanvasGlobalSettingsState = 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), + global: zCanvasGlobalSettingsState, + canvases: z.array(zCanvasInstanceSettingsState), +}); type CanvasSettingsState = z.infer; -const getInitialState = (): CanvasSettingsState => ({ + +const getInitialCanvasGlobalSettingsState = (): CanvasGlobalSettingsState => ({ 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, + global: getInitialCanvasGlobalSettingsState(), + 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.global.clipToBbox = clipToBbox; }, settingsDynamicGridToggled: (state) => { - state.dynamicGrid = !state.dynamicGrid; + state.global.dynamicGrid = !state.global.dynamicGrid; }, settingsShowHUDToggled: (state) => { - state.showHUD = !state.showHUD; + state.global.showHUD = !state.global.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.global.invertScrollForToolWidth = invertScrollForToolWidth; }, settingsOutputOnlyMaskedRegionsToggled: (state) => { - state.outputOnlyMaskedRegions = !state.outputOnlyMaskedRegions; + state.global.outputOnlyMaskedRegions = !state.global.outputOnlyMaskedRegions; }, settingsAutoProcessToggled: (state) => { - state.autoProcess = !state.autoProcess; + state.global.autoProcess = !state.global.autoProcess; }, settingsSnapToGridToggled: (state) => { - state.snapToGrid = !state.snapToGrid; + state.global.snapToGrid = !state.global.snapToGrid; }, settingsShowProgressOnCanvasToggled: (state) => { - state.showProgressOnCanvas = !state.showProgressOnCanvas; + state.global.showProgressOnCanvas = !state.global.showProgressOnCanvas; }, settingsBboxOverlayToggled: (state) => { - state.bboxOverlay = !state.bboxOverlay; + state.global.bboxOverlay = !state.global.bboxOverlay; }, settingsPreserveMaskToggled: (state) => { - state.preserveMask = !state.preserveMask; + state.global.preserveMask = !state.global.preserveMask; }, settingsIsolatedStagingPreviewToggled: (state) => { - state.isolatedStagingPreview = !state.isolatedStagingPreview; + state.global.isolatedStagingPreview = !state.global.isolatedStagingPreview; }, settingsIsolatedLayerPreviewToggled: (state) => { - state.isolatedLayerPreview = !state.isolatedLayerPreview; + state.global.isolatedLayerPreview = !state.global.isolatedLayerPreview; }, settingsPressureSensitivityToggled: (state) => { - state.pressureSensitivity = !state.pressureSensitivity; + state.global.pressureSensitivity = !state.global.pressureSensitivity; }, settingsRuleOfThirdsToggled: (state) => { - state.ruleOfThirds = !state.ruleOfThirds; + state.global.ruleOfThirds = !state.global.ruleOfThirds; }, settingsSaveAllImagesToGalleryToggled: (state) => { - state.saveAllImagesToGallery = !state.saveAllImagesToGallery; + state.global.saveAllImagesToGallery = !state.global.saveAllImagesToGallery; }, settingsStagingAreaAutoSwitchChanged: ( state, - action: PayloadAction + action: PayloadAction<{ stagingAreaAutoSwitch: CanvasGlobalSettingsState['stagingAreaAutoSwitch'] }> ) => { - state.stagingAreaAutoSwitch = action.payload; + const { stagingAreaAutoSwitch } = action.payload; + + state.global.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, + global: { + ...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( + selectCanvasGlobalSettings, + (state: RootState) => selectCanvasInstanceSettings(state, canvasId), + (globalSettings, instanceSettings) => { + return { + ...globalSettings, + ...instanceSettings, + }; + } + ); +const selectCanvasGlobalSettings = (state: RootState) => state.canvasSettings.global; +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 buildCanvasGlobalSettingsSelector = + (selector: Selector) => + (state: RootState) => + selector(selectCanvasGlobalSettings(state)); +const buildCanvasInstanceSettingsSelector = + (selector: Selector) => + (state: RootState, canvasId: string) => + selector(selectCanvasInstanceSettings(state, canvasId)); + +export const selectPreserveMask = buildCanvasGlobalSettingsSelector((settings) => settings.preserveMask); +export const selectOutputOnlyMaskedRegions = buildCanvasGlobalSettingsSelector( (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 = buildCanvasGlobalSettingsSelector((settings) => settings.dynamicGrid); +export const selectInvertScrollForToolWidth = buildCanvasGlobalSettingsSelector( + (settings) => settings.invertScrollForToolWidth +); +export const selectBboxOverlay = buildCanvasGlobalSettingsSelector((settings) => settings.bboxOverlay); +export const selectShowHUD = buildCanvasGlobalSettingsSelector((settings) => settings.showHUD); +export const selectClipToBbox = buildCanvasGlobalSettingsSelector((settings) => settings.clipToBbox); +export const selectAutoProcess = buildCanvasGlobalSettingsSelector((settings) => settings.autoProcess); +export const selectSnapToGrid = buildCanvasGlobalSettingsSelector((settings) => settings.snapToGrid); +export const selectShowProgressOnCanvas = buildCanvasGlobalSettingsSelector( + (settings) => settings.showProgressOnCanvas +); +export const selectIsolatedStagingPreview = buildCanvasGlobalSettingsSelector( + (settings) => settings.isolatedStagingPreview +); +export const selectIsolatedLayerPreview = buildCanvasGlobalSettingsSelector( + (settings) => settings.isolatedLayerPreview +); +export const selectPressureSensitivity = buildCanvasGlobalSettingsSelector((settings) => settings.pressureSensitivity); +export const selectRuleOfThirds = buildCanvasGlobalSettingsSelector((settings) => settings.ruleOfThirds); +export const selectSaveAllImagesToGallery = buildCanvasGlobalSettingsSelector( + (settings) => settings.saveAllImagesToGallery +); +export const selectStagingAreaAutoSwitch = buildCanvasGlobalSettingsSelector( + (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/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/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 1181080d67c..85940ed725a 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(canvasAdded({ isSelected: true })); + dispatch(addCanvas({ 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(canvasAdded({ isSelected: true })); + dispatch(addCanvas({ 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(canvasAdded({ isSelected: true })); + dispatch(addCanvas({ 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(canvasAdded({ isSelected: true })); + dispatch(addCanvas({ 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(canvasAdded({ isSelected: true })); + 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 6e962be053c..870f33aee3a 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/CanvasWorkspacePanel.tsx @@ -19,8 +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 { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { selectCanvases } from 'features/controlLayers/store/selectors'; +import { selectSelectedCanvasId } from 'features/controlLayers/store/selectors'; import { memo, useCallback } from 'react'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; @@ -53,25 +52,23 @@ const canvasBgSx = { interface CanvasProps { canvasId: string; - isSelected: boolean } -const Canvas = memo(({ canvasId, isSelected }: CanvasProps) => { - const sessionId = useAppSelector(selectCanvasSessionId); - const dynamicGrid = useAppSelector(selectDynamicGrid); - const showHUD = useAppSelector(selectShowHUD); +const Canvas = memo(({ canvasId }: CanvasProps) => { + const dynamicGrid = useAppSelector((state) => selectDynamicGrid(state)); + const showHUD = useAppSelector((state) => selectShowHUD(state)); const renderMenu = useCallback(() => { return ; }, []); return ( -