feat(ui): enable control adapters on image drop (#4627)

## What type of PR is this? (check all applicable)

- [ ] Refactor
- [x] Feature
- [ ] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [x] Yes
- [ ] No, because:

## Description

[feat(ui): enable control adapters on image
drop](aa4b56baf2)

- Dropping/uploading an image on control adapter enables it (controlnet
& ip adapter)
- The image components are always enabled to allow this
This commit is contained in:
blessedcoolant 2023-09-21 10:25:04 +05:30 committed by GitHub
commit 19e487b5ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 72 additions and 40 deletions

View File

@ -4,7 +4,9 @@ import { parseify } from 'common/util/serialize';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { import {
controlNetImageChanged, controlNetImageChanged,
controlNetIsEnabledChanged,
ipAdapterImageChanged, ipAdapterImageChanged,
isIPAdapterEnabledChanged,
} from 'features/controlNet/store/controlNetSlice'; } from 'features/controlNet/store/controlNetSlice';
import { import {
TypesafeDraggableData, TypesafeDraggableData,
@ -99,6 +101,12 @@ export const addImageDroppedListener = () => {
controlNetId, controlNetId,
}) })
); );
dispatch(
controlNetIsEnabledChanged({
controlNetId,
isEnabled: true,
})
);
return; return;
} }
@ -111,6 +119,7 @@ export const addImageDroppedListener = () => {
activeData.payload.imageDTO activeData.payload.imageDTO
) { ) {
dispatch(ipAdapterImageChanged(activeData.payload.imageDTO)); dispatch(ipAdapterImageChanged(activeData.payload.imageDTO));
dispatch(isIPAdapterEnabledChanged(true));
return; return;
} }

View File

