From 0e55488ff6aa6969dfd92019999040ae2632ee00 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 1 May 2024 18:36:08 +1000 Subject: [PATCH] refactor(ui): wire up CA logic across (wip) --- .../middleware/listenerMiddleware/index.ts | 3 - .../controlLayersToControlAdapterBridge.ts | 144 ---------------- .../src/common/hooks/useIsReadyToEnqueue.ts | 50 +++--- .../components/AddLayerButton.tsx | 21 +-- .../components/AddPromptButtons.tsx | 14 +- .../components/CALayer/CALayer.tsx | 28 +--- .../components/CALayer/CALayerConfig.tsx | 109 +++++++++++-- .../CALayer/CALayerImagePreview.tsx | 43 ++--- .../components/CALayer/CALayerOpacity.tsx | 4 +- .../components/CALayer/CALayerProcessor.tsx | 18 +- .../CALayer/CALayerProcessorCombobox.tsx | 48 ++---- ....tsx => ControlAdapterBeginEndStepPct.tsx} | 35 +--- ...sx => ControlAdapterControlModeSelect.tsx} | 26 +-- ...ox.tsx => ControlAdapterModelCombobox.tsx} | 25 +-- ...yerWeight.tsx => ControlAdapterWeight.tsx} | 21 +-- .../components/DeleteAllLayersButton.tsx | 13 +- .../ControlAdapterLayerConfig copy.tsx | 75 --------- .../components/IPALayer/IPALayer.tsx | 20 +-- .../components/IPALayer/IPALayerConfig.tsx | 105 ++++++++++++ .../IPALayer/IPALayerModelCombobox.tsx | 100 ++++++++++++ .../IPALayer/IPAdapterImagePreview.tsx | 119 ++++++++++++++ .../components/IPALayer/IPAdapterMethod.tsx | 44 +++++ .../IPALayer/ParamControlAdapterModel.tsx | 136 ---------------- .../LayerCommon/LayerDeleteButton.tsx | 4 +- .../LayerCommon/LayerMenuRGActions.tsx | 8 +- .../components/RGLayer/RGLayer.tsx | 2 +- .../RGLayer/RGLayerIPAdapterList.tsx | 20 ++- .../controlLayers/hooks/addLayerHooks.ts | 95 +++++++++++ .../controlLayers/hooks/mouseEventHooks.ts | 6 +- .../hooks/useControlLayersTitle.ts | 2 +- .../controlLayers/store/controlLayersSlice.ts | 154 +++++++++++------- .../controlLayers/util/controlAdapters.ts | 10 +- .../features/controlLayers/util/renderers.ts | 18 +- .../ControlSettingsAccordion.tsx | 88 +++++----- 34 files changed, 852 insertions(+), 756 deletions(-) delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts rename invokeai/frontend/web/src/features/controlLayers/components/CALayer/{CALayerBeginEndStepPct.tsx => ControlAdapterBeginEndStepPct.tsx} (54%) rename invokeai/frontend/web/src/features/controlLayers/components/CALayer/{CALayerControlMode.tsx => ControlAdapterControlModeSelect.tsx} (68%) rename invokeai/frontend/web/src/features/controlLayers/components/CALayer/{CALayerModelCombobox.tsx => ControlAdapterModelCombobox.tsx} (72%) rename invokeai/frontend/web/src/features/controlLayers/components/CALayer/{CALayerWeight.tsx => ControlAdapterWeight.tsx} (72%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ParamControlAdapterModel.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index ac039c2df6..cd0c1290e9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -16,7 +16,6 @@ import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listen import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet'; import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged'; import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery'; -import { addControlLayersToControlAdapterBridge } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess'; import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed'; import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas'; @@ -158,5 +157,3 @@ addUpscaleRequestedListener(startAppListening); addDynamicPromptsListener(startAppListening); addSetDefaultSettingsListener(startAppListening); - -addControlLayersToControlAdapterBridge(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts deleted file mode 100644 index 81672758c9..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import { controlAdapterAdded, controlAdapterRemoved } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlNetConfig, IPAdapterConfig } from 'features/controlAdapters/store/types'; -import { isControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import { - caLayerAdded, - ipaLayerAdded, - layerDeleted, - rgLayerAdded, - rgLayerIPAdapterAdded, - rgLayerIPAdapterDeleted, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; -import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; -import { isControlNetModelConfig, isIPAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; - -export const guidanceLayerAdded = createAction('controlLayers/guidanceLayerAdded'); -export const guidanceLayerDeleted = createAction('controlLayers/guidanceLayerDeleted'); -export const allLayersDeleted = createAction('controlLayers/allLayersDeleted'); -export const guidanceLayerIPAdapterAdded = createAction('controlLayers/guidanceLayerIPAdapterAdded'); -export const guidanceLayerIPAdapterDeleted = createAction<{ layerId: string; ipAdapterId: string }>( - 'controlLayers/guidanceLayerIPAdapterDeleted' -); - -export const addControlLayersToControlAdapterBridge = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: guidanceLayerAdded, - effect: (action, { dispatch, getState }) => { - const type = action.payload; - const layerId = uuidv4(); - if (type === 'regional_guidance_layer') { - dispatch(rgLayerAdded({ layerId })); - return; - } - - const state = getState(); - const baseModel = state.generation.model?.base; - const modelConfigs = modelsApi.endpoints.getModelConfigs.select(undefined)(state).data; - - if (type === 'ip_adapter_layer') { - const ipAdapterId = uuidv4(); - const overrides: Partial = { - id: ipAdapterId, - }; - - // Find and select the first matching model - if (modelConfigs) { - const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isIPAdapterModelConfig); - overrides.model = models.find((m) => m.base === baseModel) ?? null; - } - dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides })); - dispatch(ipaLayerAdded({ layerId, ipAdapterId })); - return; - } - - if (type === 'control_adapter_layer') { - const controlNetId = uuidv4(); - const overrides: Partial = { - id: controlNetId, - }; - - // Find and select the first matching model - if (modelConfigs) { - const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isControlNetModelConfig); - const model = models.find((m) => m.base === baseModel) ?? null; - overrides.model = model; - const defaultPreprocessor = model?.default_settings?.preprocessor; - overrides.processorType = isControlAdapterProcessorType(defaultPreprocessor) ? defaultPreprocessor : 'none'; - overrides.processorNode = CONTROLNET_PROCESSORS[overrides.processorType].buildDefaults(baseModel); - } - dispatch(controlAdapterAdded({ type: 'controlnet', overrides })); - dispatch(caLayerAdded({ layerId, controlNetId })); - return; - } - }, - }); - - startAppListening({ - actionCreator: guidanceLayerDeleted, - effect: (action, { getState, dispatch }) => { - const layerId = action.payload; - const state = getState(); - const layer = state.controlLayers.present.layers.find((l) => l.id === layerId); - assert(layer, `Layer ${layerId} not found`); - - if (layer.type === 'ip_adapter_layer') { - dispatch(controlAdapterRemoved({ id: layer.ipAdapterId })); - } else if (layer.type === 'control_adapter_layer') { - dispatch(controlAdapterRemoved({ id: layer.controlNetId })); - } else if (layer.type === 'regional_guidance_layer') { - for (const ipAdapterId of layer.ipAdapterIds) { - dispatch(controlAdapterRemoved({ id: ipAdapterId })); - } - } - dispatch(layerDeleted(layerId)); - }, - }); - - startAppListening({ - actionCreator: allLayersDeleted, - effect: (action, { dispatch, getOriginalState }) => { - const state = getOriginalState(); - for (const layer of state.controlLayers.present.layers) { - dispatch(guidanceLayerDeleted(layer.id)); - } - }, - }); - - startAppListening({ - actionCreator: guidanceLayerIPAdapterAdded, - effect: (action, { dispatch, getState }) => { - const layerId = action.payload; - const ipAdapterId = uuidv4(); - const overrides: Partial = { - id: ipAdapterId, - }; - - // Find and select the first matching model - const state = getState(); - const baseModel = state.generation.model?.base; - const modelConfigs = modelsApi.endpoints.getModelConfigs.select(undefined)(state).data; - if (modelConfigs) { - const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isIPAdapterModelConfig); - overrides.model = models.find((m) => m.base === baseModel) ?? null; - } - - dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides })); - dispatch(rgLayerIPAdapterAdded({ layerId, ipAdapterId })); - }, - }); - - startAppListening({ - actionCreator: guidanceLayerIPAdapterDeleted, - effect: (action, { dispatch }) => { - const { layerId, ipAdapterId } = action.payload; - dispatch(controlAdapterRemoved({ id: ipAdapterId })); - dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); - }, - }); -}; diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index d765e987eb..b5650209a4 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -101,33 +101,35 @@ const selector = createMemoizedSelector( if (activeTabName === 'txt2img') { // Special handling for control layers on txt2img - const enabledControlLayersAdapterIds = controlLayers.present.layers - .filter((l) => l.isEnabled) - .flatMap((layer) => { - if (layer.type === 'regional_guidance_layer') { - return layer.ipAdapterIds; - } - if (layer.type === 'control_adapter_layer') { - return [layer.controlNetId]; - } - if (layer.type === 'ip_adapter_layer') { - return [layer.ipAdapterId]; - } - }); + const enabledControlLayersAdapterIds = [] + // const enabledControlLayersAdapterIds = controlLayers.present.layers + // .filter((l) => l.isEnabled) + // .flatMap((layer) => { + // if (layer.type === 'regional_guidance_layer') { + // return layer.ipAdapterIds; + // } + // if (layer.type === 'control_adapter_layer') { + // return [layer.controlNetId]; + // } + // if (layer.type === 'ip_adapter_layer') { + // return [layer.ipAdapterId]; + // } + // }); enabledControlAdapters = enabledControlAdapters.filter((ca) => enabledControlLayersAdapterIds.includes(ca.id)); } else { - const allControlLayerAdapterIds = controlLayers.present.layers.flatMap((layer) => { - if (layer.type === 'regional_guidance_layer') { - return layer.ipAdapterIds; - } - if (layer.type === 'control_adapter_layer') { - return [layer.controlNetId]; - } - if (layer.type === 'ip_adapter_layer') { - return [layer.ipAdapterId]; - } - }); + const allControlLayerAdapterIds = [] + // const allControlLayerAdapterIds = controlLayers.present.layers.flatMap((layer) => { + // if (layer.type === 'regional_guidance_layer') { + // return layer.ipAdapterIds; + // } + // if (layer.type === 'control_adapter_layer') { + // return [layer.controlNetId]; + // } + // if (layer.type === 'ip_adapter_layer') { + // return [layer.ipAdapterId]; + // } + // }); enabledControlAdapters = enabledControlAdapters.filter((ca) => !allControlLayerAdapterIds.includes(ca.id)); } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index b521153239..3eb97dddff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,6 +1,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { guidanceLayerAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { useAppDispatch } from 'app/store/storeHooks'; +import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; +import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -8,14 +9,10 @@ import { PiPlusBold } from 'react-icons/pi'; export const AddLayerButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const addRegionalGuidanceLayer = useCallback(() => { - dispatch(guidanceLayerAdded('regional_guidance_layer')); - }, [dispatch]); - const addControlAdapterLayer = useCallback(() => { - dispatch(guidanceLayerAdded('control_adapter_layer')); - }, [dispatch]); - const addIPAdapterLayer = useCallback(() => { - dispatch(guidanceLayerAdded('ip_adapter_layer')); + const [addCALayer, isAddCALayerDisabled] = useAddCALayer(); + const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); + const addRGLayer = useCallback(() => { + dispatch(rgLayerAdded()); }, [dispatch]); return ( @@ -24,13 +21,13 @@ export const AddLayerButton = memo(() => { {t('controlLayers.addLayer')} - } onClick={addRegionalGuidanceLayer}> + } onClick={addRGLayer}> {t('controlLayers.regionalGuidanceLayer')} - } onClick={addControlAdapterLayer}> + } onClick={addCALayer} isDisabled={isAddCALayerDisabled}> {t('controlLayers.globalControlAdapterLayer')} - } onClick={addIPAdapterLayer}> + } onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> {t('controlLayers.globalIPAdapterLayer')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index d943b33f60..26d9c8ce69 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -1,7 +1,7 @@ import { Button, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { isRegionalGuidanceLayer, rgLayerNegativePromptChanged, @@ -19,6 +19,7 @@ type AddPromptButtonProps = { export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const selectValidActions = useMemo( () => createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { @@ -38,9 +39,6 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { const addNegativePrompt = useCallback(() => { dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); - const addIPAdapter = useCallback(() => { - dispatch(guidanceLayerIPAdapterAdded(layerId)); - }, [dispatch, layerId]); return ( @@ -62,7 +60,13 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { > {t('common.negativePrompt')} - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index 7d582236bf..864e48c1d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -1,18 +1,12 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig'; +import { CALayerConfig } from 'features/controlLayers/components/CALayer/CALayerConfig'; 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 { - isControlAdapterLayer, - layerSelected, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { assert } from 'tsafe'; +import { layerSelected, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; import CALayerOpacity from './CALayerOpacity'; @@ -22,19 +16,7 @@ type Props = { export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const selector = useMemo( - () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); - assert(isControlAdapterLayer(layer), `Layer ${layerId} not found or not a ControlNet layer`); - return { - controlNetId: layer.controlNetId, - isSelected: layerId === controlLayers.present.selectedLayerId, - }; - }), - [layerId] - ); - const { controlNetId, isSelected } = useAppSelector(selector); + const isSelected = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).isSelected); const onClickCapture = useCallback(() => { // Must be capture so that the layer is selected before deleting/resetting/etc dispatch(layerSelected(layerId)); @@ -61,7 +43,7 @@ export const CALayer = memo(({ layerId }: Props) => { {isOpen && ( - + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx index 7627fe4364..c998c30f14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx @@ -1,33 +1,101 @@ import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { CALayerModelCombobox } from 'features/controlLayers/components/CALayer/CALayerModelCombobox'; -import { selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; -import { memo } from 'react'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { ControlAdapterModelCombobox } from 'features/controlLayers/components/CALayer/ControlAdapterModelCombobox'; +import { + caLayerControlModeChanged, + caLayerImageChanged, + caLayerModelChanged, + caLayerProcessorConfigChanged, + caOrIPALayerBeginEndStepPctChanged, + caOrIPALayerWeightChanged, + selectCALayer, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlMode, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCaretUpBold } from 'react-icons/pi'; import { useToggle } from 'react-use'; +import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; -import { CALayerBeginEndStepPct } from './CALayerBeginEndStepPct'; -import { CALayerControlMode } from './CALayerControlMode'; import { CALayerImagePreview } from './CALayerImagePreview'; import { CALayerProcessor } from './CALayerProcessor'; import { CALayerProcessorCombobox } from './CALayerProcessorCombobox'; -import { CALayerWeight } from './CALayerWeight'; +import { ControlAdapterBeginEndStepPct } from './ControlAdapterBeginEndStepPct'; +import { ControlAdapterControlModeSelect } from './ControlAdapterControlModeSelect'; +import { ControlAdapterWeight } from './ControlAdapterWeight'; type Props = { layerId: string; }; export const CALayerConfig = memo(({ layerId }: Props) => { - const caType = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.type); + const dispatch = useAppDispatch(); + const controlAdapter = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter); const { t } = useTranslation(); const [isExpanded, toggleIsExpanded] = useToggle(false); + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch( + caOrIPALayerBeginEndStepPctChanged({ + layerId, + beginEndStepPct, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeControlMode = useCallback( + (controlMode: ControlMode) => { + dispatch( + caLayerControlModeChanged({ + layerId, + controlMode, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(caOrIPALayerWeightChanged({ layerId, weight })); + }, + [dispatch, layerId] + ); + + const onChangeProcessorConfig = useCallback( + (processorConfig: ProcessorConfig | null) => { + dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); + }, + [dispatch, layerId] + ); + + const onChangeModel = useCallback( + (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { + dispatch( + caLayerModelChanged({ + layerId, + modelConfig, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(caLayerImageChanged({ layerId, imageDTO })); + }, + [dispatch, layerId] + ); + return ( - + { - {caType === 'controlnet' && } - - + {controlAdapter.type === 'controlnet' && ( + + )} + + - + {isExpanded && ( <> - - + + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx index 209725f94d..c20b408730 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx @@ -7,13 +7,8 @@ import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { - caLayerImageChanged, - heightChanged, - selectCALayer, - selectControlLayersSlice, - widthChanged, -} from 'features/controlLayers/store/controlLayersSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; import type { ControlLayerDropData, ImageDraggableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; @@ -27,10 +22,14 @@ import { useGetImageDTOQuery, useRemoveImageFromBoardMutation, } from 'services/api/endpoints/images'; -import type { ControlLayerAction } from 'services/api/types'; +import type { ControlLayerAction, ImageDTO } from 'services/api/types'; type Props = { - layerId: string; + image: ImageWithDims | null; + processedImage: ImageWithDims | null; + onChangeImage: (imageDTO: ImageDTO | null) => void; + hasProcessor: boolean; + layerId: string; // required for the dnd/upload interactions }; const selectPendingControlImages = createMemoizedSelector( @@ -38,23 +37,9 @@ const selectPendingControlImages = createMemoizedSelector( (controlAdapters) => controlAdapters.pendingControlImages ); -export const CALayerImagePreview = memo(({ layerId }: Props) => { +export const CALayerImagePreview = memo(({ image, processedImage, onChangeImage, hasProcessor, layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const selector = useMemo( - () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = selectCALayer(controlLayers.present, layerId); - const { image, processedImage, processorConfig } = layer.controlAdapter; - return { - imageName: image?.imageName ?? null, - processedImageName: processedImage?.imageName ?? null, - hasProcessor: Boolean(processorConfig), - }; - }), - [layerId] - ); - const { imageName, processedImageName, hasProcessor } = useAppSelector(selector); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const isConnected = useAppSelector((s) => s.system.isConnected); const activeTabName = useAppSelector(activeTabNameSelector); @@ -64,17 +49,19 @@ export const CALayerImagePreview = memo(({ layerId }: Props) => { const [isMouseOverImage, setIsMouseOverImage] = useState(false); - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(imageName ?? skipToken); + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( + image?.imageName ?? skipToken + ); const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - processedImageName ?? skipToken + processedImage?.imageName ?? skipToken ); const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); const [addToBoard] = useAddImageToBoardMutation(); const [removeFromBoard] = useRemoveImageFromBoardMutation(); const handleResetControlImage = useCallback(() => { - dispatch(caLayerImageChanged({ layerId, imageDTO: null })); - }, [layerId, dispatch]); + onChangeImage(null); + }, [onChangeImage]); const handleSaveControlImage = useCallback(async () => { if (!processedControlImage) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx index 3e73158343..31c8d81853 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx @@ -15,7 +15,7 @@ import { import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; -import { caLayerIsFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { caLayerIsFilterEnabledChanged, caLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -34,7 +34,7 @@ const CALayerOpacity = ({ layerId }: Props) => { const { opacity, isFilterEnabled } = useLayerOpacity(layerId); const onChangeOpacity = useCallback( (v: number) => { - dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); + dispatch(caLayerOpacityChanged({ layerId, opacity: v / 100 })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx index 05271010ba..b5ae89f53a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx @@ -1,7 +1,5 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { caLayerProcessorConfigChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { CannyProcessor } from './processors/CannyProcessor'; import { ColorMapProcessor } from './processors/ColorMapProcessor'; @@ -16,19 +14,11 @@ import { MlsdImageProcessor } from './processors/MlsdImageProcessor'; import { PidiProcessor } from './processors/PidiProcessor'; type Props = { - layerId: string; + config: ProcessorConfig | null; + onChange: (config: ProcessorConfig | null) => void; }; -export const CALayerProcessor = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const config = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.processorConfig); - const onChange = useCallback( - (processorConfig: ProcessorConfig) => { - dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); - }, - [dispatch, layerId] - ); - +export const CALayerProcessor = memo(({ config, onChange }: Props) => { if (!config) { return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx index c0f4cca2a5..a01487af44 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx @@ -1,9 +1,9 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { caLayerProcessorConfigChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { CONTROLNET_PROCESSORS, isProcessorType } from 'features/controlLayers/util/controlAdapters'; import { configSelector } from 'features/system/store/configSelectors'; import { includes, map } from 'lodash-es'; @@ -13,7 +13,8 @@ import { PiXBold } from 'react-icons/pi'; import { assert } from 'tsafe'; type Props = { - layerId: string; + config: ProcessorConfig | null; + onChange: (config: ProcessorConfig | null) => void; }; const selectDisabledProcessors = createMemoizedSelector( @@ -21,49 +22,30 @@ const selectDisabledProcessors = createMemoizedSelector( (config) => config.sd.disabledControlNetProcessors ); -export const CALayerProcessorCombobox = memo(({ layerId }: Props) => { +export const CALayerProcessorCombobox = memo(({ config, onChange }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); const disabledProcessors = useAppSelector(selectDisabledProcessors); - const processorType = useAppSelector( - (s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.processorConfig?.type ?? null - ); const options = useMemo(() => { return map(CONTROLNET_PROCESSORS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( (o) => !includes(disabledProcessors, o.value) ); }, [disabledProcessors, t]); - const onChange = useCallback( + const _onChange = useCallback( (v) => { if (!v) { - dispatch( - caLayerProcessorConfigChanged({ - layerId, - processorConfig: null, - }) - ); - return; + onChange(null); + } else { + assert(isProcessorType(v.value)); + onChange(CONTROLNET_PROCESSORS[v.value].buildDefaults()); } - assert(isProcessorType(v.value)); - dispatch( - caLayerProcessorConfigChanged({ - layerId, - processorConfig: CONTROLNET_PROCESSORS[v.value].buildDefaults(), - }) - ); }, - [dispatch, layerId] + [onChange] ); const clearProcessor = useCallback(() => { - dispatch( - caLayerProcessorConfigChanged({ - layerId, - processorConfig: null, - }) - ); - }, [dispatch, layerId]); - const value = useMemo(() => options.find((o) => o.value === processorType) ?? null, [options, processorType]); + onChange(null); + }, [onChange]); + const value = useMemo(() => options.find((o) => o.value === config?.type) ?? null, [options, config?.type]); return ( @@ -71,7 +53,7 @@ export const CALayerProcessorCombobox = memo(({ layerId }: Props) => { {t('controlnet.processor')} - + void; }; const formatPct = (v: number) => `${Math.round(v * 100)}%`; const ariaLabel = ['Begin Step %', 'End Step %']; -export const CALayerBeginEndStepPct = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); +export const ControlAdapterBeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => { const { t } = useTranslation(); - const beginEndStepPct = useAppSelector( - (s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.beginEndStepPct - ); - - const onChange = useCallback( - (v: [number, number]) => { - dispatch( - caLayerBeginEndStepPctChanged({ - layerId, - beginEndStepPct: v, - }) - ); - }, - [dispatch, layerId] - ); - const onReset = useCallback(() => { - dispatch( - caLayerBeginEndStepPctChanged({ - layerId, - beginEndStepPct: [0, 1], - }) - ); - }, [dispatch, layerId]); + onChange([0, 1]); + }, [onChange]); return ( @@ -63,4 +40,4 @@ export const CALayerBeginEndStepPct = memo(({ layerId }: Props) => { ); }); -CALayerBeginEndStepPct.displayName = 'CALayerBeginEndStepPct'; +ControlAdapterBeginEndStepPct.displayName = 'ControlAdapterBeginEndStepPct'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlMode.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterControlModeSelect.tsx similarity index 68% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlMode.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterControlModeSelect.tsx index c60d22a3a0..34f4c85467 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlMode.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterControlModeSelect.tsx @@ -1,26 +1,19 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { caLayerControlModeChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlMode } from 'features/controlLayers/util/controlAdapters'; import { isControlMode } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; type Props = { - layerId: string; + controlMode: ControlMode; + onChange: (controlMode: ControlMode) => void; }; -export const CALayerControlMode = memo(({ layerId }: Props) => { +export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const controlMode = useAppSelector((s) => { - const ca = selectCALayer(s.controlLayers.present, layerId).controlAdapter; - assert(ca.type === 'controlnet'); - return ca.controlMode; - }); - const CONTROL_MODE_DATA = useMemo( () => [ { label: t('controlnet.balanced'), value: 'balanced' }, @@ -34,14 +27,9 @@ export const CALayerControlMode = memo(({ layerId }: Props) => { const handleControlModeChange = useCallback( (v) => { assert(isControlMode(v?.value)); - dispatch( - caLayerControlModeChanged({ - layerId, - controlMode: v.value, - }) - ); + onChange(v.value); }, - [layerId, dispatch] + [onChange] ); const value = useMemo( @@ -69,4 +57,4 @@ export const CALayerControlMode = memo(({ layerId }: Props) => { ); }); -CALayerControlMode.displayName = 'CALayerControlMode'; +ControlAdapterControlModeSelect.displayName = 'ControlAdapterControlModeSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterModelCombobox.tsx similarity index 72% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerModelCombobox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterModelCombobox.tsx index 8e1e5c6891..a4b1d6b744 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterModelCombobox.tsx @@ -1,39 +1,30 @@ import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { caLayerModelChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType'; import type { AnyModelConfig, ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; type Props = { - layerId: string; + modelKey: string | null; + onChange: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; }; -export const CALayerModelCombobox = memo(({ layerId }: Props) => { +export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - - const caModelKey = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.model?.key); const currentBaseModel = useAppSelector((s) => s.generation.model?.base); - const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); - const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === caModelKey), [modelConfigs, caModelKey]); + const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); const _onChange = useCallback( (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null) => { if (!modelConfig) { return; } - dispatch( - caLayerModelChanged({ - layerId, - modelConfig, - }) - ); + onChangeModel(modelConfig); }, - [dispatch, layerId] + [onChangeModel] ); const getIsDisabled = useCallback( @@ -68,4 +59,4 @@ export const CALayerModelCombobox = memo(({ layerId }: Props) => { ); }); -CALayerModelCombobox.displayName = 'CALayerModelCombobox'; +ControlAdapterModelCombobox.displayName = 'ControlAdapterModelCombobox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerWeight.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterWeight.tsx similarity index 72% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerWeight.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterWeight.tsx index b8738fd352..4bb7bb3911 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerWeight.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterWeight.tsx @@ -1,21 +1,19 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { caLayerWeightChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; type Props = { - layerId: string; + weight: number; + onChange: (weight: number) => void; }; const formatValue = (v: number) => v.toFixed(2); const marks = [0, 1, 2]; -export const CALayerWeight = memo(({ layerId }: Props) => { +export const ControlAdapterWeight = memo(({ weight, onChange }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const weight = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.weight); const initial = useAppSelector((s) => s.config.sd.ca.weight.initial); const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax); @@ -24,13 +22,6 @@ export const CALayerWeight = memo(({ layerId }: Props) => { const coarseStep = useAppSelector((s) => s.config.sd.ca.weight.coarseStep); const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep); - const onChange = useCallback( - (weight: number) => { - dispatch(caLayerWeightChanged({ layerId, weight })); - }, - [dispatch, layerId] - ); - return ( @@ -61,4 +52,4 @@ export const CALayerWeight = memo(({ layerId }: Props) => { ); }); -CALayerWeight.displayName = 'CALayerWeight'; +ControlAdapterWeight.displayName = 'ControlAdapterWeight'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index c55864afa5..dad102b470 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -1,6 +1,6 @@ import { Button } from '@invoke-ai/ui-library'; -import { allLayersDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; @@ -8,12 +8,19 @@ import { PiTrashSimpleBold } from 'react-icons/pi'; export const DeleteAllLayersButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const isDisabled = useAppSelector((s) => s.controlLayers.present.layers.length === 0); const onClick = useCallback(() => { dispatch(allLayersDeleted()); }, [dispatch]); return ( - ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx deleted file mode 100644 index aa518c6dd4..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterProcessorComponent from 'features/controlAdapters/components/ControlAdapterProcessorComponent'; -import ControlAdapterShouldAutoConfig from 'features/controlAdapters/components/ControlAdapterShouldAutoConfig'; -import ParamControlAdapterIPMethod from 'features/controlAdapters/components/parameters/ParamControlAdapterIPMethod'; -import ParamControlAdapterProcessorSelect from 'features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect'; -import { ParamControlAdapterBeginEnd } from 'features/controlLayers/components/CALayer/CALayerBeginEndStepPct'; -import ParamControlAdapterControlMode from 'features/controlLayers/components/CALayer/CALayerControlMode'; -import { CALayerImagePreview } from 'features/controlLayers/components/CALayer/CALayerImagePreview'; -import ParamControlAdapterModel from 'features/controlLayers/components/CALayer/CALayerModelCombobox'; -import ParamControlAdapterWeight from 'features/controlLayers/components/CALayer/CALayerWeight'; -import { selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiCaretUpBold } from 'react-icons/pi'; -import { useToggle } from 'react-use'; - -type Props = { - layerId: string; -}; - -export const CALayerCAConfig = memo(({ layerId }: Props) => { - const caType = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.type); - const { t } = useTranslation(); - const [isExpanded, toggleIsExpanded] = useToggle(false); - - return ( - - - - {' '} - - - {controlAdapterType !== 'ip_adapter' && ( - - } - /> - )} - - - - {controlAdapterType === 'ip_adapter' && } - {controlAdapterType === 'controlnet' && } - - - - - - - - {isExpanded && ( - <> - - - - - )} - - ); -}); - -CALayerCAConfig.displayName = 'CALayerCAConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx index 2dee5d95a6..71b06e6830 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -1,29 +1,15 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig'; +import { IPALayerConfig } from 'features/controlLayers/components/IPALayer/IPALayerConfig'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; -import { isIPAdapterLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useMemo } from 'react'; -import { assert } from 'tsafe'; +import { memo } from 'react'; type Props = { layerId: string; }; export const IPALayer = memo(({ layerId }: Props) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); - assert(isIPAdapterLayer(layer), `Layer ${layerId} not found or not an IP Adapter layer`); - return layer.ipAdapterId; - }), - [layerId] - ); - const ipAdapterId = useAppSelector(selector); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( @@ -36,7 +22,7 @@ export const IPALayer = memo(({ layerId }: Props) => { {isOpen && ( - + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx new file mode 100644 index 0000000000..f1b035da1c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx @@ -0,0 +1,105 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { ControlAdapterBeginEndStepPct } from 'features/controlLayers/components/CALayer/ControlAdapterBeginEndStepPct'; +import { ControlAdapterWeight } from 'features/controlLayers/components/CALayer/ControlAdapterWeight'; +import { IPAdapterImagePreview } from 'features/controlLayers/components/IPALayer/IPAdapterImagePreview'; +import { IPAdapterMethod } from 'features/controlLayers/components/IPALayer/IPAdapterMethod'; +import { IPAdapterModelCombobox } from 'features/controlLayers/components/IPALayer/IPALayerModelCombobox'; +import { + caOrIPALayerBeginEndStepPctChanged, + caOrIPALayerWeightChanged, + ipaLayerCLIPVisionModelChanged, + ipaLayerImageChanged, + ipaLayerMethodChanged, + ipaLayerModelChanged, + selectIPALayer, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; +import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; + +type Props = { + layerId: string; +}; + +export const IPALayerConfig = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const ipAdapter = useAppSelector((s) => selectIPALayer(s.controlLayers.present, layerId).ipAdapter); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch( + caOrIPALayerBeginEndStepPctChanged({ + layerId, + beginEndStepPct, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(caOrIPALayerWeightChanged({ layerId, weight })); + }, + [dispatch, layerId] + ); + + const onChangeIPMethod = useCallback( + (method: IPMethod) => { + dispatch(ipaLayerMethodChanged({ layerId, method })); + }, + [dispatch, layerId] + ); + + const onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig) => { + dispatch(ipaLayerModelChanged({ layerId, modelConfig })); + }, + [dispatch, layerId] + ); + + const onChangeCLIPVisionModel = useCallback( + (clipVisionModel: CLIPVisionModel) => { + dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel })); + }, + [dispatch, layerId] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(ipaLayerImageChanged({ layerId, imageDTO })); + }, + [dispatch, layerId] + ); + + return ( + + + + + + + + + + + + + + + + + + ); +}); + +IPALayerConfig.displayName = 'IPALayerConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx new file mode 100644 index 0000000000..facd46aed1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx @@ -0,0 +1,100 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import type { CLIPVisionModel } from 'features/controlLayers/util/controlAdapters'; +import { isCLIPVisionModel } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useIPAdapterModels } from 'services/api/hooks/modelsByType'; +import type { AnyModelConfig, IPAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +const CLIP_VISION_OPTIONS = [ + { label: 'ViT-H', value: 'ViT-H' }, + { label: 'ViT-G', value: 'ViT-G' }, +]; + +type Props = { + modelKey: string | null; + onChangeModel: (modelConfig: IPAdapterModelConfig) => void; + clipVisionModel: CLIPVisionModel; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void; +}; + +export const IPAdapterModelCombobox = memo( + ({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { + const { t } = useTranslation(); + const currentBaseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs, { isLoading }] = useIPAdapterModels(); + const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); + + const _onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig | null) => { + if (!modelConfig) { + return; + } + onChangeModel(modelConfig); + }, + [onChangeModel] + ); + + const _onChangeCLIPVisionModel = useCallback( + (v) => { + assert(isCLIPVisionModel(v?.value)); + onChangeCLIPVisionModel(v.value); + }, + [onChangeCLIPVisionModel] + ); + + const getIsDisabled = useCallback( + (model: AnyModelConfig): boolean => { + const isCompatible = currentBaseModel === model.base; + const hasMainModel = Boolean(currentBaseModel); + return !hasMainModel || !isCompatible; + }, + [currentBaseModel] + ); + + const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ + modelConfigs, + onChange: _onChangeModel, + selectedModel, + getIsDisabled, + isLoading, + }); + + const clipVisionModelValue = useMemo( + () => CLIP_VISION_OPTIONS.find((o) => o.value === clipVisionModel), + [clipVisionModel] + ); + + return ( + + + + + + + {selectedModel?.format === 'checkpoint' && ( + + + + )} + + ); + } +); + +IPAdapterModelCombobox.displayName = 'IPALayerModelCombobox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx new file mode 100644 index 0000000000..bff6d29502 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx @@ -0,0 +1,119 @@ +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 { ControlLayerDropData, ImageDraggableData } 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 { ControlLayerAction, ImageDTO } from 'services/api/types'; + +type Props = { + image: ImageWithDims | null; + onChangeImage: (imageDTO: ImageDTO | null) => void; + layerId: string; // required for the dnd/upload interactions +}; + +export const IPAdapterImagePreview = memo(({ image, onChangeImage, layerId }: 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: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( + image?.imageName ?? skipToken + ); + const handleResetControlImage = useCallback(() => { + onChangeImage(null); + }, [onChangeImage]); + + const handleSetControlImageToDimensions = useCallback(() => { + if (!controlImage) { + return; + } + + if (activeTabName === 'unifiedCanvas') { + dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)); + } else { + if (shift) { + const { width, height } = controlImage; + dispatch(widthChanged({ width, updateAspectRatio: true })); + dispatch(heightChanged({ height, updateAspectRatio: true })); + } else { + const { width, height } = calculateNewSize( + controlImage.width / controlImage.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, updateAspectRatio: true })); + dispatch(heightChanged({ height, updateAspectRatio: true })); + } + } + }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); + + const draggableData = useMemo(() => { + if (controlImage) { + return { + id: layerId, + payloadType: 'IMAGE_DTO', + payload: { imageDTO: controlImage }, + }; + } + }, [controlImage, layerId]); + + const droppableData = useMemo( + () => ({ + id: layerId, + actionType: 'SET_CONTROL_LAYER_IMAGE', + context: { layerId }, + }), + [layerId] + ); + + const postUploadAction = useMemo(() => ({ type: 'SET_CONTROL_LAYER_IMAGE', layerId }), [layerId]); + + useEffect(() => { + if (isConnected && isErrorControlImage) { + handleResetControlImage(); + } + }, [handleResetControlImage, isConnected, isErrorControlImage]); + + return ( + + + + <> + : undefined} + tooltip={t('controlnet.resetControlImage')} + /> + : undefined} + tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} + styleOverrides={setControlImageDimensionsStyleOverrides} + /> + + + ); +}); + +IPAdapterImagePreview.displayName = 'IPAdapterImagePreview'; + +const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 6 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx new file mode 100644 index 0000000000..70fd63f9c0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx @@ -0,0 +1,44 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import type { IPMethod } from 'features/controlLayers/util/controlAdapters'; +import { isIPMethod } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { assert } from 'tsafe'; + +type Props = { + method: IPMethod; + onChange: (method: IPMethod) => void; +}; + +export const IPAdapterMethod = memo(({ method, onChange }: Props) => { + const { t } = useTranslation(); + const options: { label: string; value: IPMethod }[] = useMemo( + () => [ + { label: t('controlnet.full'), value: 'full' }, + { label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' }, + { label: `${t('controlnet.composition')} (${t('common.beta')})`, value: 'composition' }, + ], + [t] + ); + const _onChange = useCallback( + (v) => { + assert(isIPMethod(v?.value)); + onChange(v.value); + }, + [onChange] + ); + const value = useMemo(() => options.find((o) => o.value === method), [options, method]); + + return ( + + + {t('controlnet.ipAdapterMethod')} + + + + ); +}); + +IPAdapterMethod.displayName = 'IPAdapterMethod'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ParamControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ParamControlAdapterModel.tsx deleted file mode 100644 index 73a7d695b3..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ParamControlAdapterModel.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; -import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { useControlAdapterCLIPVisionModel } from 'features/controlAdapters/hooks/useControlAdapterCLIPVisionModel'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterModel } from 'features/controlAdapters/hooks/useControlAdapterModel'; -import { useControlAdapterModels } from 'features/controlAdapters/hooks/useControlAdapterModels'; -import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType'; -import { - controlAdapterCLIPVisionModelChanged, - controlAdapterModelChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { CLIPVisionModel } from 'features/controlAdapters/store/types'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import type { - AnyModelConfig, - ControlNetModelConfig, - IPAdapterModelConfig, - T2IAdapterModelConfig, -} from 'services/api/types'; - -type ParamControlAdapterModelProps = { - id: string; -}; - -const selectMainModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model); - -const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { - const isEnabled = useControlAdapterIsEnabled(id); - const controlAdapterType = useControlAdapterType(id); - const { modelConfig } = useControlAdapterModel(id); - const dispatch = useAppDispatch(); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base); - const currentCLIPVisionModel = useControlAdapterCLIPVisionModel(id); - const mainModel = useAppSelector(selectMainModel); - const { t } = useTranslation(); - - const [modelConfigs, { isLoading }] = useControlAdapterModels(controlAdapterType); - - const _onChange = useCallback( - (modelConfig: ControlNetModelConfig | IPAdapterModelConfig | T2IAdapterModelConfig | null) => { - if (!modelConfig) { - return; - } - dispatch( - controlAdapterModelChanged({ - id, - modelConfig, - }) - ); - }, - [dispatch, id] - ); - - const onCLIPVisionModelChange = useCallback( - (v) => { - if (!v?.value) { - return; - } - dispatch(controlAdapterCLIPVisionModelChanged({ id, clipVisionModel: v.value as CLIPVisionModel })); - }, - [dispatch, id] - ); - - const selectedModel = useMemo( - () => (modelConfig && controlAdapterType ? { ...modelConfig, model_type: controlAdapterType } : null), - [controlAdapterType, modelConfig] - ); - - const getIsDisabled = useCallback( - (model: AnyModelConfig): boolean => { - const isCompatible = currentBaseModel === model.base; - const hasMainModel = Boolean(currentBaseModel); - return !hasMainModel || !isCompatible; - }, - [currentBaseModel] - ); - - const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ - modelConfigs, - onChange: _onChange, - selectedModel, - getIsDisabled, - isLoading, - }); - - const clipVisionOptions = useMemo( - () => [ - { label: 'ViT-H', value: 'ViT-H' }, - { label: 'ViT-G', value: 'ViT-G' }, - ], - [] - ); - - const clipVisionModel = useMemo( - () => clipVisionOptions.find((o) => o.value === currentCLIPVisionModel), - [clipVisionOptions, currentCLIPVisionModel] - ); - - return ( - - - - - - - {modelConfig?.type === 'ip_adapter' && modelConfig.format === 'checkpoint' && ( - - - - )} - - ); -}; - -export default memo(ParamControlAdapterModel); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx index 0c74b2a9ea..0cd7d83dfe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { guidanceLayerDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; +import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; @@ -12,7 +12,7 @@ export const LayerDeleteButton = memo(({ layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const deleteLayer = useCallback(() => { - dispatch(guidanceLayerDeleted(layerId)); + dispatch(layerDeleted(layerId)); }, [dispatch, layerId]); return ( { const dispatch = useAppDispatch(); const { t } = useTranslation(); + const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const selectValidActions = useMemo( () => createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { @@ -37,9 +38,6 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { const addNegativePrompt = useCallback(() => { dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); - const addIPAdapter = useCallback(() => { - dispatch(guidanceLayerIPAdapterAdded(layerId)); - }, [dispatch, layerId]); return ( <> }> @@ -48,7 +46,7 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { }> {t('controlLayers.addNegativePrompt')} - }> + } isDisabled={isAddIPAdapterDisabled}> {t('controlLayers.addIPAdapter')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index a3dbfb00e7..baed22f6ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -38,7 +38,7 @@ export const RGLayer = memo(({ layerId }: Props) => { color: rgbColorToString(layer.previewColor), hasPositivePrompt: layer.positivePrompt !== null, hasNegativePrompt: layer.negativePrompt !== null, - hasIPAdapters: layer.ipAdapterIds.length > 0, + hasIPAdapters: layer.ipAdapters.length > 0, isSelected: layerId === controlLayers.present.selectedLayerId, autoNegative: layer.autoNegative, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx index 3a1f6d79cd..cb3c371c67 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx @@ -1,9 +1,11 @@ import { Divider, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { guidanceLayerIPAdapterDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig'; -import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { + isRegionalGuidanceLayer, + rgLayerIPAdapterDeleted, + selectControlLayersSlice, +} from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback, useMemo } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; import { assert } from 'tsafe'; @@ -18,19 +20,19 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); - return layer.ipAdapterIds; + return layer.ipAdapters; }), [layerId] ); - const ipAdapterIds = useAppSelector(selectIPAdapterIds); + const ipAdapters = useAppSelector(selectIPAdapterIds); - if (ipAdapterIds.length === 0) { + if (ipAdapters.length === 0) { return null; } return ( <> - {ipAdapterIds.map((id, index) => ( + {ipAdapters.map(({ id }, index) => ( {index > 0 && ( @@ -55,7 +57,7 @@ type IPAdapterListItemProps = { const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber }: IPAdapterListItemProps) => { const dispatch = useAppDispatch(); const onDeleteIPAdapter = useCallback(() => { - dispatch(guidanceLayerIPAdapterDeleted({ layerId, ipAdapterId })); + dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); }, [dispatch, ipAdapterId, layerId]); return ( @@ -72,7 +74,7 @@ const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber } colorScheme="error" /> - + {/* */} ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts new file mode 100644 index 0000000000..17f0d4bf2d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -0,0 +1,95 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { caLayerAdded, ipaLayerAdded, rgLayerIPAdapterAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { + buildControlNet, + buildIPAdapter, + buildT2IAdapter, + CONTROLNET_PROCESSORS, + isProcessorType, +} from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import { useCallback, useMemo } from 'react'; +import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType'; +import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; + +export const useAddCALayer = () => { + const dispatch = useAppDispatch(); + const baseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs] = useControlNetAndT2IAdapterModels(); + const model: ControlNetModelConfig | T2IAdapterModelConfig | null = useMemo(() => { + // prefer to use a model that matches the base model + const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); + return compatibleModels[0] ?? modelConfigs[0] ?? null; + }, [baseModel, modelConfigs]); + const isDisabled = useMemo(() => !model, [model]); + const addCALayer = useCallback(() => { + if (!model) { + return; + } + + const id = uuidv4(); + const defaultPreprocessor = model.default_settings?.preprocessor; + const processorConfig = isProcessorType(defaultPreprocessor) + ? CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(baseModel) + : null; + + const builder = model.type === 'controlnet' ? buildControlNet : buildT2IAdapter; + const controlAdapter = builder(id, { + model: zModelIdentifierField.parse(model), + processorConfig, + }); + + dispatch(caLayerAdded(controlAdapter)); + }, [dispatch, model, baseModel]); + + return [addCALayer, isDisabled] as const; +}; + +export const useAddIPALayer = () => { + const dispatch = useAppDispatch(); + const baseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs] = useIPAdapterModels(); + const model: IPAdapterModelConfig | null = useMemo(() => { + // prefer to use a model that matches the base model + const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); + return compatibleModels[0] ?? modelConfigs[0] ?? null; + }, [baseModel, modelConfigs]); + const isDisabled = useMemo(() => !model, [model]); + const addIPALayer = useCallback(() => { + if (!model) { + return; + } + const id = uuidv4(); + const ipAdapter = buildIPAdapter(id, { + model: zModelIdentifierField.parse(model), + }); + dispatch(ipaLayerAdded(ipAdapter)); + }, [dispatch, model]); + + return [addIPALayer, isDisabled] as const; +}; + +export const useAddIPAdapterToIPALayer = (layerId: string) => { + const dispatch = useAppDispatch(); + const baseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs] = useIPAdapterModels(); + const model: IPAdapterModelConfig | null = useMemo(() => { + // prefer to use a model that matches the base model + const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); + return compatibleModels[0] ?? modelConfigs[0] ?? null; + }, [baseModel, modelConfigs]); + const isDisabled = useMemo(() => !model, [model]); + const addIPAdapter = useCallback(() => { + if (!model) { + return; + } + const id = uuidv4(); + const ipAdapter = buildIPAdapter(id, { + model: zModelIdentifierField.parse(model), + }); + dispatch(rgLayerIPAdapterAdded({ layerId, ipAdapter })); + }, [dispatch, model, layerId]); + + return [addIPAdapter, isDisabled] as const; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts index bb744b0535..e3e87d0c42 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts @@ -9,7 +9,7 @@ import { $lastMouseDownPos, $tool, brushSizeChanged, - rfLayerLineAdded, + rgLayerLineAdded, rgLayerPointsAdded, rgLayerRectAdded, } from 'features/controlLayers/store/controlLayersSlice'; @@ -71,7 +71,7 @@ export const useMouseEvents = () => { } if (tool === 'brush' || tool === 'eraser') { dispatch( - rfLayerLineAdded({ + rgLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool, @@ -181,7 +181,7 @@ export const useMouseEvents = () => { } if (tool === 'brush' || tool === 'eraser') { dispatch( - rfLayerLineAdded({ + rgLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool, diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts index 93c8bec8a6..56d380b1d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts @@ -13,7 +13,7 @@ const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlL .filter((l) => l.isEnabled) .filter((l) => { const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); - const hasAtLeastOneImagePrompt = l.ipAdapterIds.length > 0; + const hasAtLeastOneImagePrompt = l.ipAdapters.length > 0; return hasTextPrompt || hasAtLeastOneImagePrompt; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 8ea3bb5bee..4d7feaa6ee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -25,7 +25,7 @@ import { isEqual, partition } from 'lodash-es'; import { atom } from 'nanostores'; import type { RgbColor } from 'react-colorful'; import type { UndoableOptions } from 'redux-undo'; -import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; +import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -88,11 +88,19 @@ export const selectCALayer = (state: ControlLayersState, layerId: string): Contr assert(isControlAdapterLayer(layer)); return layer; }; -const selectIPALayer = (state: ControlLayersState, layerId: string): IPAdapterLayer => { +export const selectIPALayer = (state: ControlLayersState, layerId: string): IPAdapterLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isIPAdapterLayer(layer)); return layer; }; +export const selectCAOrIPALayer = ( + state: ControlLayersState, + layerId: string +): ControlAdapterLayer | IPAdapterLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isControlAdapterLayer(layer) || isIPAdapterLayer(layer)); + return layer; +}; const selectRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer)); @@ -199,6 +207,10 @@ export const controlLayersSlice = createSlice({ state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId); state.selectedLayerId = state.layers[0]?.id ?? null; }, + allLayersDeleted: (state) => { + state.layers = []; + state.selectedLayerId = null; + }, //#endregion //#region CA Layers @@ -272,19 +284,6 @@ export const controlLayersSlice = createSlice({ layer.controlAdapter.processorConfig = candidateProcessorConfig; } }, - caLayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { - const { layerId, weight } = action.payload; - const layer = selectCALayer(state, layerId); - layer.controlAdapter.weight = weight; - }, - caLayerBeginEndStepPctChanged: ( - state, - action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> - ) => { - const { layerId, beginEndStepPct } = action.payload; - const layer = selectCALayer(state, layerId); - layer.controlAdapter.beginEndStepPct = beginEndStepPct; - }, caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlMode }>) => { const { layerId, controlMode } = action.payload; const layer = selectCALayer(state, layerId); @@ -348,6 +347,21 @@ export const controlLayersSlice = createSlice({ const layer = selectIPALayer(state, layerId); layer.ipAdapter.method = method; }, + ipaLayerModelChanged: ( + state, + action: PayloadAction<{ + layerId: string; + modelConfig: IPAdapterModelConfig | null; + }> + ) => { + const { layerId, modelConfig } = action.payload; + const layer = selectIPALayer(state, layerId); + if (!modelConfig) { + layer.ipAdapter.model = null; + return; + } + layer.ipAdapter.model = zModelIdentifierField.parse(modelConfig); + }, ipaLayerCLIPVisionModelChanged: ( state, action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }> @@ -358,34 +372,61 @@ export const controlLayersSlice = createSlice({ }, //#endregion - //#region RG Layers - rgLayerAdded: (state, action: PayloadAction<{ layerId: string }>) => { - const { layerId } = action.payload; - const layer: RegionalGuidanceLayer = { - id: getRGLayerId(layerId), - type: 'regional_guidance_layer', - isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, - maskObjects: [], - previewColor: getVectorMaskPreviewColor(state), - x: 0, - y: 0, - autoNegative: 'invert', - needsPixelBbox: false, - positivePrompt: '', - negativePrompt: null, - ipAdapters: [], - 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; - } + //#region CA or IPA Layers + caOrIPALayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { + const { layerId, weight } = action.payload; + const layer = selectCAOrIPALayer(state, layerId); + if (layer.type === 'control_adapter_layer') { + layer.controlAdapter.weight = weight; + } else { + layer.ipAdapter.weight = weight; } }, + caOrIPALayerBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> + ) => { + const { layerId, beginEndStepPct } = action.payload; + const layer = selectCAOrIPALayer(state, layerId); + if (layer.type === 'control_adapter_layer') { + layer.controlAdapter.beginEndStepPct = beginEndStepPct; + } else { + layer.ipAdapter.beginEndStepPct = beginEndStepPct; + } + }, + //#endregion + + //#region RG Layers + rgLayerAdded: { + reducer: (state, action: PayloadAction<{ layerId: string }>) => { + const { layerId } = action.payload; + const layer: RegionalGuidanceLayer = { + id: getRGLayerId(layerId), + type: 'regional_guidance_layer', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + maskObjects: [], + previewColor: getVectorMaskPreviewColor(state), + x: 0, + y: 0, + autoNegative: 'invert', + needsPixelBbox: false, + positivePrompt: '', + negativePrompt: null, + ipAdapters: [], + 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: () => ({ payload: { layerId: uuidv4() } }), + }, rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; const layer = selectRGLayer(state, layerId); @@ -396,16 +437,6 @@ export const controlLayersSlice = createSlice({ const layer = selectRGLayer(state, layerId); layer.negativePrompt = prompt; }, - rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { - const { layerId, ipAdapter } = action.payload; - const layer = selectRGLayer(state, layerId); - layer.ipAdapters.push(ipAdapter); - }, - rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { - const { layerId, ipAdapterId } = action.payload; - const layer = selectRGLayer(state, layerId); - layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); - }, rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { const { layerId, color } = action.payload; const layer = selectRGLayer(state, layerId); @@ -483,6 +514,16 @@ export const controlLayersSlice = createSlice({ const layer = selectRGLayer(state, layerId); layer.autoNegative = autoNegative; }, + rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { + const { layerId, ipAdapter } = action.payload; + const layer = selectRGLayer(state, layerId); + layer.ipAdapters.push(ipAdapter); + }, + rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { + const { layerId, ipAdapterId } = action.payload; + const layer = selectRGLayer(state, layerId); + layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); + }, rgLayerIPAdapterImageChanged: ( state, action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }> @@ -657,13 +698,12 @@ export const { layerMovedToBack, selectedLayerReset, selectedLayerDeleted, + allLayersDeleted, // CA Layers caLayerAdded, caLayerImageChanged, caLayerProcessedImageChanged, caLayerModelChanged, - caLayerWeightChanged, - caLayerBeginEndStepPctChanged, caLayerControlModeChanged, caLayerProcessorConfigChanged, caLayerIsFilterEnabledChanged, @@ -674,18 +714,22 @@ export const { ipaLayerWeightChanged, ipaLayerBeginEndStepPctChanged, ipaLayerMethodChanged, + ipaLayerModelChanged, ipaLayerCLIPVisionModelChanged, + // CA or IPA Layers + caOrIPALayerWeightChanged, + caOrIPALayerBeginEndStepPctChanged, // RG Layers rgLayerAdded, rgLayerPositivePromptChanged, rgLayerNegativePromptChanged, - rgLayerIPAdapterAdded, - rgLayerIPAdapterDeleted, rgLayerPreviewColorChanged, rgLayerLineAdded, rgLayerPointsAdded, rgLayerRectAdded, rgLayerAutoNegativeChanged, + rgLayerIPAdapterAdded, + rgLayerIPAdapterDeleted, rgLayerIPAdapterImageChanged, rgLayerIPAdapterWeightChanged, rgLayerIPAdapterBeginEndStepPctChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index a388d65e94..3debe10791 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -72,7 +72,7 @@ export type ProcessorConfig = | PidiProcessorConfig | ZoeDepthProcessorConfig; -type ImageWithDims = { +export type ImageWithDims = { imageName: string; width: number; height: number; @@ -273,7 +273,7 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { type: 'zoe_depth_image_processor', }), }, -} +}; export const zProcessorType = z.enum([ 'canny_image_processor', 'color_map_image_processor', @@ -328,15 +328,15 @@ export const initialIPAdapter: Omit = { }; export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfig => { - return merge(deepClone(initialControlNet), { id, overrides }); + return merge(deepClone(initialControlNet), { id, ...overrides }); }; export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfig => { - return merge(deepClone(initialT2IAdapter), { id, overrides }); + return merge(deepClone(initialT2IAdapter), { id, ...overrides }); }; export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfig => { - return merge(deepClone(initialIPAdapter), { id, overrides }); + return merge(deepClone(initialIPAdapter), { id, ...overrides }); }; export const buildControlAdapterProcessor = ( diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index cbe1410a95..c79278b03d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -52,8 +52,7 @@ const STAGE_BG_DATAURL = const mapId = (object: { id: string }) => object.id; -const selectRenderableLayers = (n: Konva.Node) => - n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME; +const selectRenderableLayers = (n: Konva.Node) => n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME; const selectVectorMaskObjects = (node: Konva.Node) => { return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; @@ -432,9 +431,9 @@ const updateControlNetLayerImageSource = async ( konvaLayer: Konva.Layer, reduxLayer: ControlAdapterLayer ) => { - if (reduxLayer.imageName) { - const imageName = reduxLayer.imageName; - const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(reduxLayer.imageName)); + if (reduxLayer.controlAdapter.image) { + const { imageName } = reduxLayer.controlAdapter.image; + const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); const imageDTO = await req.unwrap(); req.unsubscribe(); const image = new Image(); @@ -442,8 +441,7 @@ const updateControlNetLayerImageSource = async ( image.onload = () => { // Find the existing image or create a new one - must find using the name, bc the id may have just changed const konvaImage = - konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`) ?? - createControlNetLayerImage(konvaLayer, image); + konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`) ?? createControlNetLayerImage(konvaLayer, image); // Update the image's attributes konvaImage.setAttrs({ @@ -502,11 +500,11 @@ const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay let imageSourceNeedsUpdate = false; if (canvasImageSource instanceof HTMLImageElement) { if ( - reduxLayer.imageName && - canvasImageSource.id !== getCALayerImageId(reduxLayer.id, reduxLayer.imageName) + reduxLayer.controlAdapter.image && + canvasImageSource.id !== getCALayerImageId(reduxLayer.id, reduxLayer.controlAdapter.image.imageName) ) { imageSourceNeedsUpdate = true; - } else if (!reduxLayer.imageName) { + } else if (!reduxLayer.controlAdapter.image) { imageSourceNeedsUpdate = true; } } else if (!canvasImageSource) { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx index d072cfde0f..eef997a11b 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx @@ -13,66 +13,52 @@ import { selectValidIPAdapters, selectValidT2IAdapters, } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { selectAllControlAdapterIds, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { Fragment, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; -const selector = createMemoizedSelector( - [selectControlAdaptersSlice, selectControlLayersSlice], - (controlAdapters, controlLayers) => { - const badges: string[] = []; - let isError = false; +const selector = createMemoizedSelector([selectControlAdaptersSlice], (controlAdapters) => { + const badges: string[] = []; + let isError = false; - const controlLayersAdapterIds = selectAllControlAdapterIds(controlLayers.present); + const enabledNonRegionalIPAdapterCount = selectAllIPAdapters(controlAdapters).filter((ca) => ca.isEnabled).length; - const enabledNonRegionalIPAdapterCount = selectAllIPAdapters(controlAdapters) - .filter((ca) => !controlLayersAdapterIds.includes(ca.id)) - .filter((ca) => ca.isEnabled).length; - - const validIPAdapterCount = selectValidIPAdapters(controlAdapters).length; - if (enabledNonRegionalIPAdapterCount > 0) { - badges.push(`${enabledNonRegionalIPAdapterCount} IP`); - } - if (enabledNonRegionalIPAdapterCount > validIPAdapterCount) { - isError = true; - } - - const enabledControlNetCount = selectAllControlNets(controlAdapters) - .filter((ca) => !controlLayersAdapterIds.includes(ca.id)) - .filter((ca) => ca.isEnabled).length; - const validControlNetCount = selectValidControlNets(controlAdapters).length; - if (enabledControlNetCount > 0) { - badges.push(`${enabledControlNetCount} ControlNet`); - } - if (enabledControlNetCount > validControlNetCount) { - isError = true; - } - - const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters) - .filter((ca) => !controlLayersAdapterIds.includes(ca.id)) - .filter((ca) => ca.isEnabled).length; - const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length; - if (enabledT2IAdapterCount > 0) { - badges.push(`${enabledT2IAdapterCount} T2I`); - } - if (enabledT2IAdapterCount > validT2IAdapterCount) { - isError = true; - } - - const controlAdapterIds = selectControlAdapterIds(controlAdapters).filter( - (id) => !controlLayersAdapterIds.includes(id) - ); - - return { - controlAdapterIds, - badges, - isError, // TODO: Add some visual indicator that the control adapters are in an error state - }; + const validIPAdapterCount = selectValidIPAdapters(controlAdapters).length; + if (enabledNonRegionalIPAdapterCount > 0) { + badges.push(`${enabledNonRegionalIPAdapterCount} IP`); } -); + if (enabledNonRegionalIPAdapterCount > validIPAdapterCount) { + isError = true; + } + + const enabledControlNetCount = selectAllControlNets(controlAdapters).filter((ca) => ca.isEnabled).length; + const validControlNetCount = selectValidControlNets(controlAdapters).length; + if (enabledControlNetCount > 0) { + badges.push(`${enabledControlNetCount} ControlNet`); + } + if (enabledControlNetCount > validControlNetCount) { + isError = true; + } + + const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters).filter((ca) => ca.isEnabled).length; + const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length; + if (enabledT2IAdapterCount > 0) { + badges.push(`${enabledT2IAdapterCount} T2I`); + } + if (enabledT2IAdapterCount > validT2IAdapterCount) { + isError = true; + } + + const controlAdapterIds = selectControlAdapterIds(controlAdapters); + + return { + controlAdapterIds, + badges, + isError, // TODO: Add some visual indicator that the control adapters are in an error state + }; +}); export const ControlSettingsAccordion: React.FC = memo(() => { const { t } = useTranslation();