From 1d213067e8a61969b7cc440fca185d3df0781c4d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 21:12:58 +1000 Subject: [PATCH] feat(ui): add initial image layer to CL --- invokeai/frontend/web/public/locales/en.json | 2 + .../listeners/imageDropped.ts | 19 +++ .../listeners/imageUploaded.ts | 12 ++ .../src/common/hooks/useIsReadyToEnqueue.ts | 3 +- .../components/AddLayerButton.tsx | 6 +- .../components/ControlLayersPanelContent.tsx | 4 + .../components/IILayer/IILayer.tsx | 81 +++++++++++++ .../IILayer/InitialImagePreview.tsx | 109 ++++++++++++++++++ .../components/LayerCommon/LayerMenu.tsx | 4 +- .../components/LayerCommon/LayerTitle.tsx | 2 + .../controlLayers/hooks/addLayerHooks.ts | 18 ++- .../controlLayers/store/controlLayersSlice.ts | 57 ++++++++- .../src/features/controlLayers/store/types.ts | 8 +- .../web/src/features/dnd/types/index.ts | 10 +- .../web/src/features/dnd/util/isValidDrop.ts | 2 + .../frontend/web/src/services/api/types.ts | 8 +- 16 files changed, 335 insertions(+), 10 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c80283b664..c211b4a574 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1543,6 +1543,8 @@ "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalIPAdapter": "Global $t(common.ipAdapter)", "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)", + "globalInitialImage": "Global Initial Image", + "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", "opacityFilter": "Opacity Filter", "clearProcessor": "Clear Processor", "resetProcessor": "Reset Processor to Defaults" 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 5db78ed75e..734867d0e1 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 @@ -9,6 +9,7 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import { caLayerImageChanged, + iiLayerImageChanged, ipaLayerImageChanged, rgLayerIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; @@ -143,6 +144,24 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } + /** + * Image dropped on II Layer Image + */ + if ( + overData.actionType === 'SET_II_LAYER_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + const { layerId } = overData.context; + dispatch( + iiLayerImageChanged({ + layerId, + imageDTO: activeData.payload.imageDTO, + }) + ); + return; + } + /** * Image dropped on Canvas */ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index d0edfafd57..8f93d023ce 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -8,6 +8,7 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import { caLayerImageChanged, + iiLayerImageChanged, ipaLayerImageChanged, rgLayerIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; @@ -146,6 +147,17 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis ); } + if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') { + const { layerId } = postUploadAction; + dispatch(iiLayerImageChanged({ layerId, imageDTO })); + dispatch( + addToast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), + }) + ); + } + if (postUploadAction?.type === 'SET_INITIAL_IMAGE') { dispatch(initialImageChanged(imageDTO)); dispatch( diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 6073564305..d06fc259df 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -16,7 +16,6 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import i18n from 'i18next'; import { forEach } from 'lodash-es'; import { getConnectedEdges } from 'reactflow'; -import { assert } from 'tsafe'; const selector = createMemoizedSelector( [ @@ -110,7 +109,7 @@ const selector = createMemoizedSelector( } else if (l.type === 'regional_guidance_layer') { return l.ipAdapters; } - assert(false); + return []; }) .forEach((ca, i) => { const hasNoModel = !ca.model; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 3eb97dddff..3102e4afa8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,6 +1,6 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; +import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,6 +11,7 @@ export const AddLayerButton = memo(() => { const dispatch = useAppDispatch(); const [addCALayer, isAddCALayerDisabled] = useAddCALayer(); const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); + const [addIILayer, isAddIILayerDisabled] = useAddIILayer(); const addRGLayer = useCallback(() => { dispatch(rgLayerAdded()); }, [dispatch]); @@ -30,6 +31,9 @@ export const AddLayerButton = memo(() => { } onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> {t('controlLayers.globalIPAdapterLayer')} + } onClick={addIILayer} isDisabled={isAddIILayerDisabled}> + {t('controlLayers.globalInitialImageLayer')} + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index ffa2856116..14bea9bc1e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -6,6 +6,7 @@ import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableCon import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; import { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; +import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; @@ -54,6 +55,9 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { if (type === 'ip_adapter_layer') { return ; } + if (type === 'initial_image_layer') { + return ; + } }); LayerWrapper.displayName = 'LayerWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx new file mode 100644 index 0000000000..3c54bffb6c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -0,0 +1,81 @@ +import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview'; +import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; +import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; +import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; +import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; +import { + iiLayerImageChanged, + layerSelected, + selectIILayerOrThrow, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { IILayerImageDropData } from 'features/dnd/types'; +import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; +import { memo, useCallback, useMemo } from 'react'; +import type { IILayerImagePostUploadAction, ImageDTO } from 'services/api/types'; + +type Props = { + layerId: string; +}; + +export const IILayer = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const layer = useAppSelector((s) => selectIILayerOrThrow(s.controlLayers.present, layerId)); + const onClick = useCallback(() => { + dispatch(layerSelected(layerId)); + }, [dispatch, layerId]); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(iiLayerImageChanged({ layerId, imageDTO })); + }, + [dispatch, layerId] + ); + + const droppableData = useMemo( + () => ({ + actionType: 'SET_II_LAYER_IMAGE', + context: { + layerId, + }, + id: layerId, + }), + [layerId] + ); + + const postUploadAction = useMemo( + () => ({ + layerId, + type: 'SET_II_LAYER_IMAGE', + }), + [layerId] + ); + + return ( + + + + + + + + + {isOpen && ( + + + + + )} + + ); +}); + +IILayer.displayName = 'IILayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx new file mode 100644 index 0000000000..740e81cbde --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx @@ -0,0 +1,109 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +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 { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; +import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +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'; +import type { ImageDTO, PostUploadAction } from 'services/api/types'; + +type Props = { + image: ImageWithDims | null; + onChangeImage: (imageDTO: ImageDTO | null) => void; + droppableData: TypesafeDroppableData; + postUploadAction: PostUploadAction; +}; + +export const InitialImagePreview = memo(({ image, onChangeImage, droppableData, postUploadAction }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isConnected = useAppSelector((s) => s.system.isConnected); + const activeTabName = useAppSelector(activeTabNameSelector); + const optimalDimension = useAppSelector(selectOptimalDimension); + const shift = useShiftModifier(); + + const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.imageName ?? skipToken); + + const onReset = useCallback(() => { + onChangeImage(null); + }, [onChangeImage]); + + const onUseSize = useCallback(() => { + if (!imageDTO) { + return; + } + + if (activeTabName === 'unifiedCanvas') { + dispatch(setBoundingBoxDimensions({ width: imageDTO.width, height: imageDTO.height }, optimalDimension)); + } else { + const options = { updateAspectRatio: true, clamp: true }; + if (shift) { + const { width, height } = imageDTO; + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } else { + const { width, height } = calculateNewSize( + imageDTO.width / imageDTO.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } + } + }, [imageDTO, activeTabName, dispatch, optimalDimension, shift]); + + const draggableData = useMemo(() => { + if (imageDTO) { + return { + id: 'initial_image_layer', + payloadType: 'IMAGE_DTO', + payload: { imageDTO: imageDTO }, + }; + } + }, [imageDTO]); + + useEffect(() => { + if (isConnected && isErrorControlImage) { + onReset(); + } + }, [onReset, isConnected, isErrorControlImage]); + + return ( + + + + <> + : undefined} + tooltip={t('controlnet.resetControlImage')} + /> + : undefined} + tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} + styleOverrides={useSizeStyleOverrides} + /> + + + ); +}); + +InitialImagePreview.displayName = 'InitialImagePreview'; + +const useSizeStyleOverrides: SystemStyleObject = { mt: 6 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx index b83f48188f..12074d12b8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx @@ -37,7 +37,9 @@ export const LayerMenu = memo(({ layerId }: Props) => { )} - {(layerType === 'regional_guidance_layer' || layerType === 'control_adapter_layer') && ( + {(layerType === 'regional_guidance_layer' || + layerType === 'control_adapter_layer' || + layerType === 'initial_image_layer') && ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx index ec13ff7bcc..b29c3753fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx @@ -16,6 +16,8 @@ export const LayerTitle = memo(({ type }: Props) => { return t('controlLayers.globalControlAdapter'); } else if (type === 'ip_adapter_layer') { return t('controlLayers.globalIPAdapter'); + } else if (type === 'initial_image_layer') { + return t('controlLayers.globalInitialImage'); } }, [t, type]); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 7a4e7ebc09..dcbbeb8db5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -1,5 +1,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { caLayerAdded, ipaLayerAdded, rgLayerIPAdapterAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { + caLayerAdded, + iiLayerAdded, + ipaLayerAdded, + isInitialImageLayer, + rgLayerIPAdapterAdded, +} from 'features/controlLayers/store/controlLayersSlice'; import { buildControlNet, buildIPAdapter, @@ -93,3 +99,13 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => { return [addIPAdapter, isDisabled] as const; }; + +export const useAddIILayer = () => { + const dispatch = useAppDispatch(); + const isDisabled = useAppSelector((s) => Boolean(s.controlLayers.present.layers.find(isInitialImageLayer))); + const addIILayer = useCallback(() => { + dispatch(iiLayerAdded(null)); + }, [dispatch]); + + return [addIILayer, isDisabled] as const; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 9f36401e83..bd130a0236 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -39,6 +39,7 @@ import type { ControlAdapterLayer, ControlLayersState, DrawingTool, + InitialImageLayer, IPAdapterLayer, Layer, RegionalGuidanceLayer, @@ -71,8 +72,13 @@ export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanc export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => layer?.type === 'control_adapter_layer'; export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => layer?.type === 'ip_adapter_layer'; -export const isRenderableLayer = (layer?: Layer): layer is RegionalGuidanceLayer | ControlAdapterLayer => - layer?.type === 'regional_guidance_layer' || layer?.type === 'control_adapter_layer'; +export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => layer?.type === 'initial_image_layer'; +export const isRenderableLayer = ( + layer?: Layer +): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer => + layer?.type === 'regional_guidance_layer' || + layer?.type === 'control_adapter_layer' || + layer?.type === 'initial_image_layer'; const resetLayer = (layer: Layer) => { if (layer.type === 'regional_guidance_layer') { layer.maskObjects = []; @@ -94,6 +100,11 @@ export const selectIPALayerOrThrow = (state: ControlLayersState, layerId: string assert(isIPAdapterLayer(layer)); return layer; }; +export const selectIILayerOrThrow = (state: ControlLayersState, layerId: string): InitialImageLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isInitialImageLayer(layer)); + return layer; +}; const selectCAOrIPALayerOrThrow = ( state: ControlLayersState, layerId: string @@ -611,6 +622,45 @@ export const controlLayersSlice = createSlice({ }, //#endregion + //#region Initial Image Layer + iiLayerAdded: { + reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { + const { layerId, imageDTO } = action.payload; + // Highlander! There can be only one! + assert(!state.layers.find(isInitialImageLayer)); + const layer: InitialImageLayer = { + id: layerId, + type: 'initial_image_layer', + x: 0, + y: 0, + bbox: null, + bboxNeedsUpdate: false, + isEnabled: true, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + isSelected: true, + }; + state.layers.push(layer); + state.selectedLayerId = layer.id; + for (const layer of state.layers.filter(isRenderableLayer)) { + if (layer.id !== layerId) { + layer.isSelected = false; + } + } + }, + prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: 'initial_image_layer', imageDTO } }), + }, + iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { + const { layerId, imageDTO } = action.payload; + const layer = selectIILayerOrThrow(state, layerId); + if (layer) { + layer.bbox = null; + layer.bboxNeedsUpdate = true; + layer.isEnabled = true; + layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + } + }, + //#endregion + //#region Globals positivePromptChanged: (state, action: PayloadAction) => { state.positivePrompt = action.payload; @@ -780,6 +830,9 @@ export const { rgLayerIPAdapterMethodChanged, rgLayerIPAdapterModelChanged, rgLayerIPAdapterCLIPVisionModelChanged, + // II Layer + iiLayerAdded, + iiLayerImageChanged, // Globals positivePromptChanged, negativePromptChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index cbf47ff3ad..efcfb0f0bc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,5 +1,6 @@ import type { ControlNetConfigV2, + ImageWithDims, IPAdapterConfigV2, T2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; @@ -73,7 +74,12 @@ export type RegionalGuidanceLayer = RenderableLayerBase & { needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object }; -export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer; +export type InitialImageLayer = RenderableLayerBase & { + type: 'initial_image_layer'; + image: ImageWithDims | null; +}; + +export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer | InitialImageLayer; export type ControlLayersState = { _version: 1; diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 7d109473ed..b8d3cfe31e 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -55,6 +55,13 @@ export type RGLayerIPAdapterImageDropData = BaseDropData & { }; }; +export type IILayerImageDropData = BaseDropData & { + actionType: 'SET_II_LAYER_IMAGE'; + context: { + layerId: string; + }; +}; + export type CanvasInitialImageDropData = BaseDropData & { actionType: 'SET_CANVAS_INITIAL_IMAGE'; }; @@ -86,7 +93,8 @@ export type TypesafeDroppableData = | RemoveFromBoardDropData | CALayerImageDropData | IPALayerImageDropData - | RGLayerIPAdapterImageDropData; + | RGLayerIPAdapterImageDropData + | IILayerImageDropData; 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 c1da111087..757a21bd5c 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -25,6 +25,8 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: return payloadType === 'IMAGE_DTO'; case 'SET_RG_LAYER_IP_ADAPTER_IMAGE': return payloadType === 'IMAGE_DTO'; + case 'SET_II_LAYER_IMAGE': + return payloadType === 'IMAGE_DTO'; case 'SET_CANVAS_INITIAL_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SET_NODES_IMAGE': diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 5b88170d41..183b81478d 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -193,6 +193,11 @@ export type RGLayerIPAdapterImagePostUploadAction = { ipAdapterId: string; }; +export type IILayerImagePostUploadAction = { + type: 'SET_II_LAYER_IMAGE'; + layerId: string; +}; + type InitialImageAction = { type: 'SET_INITIAL_IMAGE'; }; @@ -225,4 +230,5 @@ export type PostUploadAction = | AddToBatchAction | CALayerImagePostUploadAction | IPALayerImagePostUploadAction - | RGLayerIPAdapterImagePostUploadAction; + | RGLayerIPAdapterImagePostUploadAction + | IILayerImagePostUploadAction;