@ -3,7 +3,9 @@ import { logger } from 'app/logging/logger';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { import {
controlNetImageChanged, controlNetImageChanged,
controlNetIsEnabledChanged,
ipAdapterImageChanged, ipAdapterImageChanged,
isIPAdapterEnabledChanged,
} from 'features/controlNet/store/controlNetSlice'; } from 'features/controlNet/store/controlNetSlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { initialImageChanged } from 'features/parameters/store/generationSlice'; import { initialImageChanged } from 'features/parameters/store/generationSlice';
@ -87,6 +89,12 @@ export const addImageUploadedFulfilledListener = () => {
if (postUploadAction?.type === 'SET_CONTROLNET_IMAGE') { if (postUploadAction?.type === 'SET_CONTROLNET_IMAGE') {
const { controlNetId } = postUploadAction; const { controlNetId } = postUploadAction;
dispatch(
controlNetIsEnabledChanged({
controlNetId,
isEnabled: true,
})
);
dispatch( dispatch(
controlNetImageChanged({ controlNetImageChanged({
controlNetId, controlNetId,
@ -104,6 +112,7 @@ export const addImageUploadedFulfilledListener = () => {
if (postUploadAction?.type === 'SET_IP_ADAPTER_IMAGE') { if (postUploadAction?.type === 'SET_IP_ADAPTER_IMAGE') {
dispatch(ipAdapterImageChanged(imageDTO)); dispatch(ipAdapterImageChanged(imageDTO));
dispatch(isIPAdapterEnabledChanged(true));
dispatch( dispatch(
addToast({ addToast({
...DEFAULT_UPLOADED_TOAST, ...DEFAULT_UPLOADED_TOAST,

View File

@ -1,12 +1,12 @@
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { memo, useCallback } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import { FaCopy, FaTrash } from 'react-icons/fa'; import { FaCopy, FaTrash } from 'react-icons/fa';
import { import {
ControlNetConfig, ControlNetConfig,
controlNetDuplicated, controlNetDuplicated,
controlNetRemoved, controlNetRemoved,
controlNetToggled, controlNetIsEnabledChanged,
} from '../store/controlNetSlice'; } from '../store/controlNetSlice';
import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetModel from './parameters/ParamControlNetModel';
import ParamControlNetWeight from './parameters/ParamControlNetWeight'; import ParamControlNetWeight from './parameters/ParamControlNetWeight';
@ -77,9 +77,17 @@ const ControlNet = (props: ControlNetProps) => {
); );
}, [controlNetId, dispatch]); }, [controlNetId, dispatch]);
const handleToggleIsEnabled = useCallback(() => { const handleToggleIsEnabled = useCallback(
dispatch(controlNetToggled({ controlNetId })); (e: ChangeEvent<HTMLInputElement>) => {
}, [controlNetId, dispatch]); dispatch(
controlNetIsEnabledChanged({
controlNetId,
isEnabled: e.target.checked,
})
);
},
[controlNetId, dispatch]
);
return ( return (
<Flex <Flex
@ -106,8 +114,8 @@ const ControlNet = (props: ControlNetProps) => {
sx={{ sx={{
w: 'full', w: 'full',
minW: 0, minW: 0,
opacity: isEnabled ? 1 : 0.5, // opacity: isEnabled ? 1 : 0.5,
pointerEvents: isEnabled ? 'auto' : 'none', // pointerEvents: isEnabled ? 'auto' : 'none',
transitionProperty: 'common', transitionProperty: 'common',
transitionDuration: '0.1s', transitionDuration: '0.1s',
}} }}

View File

@ -13,6 +13,7 @@ import {
import { setHeight, setWidth } from 'features/parameters/store/generationSlice'; import { setHeight, setWidth } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaRulerVertical, FaSave, FaUndo } from 'react-icons/fa'; import { FaRulerVertical, FaSave, FaUndo } from 'react-icons/fa';
import { import {
useAddImageToBoardMutation, useAddImageToBoardMutation,
@ -26,7 +27,6 @@ import {
ControlNetConfig, ControlNetConfig,
controlNetImageChanged, controlNetImageChanged,
} from '../store/controlNetSlice'; } from '../store/controlNetSlice';
import { useTranslation } from 'react-i18next';
type Props = { type Props = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
@ -52,7 +52,6 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => {
controlImage: controlImageName, controlImage: controlImageName,
processedControlImage: processedControlImageName, processedControlImage: processedControlImageName,
processorType, processorType,
isEnabled,
controlNetId, controlNetId,
} = controlNet; } = controlNet;
@ -172,15 +171,13 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => {
h: isSmall ? 28 : 366, // magic no touch h: isSmall ? 28 : 366, // magic no touch
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
pointerEvents: isEnabled ? 'auto' : 'none',
opacity: isEnabled ? 1 : 0.5,
}} }}
> >
<IAIDndImage <IAIDndImage
draggableData={draggableData} draggableData={draggableData}
droppableData={droppableData} droppableData={droppableData}
imageDTO={controlImage} imageDTO={controlImage}
isDropDisabled={shouldShowProcessedImage || !isEnabled} isDropDisabled={shouldShowProcessedImage}
postUploadAction={postUploadAction} postUploadAction={postUploadAction}
/> />
@ -202,7 +199,6 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => {
droppableData={droppableData} droppableData={droppableData}
imageDTO={processedControlImage} imageDTO={processedControlImage}
isUploadDisabled={true} isUploadDisabled={true}
isDropDisabled={!isEnabled}
/> />
</Box> </Box>

View File

@ -3,8 +3,8 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISwitch from 'common/components/IAISwitch'; import IAISwitch from 'common/components/IAISwitch';
import { isIPAdapterEnableToggled } from 'features/controlNet/store/controlNetSlice'; import { isIPAdapterEnabledChanged } from 'features/controlNet/store/controlNetSlice';
import { memo, useCallback } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
@ -22,9 +22,12 @@ const ParamIPAdapterFeatureToggle = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = useCallback(() => { const handleChange = useCallback(
dispatch(isIPAdapterEnableToggled()); (e: ChangeEvent<HTMLInputElement>) => {
}, [dispatch]); dispatch(isIPAdapterEnabledChanged(e.target.checked));
},
[dispatch]
);
return ( return (
<IAISwitch <IAISwitch

View File

@ -1,7 +1,9 @@
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { skipToken } from '@reduxjs/toolkit/dist/query'; import { skipToken } from '@reduxjs/toolkit/dist/query';
import { RootState } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
@ -16,15 +18,17 @@ import { FaUndo } from 'react-icons/fa';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { PostUploadAction } from 'services/api/types'; import { PostUploadAction } from 'services/api/types';
const selector = createSelector(
stateSelector,
({ controlNet }) => {
const { ipAdapterInfo } = controlNet;
return { ipAdapterInfo };
},
defaultSelectorOptions
);
const ParamIPAdapterImage = () => { const ParamIPAdapterImage = () => {
const ipAdapterInfo = useAppSelector( const { ipAdapterInfo } = useAppSelector(selector);
(state: RootState) => state.controlNet.ipAdapterInfo
);
const isIPAdapterEnabled = useAppSelector(
(state: RootState) => state.controlNet.isIPAdapterEnabled
);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -71,8 +75,6 @@ const ParamIPAdapterImage = () => {
droppableData={droppableData} droppableData={droppableData}
draggableData={draggableData} draggableData={draggableData}
postUploadAction={postUploadAction} postUploadAction={postUploadAction}
isUploadDisabled={!isIPAdapterEnabled}
isDropDisabled={!isIPAdapterEnabled}
dropLabel={t('toast.setIPAdapterImage')} dropLabel={t('toast.setIPAdapterImage')}
noContentFallback={ noContentFallback={
<IAINoContentFallback <IAINoContentFallback

View File

@ -11,6 +11,9 @@ import { useTranslation } from 'react-i18next';
import { useGetIPAdapterModelsQuery } from 'services/api/endpoints/models'; import { useGetIPAdapterModelsQuery } from 'services/api/endpoints/models';
const ParamIPAdapterModelSelect = () => { const ParamIPAdapterModelSelect = () => {
const isEnabled = useAppSelector(
(state: RootState) => state.controlNet.isIPAdapterEnabled
);
const ipAdapterModel = useAppSelector( const ipAdapterModel = useAppSelector(
(state: RootState) => state.controlNet.ipAdapterInfo.model (state: RootState) => state.controlNet.ipAdapterInfo.model
); );
@ -90,6 +93,7 @@ const ParamIPAdapterModelSelect = () => {
data={data} data={data}
onChange={handleValueChanged} onChange={handleValueChanged}
sx={{ width: '100%' }} sx={{ width: '100%' }}
disabled={!isEnabled}
/> />
); );
}; };

View File

@ -146,16 +146,16 @@ export const controlNetSlice = createSlice({
const { controlNetId } = action.payload; const { controlNetId } = action.payload;
delete state.controlNets[controlNetId]; delete state.controlNets[controlNetId];
}, },
controlNetToggled: ( controlNetIsEnabledChanged: (
state, state,
action: PayloadAction<{ controlNetId: string }> action: PayloadAction<{ controlNetId: string; isEnabled: boolean }>
) => { ) => {
const { controlNetId } = action.payload; const { controlNetId, isEnabled } = action.payload;
const cn = state.controlNets[controlNetId]; const cn = state.controlNets[controlNetId];
if (!cn) { if (!cn) {
return; return;
} }
cn.isEnabled = !cn.isEnabled; cn.isEnabled = isEnabled;
}, },
controlNetImageChanged: ( controlNetImageChanged: (
state, state,
@ -377,8 +377,8 @@ export const controlNetSlice = createSlice({
controlNetReset: () => { controlNetReset: () => {
return { ...initialControlNetState }; return { ...initialControlNetState };
}, },
isIPAdapterEnableToggled: (state) => { isIPAdapterEnabledChanged: (state, action: PayloadAction<boolean>) => {
state.isIPAdapterEnabled = !state.isIPAdapterEnabled; state.isIPAdapterEnabled = action.payload;
}, },
ipAdapterImageChanged: (state, action: PayloadAction<ImageDTO | null>) => { ipAdapterImageChanged: (state, action: PayloadAction<ImageDTO | null>) => {
state.ipAdapterInfo.adapterImage = action.payload; state.ipAdapterInfo.adapterImage = action.payload;
@ -450,7 +450,7 @@ export const {
controlNetRemoved, controlNetRemoved,
controlNetImageChanged, controlNetImageChanged,
controlNetProcessedImageChanged, controlNetProcessedImageChanged,
controlNetToggled, controlNetIsEnabledChanged,
controlNetModelChanged, controlNetModelChanged,
controlNetWeightChanged, controlNetWeightChanged,
controlNetBeginStepPctChanged, controlNetBeginStepPctChanged,
@ -461,7 +461,7 @@ export const {
controlNetProcessorTypeChanged, controlNetProcessorTypeChanged,
controlNetReset, controlNetReset,
controlNetAutoConfigToggled, controlNetAutoConfigToggled,
isIPAdapterEnableToggled, isIPAdapterEnabledChanged,
ipAdapterImageChanged, ipAdapterImageChanged,
ipAdapterWeightChanged, ipAdapterWeightChanged,
ipAdapterModelChanged, ipAdapterModelChanged,

View File

@ -23,17 +23,18 @@ import { v4 as uuidv4 } from 'uuid';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
({ controlNet }) => { ({ controlNet }) => {
const { controlNets, isEnabled, isIPAdapterEnabled } = controlNet; const { controlNets, isEnabled, isIPAdapterEnabled, ipAdapterInfo } =
controlNet;
const validControlNets = getValidControlNets(controlNets); const validControlNets = getValidControlNets(controlNets);
const isIPAdapterValid = ipAdapterInfo.model && ipAdapterInfo.adapterImage;
let activeLabel = undefined; let activeLabel = undefined;
if (isEnabled && validControlNets.length > 0) { if (isEnabled && validControlNets.length > 0) {
activeLabel = `${validControlNets.length} ControlNet`; activeLabel = `${validControlNets.length} ControlNet`;
} }
if (isIPAdapterEnabled) { if (isIPAdapterEnabled && isIPAdapterValid) {
if (activeLabel) { if (activeLabel) {
activeLabel = `${activeLabel}, IP Adapter`; activeLabel = `${activeLabel}, IP Adapter`;
} else { } else {