diff --git a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts index 34d2f8ea88..29db2ff0b3 100644 --- a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts +++ b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts @@ -3,7 +3,7 @@ import { useAppStore } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { withResultAsync } from 'common/util/result'; import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice'; -import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice'; import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { imageDTOToImageObject } from 'features/controlLayers/store/util'; import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; @@ -90,7 +90,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => { const overrides: Partial = { objects: [imageObject], }; - store.dispatch(canvasSessionStarted({ sessionType: 'advanced' })); + store.dispatch(canvasSessionTypeChanged({ type: 'advanced' })); store.dispatch(rasterLayerAdded({ overrides, isSelected: true })); store.dispatch(setActiveTab('canvas')); store.dispatch(sentImageToCanvas()); @@ -162,14 +162,14 @@ export const useStudioInitAction = (action?: StudioInitAction) => { switch (destination) { case 'generation': // Go to the canvas tab, open the image viewer, and enable send-to-gallery mode - store.dispatch(canvasSessionStarted({ sessionType: 'simple' })); + store.dispatch(canvasSessionTypeChanged({ type: 'simple' })); store.dispatch(setActiveTab('canvas')); store.dispatch(activeTabCanvasRightPanelChanged('gallery')); $imageViewer.set(true); break; case 'canvas': // Go to the canvas tab, close the image viewer, and disable send-to-gallery mode - store.dispatch(canvasSessionStarted({ sessionType: 'advanced' })); + store.dispatch(canvasSessionTypeChanged({ type: 'advanced' })); store.dispatch(setActiveTab('canvas')); $imageViewer.set(false); break; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 2d8942f9e5..5db4acb243 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -1,7 +1,6 @@ import type { TypedStartListening } from '@reduxjs/toolkit'; import { addListener, createListenerMiddleware } from '@reduxjs/toolkit'; import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener'; -import { addStagingListeners } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener'; import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued'; import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived'; import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted'; @@ -65,9 +64,6 @@ addEnqueueRequestedUpscale(startAppListening); addAnyEnqueuedListener(startAppListening); addBatchEnqueuedListener(startAppListening); -// Canvas actions -addStagingListeners(startAppListening); - // Socket.IO addSocketConnectedEventListener(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts deleted file mode 100644 index 6ef2af6c2f..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { isAnyOf } from '@reduxjs/toolkit'; -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { canvasReset } from 'features/controlLayers/store/actions'; -import { stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { queueApi } from 'services/api/endpoints/queue'; - -const log = logger('canvas'); - -const matchCanvasOrStagingAreaReset = isAnyOf(stagingAreaReset, canvasReset); - -export const addStagingListeners = (startAppListening: AppStartListening) => { - startAppListening({ - matcher: matchCanvasOrStagingAreaReset, - effect: async (_, { dispatch }) => { - try { - const req = dispatch( - queueApi.endpoints.cancelByDestination.initiate( - { destination: 'canvas' }, - { fixedCacheKey: 'cancelByBatchOrigin' } - ) - ); - const { canceled } = await req.unwrap(); - req.reset(); - - if (canceled > 0) { - log.debug(`Canceled ${canceled} canvas batches`); - toast({ - id: 'CANCEL_BATCH_SUCCEEDED', - title: t('queue.cancelBatchSucceeded'), - status: 'success', - }); - } - } catch { - log.error('Failed to cancel canvas batches'); - toast({ - id: 'CANCEL_BATCH_FAILED', - title: t('queue.cancelBatchFailed'), - status: 'error', - }); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 79413b94a0..8deabbe967 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -5,7 +5,10 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError'; import { withResult, withResultAsync } from 'common/util/result'; import { parseify } from 'common/util/serialize'; -import { canvasSessionStarted, selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { + canvasSessionGenerationStarted, + selectCanvasSessionId, +} from 'features/controlLayers/store/canvasStagingAreaSlice'; import { $canvasManager } from 'features/controlLayers/store/ephemeral'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph'; @@ -33,11 +36,14 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) effect: async (action, { getState, dispatch }) => { log.debug('Enqueue requested'); - if (!selectCanvasSession(getState())) { - dispatch(canvasSessionStarted({ sessionType: 'simple' })); + if (!selectCanvasSessionId(getState())) { + dispatch(canvasSessionGenerationStarted()); } const state = getState(); + const destination = state.canvasSession.id; + assert(destination !== null); + const { prepend } = action.payload; const manager = $canvasManager.get(); @@ -96,8 +102,6 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) const { g, seedFieldIdentifier, positivePromptFieldIdentifier } = buildGraphResult.value; - const destination = state.canvasSession.session?.id ?? 'canvas'; - const prepareBatchResult = withResult(() => prepareLinearUIBatch({ state, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx index 47fd674016..0155f52c06 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AdvancedSession/AdvancedSession.tsx @@ -20,7 +20,6 @@ import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasT import { Transform } from 'features/controlLayers/components/Transform/Transform'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice'; -import type { AdvancedSessionIdentifier } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { memo, useCallback } from 'react'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; @@ -53,7 +52,7 @@ const canvasBgSx = { }, }; -export const AdvancedSession = memo(({ session }: { session: AdvancedSessionIdentifier }) => { +export const AdvancedSession = memo(({ id }: { id: string | null }) => { const dynamicGrid = useAppSelector(selectDynamicGrid); const showHUD = useAppSelector(selectShowHUD); @@ -107,27 +106,29 @@ export const AdvancedSession = memo(({ session }: { session: AdvancedSessionIden )} - - - - - + {id !== null && ( + + + + + + + + + - - - - - - + + + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx index 98b7bf3a5b..242e3e363c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx @@ -2,26 +2,27 @@ import { useAppSelector } from 'app/store/storeHooks'; import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession'; import { NoSession } from 'features/controlLayers/components/NoSession/NoSession'; import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession'; -import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectCanvasSessionId, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { memo } from 'react'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; export const CanvasMainPanelContent = memo(() => { - const session = useAppSelector(selectCanvasSession); + const type = useAppSelector(selectCanvasSessionType); + const id = useAppSelector(selectCanvasSessionId); - if (session === null) { - return ; + if (type === 'simple') { + if (id === null) { + return ; + } else { + return ; + } } - if (session.type === 'simple') { - return ; + if (type === 'advanced') { + return ; } - if (session.type === 'advanced') { - return ; - } - - assert>(false, 'Unexpected session'); + assert>(false, 'Unexpected session type'); }); CanvasMainPanelContent.displayName = 'CanvasMainPanelContent'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx index fd65ae8095..84466bf0cf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/NewSessionConfirmationAlertDialog.tsx @@ -2,7 +2,7 @@ import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text } import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { buildUseBoolean } from 'common/hooks/useBoolean'; -import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { selectSystemShouldConfirmOnNewSession, shouldConfirmOnNewSessionToggled, @@ -20,7 +20,7 @@ export const useNewGallerySession = () => { const newSessionDialog = useNewGallerySessionDialog(); const newGallerySessionImmediate = useCallback(() => { - dispatch(canvasSessionStarted({ sessionType: 'simple' })); + dispatch(canvasSessionTypeChanged({ type: 'simple' })); dispatch(activeTabCanvasRightPanelChanged('gallery')); }, [dispatch]); @@ -41,7 +41,7 @@ export const useNewCanvasSession = () => { const newSessionDialog = useNewCanvasSessionDialog(); const newCanvasSessionImmediate = useCallback(() => { - dispatch(canvasSessionStarted({ sessionType: 'advanced' })); + dispatch(canvasSessionTypeChanged({ type: 'advanced' })); dispatch(activeTabCanvasRightPanelChanged('layers')); }, [dispatch]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx index 4586b89573..607fcc5df8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/NoSession/NoSession.tsx @@ -5,13 +5,13 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { GenerateWithControlImage } from 'features/controlLayers/components/NoSession/GenerateWithControlImage'; import { GenerateWithStartingImage } from 'features/controlLayers/components/NoSession/GenerateWithStartingImage'; import { GenerateWithStartingImageAndInpaintMask } from 'features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask'; -import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { memo, useCallback } from 'react'; export const NoSession = memo(() => { const dispatch = useAppDispatch(); const newSesh = useCallback(() => { - dispatch(canvasSessionStarted({ sessionType: 'advanced' })); + dispatch(canvasSessionTypeChanged({ type: 'advanced' })); }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx index 2b543c6361..621bf95141 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx @@ -1,11 +1,10 @@ import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context'; import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea'; -import type { SimpleSessionIdentifier } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { memo } from 'react'; -export const SimpleSession = memo(({ session }: { session: SimpleSessionIdentifier }) => { +export const SimpleSession = memo(({ id }: { id: string }) => { return ( - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx index 7e8963c298..4e265a5e18 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx @@ -3,7 +3,7 @@ import { Button, Flex, FormControl, FormLabel, Spacer, Switch, Text } from '@inv import { useStore } from '@nanostores/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context'; -import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; @@ -13,7 +13,7 @@ export const StagingAreaHeader = memo(() => { const dispatch = useAppDispatch(); const startOver = useCallback(() => { - dispatch(canvasSessionStarted({ sessionType: 'simple' })); + dispatch(canvasSessionTypeChanged({ type: 'simple' })); }, [dispatch]); const onChangeAutoSwitch = useCallback( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx index d4089d761d..cd4758c56b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx @@ -3,10 +3,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { EMPTY_ARRAY } from 'app/store/constants'; import { useAppStore } from 'app/store/nanostores/store'; import { getOutputImageName } from 'features/controlLayers/components/SimpleSession/shared'; -import type { - AdvancedSessionIdentifier, - SimpleSessionIdentifier, -} from 'features/controlLayers/store/canvasStagingAreaSlice'; import type { ProgressImage } from 'features/nodes/types/common'; import type { Atom, WritableAtom } from 'nanostores'; import { atom, computed, effect } from 'nanostores'; @@ -100,7 +96,7 @@ export const clearProgressImage = ($progressData: WritableAtom; $itemCount: Atom; $hasItems: Atom; @@ -120,12 +116,13 @@ type CanvasSessionContextValue = { const CanvasSessionContext = createContext(null); export const CanvasSessionContextProvider = memo( - ({ session, children }: PropsWithChildren<{ session: SimpleSessionIdentifier | AdvancedSessionIdentifier }>) => { + ({ id, type, children }: PropsWithChildren<{ id: string; type: 'simple' | 'advanced' }>) => { /** * For best performance and interop with the Canvas, which is outside react but needs to interact with the react * app, all canvas session state is packaged as nanostores atoms. The trickiest part is syncing the queue items * with a nanostores atom. */ + const session = useMemo(() => ({ type, id }), [type, id]); /** * App store diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx index 147b473af7..692aa774e2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx @@ -5,7 +5,6 @@ import { useIsRegionFocused } from 'common/hooks/focus'; import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice'; -import { selectImageCount, stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; import type { CanvasRasterLayerState } from 'features/controlLayers/store/types'; import { imageNameToImageObject } from 'features/controlLayers/store/util'; @@ -20,7 +19,6 @@ export const StagingAreaToolbarAcceptButton = memo(() => { const canvasManager = useCanvasManager(); const bboxRect = useAppSelector(selectBboxRect); const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); - const imageCount = useAppSelector(selectImageCount); const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage); const isCanvasFocused = useIsRegionFocused('canvas'); const selectedItemImageName = useStore(ctx.$selectedItemOutputImageName); @@ -39,7 +37,6 @@ export const StagingAreaToolbarAcceptButton = memo(() => { }; dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' })); - dispatch(stagingAreaReset()); }, [bboxRect, selectedItemImageName, dispatch, selectedEntityIdentifier?.type]); useHotkeys( @@ -47,9 +44,9 @@ export const StagingAreaToolbarAcceptButton = memo(() => { acceptSelected, { preventDefault: true, - enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1, + enabled: isCanvasFocused && shouldShowStagedImage && selectedItemImageName !== null, }, - [isCanvasFocused, shouldShowStagedImage, imageCount] + [isCanvasFocused, shouldShowStagedImage, selectedItemImageName] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx index ce5f5707b1..4301183fe8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx @@ -3,7 +3,6 @@ import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { withResultAsync } from 'common/util/result'; import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context'; -import { selectSelectedImage } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; import { toast } from 'features/toast/toast'; import { memo, useCallback } from 'react'; @@ -15,7 +14,6 @@ const TOAST_ID = 'SAVE_STAGING_AREA_IMAGE_TO_GALLERY'; export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => { const autoAddBoardId = useAppSelector(selectAutoAddBoardId); - const selectedImage = useAppSelector(selectSelectedImage); const ctx = useCanvasSessionContext(); const imageName = useStore(ctx.$selectedItemOutputImageName); @@ -63,7 +61,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => { icon={} onClick={saveSelectedImageToGallery} colorScheme="invokeBlue" - isDisabled={!selectedImage} + isDisabled={!imageName} /> ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index f517d96b02..60bfd2f309 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -29,7 +29,7 @@ import { rasterLayerAdded, rgAdded, } from 'features/controlLayers/store/canvasSlice'; -import { selectCanvasStagingAreaSlice } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectCanvasSessionSlice } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { selectAllRenderableEntities, selectBbox, @@ -537,7 +537,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { * Gets the canvas staging area state from redux. */ getStagingArea = () => { - return this.runSelector(selectCanvasStagingAreaSlice); + return this.runSelector(selectCanvasSessionSlice); }; /** diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index d3181c7e9c..cda6f3742b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -6,7 +6,7 @@ import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { canvasReset } from 'features/controlLayers/store/actions'; -import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { modelChanged } from 'features/controlLayers/store/paramsSlice'; import { selectAllEntities, @@ -1846,7 +1846,7 @@ export const canvasSlice = createSlice({ syncScaledSize(state); } }); - builder.addCase(canvasSessionStarted, (state) => resetState(state)); + builder.addCase(canvasSessionReset, (state) => resetState(state)); }, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index 4a403b6baf..bbaabff71a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -3,31 +3,15 @@ import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { canvasReset } from 'features/controlLayers/store/actions'; -import type { StagingAreaImage, StagingAreaProgressImage } from 'features/controlLayers/store/types'; -import { selectCanvasQueueCounts } from 'services/api/endpoints/queue'; - -export type SimpleSessionIdentifier = { - type: 'simple'; - id: string; -}; - -export type AdvancedSessionIdentifier = { - type: 'advanced'; - id: string; -}; type CanvasStagingAreaState = { - session: SimpleSessionIdentifier | AdvancedSessionIdentifier | null; - sessionType: 'simple' | 'advanced' | null; - images: (StagingAreaImage | StagingAreaProgressImage)[]; - selectedImageIndex: number; + type: 'simple' | 'advanced'; + id: string | null; }; const INITIAL_STATE: CanvasStagingAreaState = { - session: null, - sessionType: null, - images: [], - selectedImageIndex: 0, + type: 'simple', + id: null, }; const getInitialState = (): CanvasStagingAreaState => deepClone(INITIAL_STATE); @@ -36,68 +20,24 @@ export const canvasSessionSlice = createSlice({ name: 'canvasSession', initialState: getInitialState(), reducers: { - sessionChanged: (state, action: PayloadAction<{ session: CanvasStagingAreaState['session'] }>) => { - const { session } = action.payload; - state.session = session; + canvasSessionTypeChanged: (state, action: PayloadAction<{ type: CanvasStagingAreaState['type'] }>) => { + const { type } = action.payload; + state.type = type; + state.id = null; }, - stagingAreaImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => { - const { stagingAreaImage } = action.payload; - let didReplace = false; - const newImages = []; - for (const i of state.images) { - if (i.sessionId === stagingAreaImage.sessionId) { - newImages.push(stagingAreaImage); - didReplace = true; - } else { - newImages.push(i); - } - } - if (!didReplace) { - newImages.push(stagingAreaImage); - } - state.images = newImages; - }, - stagingAreaGenerationStarted: (state, action: PayloadAction<{ sessionId: string }>) => { - const { sessionId } = action.payload; - state.images.push({ type: 'progress', sessionId }); - }, - stagingAreaGenerationFinished: (state, action: PayloadAction<{ sessionId: string }>) => { - const { sessionId } = action.payload; - state.images = state.images.filter((data) => data.sessionId !== sessionId); - }, - stagingAreaImageSelected: (state, action: PayloadAction<{ index: number }>) => { - const { index } = action.payload; - state.selectedImageIndex = index; - }, - stagingAreaNextStagedImageSelected: (state) => { - state.selectedImageIndex = (state.selectedImageIndex + 1) % state.images.length; - }, - stagingAreaPrevStagedImageSelected: (state) => { - state.selectedImageIndex = (state.selectedImageIndex - 1 + state.images.length) % state.images.length; - }, - stagingAreaStagedImageDiscarded: (state, action: PayloadAction<{ index: number }>) => { - const { index } = action.payload; - state.images.splice(index, 1); - state.selectedImageIndex = Math.min(state.selectedImageIndex, state.images.length - 1); - }, - stagingAreaReset: (state) => { - state.images = []; - state.selectedImageIndex = 0; - }, - canvasSessionStarted: { - reducer: (state, action: PayloadAction<{ session: CanvasStagingAreaState['session'] }>) => { - const { session } = action.payload; - state.session = session; + canvasSessionGenerationStarted: { + reducer: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.id = id; }, - prepare: (payload: { sessionType: 'simple' | 'advanced' }) => ({ - payload: { - session: { - type: payload.sessionType, - id: getPrefixedId(`canvas:${payload.sessionType}`), - }, - }, + prepare: () => ({ + payload: { id: getPrefixedId('canvas') }, }), }, + canvasSessionGenerationFinished: (state) => { + state.id = null; + }, + canvasSessionReset: () => getInitialState(), }, extraReducers(builder) { builder.addCase(canvasReset, () => getInitialState()); @@ -105,16 +45,10 @@ export const canvasSessionSlice = createSlice({ }); export const { - sessionChanged, - stagingAreaImageStaged, - stagingAreaGenerationStarted, - stagingAreaGenerationFinished, - stagingAreaStagedImageDiscarded, - stagingAreaReset, - stagingAreaImageSelected, - stagingAreaNextStagedImageSelected, - stagingAreaPrevStagedImageSelected, - canvasSessionStarted, + canvasSessionTypeChanged, + canvasSessionGenerationStarted, + canvasSessionReset, + canvasSessionGenerationFinished, } = canvasSessionSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -129,44 +63,8 @@ export const canvasStagingAreaPersistConfig: PersistConfig s[canvasSessionSlice.name]; +export const selectCanvasSessionSlice = (s: RootState) => s[canvasSessionSlice.name]; -/** - * Selects if we should be staging images. This is true if: - * - There are staged images. - * - There are any in-progress or pending canvas queue items. - */ -export const selectIsStaging = createSelector( - selectCanvasQueueCounts, - selectCanvasStagingAreaSlice, - ({ data }, staging) => { - if (staging.images.length > 0) { - return true; - } - if (!data) { - return false; - } - return data.in_progress > 0 || data.pending > 0; - } -); -export const selectStagedImageIndex = createSelector( - selectCanvasStagingAreaSlice, - (stagingArea) => stagingArea.selectedImageIndex -); -export const selectSelectedImage = createSelector( - [selectCanvasStagingAreaSlice, selectStagedImageIndex], - (stagingArea, index) => stagingArea.images[index] ?? null -); -export const selectStagedImages = createSelector(selectCanvasStagingAreaSlice, (stagingArea) => stagingArea.images); -export const selectImageCount = createSelector( - selectCanvasStagingAreaSlice, - (stagingArea) => stagingArea.images.length -); -export const selectCanvasSessionType = createSelector( - selectCanvasStagingAreaSlice, - (canvasSession) => canvasSession.sessionType -); -export const selectCanvasSession = createSelector( - selectCanvasStagingAreaSlice, - (canvasSession) => canvasSession.session -); +export const selectIsStaging = createSelector(selectCanvasSessionSlice, ({ id }) => id !== null); +export const selectCanvasSessionType = createSelector(selectCanvasSessionSlice, ({ type }) => type); +export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ id }) => id); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts index e4d91cc2f2..ea34668a84 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts @@ -1,7 +1,7 @@ import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; -import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice'; import type { LoRA } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { LoRAModelConfig } from 'services/api/types'; @@ -64,7 +64,7 @@ export const lorasSlice = createSlice({ }, }, extraReducers(builder) { - builder.addCase(canvasSessionStarted, () => { + builder.addCase(canvasSessionTypeChanged, () => { // When a new session is requested, clear all LoRAs return deepClone(initialState); }); diff --git a/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts b/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts index c9499ad613..0bfe797ad1 100644 --- a/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts +++ b/invokeai/frontend/web/src/features/hrf/store/hrfSlice.ts @@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; -import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice'; import type { ParameterHRFMethod, ParameterStrength } from 'features/parameters/types/parameterSchemas'; interface HRFState { @@ -34,7 +34,7 @@ export const hrfSlice = createSlice({ }, }, extraReducers(builder) { - builder.addCase(canvasSessionStarted, () => { + builder.addCase(canvasSessionTypeChanged, () => { return deepClone(initialHRFState); }); }, diff --git a/invokeai/frontend/web/src/features/imageActions/actions.ts b/invokeai/frontend/web/src/features/imageActions/actions.ts index d432433903..b73577bbdf 100644 --- a/invokeai/frontend/web/src/features/imageActions/actions.ts +++ b/invokeai/frontend/web/src/features/imageActions/actions.ts @@ -15,7 +15,7 @@ import { rgAdded, rgIPAdapterImageChanged, } from 'features/controlLayers/store/canvasSlice'; -import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors'; import type { CanvasControlLayerState, @@ -194,7 +194,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasSessionStarted({ sessionType: 'advanced' })); + dispatch(canvasSessionTypeChanged({ type: 'advanced' })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(rasterLayerAdded({ overrides, isSelected: true })); @@ -211,7 +211,7 @@ export const newCanvasFromImage = async (arg: { controlAdapter: deepClone(initialControlNet), } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasSessionStarted({ sessionType: 'advanced' })); + dispatch(canvasSessionTypeChanged({ type: 'advanced' })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(controlLayerAdded({ overrides, isSelected: true })); @@ -227,7 +227,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasSessionStarted({ sessionType: 'advanced' })); + dispatch(canvasSessionTypeChanged({ type: 'advanced' })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(inpaintMaskAdded({ overrides, isSelected: true })); @@ -243,7 +243,7 @@ export const newCanvasFromImage = async (arg: { objects: [imageObject], } satisfies Partial; addFitOnLayerInitCallback(overrides.id); - dispatch(canvasSessionStarted({ sessionType: 'advanced' })); + dispatch(canvasSessionTypeChanged({ type: 'advanced' })); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); dispatch(rgAdded({ overrides, isSelected: true })); @@ -256,7 +256,7 @@ export const newCanvasFromImage = async (arg: { case 'reference_image': { const ipAdapter = deepClone(selectDefaultRefImageConfig(getState())); ipAdapter.image = imageDTOToImageWithDims(imageDTO); - dispatch(canvasSessionStarted({ sessionType: 'advanced' })); + dispatch(canvasSessionTypeChanged({ type: 'advanced' })); dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true })); if (withInpaintMask) { dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true })); @@ -268,7 +268,7 @@ export const newCanvasFromImage = async (arg: { const ipAdapter = deepClone(selectDefaultIPAdapter(getState())); ipAdapter.image = imageDTOToImageWithDims(imageDTO); const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }]; - dispatch(canvasSessionStarted({ sessionType: 'advanced' })); + dispatch(canvasSessionTypeChanged({ type: 'advanced' })); 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/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index 2d0eb03d44..a34b7b95b2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -36,9 +36,9 @@ export const getBoardField = (state: RootState): BoardField | undefined => { export const selectCanvasOutputFields = (state: RootState) => { // Advanced session means working on canvas - images are not saved to gallery or added to a board. // Simple session means working in YOLO mode - images are saved to gallery & board. - const sessionType = selectCanvasSessionType(state); - const is_intermediate = sessionType === 'advanced'; - const board = sessionType === 'advanced' ? undefined : getBoardField(state); + const type = selectCanvasSessionType(state); + const is_intermediate = type === 'advanced'; + const board = type === 'advanced' ? undefined : getBoardField(state); return { is_intermediate, diff --git a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts index efee56c9fe..11a775c26d 100644 --- a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts +++ b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts @@ -2,7 +2,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; -import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSessionGenerationStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { atom } from 'nanostores'; import { stylePresetsApi } from 'services/api/endpoints/stylePresets'; @@ -29,7 +29,7 @@ export const stylePresetSlice = createSlice({ }, }, extraReducers(builder) { - builder.addCase(canvasSessionStarted, () => { + builder.addCase(canvasSessionGenerationStarted, () => { return deepClone(initialState); }); builder.addMatcher(stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled, (state, action) => { diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx index f80a956319..973fb1de87 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingLeftPanelButtons.tsx @@ -2,7 +2,7 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo import { useAppSelector } from 'app/store/storeHooks'; import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip'; import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; @@ -22,11 +22,11 @@ import { useGetQueueStatusQuery } from 'services/api/endpoints/queue'; export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) => { const tab = useAppSelector(selectActiveTab); - const session = useAppSelector(selectCanvasSession); + const type = useAppSelector(selectCanvasSessionType); return ( - {tab === 'canvas' && session?.type === 'advanced' && ( + {tab === 'canvas' && type === 'advanced' && ( diff --git a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx index 049e52f901..80d3ed935f 100644 --- a/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/RightPanelContent.tsx @@ -4,7 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper'; import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; -import { selectCanvasSession } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent'; import { Gallery } from 'features/gallery/components/Gallery'; import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar'; @@ -28,7 +28,7 @@ export const RightPanelContent = memo(() => { const boardSearchText = useAppSelector(selectBoardSearchText); const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length }); const imperativePanelGroupRef = useRef(null); - const session = useAppSelector(selectCanvasSession); + const type = useAppSelector(selectCanvasSessionType); const boardsListPanelOptions = useMemo( () => ({ @@ -77,7 +77,7 @@ export const RightPanelContent = memo(() => { - {session?.type === 'advanced' && ( + {type === 'advanced' && ( <> diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 3c4bf462bc..aa4b3ec0e0 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -1,7 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; -import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice'; import type { Dimensions } from 'features/controlLayers/store/types'; import { workflowLoaded } from 'features/nodes/store/nodesSlice'; import { atom } from 'nanostores'; @@ -56,7 +56,7 @@ export const uiSlice = createSlice({ builder.addCase(workflowLoaded, (state) => { state.activeTab = 'workflows'; }); - builder.addCase(canvasSessionStarted, (state) => { + builder.addCase(canvasSessionTypeChanged, (state) => { state.activeTab = 'canvas'; }); },