diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index fb4ffbca7c..cb93b9a255 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -4,6 +4,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { parseify } from 'common/util/serialize'; import { caImageChanged, + iiImageChanged, ipaImageChanged, layerImageAdded, rgIPAdapterImageChanged, @@ -110,6 +111,18 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } + /** + * Image dropped on Raster layer + */ + if ( + overData.actionType === 'SET_INITIAL_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + dispatch(iiImageChanged({ imageDTO: activeData.payload.imageDTO })); + return; + } + /** * Image dropped on node image field */ diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 4c8572a5f9..94e706b68a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; +import { InitialImage } from 'features/controlLayers/components/InitialImage/InitialImage'; import { IM } from 'features/controlLayers/components/InpaintMask/IM'; import { memo } from 'react'; @@ -17,6 +18,7 @@ export const ControlLayersPanelContent = memo(() => { {isCanvasSessionActive && } + {!isCanvasSessionActive && } ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx new file mode 100644 index 0000000000..00fdc673c0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx @@ -0,0 +1,25 @@ +import { useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; +import { InitialImageHeader } from 'features/controlLayers/components/InitialImage/InitialImageHeader'; +import { InitialImageSettings } from 'features/controlLayers/components/InitialImage/InitialImageSettings'; +import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; + +export const InitialImage = memo(() => { + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'initial_image'); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const onSelect = useCallback(() => { + dispatch(entitySelected({ id: 'initial_image', type: 'initial_image' })); + }, [dispatch]); + + return ( + + + {isOpen && } + + ); +}); + +InitialImage.displayName = 'InitialImage'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageActionsMenu.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx new file mode 100644 index 0000000000..8af3e98f40 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx @@ -0,0 +1,34 @@ +import { Spacer } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; +import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader'; +import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; +import { iiIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + onToggleVisibility: () => void; +}; + +export const InitialImageHeader = memo(({ onToggleVisibility }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isEnabled = useAppSelector((s) => s.canvasV2.initialImage.isEnabled); + const onToggleIsEnabled = useCallback(() => { + dispatch(iiIsEnabledToggled()); + }, [dispatch]); + const title = useMemo(() => { + return `${t('controlLayers.initialImage')}`; + }, [t]); + + return ( + + + + + + ); +}); + +InitialImageHeader.displayName = 'InitialImageHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx new file mode 100644 index 0000000000..248c09a660 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx @@ -0,0 +1,100 @@ +import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIDndImage from 'common/components/IAIDndImage'; +import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import { documentHeightChanged, documentWidthChanged, iiReset } from 'features/controlLayers/store/canvasV2Slice'; +import { selectOptimalDimension } from 'features/controlLayers/store/selectors'; +import type { ImageDraggableData, InitialImageDropData } from 'features/dnd/types'; +import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize'; +import { memo, useCallback, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; + +export const InitialImagePreview = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const initialImage = useAppSelector((s) => s.canvasV2.initialImage); + const isConnected = useAppSelector((s) => s.system.isConnected); + const optimalDimension = useAppSelector(selectOptimalDimension); + const shift = useShiftModifier(); + + const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery( + initialImage.imageObject?.image.name ?? skipToken + ); + + const onReset = useCallback(() => { + dispatch(iiReset()); + }, [dispatch]); + + const onUseSize = useCallback(() => { + if (!imageDTO) { + return; + } + + const options = { updateAspectRatio: true, clamp: true }; + if (shift) { + const { width, height } = imageDTO; + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); + } else { + const { width, height } = calculateNewSize(imageDTO.width / imageDTO.height, optimalDimension * optimalDimension); + dispatch(documentWidthChanged({ width, ...options })); + dispatch(documentHeightChanged({ height, ...options })); + } + }, [imageDTO, dispatch, optimalDimension, shift]); + + const draggableData = useMemo(() => { + if (imageDTO) { + return { + id: 'initial_image', + payloadType: 'IMAGE_DTO', + payload: { imageDTO }, + }; + } + }, [imageDTO]); + + const droppableData = useMemo( + () => ({ id: 'initial_image', actionType: 'SET_INITIAL_IMAGE' }), + [] + ); + + useEffect(() => { + if (isConnected && isErrorControlImage) { + onReset(); + } + }, [onReset, isConnected, isErrorControlImage]); + + return ( + + + + + {imageDTO && ( + + } + tooltip={t('controlnet.resetControlImage')} + /> + } + tooltip={ + shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions') + } + /> + + )} + + + ); +}); + +InitialImagePreview.displayName = 'InitialImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx new file mode 100644 index 0000000000..9c9da2f536 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx @@ -0,0 +1,13 @@ +import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { InitialImagePreview } from 'features/controlLayers/components/InitialImage/InitialImagePreview'; +import { memo } from 'react'; + +export const InitialImageSettings = memo(() => { + return ( + + + + ); +}); + +InitialImageSettings.displayName = 'InitialImageSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts new file mode 100644 index 0000000000..e09fc42855 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts @@ -0,0 +1,73 @@ +import { CanvasImage } from 'features/controlLayers/konva/CanvasImage'; +import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { getObjectGroupId } from 'features/controlLayers/konva/naming'; +import type { InitialImageEntity } from 'features/controlLayers/store/types'; +import Konva from 'konva'; +import { v4 as uuidv4 } from 'uuid'; + +export class CanvasInitialImage { + id = 'initial_image'; + manager: CanvasManager; + layer: Konva.Layer; + group: Konva.Group; + objectsGroup: Konva.Group; + image: CanvasImage | null; + private initialImageState: InitialImageEntity; + + constructor(initialImageState: InitialImageEntity, manager: CanvasManager) { + this.manager = manager; + this.layer = new Konva.Layer({ + id: this.id, + imageSmoothingEnabled: true, + listening: false, + }); + this.group = new Konva.Group({ + id: getObjectGroupId(this.layer.id(), uuidv4()), + listening: false, + }); + this.objectsGroup = new Konva.Group({ listening: false }); + this.group.add(this.objectsGroup); + this.layer.add(this.group); + + this.image = null; + this.initialImageState = initialImageState; + } + + async render(initialImageState: InitialImageEntity) { + this.initialImageState = initialImageState; + + if (!this.initialImageState.imageObject) { + this.layer.visible(false); + return; + } + + const imageObject = this.initialImageState.imageObject; + + if (!imageObject) { + if (this.image) { + this.image.konvaImageGroup.visible(false); + } + } else if (!this.image) { + this.image = await new CanvasImage(imageObject, { + onLoad: () => { + this.updateGroup(); + }, + }); + this.objectsGroup.add(this.image.konvaImageGroup); + await this.image.updateImageSource(imageObject.image.name); + } else if (!this.image.isLoading && !this.image.isError) { + await this.image.update(imageObject); + } + + this.updateGroup(); + } + + updateGroup() { + const visible = this.initialImageState ? this.initialImageState.isEnabled : false; + this.layer.visible(visible); + } + + destroy(): void { + this.layer.destroy(); + } +} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts index 4dcd7be36b..b30dae3f5d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts @@ -13,7 +13,7 @@ import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; export class CanvasInpaintMask { - id: string; + id = 'inpaint_mask'; manager: CanvasManager; layer: Konva.Layer; group: Konva.Group; @@ -25,7 +25,6 @@ export class CanvasInpaintMask { private inpaintMaskState: InpaintMaskEntity; constructor(entity: InpaintMaskEntity, manager: CanvasManager) { - this.id = 'inpaint_mask'; this.manager = manager; this.layer = new Konva.Layer({ id: this.id }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 804faa45e4..17b5394602 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -1,15 +1,17 @@ import type { Store } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage'; import { + getCompositeLayerImage, getControlAdapterImage, getGenerationMode, - getImageSourceImage, + getInitialImage, getInpaintMaskImage, getRegionMaskImage, } from 'features/controlLayers/konva/util'; import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; +import type { CanvasV2State, GenerationMode } from 'features/controlLayers/store/types'; import type Konva from 'konva'; import { atom } from 'nanostores'; import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images'; @@ -58,6 +60,7 @@ export class CanvasManager { layers: Map; regions: Map; inpaintMask: CanvasInpaintMask; + initialImage: CanvasInitialImage; util: Util; stateApi: CanvasStateApi; preview: CanvasPreview; @@ -102,6 +105,13 @@ export class CanvasManager { this.layers = new Map(); this.regions = new Map(); this.controlAdapters = new Map(); + + this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this); + this.stage.add(this.initialImage.layer); + } + + async renderInitialImage() { + this.initialImage.render(this.stateApi.getInitialImageState()); } async renderLayers() { @@ -180,6 +190,7 @@ export class CanvasManager { const regions = getRegionsState().entities; let zIndex = 0; this.background.layer.zIndex(++zIndex); + this.initialImage.layer.zIndex(++zIndex); for (const layer of layers) { this.layers.get(layer.id)?.layer.zIndex(++zIndex); } @@ -225,6 +236,17 @@ export class CanvasManager { this.renderLayers(); } + if ( + this.isFirstRender || + state.initialImage !== this.prevState.initialImage || + state.document !== this.prevState.document || + state.tool.selected !== this.prevState.tool.selected || + state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id + ) { + log.debug('Rendering intial image'); + this.renderInitialImage(); + } + if ( this.isFirstRender || state.regions.entities !== this.prevState.regions.entities || @@ -367,8 +389,19 @@ export class CanvasManager { } }; - getGenerationMode() { - return getGenerationMode({ manager: this }); + getGenerationMode(): GenerationMode { + const session = this.stateApi.getSession(); + if (session.isActive) { + return getGenerationMode({ manager: this }); + } + + const initialImageState = this.stateApi.getInitialImageState(); + + if (initialImageState.imageObject && initialImageState.isEnabled) { + return 'img2img'; + } + + return 'txt2img'; } getControlAdapterImage(arg: Omit[0], 'manager'>) { @@ -383,7 +416,11 @@ export class CanvasManager { return getInpaintMaskImage({ ...arg, manager: this }); } - getImageSourceImage(arg: Omit[0], 'manager'>) { - return getImageSourceImage({ ...arg, manager: this }); + getInitialImage(arg: Omit[0], 'manager'>) { + if (this.stateApi.getSession().isActive) { + return getCompositeLayerImage({ ...arg, manager: this }); + } else { + return getInitialImage({ ...arg, manager: this }); + } } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index d485290c15..cac8dcce27 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -228,6 +228,9 @@ export class CanvasStateApi { getInpaintMaskState = () => { return this.getState().inpaintMask; }; + getInitialImageState = () => { + return this.getState().initialImage; + }; getMaskOpacity = () => { return this.getState().settings.maskOpacity; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index e729306ca0..103ef2811d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -317,6 +317,23 @@ export function getControlAdapterLayerClone(arg: { manager: CanvasManager; id: s return controlAdapterClone; } +export function getInitialImageLayerClone(arg: { manager: CanvasManager }): Konva.Layer { + const { manager } = arg; + + const initialImage = manager.initialImage; + + const initialImageClone = initialImage.layer.clone(); + const objectGroupClone = initialImage.group.clone(); + + initialImageClone.destroyChildren(); + initialImageClone.add(objectGroupClone); + + objectGroupClone.opacity(1); + objectGroupClone.cache(); + + return initialImageClone; +} + export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Konva.Stage { const { manager } = arg; @@ -435,6 +452,34 @@ export async function getControlAdapterImage(arg: { return imageDTO; } +export async function getInitialImage(arg: { + manager: CanvasManager; + bbox?: Rect; + preview?: boolean; +}): Promise { + const { manager, bbox, preview = false } = arg; + + // if (region.imageCache) { + // const imageDTO = await this.util.getImageDTO(region.imageCache.name); + // if (imageDTO) { + // return imageDTO; + // } + // } + + const layerClone = getInitialImageLayerClone({ manager }); + const blob = await konvaNodeToBlob(layerClone, bbox); + + if (preview) { + previewBlob(blob, 'initial image'); + } + + layerClone.destroy(); + + const imageDTO = await manager.util.uploadImage(blob, 'initial_image.png', 'other', true); + // manager.stateApi.onRegionMaskImageCached(ca.id, imageDTO); + return imageDTO; +} + export async function getInpaintMaskImage(arg: { manager: CanvasManager; bbox?: Rect; @@ -464,7 +509,7 @@ export async function getInpaintMaskImage(arg: { return imageDTO; } -export async function getImageSourceImage(arg: { +export async function getCompositeLayerImage(arg: { manager: CanvasManager; bbox?: Rect; preview?: boolean; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index a19556ed3a..28781563fd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -6,6 +6,7 @@ import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers'; import { documentReducers } from 'features/controlLayers/store/documentReducers'; +import { initialImageReducers } from 'features/controlLayers/store/initialImageReducers'; import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers'; import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers'; import { layersReducers } from 'features/controlLayers/store/layersReducers'; @@ -30,6 +31,14 @@ const initialState: CanvasV2State = { ipAdapters: { entities: [] }, regions: { entities: [] }, loras: [], + initialImage: { + id: 'initial_image', + type: 'initial_image', + bbox: null, + bboxNeedsUpdate: false, + isEnabled: true, + imageObject: null, + }, inpaintMask: { id: 'inpaint_mask', type: 'inpaint_mask', @@ -141,6 +150,7 @@ export const canvasV2Slice = createSlice({ ...inpaintMaskReducers, ...sessionReducers, ...documentReducers, + ...initialImageReducers, entitySelected: (state, action: PayloadAction) => { state.selectedEntityIdentifier = action.payload; }, @@ -338,6 +348,11 @@ export const { sessionStagingCanceled, sessionNextStagedImageSelected, sessionPrevStagedImageSelected, + // Initial image + iiRecalled, + iiIsEnabledToggled, + iiReset, + iiImageChanged, } = canvasV2Slice.actions; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts index 16fb34f7c0..a4649ca541 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts @@ -28,6 +28,11 @@ export const documentReducers = { if (!state.session.isActive) { state.bbox.rect.width = state.document.rect.width; state.bbox.rect.height = state.document.rect.height; + + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.document.rect.width; + state.initialImage.imageObject.height = state.document.rect.height; + } } }, documentHeightChanged: ( @@ -51,6 +56,11 @@ export const documentReducers = { if (!state.session.isActive) { state.bbox.rect.width = state.document.rect.width; state.bbox.rect.height = state.document.rect.height; + + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.document.rect.width; + state.initialImage.imageObject.height = state.document.rect.height; + } } }, documentAspectRatioLockToggled: (state) => { @@ -74,6 +84,11 @@ export const documentReducers = { if (!state.session.isActive) { state.bbox.rect.width = state.document.rect.width; state.bbox.rect.height = state.document.rect.height; + + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.document.rect.width; + state.initialImage.imageObject.height = state.document.rect.height; + } } }, documentDimensionsSwapped: (state) => { @@ -95,6 +110,11 @@ export const documentReducers = { if (!state.session.isActive) { state.bbox.rect.width = state.document.rect.width; state.bbox.rect.height = state.document.rect.height; + + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.document.rect.width; + state.initialImage.imageObject.height = state.document.rect.height; + } } }, documentSizeOptimized: (state) => { @@ -111,6 +131,11 @@ export const documentReducers = { if (!state.session.isActive) { state.bbox.rect.width = state.document.rect.width; state.bbox.rect.height = state.document.rect.height; + + if (state.initialImage.imageObject) { + state.initialImage.imageObject.width = state.document.rect.width; + state.initialImage.imageObject.height = state.document.rect.height; + } } }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts new file mode 100644 index 0000000000..b30af45ab5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts @@ -0,0 +1,38 @@ +import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash-es'; +import type { ImageDTO } from 'services/api/types'; + +import type { CanvasV2State, InitialImageEntity } from './types'; +import { imageDTOToImageObject } from './types'; + +export const initialImageReducers = { + iiRecalled: (state, action: PayloadAction<{ data: InitialImageEntity }>) => { + const { data } = action.payload; + state.initialImage = data; + state.selectedEntityIdentifier = { type: 'initial_image', id: 'initial_image' }; + }, + iiIsEnabledToggled: (state) => { + if (!state.initialImage) { + return; + } + state.initialImage.isEnabled = !state.initialImage.isEnabled; + }, + iiReset: (state) => { + state.initialImage.imageObject = null; + }, + iiImageChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => { + const { imageDTO } = action.payload; + if (!state.initialImage) { + return; + } + const newImageObject = imageDTOToImageObject('initial_image', 'initial_image_object', imageDTO); + if (isEqual(newImageObject, state.initialImage.imageObject)) { + return; + } + state.initialImage.bbox = null; + state.initialImage.bboxNeedsUpdate = true; + state.initialImage.isEnabled = true; + state.initialImage.imageObject = newImageObject; + state.selectedEntityIdentifier = { type: 'initial_image', id: 'initial_image' }; + }, +} satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index f215e1b194..6a1ea536f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -668,6 +668,16 @@ const zInpaintMaskEntity = z.object({ }); export type InpaintMaskEntity = z.infer; +const zInitialImageEntity = z.object({ + id: z.literal('initial_image'), + type: z.literal('initial_image'), + isEnabled: z.boolean(), + bbox: zRect.nullable(), + bboxNeedsUpdate: z.boolean(), + imageObject: zImageObject.nullable(), +}); +export type InitialImageEntity = z.infer; + const zControlAdapterEntityBase = z.object({ id: zId, type: z.literal('control_adapter'), @@ -790,7 +800,13 @@ export type BoundingBoxScaleMethod = z.infer; export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => zBoundingBoxScaleMethod.safeParse(v).success; -export type CanvasEntity = LayerEntity | ControlAdapterEntity | RegionEntity | InpaintMaskEntity | IPAdapterEntity; +export type CanvasEntity = + | LayerEntity + | ControlAdapterEntity + | RegionEntity + | InpaintMaskEntity + | IPAdapterEntity + | InitialImageEntity; export type CanvasEntityIdentifier = Pick; export type Size = { @@ -822,6 +838,7 @@ export type CanvasV2State = { ipAdapters: { entities: IPAdapterEntity[] }; regions: { entities: RegionEntity[] }; loras: LoRA[]; + initialImage: InitialImageEntity; tool: { selected: Tool; selectedBuffer: Tool | null; diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 692454ea50..b041546ec8 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -66,6 +66,10 @@ type UpscaleInitialImageDropData = BaseDropData & { actionType: 'SET_UPSCALE_INITIAL_IMAGE'; }; +export type InitialImageDropData = BaseDropData & { + actionType: 'SET_INITIAL_IMAGE'; +}; + type NodesImageDropData = BaseDropData & { actionType: 'SET_NODES_IMAGE'; context: { @@ -101,7 +105,8 @@ export type TypesafeDroppableData = | RGIPAdapterImageDropData | SelectForCompareDropData | RasterLayerImageDropData - | UpscaleInitialImageDropData; + | UpscaleInitialImageDropData + | LayerImageDropData; type BaseDragData = { id: string; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 128e5c5d50..80ea701727 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -29,6 +29,8 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? return payloadType === 'IMAGE_DTO'; case 'SELECT_FOR_COMPARE': return payloadType === 'IMAGE_DTO'; + case 'SET_INITIAL_IMAGE': + return payloadType === 'IMAGE_DTO'; case 'ADD_TO_BOARD': { // If the board is the same, don't allow the drop diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index 953e3505ef..2bac462f12 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -17,8 +17,8 @@ export const addImageToImage = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.getImageSourceImage({ bbox: cropBbox }); + const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); + const initialImage = await manager.getInitialImage({ bbox: cropBbox }); if (!isEqual(scaledSize, originalSize)) { // Resize the initial image to the scaled size, denoise, then resize back to the original size 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 0b4520385f..fcdd49b3ff 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 @@ -21,8 +21,8 @@ export const addInpaint = async ( ): Promise> => { denoise.denoising_start = denoising_start; - const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.getImageSourceImage({ bbox: cropBbox }); + const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']); + const initialImage = await manager.getInitialImage({ bbox: cropBbox }); const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox }); if (!isEqual(scaledSize, originalSize)) { 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 e5c774d953..1dde41fb0c 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 @@ -23,7 +23,7 @@ export const addOutpaint = async ( denoise.denoising_start = denoising_start; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.getImageSourceImage({ bbox: cropBbox }); + const initialImage = await manager.getInitialImage({ bbox: cropBbox }); const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox }); const infill = getInfill(g, compositing); 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 ab3fcef493..e2b7a33cf5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -1,3 +1,4 @@ +import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; @@ -33,9 +34,11 @@ import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { addRegions } from './addRegions'; +const log = logger('system'); export const buildSD1Graph = async (state: RootState, manager: CanvasManager): Promise => { const generationMode = manager.getGenerationMode(); + log.debug({ generationMode }, 'Building SD1/SD2 graph'); const { bbox, params } = state.canvasV2; 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 c6bb11b9ba..9b4490660d 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 @@ -1,3 +1,4 @@ +import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; @@ -32,9 +33,11 @@ import { isNonRefinerMainModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { addRegions } from './addRegions'; +const log = logger('system'); export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): Promise => { const generationMode = manager.getGenerationMode(); + log.debug({ generationMode }, 'Building SDXL graph'); const { bbox, params } = state.canvasV2;