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 index a15a1efb14..c6efd494e9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -1,11 +1,10 @@ -import { isAnyOf } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { layerAdded, layerImageAdded, - sessionStagedImageAccepted, - sessionStagingCanceled, + sessionStagingAreaImageAccepted, + sessionStagingAreaReset, } from 'features/controlLayers/store/canvasV2Slice'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; @@ -14,7 +13,7 @@ import { assert } from 'tsafe'; export const addStagingListeners = (startAppListening: AppStartListening) => { startAppListening({ - matcher: isAnyOf(sessionStagingCanceled, sessionStagedImageAccepted), + actionCreator: sessionStagingAreaReset, effect: async (_, { dispatch }) => { const log = logger('canvas'); @@ -47,10 +46,10 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { }); startAppListening({ - actionCreator: sessionStagedImageAccepted, + actionCreator: sessionStagingAreaImageAccepted, effect: async (action, api) => { - const { imageDTO } = action.payload; - const { layers, selectedEntityIdentifier, bbox } = api.getState().canvasV2; + const { index } = action.payload; + const { layers, selectedEntityIdentifier } = api.getState().canvasV2; let layer = layers.entities.find((layer) => layer.id === selectedEntityIdentifier?.id); if (!layer) { @@ -63,11 +62,21 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { layer = api.getState().canvasV2.layers.entities[0]; } + const stagedImage = api.getState().canvasV2.session.stagedImages[index]; + + assert(stagedImage, 'No staged image found to accept'); assert(layer, 'No layer found to stage image'); const { id } = layer; - api.dispatch(layerImageAdded({ id, imageDTO, pos: { x: bbox.rect.x - layer.x, y: bbox.rect.y - layer.y } })); + api.dispatch( + layerImageAdded({ + id, + imageDTO: stagedImage.imageDTO, + pos: { x: stagedImage.rect.x - layer.x, y: stagedImage.rect.y - layer.y }, + }) + ); + api.dispatch(sessionStagingAreaReset()); }, }); }; 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 74425a4600..6d53d3a84d 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 @@ -1,7 +1,7 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { getCanvasManager } from 'features/controlLayers/konva/CanvasManager'; -import { sessionStagingCanceled, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; +import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph'; import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph'; @@ -49,7 +49,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) await req.unwrap(); } catch { if (didStartStaging && getState().canvasV2.session.isStaging) { - dispatch(sessionStagingCanceled()); + dispatch(sessionStagingAreaReset()); } } }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 279f868d9f..0e3cffe22b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -6,7 +6,6 @@ import { $lastProgressEvent, sessionImageStaged } from 'features/controlLayers/s import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; -import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; import { getCategories, getListImagesUrl } from 'services/api/util'; @@ -42,7 +41,10 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi } // This complete event has an associated image output - if (data.result.type === 'image_output' && !nodeTypeDenylist.includes(data.invocation.type)) { + if ( + (data.result.type === 'image_output' || data.result.type === 'canvas_v2_mask_and_crop_output') && + !nodeTypeDenylist.includes(data.invocation.type) + ) { const { image_name } = data.result.image; const { gallery, canvasV2 } = getState(); @@ -57,9 +59,10 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi imageDTORequest.unsubscribe(); // handle tab-specific logic - if (data.origin === 'canvas' && data.invocation_source_id === CANVAS_OUTPUT) { + if (data.origin === 'canvas' && data.result.type === 'canvas_v2_mask_and_crop_output') { + const { x, y, width, height } = data.result; if (canvasV2.session.isStaging) { - dispatch(sessionImageStaged({ imageDTO })); + dispatch(sessionImageStaged({ imageDTO, rect: { x, y, width, height } })); } else if (!canvasV2.session.isActive) { $lastProgressEvent.set(null); } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index 62b243157c..ed2408cc7e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -3,11 +3,11 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { $shouldShowStagedImage, - sessionStagingCanceled, - sessionStagedImageAccepted, - sessionStagedImageDiscarded, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, + sessionStagedImageDiscarded, + sessionStagingAreaImageAccepted, + sessionStagingAreaReset, } from 'features/controlLayers/store/canvasV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -40,7 +40,7 @@ export const StagingAreaToolbarContent = memo(() => { const stagingArea = useAppSelector((s) => s.canvasV2.session); const shouldShowStagedImage = useStore($shouldShowStagedImage); const images = useMemo(() => stagingArea.stagedImages, [stagingArea]); - const selectedImageDTO = useMemo(() => { + const selectedImage = useMemo(() => { return images[stagingArea.selectedStagedImageIndex] ?? null; }, [images, stagingArea.selectedStagedImageIndex]); @@ -57,25 +57,25 @@ export const StagingAreaToolbarContent = memo(() => { }, [dispatch]); const onAccept = useCallback(() => { - if (!selectedImageDTO) { + if (!selectedImage) { return; } - dispatch(sessionStagedImageAccepted({ imageDTO: selectedImageDTO })); - }, [dispatch, selectedImageDTO]); + dispatch(sessionStagingAreaImageAccepted({ index: stagingArea.selectedStagedImageIndex })); + }, [dispatch, selectedImage, stagingArea.selectedStagedImageIndex]); const onDiscardOne = useCallback(() => { - if (!selectedImageDTO) { + if (!selectedImage) { return; } if (images.length === 1) { - dispatch(sessionStagingCanceled()); + dispatch(sessionStagingAreaReset()); } else { - dispatch(sessionStagedImageDiscarded({ imageDTO: selectedImageDTO })); + dispatch(sessionStagedImageDiscarded({ index: stagingArea.selectedStagedImageIndex })); } - }, [dispatch, selectedImageDTO, images.length]); + }, [selectedImage, images.length, dispatch, stagingArea.selectedStagedImageIndex]); const onDiscardAll = useCallback(() => { - dispatch(sessionStagingCanceled()); + dispatch(sessionStagingAreaReset()); }, [dispatch]); const onToggleShouldShowStagedImage = useCallback(() => { @@ -145,7 +145,7 @@ export const StagingAreaToolbarContent = memo(() => { icon={} onClick={onAccept} colorScheme="invokeBlue" - isDisabled={!selectedImageDTO} + isDisabled={!selectedImage} /> { icon={} onClick={onSaveStagingImage} colorScheme="invokeBlue" - isDisabled={!selectedImageDTO || !selectedImageDTO.is_intermediate} + isDisabled={!selectedImage || !selectedImage.imageDTO.is_intermediate} /> { onClick={onDiscardOne} colorScheme="invokeBlue" fontSize={16} - isDisabled={!selectedImageDTO} + isDisabled={!selectedImage} /> { - if (this.imageDTO) { - konvaImage.width(this.imageDTO.width); - konvaImage.height(this.imageDTO.height); + if (this.selectedImage) { + konvaImage.width(this.selectedImage.rect.width); + konvaImage.height(this.selectedImage.rect.height); } this.manager.stateApi.resetLastProgressEvent(); this.image?.konvaImageGroup.visible(shouldShowStagedImage); @@ -60,7 +64,7 @@ export class CanvasStagingArea { } ); this.group.add(this.image.konvaImageGroup); - await this.image.updateImageSource(this.imageDTO.image_name); + await this.image.updateImageSource(this.selectedImage.imageDTO.image_name); this.image.konvaImageGroup.visible(shouldShowStagedImage); } } else { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 22accd05c1..b96fa09da0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -339,8 +339,7 @@ export const { sessionStartedStaging, sessionImageStaged, sessionStagedImageDiscarded, - sessionStagedImageAccepted, - sessionStagingCanceled, + sessionStagingAreaReset, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, // Initial image @@ -383,3 +382,6 @@ export const canvasV2PersistConfig: PersistConfig = { }; export const sessionRequested = createAction(`${canvasV2Slice.name}/sessionRequested`); +export const sessionStagingAreaImageAccepted = createAction<{ index: number }>( + `${canvasV2Slice.name}/sessionStagingAreaImageAccepted` +); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts index 7733793851..8b6d240f09 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/sessionReducers.ts @@ -1,6 +1,5 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; -import type { ImageDTO } from 'services/api/types'; +import type { CanvasV2State, StagingAreaImage } from 'features/controlLayers/store/types'; export const sessionReducers = { sessionStarted: (state) => { @@ -15,9 +14,9 @@ export const sessionReducers = { state.tool.selectedBuffer = state.tool.selected; state.tool.selected = 'view'; }, - sessionImageStaged: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { - const { imageDTO } = action.payload; - state.session.stagedImages.push(imageDTO); + sessionImageStaged: (state, action: PayloadAction) => { + const { imageDTO, rect } = action.payload; + state.session.stagedImages.push({ imageDTO, rect }); state.session.selectedStagedImageIndex = state.session.stagedImages.length - 1; }, sessionNextStagedImageSelected: (state) => { @@ -29,9 +28,9 @@ export const sessionReducers = { (state.session.selectedStagedImageIndex - 1 + state.session.stagedImages.length) % state.session.stagedImages.length; }, - sessionStagedImageDiscarded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { - const { imageDTO } = action.payload; - state.session.stagedImages = state.session.stagedImages.filter((image) => image.image_name !== imageDTO.image_name); + sessionStagedImageDiscarded: (state, action: PayloadAction<{ index: number }>) => { + const { index } = action.payload; + state.session.stagedImages = state.session.stagedImages.splice(index, 1); state.session.selectedStagedImageIndex = Math.min( state.session.selectedStagedImageIndex, state.session.stagedImages.length - 1 @@ -40,17 +39,7 @@ export const sessionReducers = { state.session.isStaging = false; } }, - sessionStagedImageAccepted: (state, _: PayloadAction<{ imageDTO: ImageDTO }>) => { - // When we finish staging, reset the tool back to the previous selection. - state.session.isStaging = false; - state.session.stagedImages = []; - state.session.selectedStagedImageIndex = 0; - if (state.tool.selectedBuffer) { - state.tool.selected = state.tool.selectedBuffer; - state.tool.selectedBuffer = null; - } - }, - sessionStagingCanceled: (state) => { + sessionStagingAreaReset: (state) => { state.session.isStaging = false; state.session.stagedImages = []; state.session.selectedStagedImageIndex = 0; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 0a6f21c2e7..bafc43727c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -826,6 +826,11 @@ export type LoRA = { weight: number; }; +export type StagingAreaImage = { + imageDTO: ImageDTO; + rect: Rect; +}; + export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; @@ -913,7 +918,7 @@ export type CanvasV2State = { session: { isActive: boolean; isStaging: boolean; - stagedImages: ImageDTO[]; + stagedImages: StagingAreaImage[]; selectedStagedImageIndex: number; }; }; 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 fcdd49b3ff..ce001e44a5 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 @@ -18,7 +18,7 @@ export const addInpaint = async ( compositing: CanvasV2State['compositing'], denoising_start: number, vaePrecision: ParameterPrecision -): Promise> => { +): Promise> => { denoise.denoising_start = denoising_start; const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); @@ -64,10 +64,10 @@ export const addInpaint = async ( fp32: vaePrecision === 'fp32', }); const canvasPasteBack = g.addNode({ - id: 'canvas_paste_back', - type: 'canvas_paste_back', - mask_blur: compositing.maskBlur, - source_image: { image_name: initialImage.image_name }, + id: 'canvas_v2_mask_and_crop', + type: 'canvas_v2_mask_and_crop', + invert: true, + crop_visible: true, }); // Resize initial image and mask to scaled size, feed into to gradient mask @@ -88,7 +88,7 @@ export const addInpaint = async ( g.addEdge(createGradientMask, 'expanded_mask_area', resizeMaskToOriginalSize, 'image'); // Finally, paste the generated masked image back onto the original image - g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); + g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'image'); g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); return canvasPasteBack; @@ -111,10 +111,10 @@ export const addInpaint = async ( image: { image_name: initialImage.image_name }, }); const canvasPasteBack = g.addNode({ - id: 'canvas_paste_back', - type: 'canvas_paste_back', - mask_blur: compositing.maskBlur, - source_image: { image_name: initialImage.image_name }, + id: 'canvas_v2_mask_and_crop', + type: 'canvas_v2_mask_and_crop', + invert: true, + crop_visible: true, }); g.addEdge(alphaToMask, 'image', createGradientMask, 'mask'); g.addEdge(i2l, 'latents', denoise, 'latents'); @@ -124,7 +124,7 @@ export const addInpaint = async ( g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); - g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); + g.addEdge(l2i, 'image', canvasPasteBack, 'image'); return canvasPasteBack; } 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 c65443f0f3..8d8caae6d6 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 @@ -19,7 +19,7 @@ export const addOutpaint = async ( compositing: CanvasV2State['compositing'], denoising_start: number, vaePrecision: ParameterPrecision -): Promise> => { +): Promise> => { denoise.denoising_start = denoising_start; const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); @@ -99,10 +99,10 @@ export const addOutpaint = async ( ...originalSize, }); const canvasPasteBack = g.addNode({ - id: 'canvas_paste_back', - type: 'canvas_paste_back', - mask_blur: compositing.maskBlur, - source_image: { image_name: initialImage.image_name }, + id: 'canvas_v2_mask_and_crop', + type: 'canvas_v2_mask_and_crop', + invert: true, + crop_visible: true, }); // Resize initial image and mask to scaled size, feed into to gradient mask @@ -112,7 +112,7 @@ export const addOutpaint = async ( g.addEdge(createGradientMask, 'expanded_mask_area', resizeOutputMaskToOriginalSize, 'image'); // Finally, paste the generated masked image back onto the original image - g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'target_image'); + g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'image'); g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask'); return canvasPasteBack; @@ -145,9 +145,10 @@ export const addOutpaint = async ( image: { image_name: initialImage.image_name }, }); const canvasPasteBack = g.addNode({ - id: 'canvas_paste_back', - type: 'canvas_paste_back', - mask_blur: compositing.maskBlur, + id: 'canvas_v2_mask_and_crop', + type: 'canvas_v2_mask_and_crop', + invert: true, + crop_visible: true, }); g.addEdge(maskAlphaToMask, 'image', maskCombine, 'mask1'); g.addEdge(initialImageAlphaToMask, 'image', maskCombine, 'mask2'); @@ -159,8 +160,7 @@ export const addOutpaint = async ( g.addEdge(modelLoader, 'unet', createGradientMask, 'unet'); g.addEdge(createGradientMask, 'denoise_mask', denoise, 'denoise_mask'); g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask'); - g.addEdge(infill, 'image', canvasPasteBack, 'source_image'); - g.addEdge(l2i, 'image', canvasPasteBack, 'target_image'); + g.addEdge(l2i, 'image', canvasPasteBack, 'image'); return canvasPasteBack; } diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index e2b7a33cf5..7f99ce6deb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -122,7 +122,7 @@ export const buildSD1Graph = async (state: RootState, manager: CanvasManager): P }) : null; - let canvasOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> = l2i; + let canvasOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop'> = l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', clipSkip, 'clip'); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 9b4490660d..34868e8602 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -121,7 +121,7 @@ export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): }) : null; - let canvasOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_paste_back'> = l2i; + let canvasOutput: Invocation<'l2i' | 'img_nsfw' | 'img_watermark' | 'img_resize' | 'canvas_v2_mask_and_crop'> = l2i; g.addEdge(modelLoader, 'unet', denoise, 'unet'); g.addEdge(modelLoader, 'clip', posCond, 'clip');