From 905baf278720201faa2188d23e79c350c90f1db3 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 1 May 2024 21:36:51 +1000
Subject: [PATCH] refactor(ui): continue wiring up CA logic across (wip)
It works!
---
invokeai/frontend/web/public/locales/en.json | 1 +
.../listeners/imageDropped.ts | 60 +++
.../listeners/imageUploaded.ts | 38 ++
.../src/common/hooks/useIsReadyToEnqueue.ts | 147 +++---
.../components/CALayer/CALayer.tsx | 4 +-
.../components/CALayer/CALayerConfig.tsx | 149 ------
.../CALayer/CALayerControlAdapterWrapper.tsx | 121 +++++
.../CALayer/CALayerImagePreview.tsx | 231 ---------
.../ControlAndIPAdapter/ControlAdapter.tsx | 111 +++++
.../ControlAdapterBeginEndStepPct.tsx | 0
.../ControlAdapterControlModeSelect.tsx | 0
.../ControlAdapterImagePreview.tsx | 234 +++++++++
.../ControlAdapterModelCombobox.tsx | 0
.../ControlAdapterProcessorConfig.tsx} | 4 +-
.../ControlAdapterProcessorTypeSelect.tsx} | 6 +-
.../ControlAdapterWeight.tsx | 0
.../ControlAndIPAdapter/IPAdapter.tsx | 72 +++
.../IPAdapterImagePreview.tsx | 114 +++++
.../IPAdapterMethod.tsx | 0
.../IPAdapterModelSelect.tsx} | 4 +-
.../processors/CannyProcessor.tsx | 2 +-
.../processors/ColorMapProcessor.tsx | 2 +-
.../processors/ContentShuffleProcessor.tsx | 2 +-
.../processors/DWOpenposeProcessor.tsx | 2 +-
.../processors/DepthAnythingProcessor.tsx | 2 +-
.../processors/HedProcessor.tsx | 2 +-
.../processors/LineartProcessor.tsx | 2 +-
.../processors/MediapipeFaceProcessor.tsx | 2 +-
.../processors/MidasDepthProcessor.tsx | 2 +-
.../processors/MlsdImageProcessor.tsx | 2 +-
.../processors/PidiProcessor.tsx | 2 +-
.../processors/ProcessorWrapper.tsx | 0
.../processors/types.ts | 0
.../components/IPALayer/IPALayer.tsx | 4 +-
.../components/IPALayer/IPALayerConfig.tsx | 105 ----
.../IPALayer/IPALayerIPAdapterWrapper.tsx | 106 ++++
.../IPALayer/IPAdapterImagePreview.tsx | 119 -----
.../RGLayer/RGLayerIPAdapterList.tsx | 49 +-
.../RGLayer/RGLayerIPAdapterWrapper.tsx | 131 +++++
.../hooks/useControlLayersTitle.ts | 3 -
.../controlLayers/store/controlLayersSlice.ts | 34 +-
.../src/features/controlLayers/store/types.ts | 1 -
.../controlLayers/util/controlAdapters.ts | 15 +-
.../web/src/features/dnd/types/index.ts | 23 +-
.../web/src/features/dnd/util/isValidDrop.ts | 6 +
.../util/graph/addControlLayersToGraph.ts | 458 +++++++++++++++---
.../util/graph/addControlNetToLinearGraph.ts | 42 +-
.../util/graph/addIPAdapterToLinearGraph.ts | 49 +-
.../util/graph/addT2IAdapterToLinearGraph.ts | 43 +-
.../graph/buildLinearSDXLTextToImageGraph.ts | 11 -
.../util/graph/buildLinearTextToImageGraph.ts | 11 -
.../frontend/web/src/services/api/types.ts | 19 +-
52 files changed, 1596 insertions(+), 951 deletions(-)
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/ControlAdapterBeginEndStepPct.tsx (100%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/ControlAdapterControlModeSelect.tsx (100%)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/ControlAdapterModelCombobox.tsx (100%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer/CALayerProcessor.tsx => ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx} (94%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer/CALayerProcessorCombobox.tsx => ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx} (91%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/ControlAdapterWeight.tsx (100%)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx
rename invokeai/frontend/web/src/features/controlLayers/components/{IPALayer => ControlAndIPAdapter}/IPAdapterMethod.tsx (100%)
rename invokeai/frontend/web/src/features/controlLayers/components/{IPALayer/IPALayerModelCombobox.tsx => ControlAndIPAdapter/IPAdapterModelSelect.tsx} (97%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/CannyProcessor.tsx (97%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/ColorMapProcessor.tsx (96%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/ContentShuffleProcessor.tsx (97%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/DWOpenposeProcessor.tsx (97%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/DepthAnythingProcessor.tsx (97%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/HedProcessor.tsx (95%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/LineartProcessor.tsx (95%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/MediapipeFaceProcessor.tsx (97%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/MidasDepthProcessor.tsx (97%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/MlsdImageProcessor.tsx (97%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/PidiProcessor.tsx (96%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/ProcessorWrapper.tsx (100%)
rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/types.ts (100%)
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx
delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 885a937de3..fd6eef527b 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -917,6 +917,7 @@
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} missing input",
"missingNodeTemplate": "Missing node template",
"noControlImageForControlAdapter": "Control Adapter #{{number}} has no control image",
+ "imageNotProcessedForControlAdapter": "Control Adapter #{{number}}'s image is not processed",
"noInitialImageSelected": "No initial image selected",
"noModelForControlAdapter": "Control Adapter #{{number}} has no model selected.",
"incompatibleBaseModelForControlAdapter": "Control Adapter #{{number}} model is incompatible with main model.",
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
index 307e3487dd..de2ac3a39a 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
@@ -7,6 +7,11 @@ import {
controlAdapterImageChanged,
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
+import {
+ caLayerImageChanged,
+ ipaLayerImageChanged,
+ rgLayerIPAdapterImageChanged,
+} from 'features/controlLayers/store/controlLayersSlice';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
@@ -83,6 +88,61 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
return;
}
+ /**
+ * Image dropped on Control Adapter Layer
+ */
+ if (
+ overData.actionType === 'SET_CA_LAYER_IMAGE' &&
+ activeData.payloadType === 'IMAGE_DTO' &&
+ activeData.payload.imageDTO
+ ) {
+ const { layerId } = overData.context;
+ dispatch(
+ caLayerImageChanged({
+ layerId,
+ imageDTO: activeData.payload.imageDTO,
+ })
+ );
+ return;
+ }
+
+ /**
+ * Image dropped on IP Adapter Layer
+ */
+ if (
+ overData.actionType === 'SET_IPA_LAYER_IMAGE' &&
+ activeData.payloadType === 'IMAGE_DTO' &&
+ activeData.payload.imageDTO
+ ) {
+ const { layerId } = overData.context;
+ dispatch(
+ ipaLayerImageChanged({
+ layerId,
+ imageDTO: activeData.payload.imageDTO,
+ })
+ );
+ return;
+ }
+
+ /**
+ * Image dropped on RG Layer IP Adapter
+ */
+ if (
+ overData.actionType === 'SET_RG_LAYER_IP_ADAPTER_IMAGE' &&
+ activeData.payloadType === 'IMAGE_DTO' &&
+ activeData.payload.imageDTO
+ ) {
+ const { layerId, ipAdapterId } = overData.context;
+ dispatch(
+ rgLayerIPAdapterImageChanged({
+ layerId,
+ ipAdapterId,
+ imageDTO: activeData.payload.imageDTO,
+ })
+ );
+ return;
+ }
+
/**
* Image dropped on Canvas
*/
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
index a2ca4baeb1..fd568ef1bd 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
@@ -6,6 +6,11 @@ import {
controlAdapterImageChanged,
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
+import {
+ caLayerImageChanged,
+ ipaLayerImageChanged,
+ rgLayerIPAdapterImageChanged,
+} from 'features/controlLayers/store/controlLayersSlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice';
@@ -108,6 +113,39 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
return;
}
+ if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') {
+ const { layerId } = postUploadAction;
+ dispatch(caLayerImageChanged({ layerId, imageDTO }));
+ dispatch(
+ addToast({
+ ...DEFAULT_UPLOADED_TOAST,
+ description: t('toast.setControlImage'),
+ })
+ );
+ }
+
+ if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') {
+ const { layerId } = postUploadAction;
+ dispatch(ipaLayerImageChanged({ layerId, imageDTO }));
+ dispatch(
+ addToast({
+ ...DEFAULT_UPLOADED_TOAST,
+ description: t('toast.setControlImage'),
+ })
+ );
+ }
+
+ if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') {
+ const { layerId, ipAdapterId } = postUploadAction;
+ dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
+ dispatch(
+ addToast({
+ ...DEFAULT_UPLOADED_TOAST,
+ description: t('toast.setControlImage'),
+ })
+ );
+ }
+
if (postUploadAction?.type === 'SET_INITIAL_IMAGE') {
dispatch(initialImageChanged(imageDTO));
dispatch(
diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts
index b5650209a4..6073564305 100644
--- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts
+++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts
@@ -16,6 +16,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import i18n from 'i18next';
import { forEach } from 'lodash-es';
import { getConnectedEdges } from 'reactflow';
+import { assert } from 'tsafe';
const selector = createMemoizedSelector(
[
@@ -97,73 +98,93 @@ const selector = createMemoizedSelector(
reasons.push(i18n.t('parameters.invoke.noModelSelected'));
}
- let enabledControlAdapters = selectControlAdapterAll(controlAdapters).filter((ca) => ca.isEnabled);
-
if (activeTabName === 'txt2img') {
- // Special handling for control layers on txt2img
- 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];
- // }
- // });
+ // Handling for Control Layers - only exists on txt2img tab now
+ controlLayers.present.layers
+ .filter((l) => l.isEnabled)
+ .flatMap((l) => {
+ if (l.type === 'control_adapter_layer') {
+ return l.controlAdapter;
+ } else if (l.type === 'ip_adapter_layer') {
+ return l.ipAdapter;
+ } else if (l.type === 'regional_guidance_layer') {
+ return l.ipAdapters;
+ }
+ assert(false);
+ })
+ .forEach((ca, i) => {
+ const hasNoModel = !ca.model;
+ const mismatchedModelBase = ca.model?.base !== model?.base;
+ const hasNoImage = !ca.image;
+ const imageNotProcessed =
+ (ca.type === 'controlnet' || ca.type === 't2i_adapter') && !ca.processedImage && ca.processorConfig;
- enabledControlAdapters = enabledControlAdapters.filter((ca) => enabledControlLayersAdapterIds.includes(ca.id));
+ if (hasNoModel) {
+ reasons.push(
+ i18n.t('parameters.invoke.noModelForControlAdapter', {
+ number: i + 1,
+ })
+ );
+ }
+ if (mismatchedModelBase) {
+ // This should never happen, just a sanity check
+ reasons.push(
+ i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', {
+ number: i + 1,
+ })
+ );
+ }
+ if (hasNoImage) {
+ reasons.push(
+ i18n.t('parameters.invoke.noControlImageForControlAdapter', {
+ number: i + 1,
+ })
+ );
+ }
+ if (imageNotProcessed) {
+ reasons.push(
+ i18n.t('parameters.invoke.imageNotProcessedForControlAdapter', {
+ number: i + 1,
+ })
+ );
+ }
+ });
} else {
- 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));
+ // Handling for all other tabs
+ selectControlAdapterAll(controlAdapters)
+ .filter((ca) => ca.isEnabled)
+ .forEach((ca, i) => {
+ if (!ca.isEnabled) {
+ return;
+ }
+
+ if (!ca.model) {
+ reasons.push(
+ i18n.t('parameters.invoke.noModelForControlAdapter', {
+ number: i + 1,
+ })
+ );
+ } else if (ca.model.base !== model?.base) {
+ // This should never happen, just a sanity check
+ reasons.push(
+ i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', {
+ number: i + 1,
+ })
+ );
+ }
+
+ if (
+ !ca.controlImage ||
+ (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
+ ) {
+ reasons.push(
+ i18n.t('parameters.invoke.noControlImageForControlAdapter', {
+ number: i + 1,
+ })
+ );
+ }
+ });
}
-
- enabledControlAdapters.forEach((ca, i) => {
- if (!ca.isEnabled) {
- return;
- }
-
- if (!ca.model) {
- reasons.push(
- i18n.t('parameters.invoke.noModelForControlAdapter', {
- number: i + 1,
- })
- );
- } else if (ca.model.base !== model?.base) {
- // This should never happen, just a sanity check
- reasons.push(
- i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', {
- number: i + 1,
- })
- );
- }
-
- if (
- !ca.controlImage ||
- (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
- ) {
- reasons.push(
- i18n.t('parameters.invoke.noControlImageForControlAdapter', {
- number: i + 1,
- })
- );
- }
- });
}
return { isReady: !reasons.length, reasons };
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 864e48c1d2..f9edf42c2f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx
@@ -1,6 +1,6 @@
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { CALayerConfig } from 'features/controlLayers/components/CALayer/CALayerConfig';
+import { CALayerControlAdapterWrapper } from 'features/controlLayers/components/CALayer/CALayerControlAdapterWrapper';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
@@ -43,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
deleted file mode 100644
index c998c30f14..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
-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 { CALayerImagePreview } from './CALayerImagePreview';
-import { CALayerProcessor } from './CALayerProcessor';
-import { CALayerProcessorCombobox } from './CALayerProcessorCombobox';
-import { ControlAdapterBeginEndStepPct } from './ControlAdapterBeginEndStepPct';
-import { ControlAdapterControlModeSelect } from './ControlAdapterControlModeSelect';
-import { ControlAdapterWeight } from './ControlAdapterWeight';
-
-type Props = {
- layerId: string;
-};
-
-export const CALayerConfig = memo(({ layerId }: Props) => {
- 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 (
-
-
-
-
-
-
-
- }
- />
-
-
-
- {controlAdapter.type === 'controlnet' && (
-
- )}
-
-
-
-
-
-
-
- {isExpanded && (
- <>
-
-
- >
- )}
-
- );
-});
-
-CALayerConfig.displayName = 'CALayerConfig';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx
new file mode 100644
index 0000000000..2a2edeb8d8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx
@@ -0,0 +1,121 @@
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { ControlAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapter';
+import {
+ caLayerControlModeChanged,
+ caLayerImageChanged,
+ caLayerModelChanged,
+ caLayerProcessorConfigChanged,
+ caOrIPALayerBeginEndStepPctChanged,
+ caOrIPALayerWeightChanged,
+ selectCALayer,
+} from 'features/controlLayers/store/controlLayersSlice';
+import type { ControlMode, ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
+import type { CALayerImageDropData } from 'features/dnd/types';
+import { memo, useCallback, useMemo } from 'react';
+import type {
+ CALayerImagePostUploadAction,
+ ControlNetModelConfig,
+ ImageDTO,
+ T2IAdapterModelConfig,
+} from 'services/api/types';
+
+type Props = {
+ layerId: string;
+};
+
+export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
+ const dispatch = useAppDispatch();
+ const controlAdapter = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter);
+
+ 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]
+ );
+
+ const droppableData = useMemo(
+ () => ({
+ actionType: 'SET_CA_LAYER_IMAGE',
+ context: {
+ layerId,
+ },
+ id: layerId,
+ }),
+ [layerId]
+ );
+
+ const postUploadAction = useMemo(
+ () => ({
+ layerId,
+ type: 'SET_CA_LAYER_IMAGE',
+ }),
+ [layerId]
+ );
+
+ return (
+
+ );
+});
+
+CALayerControlAdapterWrapper.displayName = 'CALayerControlAdapterWrapper';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx
deleted file mode 100644
index c20b408730..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx
+++ /dev/null
@@ -1,231 +0,0 @@
-import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import { Box, Flex, Spinner, useShiftModifier } from '@invoke-ai/ui-library';
-import { skipToken } from '@reduxjs/toolkit/query';
-import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
-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 { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
-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, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi';
-import {
- useAddImageToBoardMutation,
- useChangeImageIsIntermediateMutation,
- useGetImageDTOQuery,
- useRemoveImageFromBoardMutation,
-} from 'services/api/endpoints/images';
-import type { ControlLayerAction, ImageDTO } from 'services/api/types';
-
-type Props = {
- image: ImageWithDims | null;
- processedImage: ImageWithDims | null;
- onChangeImage: (imageDTO: ImageDTO | null) => void;
- hasProcessor: boolean;
- layerId: string; // required for the dnd/upload interactions
-};
-
-const selectPendingControlImages = createMemoizedSelector(
- selectControlAdaptersSlice,
- (controlAdapters) => controlAdapters.pendingControlImages
-);
-
-export const CALayerImagePreview = memo(({ image, processedImage, onChangeImage, hasProcessor, layerId }: Props) => {
- const { t } = useTranslation();
- const dispatch = useAppDispatch();
- const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
- const isConnected = useAppSelector((s) => s.system.isConnected);
- const activeTabName = useAppSelector(activeTabNameSelector);
- const optimalDimension = useAppSelector(selectOptimalDimension);
- const pendingControlImages = useAppSelector(selectPendingControlImages);
- const shift = useShiftModifier();
-
- const [isMouseOverImage, setIsMouseOverImage] = useState(false);
-
- const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
- image?.imageName ?? skipToken
- );
- const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery(
- processedImage?.imageName ?? skipToken
- );
-
- const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
- const [addToBoard] = useAddImageToBoardMutation();
- const [removeFromBoard] = useRemoveImageFromBoardMutation();
- const handleResetControlImage = useCallback(() => {
- onChangeImage(null);
- }, [onChangeImage]);
-
- const handleSaveControlImage = useCallback(async () => {
- if (!processedControlImage) {
- return;
- }
-
- await changeIsIntermediate({
- imageDTO: processedControlImage,
- is_intermediate: false,
- }).unwrap();
-
- if (autoAddBoardId !== 'none') {
- addToBoard({
- imageDTO: processedControlImage,
- board_id: autoAddBoardId,
- });
- } else {
- removeFromBoard({ imageDTO: processedControlImage });
- }
- }, [processedControlImage, changeIsIntermediate, autoAddBoardId, addToBoard, removeFromBoard]);
-
- 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 handleMouseEnter = useCallback(() => {
- setIsMouseOverImage(true);
- }, []);
-
- const handleMouseLeave = useCallback(() => {
- setIsMouseOverImage(false);
- }, []);
-
- 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]);
-
- const shouldShowProcessedImage =
- controlImage &&
- processedControlImage &&
- !isMouseOverImage &&
- !pendingControlImages.includes(layerId) &&
- hasProcessor;
-
- useEffect(() => {
- if (isConnected && (isErrorControlImage || isErrorProcessedControlImage)) {
- handleResetControlImage();
- }
- }, [handleResetControlImage, isConnected, isErrorControlImage, isErrorProcessedControlImage]);
-
- return (
-
-
-
-
-
-
-
- <>
- : undefined}
- tooltip={t('controlnet.resetControlImage')}
- />
- : undefined}
- tooltip={t('controlnet.saveControlImage')}
- styleOverrides={saveControlImageStyleOverrides}
- />
- : undefined}
- tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
- styleOverrides={setControlImageDimensionsStyleOverrides}
- />
- >
-
- {pendingControlImages.includes(layerId) && (
-
-
-
- )}
-
- );
-});
-
-CALayerImagePreview.displayName = 'CALayerImagePreview';
-
-const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 };
-const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 };
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx
new file mode 100644
index 0000000000..972198cc7e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx
@@ -0,0 +1,111 @@
+import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
+import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox';
+import type {
+ ControlMode,
+ ControlNetConfig,
+ ProcessorConfig,
+ T2IAdapterConfig,
+} from 'features/controlLayers/util/controlAdapters';
+import type { TypesafeDroppableData } from 'features/dnd/types';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiCaretUpBold } from 'react-icons/pi';
+import { useToggle } from 'react-use';
+import type { ControlNetModelConfig, ImageDTO, PostUploadAction, T2IAdapterModelConfig } from 'services/api/types';
+
+import { ControlAdapterBeginEndStepPct } from './ControlAdapterBeginEndStepPct';
+import { ControlAdapterControlModeSelect } from './ControlAdapterControlModeSelect';
+import { ControlAdapterImagePreview } from './ControlAdapterImagePreview';
+import { ControlAdapterProcessorConfig } from './ControlAdapterProcessorConfig';
+import { ControlAdapterProcessorTypeSelect } from './ControlAdapterProcessorTypeSelect';
+import { ControlAdapterWeight } from './ControlAdapterWeight';
+
+type Props = {
+ controlAdapter: ControlNetConfig | T2IAdapterConfig;
+ onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void;
+ onChangeControlMode: (controlMode: ControlMode) => void;
+ onChangeWeight: (weight: number) => void;
+ onChangeProcessorConfig: (processorConfig: ProcessorConfig | null) => void;
+ onChangeModel: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void;
+ onChangeImage: (imageDTO: ImageDTO | null) => void;
+ droppableData: TypesafeDroppableData;
+ postUploadAction: PostUploadAction;
+};
+
+export const ControlAdapter = memo(
+ ({
+ controlAdapter,
+ onChangeBeginEndStepPct,
+ onChangeControlMode,
+ onChangeWeight,
+ onChangeProcessorConfig,
+ onChangeModel,
+ onChangeImage,
+ droppableData,
+ postUploadAction,
+ }: Props) => {
+ const { t } = useTranslation();
+ const [isExpanded, toggleIsExpanded] = useToggle(false);
+
+ return (
+
+
+
+
+
+
+
+ }
+ />
+
+
+
+ {controlAdapter.type === 'controlnet' && (
+
+ )}
+
+
+
+
+
+
+
+ {isExpanded && (
+ <>
+
+
+ >
+ )}
+
+ );
+ }
+);
+
+ControlAdapter.displayName = 'ControlAdapter';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterBeginEndStepPct.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterBeginEndStepPct.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterControlModeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterControlModeSelect.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx
new file mode 100644
index 0000000000..e4f53c1c70
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx
@@ -0,0 +1,234 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Box, Flex, Spinner, useShiftModifier } from '@invoke-ai/ui-library';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
+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 { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
+import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
+import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters';
+import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
+import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
+import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
+import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
+import { memo, useCallback, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi';
+import {
+ useAddImageToBoardMutation,
+ useChangeImageIsIntermediateMutation,
+ useGetImageDTOQuery,
+ useRemoveImageFromBoardMutation,
+} from 'services/api/endpoints/images';
+import type { ImageDTO, PostUploadAction } from 'services/api/types';
+
+type Props = {
+ controlAdapterId: string;
+ image: ImageWithDims | null;
+ processedImage: ImageWithDims | null;
+ onChangeImage: (imageDTO: ImageDTO | null) => void;
+ hasProcessor: boolean;
+ droppableData: TypesafeDroppableData;
+ postUploadAction: PostUploadAction;
+};
+
+const selectPendingControlImages = createMemoizedSelector(
+ selectControlAdaptersSlice,
+ (controlAdapters) => controlAdapters.pendingControlImages
+);
+
+export const ControlAdapterImagePreview = memo(
+ ({
+ image,
+ processedImage,
+ onChangeImage,
+ hasProcessor,
+ controlAdapterId,
+ droppableData,
+ postUploadAction,
+ }: Props) => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
+ const isConnected = useAppSelector((s) => s.system.isConnected);
+ const activeTabName = useAppSelector(activeTabNameSelector);
+ const optimalDimension = useAppSelector(selectOptimalDimension);
+ const pendingControlImages = useAppSelector(selectPendingControlImages);
+ const shift = useShiftModifier();
+
+ const [isMouseOverImage, setIsMouseOverImage] = useState(false);
+
+ const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
+ image?.imageName ?? skipToken
+ );
+ const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery(
+ processedImage?.imageName ?? skipToken
+ );
+
+ const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
+ const [addToBoard] = useAddImageToBoardMutation();
+ const [removeFromBoard] = useRemoveImageFromBoardMutation();
+ const handleResetControlImage = useCallback(() => {
+ onChangeImage(null);
+ }, [onChangeImage]);
+
+ const handleSaveControlImage = useCallback(async () => {
+ if (!processedControlImage) {
+ return;
+ }
+
+ await changeIsIntermediate({
+ imageDTO: processedControlImage,
+ is_intermediate: false,
+ }).unwrap();
+
+ if (autoAddBoardId !== 'none') {
+ addToBoard({
+ imageDTO: processedControlImage,
+ board_id: autoAddBoardId,
+ });
+ } else {
+ removeFromBoard({ imageDTO: processedControlImage });
+ }
+ }, [processedControlImage, changeIsIntermediate, autoAddBoardId, addToBoard, removeFromBoard]);
+
+ 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 handleMouseEnter = useCallback(() => {
+ setIsMouseOverImage(true);
+ }, []);
+
+ const handleMouseLeave = useCallback(() => {
+ setIsMouseOverImage(false);
+ }, []);
+
+ const draggableData = useMemo(() => {
+ if (controlImage) {
+ return {
+ id: controlAdapterId,
+ payloadType: 'IMAGE_DTO',
+ payload: { imageDTO: controlImage },
+ };
+ }
+ }, [controlImage, controlAdapterId]);
+
+ const shouldShowProcessedImage =
+ controlImage &&
+ processedControlImage &&
+ !isMouseOverImage &&
+ !pendingControlImages.includes(controlAdapterId) &&
+ hasProcessor;
+
+ useEffect(() => {
+ if (isConnected && (isErrorControlImage || isErrorProcessedControlImage)) {
+ handleResetControlImage();
+ }
+ }, [handleResetControlImage, isConnected, isErrorControlImage, isErrorProcessedControlImage]);
+
+ return (
+
+
+
+
+
+
+
+ <>
+ : undefined}
+ tooltip={t('controlnet.resetControlImage')}
+ />
+ : undefined}
+ tooltip={t('controlnet.saveControlImage')}
+ styleOverrides={saveControlImageStyleOverrides}
+ />
+ : undefined}
+ tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
+ styleOverrides={setControlImageDimensionsStyleOverrides}
+ />
+ >
+
+ {pendingControlImages.includes(controlAdapterId) && (
+
+
+
+ )}
+
+ );
+ }
+);
+
+ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview';
+
+const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 };
+const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 };
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterModelCombobox.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx
similarity index 94%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx
index b5ae89f53a..034dc5454e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx
@@ -18,7 +18,7 @@ type Props = {
onChange: (config: ProcessorConfig | null) => void;
};
-export const CALayerProcessor = memo(({ config, onChange }: Props) => {
+export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) => {
if (!config) {
return null;
}
@@ -82,4 +82,4 @@ export const CALayerProcessor = memo(({ config, onChange }: Props) => {
}
});
-CALayerProcessor.displayName = 'CALayerProcessor';
+ControlAdapterProcessorConfig.displayName = 'ControlAdapterProcessorConfig';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx
similarity index 91%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx
index a01487af44..5f34946af5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx
@@ -22,7 +22,7 @@ const selectDisabledProcessors = createMemoizedSelector(
(config) => config.sd.disabledControlNetProcessors
);
-export const CALayerProcessorCombobox = memo(({ config, onChange }: Props) => {
+export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Props) => {
const { t } = useTranslation();
const disabledProcessors = useAppSelector(selectDisabledProcessors);
const options = useMemo(() => {
@@ -53,7 +53,7 @@ export const CALayerProcessorCombobox = memo(({ config, onChange }: Props) => {
{t('controlnet.processor')}
-
+
{
);
});
-CALayerProcessorCombobox.displayName = 'CALayerProcessorCombobox';
+ControlAdapterProcessorTypeSelect.displayName = 'ControlAdapterProcessorTypeSelect';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterWeight.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterWeight.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx
new file mode 100644
index 0000000000..a0aa7d79a1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx
@@ -0,0 +1,72 @@
+import { Box, Flex } from '@invoke-ai/ui-library';
+import { ControlAdapterBeginEndStepPct } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct';
+import { ControlAdapterWeight } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight';
+import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview';
+import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod';
+import { IPAdapterModelSelect } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect';
+import type { CLIPVisionModel, IPAdapterConfig, IPMethod } from 'features/controlLayers/util/controlAdapters';
+import type { TypesafeDroppableData } from 'features/dnd/types';
+import { memo } from 'react';
+import type { ImageDTO, IPAdapterModelConfig, PostUploadAction } from 'services/api/types';
+
+type Props = {
+ ipAdapter: IPAdapterConfig;
+ onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void;
+ onChangeWeight: (weight: number) => void;
+ onChangeIPMethod: (method: IPMethod) => void;
+ onChangeModel: (modelConfig: IPAdapterModelConfig) => void;
+ onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void;
+ onChangeImage: (imageDTO: ImageDTO | null) => void;
+ droppableData: TypesafeDroppableData;
+ postUploadAction: PostUploadAction;
+};
+
+export const IPAdapter = memo(
+ ({
+ ipAdapter,
+ onChangeBeginEndStepPct,
+ onChangeWeight,
+ onChangeIPMethod,
+ onChangeModel,
+ onChangeCLIPVisionModel,
+ onChangeImage,
+ droppableData,
+ postUploadAction,
+ }: Props) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+);
+
+IPAdapter.displayName = 'IPAdapter';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx
new file mode 100644
index 0000000000..7de726cda5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx
@@ -0,0 +1,114 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import IAIDndImage from 'common/components/IAIDndImage';
+import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
+import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
+import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
+import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters';
+import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
+import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
+import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
+import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
+import { memo, useCallback, useEffect, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import type { ImageDTO, PostUploadAction } from 'services/api/types';
+
+type Props = {
+ image: ImageWithDims | null;
+ onChangeImage: (imageDTO: ImageDTO | null) => void;
+ ipAdapterId: string; // required for the dnd/upload interactions
+ droppableData: TypesafeDroppableData;
+ postUploadAction: PostUploadAction;
+};
+
+export const IPAdapterImagePreview = memo(
+ ({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const isConnected = useAppSelector((s) => s.system.isConnected);
+ const activeTabName = useAppSelector(activeTabNameSelector);
+ const optimalDimension = useAppSelector(selectOptimalDimension);
+ const shift = useShiftModifier();
+
+ const { currentData: 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: ipAdapterId,
+ payloadType: 'IMAGE_DTO',
+ payload: { imageDTO: controlImage },
+ };
+ }
+ }, [controlImage, ipAdapterId]);
+
+ 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/ControlAndIPAdapter/IPAdapterMethod.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx
similarity index 97%
rename from invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx
index facd46aed1..e47bcd5182 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx
@@ -22,7 +22,7 @@ type Props = {
onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void;
};
-export const IPAdapterModelCombobox = memo(
+export const IPAdapterModelSelect = memo(
({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => {
const { t } = useTranslation();
const currentBaseModel = useAppSelector((s) => s.generation.model?.base);
@@ -97,4 +97,4 @@ export const IPAdapterModelCombobox = memo(
}
);
-IPAdapterModelCombobox.displayName = 'IPALayerModelCombobox';
+IPAdapterModelSelect.displayName = 'IPAdapterModelSelect';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx
similarity index 97%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/CannyProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx
index 5ae1e2cc0e..c4d6031912 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/CannyProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx
@@ -1,5 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import { type CannyProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx
similarity index 96%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ColorMapProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx
index e867ecfe12..90c88a071b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ColorMapProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx
@@ -1,5 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import { type ColorMapProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx
similarity index 97%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ContentShuffleProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx
index 19c75045b4..9e27d7052a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ContentShuffleProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx
@@ -1,5 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { ContentShuffleProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx
similarity index 97%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DWOpenposeProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx
index 4d6776a913..5f21b4b8f6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DWOpenposeProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx
@@ -1,5 +1,5 @@
import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { DWOpenposeProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import type { ChangeEvent } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx
similarity index 97%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DepthAnythingProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx
index 90c8b32e69..b56c331741 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DepthAnythingProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx
@@ -1,6 +1,6 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx
similarity index 95%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/HedProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx
index 3708287450..83cd015fe4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/HedProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx
@@ -1,5 +1,5 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { HedProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx
similarity index 95%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/LineartProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx
index ef18e9d61f..d882543af4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/LineartProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx
@@ -1,5 +1,5 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { LineartProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx
similarity index 97%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MediapipeFaceProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx
index e3d67f91bb..a3c2936916 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MediapipeFaceProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx
@@ -1,5 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import { CONTROLNET_PROCESSORS, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx
similarity index 97%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MidasDepthProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx
index 36f008d6be..f12619caac 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MidasDepthProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx
@@ -1,5 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { MidasDepthProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx
similarity index 97%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MlsdImageProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx
index 69dc1ce4d9..a0e02ef17a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MlsdImageProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx
@@ -1,5 +1,5 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { MlsdProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx
similarity index 96%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/PidiProcessor.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx
index e4c894ef45..4885d16e6f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/PidiProcessor.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx
@@ -1,5 +1,5 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
-import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types';
+import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { PidiProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import type { ChangeEvent } from 'react';
import { useCallback } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ProcessorWrapper.tsx
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/types.ts
similarity index 100%
rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/types.ts
rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/types.ts
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 71b06e6830..715e538679 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx
@@ -1,5 +1,5 @@
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
-import { IPALayerConfig } from 'features/controlLayers/components/IPALayer/IPALayerConfig';
+import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
@@ -22,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
deleted file mode 100644
index f1b035da1c..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-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/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx
new file mode 100644
index 0000000000..dfcfdc7c99
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx
@@ -0,0 +1,106 @@
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter';
+import {
+ caOrIPALayerBeginEndStepPctChanged,
+ caOrIPALayerWeightChanged,
+ ipaLayerCLIPVisionModelChanged,
+ ipaLayerImageChanged,
+ ipaLayerMethodChanged,
+ ipaLayerModelChanged,
+ selectIPALayer,
+} from 'features/controlLayers/store/controlLayersSlice';
+import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters';
+import type { IPALayerImageDropData } from 'features/dnd/types';
+import { memo, useCallback, useMemo } from 'react';
+import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types';
+
+type Props = {
+ layerId: string;
+};
+
+export const IPALayerIPAdapterWrapper = 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]
+ );
+
+ const droppableData = useMemo(
+ () => ({
+ actionType: 'SET_IPA_LAYER_IMAGE',
+ context: {
+ layerId,
+ },
+ id: layerId,
+ }),
+ [layerId]
+ );
+
+ const postUploadAction = useMemo(
+ () => ({
+ type: 'SET_IPA_LAYER_IMAGE',
+ layerId,
+ }),
+ [layerId]
+ );
+
+ return (
+
+ );
+});
+
+IPALayerIPAdapterWrapper.displayName = 'IPALayerIPAdapterWrapper';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx
deleted file mode 100644
index bff6d29502..0000000000
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-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/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx
index cb3c371c67..578d3789bf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx
@@ -1,13 +1,9 @@
-import { Divider, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
+import { Divider, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import {
- isRegionalGuidanceLayer,
- rgLayerIPAdapterDeleted,
- selectControlLayersSlice,
-} from 'features/controlLayers/store/controlLayersSlice';
-import { memo, useCallback, useMemo } from 'react';
-import { PiTrashSimpleBold } from 'react-icons/pi';
+import { useAppSelector } from 'app/store/storeHooks';
+import { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper';
+import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
+import { memo, useMemo } from 'react';
import { assert } from 'tsafe';
type Props = {
@@ -39,7 +35,7 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => {
)}
-
+
))}
>
@@ -47,36 +43,3 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => {
});
RGLayerIPAdapterList.displayName = 'RGLayerIPAdapterList';
-
-type IPAdapterListItemProps = {
- layerId: string;
- ipAdapterId: string;
- ipAdapterNumber: number;
-};
-
-const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber }: IPAdapterListItemProps) => {
- const dispatch = useAppDispatch();
- const onDeleteIPAdapter = useCallback(() => {
- dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId }));
- }, [dispatch, ipAdapterId, layerId]);
-
- return (
-
-
- {`IP Adapter ${ipAdapterNumber}`}
-
- }
- aria-label="Delete IP Adapter"
- onClick={onDeleteIPAdapter}
- variant="ghost"
- colorScheme="error"
- />
-
- {/* */}
-
- );
-});
-
-RGLayerIPAdapterListItem.displayName = 'RGLayerIPAdapterListItem';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx
new file mode 100644
index 0000000000..cc8b0698a5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx
@@ -0,0 +1,131 @@
+import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter';
+import {
+ rgLayerIPAdapterBeginEndStepPctChanged,
+ rgLayerIPAdapterCLIPVisionModelChanged,
+ rgLayerIPAdapterDeleted,
+ rgLayerIPAdapterImageChanged,
+ rgLayerIPAdapterMethodChanged,
+ rgLayerIPAdapterModelChanged,
+ rgLayerIPAdapterWeightChanged,
+ selectRGLayerIPAdapter,
+} from 'features/controlLayers/store/controlLayersSlice';
+import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters';
+import type { RGLayerIPAdapterImageDropData } from 'features/dnd/types';
+import { memo, useCallback, useMemo } from 'react';
+import { PiTrashSimpleBold } from 'react-icons/pi';
+import type { ImageDTO, IPAdapterModelConfig, RGLayerIPAdapterImagePostUploadAction } from 'services/api/types';
+
+type Props = {
+ layerId: string;
+ ipAdapterId: string;
+ ipAdapterNumber: number;
+};
+
+export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNumber }: Props) => {
+ const dispatch = useAppDispatch();
+ const onDeleteIPAdapter = useCallback(() => {
+ dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId }));
+ }, [dispatch, ipAdapterId, layerId]);
+ const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapter(s.controlLayers.present, layerId, ipAdapterId));
+
+ const onChangeBeginEndStepPct = useCallback(
+ (beginEndStepPct: [number, number]) => {
+ dispatch(
+ rgLayerIPAdapterBeginEndStepPctChanged({
+ layerId,
+ ipAdapterId,
+ beginEndStepPct,
+ })
+ );
+ },
+ [dispatch, ipAdapterId, layerId]
+ );
+
+ const onChangeWeight = useCallback(
+ (weight: number) => {
+ dispatch(rgLayerIPAdapterWeightChanged({ layerId, ipAdapterId, weight }));
+ },
+ [dispatch, ipAdapterId, layerId]
+ );
+
+ const onChangeIPMethod = useCallback(
+ (method: IPMethod) => {
+ dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method }));
+ },
+ [dispatch, ipAdapterId, layerId]
+ );
+
+ const onChangeModel = useCallback(
+ (modelConfig: IPAdapterModelConfig) => {
+ dispatch(rgLayerIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig }));
+ },
+ [dispatch, ipAdapterId, layerId]
+ );
+
+ const onChangeCLIPVisionModel = useCallback(
+ (clipVisionModel: CLIPVisionModel) => {
+ dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel }));
+ },
+ [dispatch, ipAdapterId, layerId]
+ );
+
+ const onChangeImage = useCallback(
+ (imageDTO: ImageDTO | null) => {
+ dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
+ },
+ [dispatch, ipAdapterId, layerId]
+ );
+
+ const droppableData = useMemo(
+ () => ({
+ actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE',
+ context: {
+ layerId,
+ ipAdapterId,
+ },
+ id: layerId,
+ }),
+ [ipAdapterId, layerId]
+ );
+
+ const postUploadAction = useMemo(
+ () => ({
+ type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE',
+ layerId,
+ ipAdapterId,
+ }),
+ [ipAdapterId, layerId]
+ );
+
+ return (
+
+
+ {`IP Adapter ${ipAdapterNumber}`}
+
+ }
+ aria-label="Delete IP Adapter"
+ onClick={onDeleteIPAdapter}
+ variant="ghost"
+ colorScheme="error"
+ />
+
+
+
+ );
+});
+
+RGLayerIPAdapterWrapper.displayName = 'RGLayerIPAdapterWrapper';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts
index 56d380b1d3..c42a27f28f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts
@@ -5,9 +5,6 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => {
- if (!controlLayers.present.isEnabled) {
- return 0;
- }
const validLayers = controlLayers.present.layers
.filter(isRegionalGuidanceLayer)
.filter((l) => l.isEnabled)
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts
index 4d7feaa6ee..92fe9d0119 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts
@@ -47,7 +47,6 @@ export const initialControlLayersState: ControlLayersState = {
brushSize: 100,
layers: [],
globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity
- isEnabled: true,
positivePrompt: '',
negativePrompt: '',
positivePrompt2: '',
@@ -77,10 +76,6 @@ const resetLayer = (layer: Layer) => {
layer.bboxNeedsUpdate = false;
return;
}
-
- if (layer.type === 'control_adapter_layer') {
- // TODO
- }
};
export const selectCALayer = (state: ControlLayersState, layerId: string): ControlAdapterLayer => {
@@ -101,12 +96,16 @@ export const selectCAOrIPALayer = (
assert(isControlAdapterLayer(layer) || isIPAdapterLayer(layer));
return layer;
};
-const selectRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => {
+export const selectRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => {
const layer = state.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer));
return layer;
};
-const selectRGLayerIPAdapter = (state: ControlLayersState, layerId: string, ipAdapterId: string): IPAdapterConfig => {
+export const selectRGLayerIPAdapter = (
+ state: ControlLayersState,
+ layerId: string,
+ ipAdapterId: string
+): IPAdapterConfig => {
const layer = state.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer));
const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId);
@@ -556,6 +555,22 @@ export const controlLayersSlice = createSlice({
const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId);
ipAdapter.method = method;
},
+ rgLayerIPAdapterModelChanged: (
+ state,
+ action: PayloadAction<{
+ layerId: string;
+ ipAdapterId: string;
+ modelConfig: IPAdapterModelConfig | null;
+ }>
+ ) => {
+ const { layerId, ipAdapterId, modelConfig } = action.payload;
+ const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId);
+ if (!modelConfig) {
+ ipAdapter.model = null;
+ return;
+ }
+ ipAdapter.model = zModelIdentifierField.parse(modelConfig);
+ },
rgLayerIPAdapterCLIPVisionModelChanged: (
state,
action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModel }>
@@ -609,9 +624,6 @@ export const controlLayersSlice = createSlice({
globalMaskLayerOpacityChanged: (state, action: PayloadAction) => {
state.globalMaskLayerOpacity = action.payload;
},
- isEnabledChanged: (state, action: PayloadAction) => {
- state.isEnabled = action.payload;
- },
undo: (state) => {
// Invalidate the bbox for all layers to prevent stale bboxes
for (const layer of state.layers.filter(isRenderableLayer)) {
@@ -734,6 +746,7 @@ export const {
rgLayerIPAdapterWeightChanged,
rgLayerIPAdapterBeginEndStepPctChanged,
rgLayerIPAdapterMethodChanged,
+ rgLayerIPAdapterModelChanged,
rgLayerIPAdapterCLIPVisionModelChanged,
// Globals
positivePromptChanged,
@@ -746,7 +759,6 @@ export const {
aspectRatioChanged,
brushSizeChanged,
globalMaskLayerOpacityChanged,
- isEnabledChanged,
undo,
redo,
} = controlLayersSlice.actions;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index 3d5ba672ec..241c8f2f84 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -77,7 +77,6 @@ export type ControlLayersState = {
layers: Layer[];
brushSize: number;
globalMaskLayerOpacity: number;
- isEnabled: boolean;
positivePrompt: ParameterPositivePrompt;
negativePrompt: ParameterNegativePrompt;
positivePrompt2: ParameterPositiveStylePromptSDXL;
diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts
index 3debe10791..0417c707e4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts
@@ -80,7 +80,6 @@ export type ImageWithDims = {
type ControlAdapterBase = {
id: string;
- isEnabled: boolean;
weight: number;
image: ImageWithDims | null;
processedImage: ImageWithDims | null;
@@ -97,11 +96,15 @@ export type ControlNetConfig = ControlAdapterBase & {
model: ParameterControlNetModel | null;
controlMode: ControlMode;
};
+export const isControlNetConfig = (ca: ControlNetConfig | T2IAdapterConfig): ca is ControlNetConfig =>
+ ca.type === 'controlnet';
export type T2IAdapterConfig = ControlAdapterBase & {
type: 't2i_adapter';
model: ParameterT2IAdapterModel | null;
};
+export const isT2IAdapterConfig = (ca: ControlNetConfig | T2IAdapterConfig): ca is T2IAdapterConfig =>
+ ca.type === 't2i_adapter';
const zCLIPVisionModel = z.enum(['ViT-H', 'ViT-G']);
export type CLIPVisionModel = z.infer;
@@ -114,7 +117,6 @@ export const isIPMethod = (v: unknown): v is IPMethod => zIPMethod.safeParse(v).
export type IPAdapterConfig = {
id: string;
type: 'ip_adapter';
- isEnabled: boolean;
weight: number;
method: IPMethod;
image: ImageWithDims | null;
@@ -295,10 +297,9 @@ export const isProcessorType = (v: unknown): v is ProcessorType => zProcessorTyp
export const initialControlNet: Omit = {
type: 'controlnet',
- isEnabled: true,
model: null,
weight: 1,
- beginEndStepPct: [0, 0],
+ beginEndStepPct: [0, 1],
controlMode: 'balanced',
image: null,
processedImage: null,
@@ -307,10 +308,9 @@ export const initialControlNet: Omit = {
export const initialT2IAdapter: Omit = {
type: 't2i_adapter',
- isEnabled: true,
model: null,
weight: 1,
- beginEndStepPct: [0, 0],
+ beginEndStepPct: [0, 1],
image: null,
processedImage: null,
processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(),
@@ -318,10 +318,9 @@ export const initialT2IAdapter: Omit = {
export const initialIPAdapter: Omit = {
type: 'ip_adapter',
- isEnabled: true,
image: null,
model: null,
- beginEndStepPct: [0, 0],
+ beginEndStepPct: [0, 1],
method: 'full',
clipVisionModel: 'ViT-H',
weight: 1,
diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts
index 739f15c882..7d109473ed 100644
--- a/invokeai/frontend/web/src/features/dnd/types/index.ts
+++ b/invokeai/frontend/web/src/features/dnd/types/index.ts
@@ -33,13 +33,28 @@ type ControlAdapterDropData = BaseDropData & {
};
};
-export type ControlLayerDropData = BaseDropData & {
- actionType: 'SET_CONTROL_LAYER_IMAGE';
+export type CALayerImageDropData = BaseDropData & {
+ actionType: 'SET_CA_LAYER_IMAGE';
context: {
layerId: string;
};
};
+export type IPALayerImageDropData = BaseDropData & {
+ actionType: 'SET_IPA_LAYER_IMAGE';
+ context: {
+ layerId: string;
+ };
+};
+
+export type RGLayerIPAdapterImageDropData = BaseDropData & {
+ actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE';
+ context: {
+ layerId: string;
+ ipAdapterId: string;
+ };
+};
+
export type CanvasInitialImageDropData = BaseDropData & {
actionType: 'SET_CANVAS_INITIAL_IMAGE';
};
@@ -69,7 +84,9 @@ export type TypesafeDroppableData =
| NodesImageDropData
| AddToBoardDropData
| RemoveFromBoardDropData
- | ControlLayerDropData;
+ | CALayerImageDropData
+ | IPALayerImageDropData
+ | RGLayerIPAdapterImageDropData;
type BaseDragData = {
id: string;
diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts
index c2c9de3f0c..c1da111087 100644
--- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts
+++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts
@@ -19,6 +19,12 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active:
return payloadType === 'IMAGE_DTO';
case 'SET_CONTROL_ADAPTER_IMAGE':
return payloadType === 'IMAGE_DTO';
+ case 'SET_CA_LAYER_IMAGE':
+ return payloadType === 'IMAGE_DTO';
+ case 'SET_IPA_LAYER_IMAGE':
+ return payloadType === 'IMAGE_DTO';
+ case 'SET_RG_LAYER_IP_ADAPTER_IMAGE':
+ return payloadType === 'IMAGE_DTO';
case 'SET_CANVAS_INITIAL_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_NODES_IMAGE':
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts
index a7236af3cc..4581b51ee1 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts
@@ -1,9 +1,23 @@
import { getStore } from 'app/store/nanostores/store';
import type { RootState } from 'app/store/store';
-import { selectAllIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice';
-import { isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice';
-import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs';
import {
+ isControlAdapterLayer,
+ isIPAdapterLayer,
+ isRegionalGuidanceLayer,
+} from 'features/controlLayers/store/controlLayersSlice';
+import {
+ type ControlNetConfig,
+ type ImageWithDims,
+ type IPAdapterConfig,
+ isControlNetConfig,
+ isT2IAdapterConfig,
+ type ProcessorConfig,
+ type T2IAdapterConfig,
+} from 'features/controlLayers/util/controlAdapters';
+import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs';
+import type { ImageField } from 'features/nodes/types/common';
+import {
+ CONTROL_NET_COLLECT,
IP_ADAPTER_COLLECT,
NEGATIVE_CONDITIONING,
NEGATIVE_CONDITIONING_COLLECT,
@@ -14,45 +28,383 @@ import {
PROMPT_REGION_NEGATIVE_COND_PREFIX,
PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX,
PROMPT_REGION_POSITIVE_COND_PREFIX,
+ T2I_ADAPTER_COLLECT,
} from 'features/nodes/util/graph/constants';
+import { upsertMetadata } from 'features/nodes/util/graph/metadata';
import { size } from 'lodash-es';
import { imagesApi } from 'services/api/endpoints/images';
-import type { CollectInvocation, Edge, IPAdapterInvocation, NonNullableGraph, S } from 'services/api/types';
+import type {
+ CollectInvocation,
+ ControlNetInvocation,
+ CoreMetadataInvocation,
+ Edge,
+ IPAdapterInvocation,
+ NonNullableGraph,
+ S,
+ T2IAdapterInvocation,
+} from 'services/api/types';
import { assert } from 'tsafe';
-export const addControlLayersToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => {
- if (!state.controlLayers.present.isEnabled) {
+const buildControlImage = (
+ image: ImageWithDims | null,
+ processedImage: ImageWithDims | null,
+ processorConfig: ProcessorConfig | null
+): ImageField => {
+ if (processedImage && processorConfig) {
+ // We've processed the image in the app - use it for the control image.
+ return {
+ image_name: processedImage.imageName,
+ };
+ } else if (image) {
+ // No processor selected, and we have an image - the user provided a processed image, use it for the control image.
+ return {
+ image_name: image.imageName,
+ };
+ }
+ assert(false, 'Attempted to add unprocessed control image');
+};
+
+const buildControlNetMetadata = (controlNet: ControlNetConfig): S['ControlNetMetadataField'] => {
+ const { beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet;
+
+ assert(model, 'ControlNet model is required');
+ assert(image, 'ControlNet image is required');
+
+ const processed_image =
+ processedImage && processorConfig
+ ? {
+ image_name: processedImage.imageName,
+ }
+ : null;
+
+ return {
+ control_model: model,
+ control_weight: weight,
+ control_mode: controlMode,
+ begin_step_percent: beginEndStepPct[0],
+ end_step_percent: beginEndStepPct[1],
+ resize_mode: 'just_resize',
+ image: {
+ image_name: image.imageName,
+ },
+ processed_image,
+ };
+};
+
+const addControlNetCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => {
+ if (graph.nodes[CONTROL_NET_COLLECT]) {
+ // You see, we've already got one!
return;
}
+ // Add the ControlNet collector
+ const controlNetIterateNode: CollectInvocation = {
+ id: CONTROL_NET_COLLECT,
+ type: 'collect',
+ is_intermediate: true,
+ };
+ graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode;
+ graph.edges.push({
+ source: { node_id: CONTROL_NET_COLLECT, field: 'collection' },
+ destination: {
+ node_id: denoiseNodeId,
+ field: 'control',
+ },
+ });
+};
+
+const addGlobalControlNetsToGraph = async (
+ controlNets: ControlNetConfig[],
+ graph: NonNullableGraph,
+ denoiseNodeId: string
+) => {
+ if (controlNets.length === 0) {
+ return;
+ }
+ const controlNetMetadata: CoreMetadataInvocation['controlnets'] = [];
+ addControlNetCollectorSafe(graph, denoiseNodeId);
+
+ for (const controlNet of controlNets) {
+ if (!controlNet.model) {
+ return;
+ }
+ const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet;
+
+ const controlNetNode: ControlNetInvocation = {
+ id: `control_net_${id}`,
+ type: 'controlnet',
+ is_intermediate: true,
+ begin_step_percent: beginEndStepPct[0],
+ end_step_percent: beginEndStepPct[1],
+ control_mode: controlMode,
+ resize_mode: 'just_resize',
+ control_model: model,
+ control_weight: weight,
+ image: buildControlImage(image, processedImage, processorConfig),
+ };
+
+ graph.nodes[controlNetNode.id] = controlNetNode;
+
+ controlNetMetadata.push(buildControlNetMetadata(controlNet));
+
+ graph.edges.push({
+ source: { node_id: controlNetNode.id, field: 'control' },
+ destination: {
+ node_id: CONTROL_NET_COLLECT,
+ field: 'item',
+ },
+ });
+ }
+ upsertMetadata(graph, { controlnets: controlNetMetadata });
+};
+
+const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfig): S['T2IAdapterMetadataField'] => {
+ const { beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter;
+
+ assert(model, 'T2I Adapter model is required');
+ assert(image, 'T2I Adapter image is required');
+
+ const processed_image =
+ processedImage && processorConfig
+ ? {
+ image_name: processedImage.imageName,
+ }
+ : null;
+
+ return {
+ t2i_adapter_model: model,
+ weight,
+ begin_step_percent: beginEndStepPct[0],
+ end_step_percent: beginEndStepPct[1],
+ resize_mode: 'just_resize',
+ image: {
+ image_name: image.imageName,
+ },
+ processed_image,
+ };
+};
+
+const addT2IAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => {
+ if (graph.nodes[T2I_ADAPTER_COLLECT]) {
+ // You see, we've already got one!
+ return;
+ }
+ // Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect
+ const t2iAdapterCollectNode: CollectInvocation = {
+ id: T2I_ADAPTER_COLLECT,
+ type: 'collect',
+ is_intermediate: true,
+ };
+ graph.nodes[T2I_ADAPTER_COLLECT] = t2iAdapterCollectNode;
+ graph.edges.push({
+ source: { node_id: T2I_ADAPTER_COLLECT, field: 'collection' },
+ destination: {
+ node_id: denoiseNodeId,
+ field: 't2i_adapter',
+ },
+ });
+};
+
+const addGlobalT2IAdaptersToGraph = async (
+ t2iAdapters: T2IAdapterConfig[],
+ graph: NonNullableGraph,
+ denoiseNodeId: string
+) => {
+ if (t2iAdapters.length === 0) {
+ return;
+ }
+ const t2iAdapterMetadata: CoreMetadataInvocation['t2iAdapters'] = [];
+ addT2IAdapterCollectorSafe(graph, denoiseNodeId);
+
+ for (const t2iAdapter of t2iAdapters) {
+ if (!t2iAdapter.model) {
+ return;
+ }
+ const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter;
+
+ const t2iAdapterNode: T2IAdapterInvocation = {
+ id: `t2i_adapter_${id}`,
+ type: 't2i_adapter',
+ is_intermediate: true,
+ begin_step_percent: beginEndStepPct[0],
+ end_step_percent: beginEndStepPct[1],
+ resize_mode: 'just_resize',
+ t2i_adapter_model: model,
+ weight: weight,
+ image: buildControlImage(image, processedImage, processorConfig),
+ };
+
+ graph.nodes[t2iAdapterNode.id] = t2iAdapterNode;
+
+ t2iAdapterMetadata.push(buildT2IAdapterMetadata(t2iAdapter));
+
+ graph.edges.push({
+ source: { node_id: t2iAdapterNode.id, field: 't2i_adapter' },
+ destination: {
+ node_id: T2I_ADAPTER_COLLECT,
+ field: 'item',
+ },
+ });
+ }
+
+ upsertMetadata(graph, { t2iAdapters: t2iAdapterMetadata });
+};
+
+const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfig): S['IPAdapterMetadataField'] => {
+ const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter;
+
+ assert(model, 'IP Adapter model is required');
+ assert(image, 'IP Adapter image is required');
+
+ return {
+ ip_adapter_model: model,
+ clip_vision_model: clipVisionModel,
+ weight,
+ method,
+ begin_step_percent: beginEndStepPct[0],
+ end_step_percent: beginEndStepPct[1],
+ image: {
+ image_name: image.imageName,
+ },
+ };
+};
+
+const addIPAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => {
+ if (graph.nodes[IP_ADAPTER_COLLECT]) {
+ // You see, we've already got one!
+ return;
+ }
+
+ const ipAdapterCollectNode: CollectInvocation = {
+ id: IP_ADAPTER_COLLECT,
+ type: 'collect',
+ is_intermediate: true,
+ };
+ graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode;
+ graph.edges.push({
+ source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' },
+ destination: {
+ node_id: denoiseNodeId,
+ field: 'ip_adapter',
+ },
+ });
+};
+
+const addGlobalIPAdaptersToGraph = async (
+ ipAdapters: IPAdapterConfig[],
+ graph: NonNullableGraph,
+ denoiseNodeId: string
+) => {
+ if (ipAdapters.length === 0) {
+ return;
+ }
+ const ipAdapterMetdata: CoreMetadataInvocation['ipAdapters'] = [];
+ addIPAdapterCollectorSafe(graph, denoiseNodeId);
+
+ for (const ipAdapter of ipAdapters) {
+ const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter;
+ assert(image, 'IP Adapter image is required');
+ assert(model, 'IP Adapter model is required');
+
+ const ipAdapterNode: IPAdapterInvocation = {
+ id: `ip_adapter_${id}`,
+ type: 'ip_adapter',
+ is_intermediate: true,
+ weight,
+ method,
+ ip_adapter_model: model,
+ clip_vision_model: clipVisionModel,
+ begin_step_percent: beginEndStepPct[0],
+ end_step_percent: beginEndStepPct[1],
+ image: {
+ image_name: image.imageName,
+ },
+ };
+
+ graph.nodes[ipAdapterNode.id] = ipAdapterNode;
+
+ ipAdapterMetdata.push(buildIPAdapterMetadata(ipAdapter));
+
+ graph.edges.push({
+ source: { node_id: ipAdapterNode.id, field: 'ip_adapter' },
+ destination: {
+ node_id: IP_ADAPTER_COLLECT,
+ field: 'item',
+ },
+ });
+ }
+
+ upsertMetadata(graph, { ipAdapters: ipAdapterMetdata });
+};
+
+export const addControlLayersToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => {
const { dispatch } = getStore();
- const isSDXL = state.generation.model?.base === 'sdxl';
- const layers = state.controlLayers.present.layers
- // Only support vector mask layers now
- // TODO: Image masks
+ const mainModel = state.generation.model;
+ assert(mainModel, 'Missing main model when building graph');
+ const isSDXL = mainModel.base === 'sdxl';
+
+ // Add global control adapters
+ const globalControlNets = state.controlLayers.present.layers
+ // Must be a CA layer
+ .filter(isControlAdapterLayer)
+ // Must be enabled
+ .filter((l) => l.isEnabled)
+ // We want the CAs themselves
+ .map((l) => l.controlAdapter)
+ // Must be a ControlNet
+ .filter(isControlNetConfig)
+ .filter((ca) => {
+ const hasModel = Boolean(ca.model);
+ const modelMatchesBase = ca.model?.base === mainModel.base;
+ const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig);
+ return hasModel && modelMatchesBase && hasControlImage;
+ });
+ addGlobalControlNetsToGraph(globalControlNets, graph, denoiseNodeId);
+
+ const globalT2IAdapters = state.controlLayers.present.layers
+ // Must be a CA layer
+ .filter(isControlAdapterLayer)
+ // Must be enabled
+ .filter((l) => l.isEnabled)
+ // We want the CAs themselves
+ .map((l) => l.controlAdapter)
+ // Must have a ControlNet CA
+ .filter(isT2IAdapterConfig)
+ .filter((ca) => {
+ const hasModel = Boolean(ca.model);
+ const modelMatchesBase = ca.model?.base === mainModel.base;
+ const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig);
+ return hasModel && modelMatchesBase && hasControlImage;
+ });
+ addGlobalT2IAdaptersToGraph(globalT2IAdapters, graph, denoiseNodeId);
+
+ const globalIPAdapters = state.controlLayers.present.layers
+ // Must be an IP Adapter layer
+ .filter(isIPAdapterLayer)
+ // Must be enabled
+ .filter((l) => l.isEnabled)
+ // We want the IP Adapters themselves
+ .map((l) => l.ipAdapter)
+ .filter((ca) => {
+ const hasModel = Boolean(ca.model);
+ const modelMatchesBase = ca.model?.base === mainModel.base;
+ const hasControlImage = Boolean(ca.image);
+ return hasModel && modelMatchesBase && hasControlImage;
+ });
+ addGlobalIPAdaptersToGraph(globalIPAdapters, graph, denoiseNodeId);
+
+ const rgLayers = state.controlLayers.present.layers
+ // Only RG layers are get masks
.filter(isRegionalGuidanceLayer)
// Only visible layers are rendered on the canvas
.filter((l) => l.isEnabled)
// Only layers with prompts get added to the graph
.filter((l) => {
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
- const hasIPAdapter = l.ipAdapterIds.length !== 0;
+ const hasIPAdapter = l.ipAdapters.length !== 0;
return hasTextPrompt || hasIPAdapter;
});
- // Collect all IP Adapter ids for IP adapter layers
- const layerIPAdapterIds = layers.flatMap((l) => l.ipAdapterIds);
-
- const regionalIPAdapters = selectAllIPAdapters(state.controlAdapters).filter(
- ({ id, model, controlImage, isEnabled }) => {
- const hasModel = Boolean(model);
- const doesBaseMatch = model?.base === state.generation.model?.base;
- const hasControlImage = controlImage;
- const isRegional = layerIPAdapterIds.includes(id);
- return isEnabled && hasModel && doesBaseMatch && hasControlImage && isRegional;
- }
- );
-
- const layerIds = layers.map((l) => l.id);
+ const layerIds = rgLayers.map((l) => l.id);
const blobs = await getRegionalPromptLayerBlobs(layerIds);
assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs');
@@ -118,27 +470,11 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab
},
});
- if (!graph.nodes[IP_ADAPTER_COLLECT] && regionalIPAdapters.length > 0) {
- const ipAdapterCollectNode: CollectInvocation = {
- id: IP_ADAPTER_COLLECT,
- type: 'collect',
- is_intermediate: true,
- };
- graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode;
- graph.edges.push({
- source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' },
- destination: {
- node_id: denoiseNodeId,
- field: 'ip_adapter',
- },
- });
- }
-
// Upload the blobs to the backend, add each to graph
// TODO: Store the uploaded image names in redux to reuse them, so long as the layer hasn't otherwise changed. This
// would be a great perf win - not only would we skip re-uploading the same image, but we'd be able to use the node
// cache (currently, when we re-use the same mask data, since it is a different image, the node cache is not used).
- for (const layer of layers) {
+ for (const layer of rgLayers) {
const blob = blobs[layer.id];
assert(blob, `Blob for layer ${layer.id} not found`);
@@ -296,36 +632,32 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab
}
}
- for (const ipAdapterId of layer.ipAdapterIds) {
- const ipAdapter = selectAllIPAdapters(state.controlAdapters)
- .filter(({ id, model, controlImage, isEnabled }) => {
- const hasModel = Boolean(model);
- const doesBaseMatch = model?.base === state.generation.model?.base;
- const hasControlImage = controlImage;
- const isRegional = layers.some((l) => l.ipAdapterIds.includes(id));
- return isEnabled && hasModel && doesBaseMatch && hasControlImage && isRegional;
- })
- .find((ca) => ca.id === ipAdapterId);
+ // TODO(psyche): For some reason, I have to explicitly annotate regionalIPAdapters here. Not sure why.
+ const regionalIPAdapters: IPAdapterConfig[] = layer.ipAdapters.filter((ipAdapter) => {
+ const hasModel = Boolean(ipAdapter.model);
+ const modelMatchesBase = ipAdapter.model?.base === mainModel.base;
+ const hasControlImage = Boolean(ipAdapter.image);
+ return hasModel && modelMatchesBase && hasControlImage;
+ });
- if (!ipAdapter?.model) {
- return;
- }
- const { id, weight, model, clipVisionModel, method, beginStepPct, endStepPct, controlImage } = ipAdapter;
-
- assert(controlImage, 'IP Adapter image is required');
+ for (const ipAdapter of regionalIPAdapters) {
+ addIPAdapterCollectorSafe(graph, denoiseNodeId);
+ const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter;
+ assert(model, 'IP Adapter model is required');
+ assert(image, 'IP Adapter image is required');
const ipAdapterNode: IPAdapterInvocation = {
id: `ip_adapter_${id}`,
type: 'ip_adapter',
is_intermediate: true,
- weight: weight,
- method: method,
+ weight,
+ method,
ip_adapter_model: model,
clip_vision_model: clipVisionModel,
- begin_step_percent: beginStepPct,
- end_step_percent: endStepPct,
+ begin_step_percent: beginEndStepPct[0],
+ end_step_percent: beginEndStepPct[1],
image: {
- image_name: controlImage,
+ image_name: image.imageName,
},
};
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts
index fb912d0be2..363d97badf 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts
@@ -1,10 +1,8 @@
import type { RootState } from 'app/store/store';
import { selectValidControlNets } from 'features/controlAdapters/store/controlAdaptersSlice';
import type { ControlAdapterProcessorType, ControlNetConfig } from 'features/controlAdapters/store/types';
-import { isControlAdapterLayer } from 'features/controlLayers/store/controlLayersSlice';
import type { ImageField } from 'features/nodes/types/common';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
-import { differenceWith, intersectionWith } from 'lodash-es';
import type {
CollectInvocation,
ControlNetInvocation,
@@ -17,9 +15,13 @@ import { assert } from 'tsafe';
import { CONTROL_NET_COLLECT } from './constants';
import { upsertMetadata } from './metadata';
-const getControlNets = (state: RootState) => {
- // Start with the valid controlnets
- const validControlNets = selectValidControlNets(state.controlAdapters).filter(
+export const addControlNetToLinearGraph = async (
+ state: RootState,
+ graph: NonNullableGraph,
+ baseNodeId: string
+): Promise => {
+ const controlNetMetadata: CoreMetadataInvocation['controlnets'] = [];
+ const controlNets = selectValidControlNets(state.controlAdapters).filter(
({ model, processedControlImage, processorType, controlImage, isEnabled }) => {
const hasModel = Boolean(model);
const doesBaseMatch = model?.base === state.generation.model?.base;
@@ -29,35 +31,9 @@ const getControlNets = (state: RootState) => {
}
);
- // txt2img tab has special handling - it uses layers exclusively, while the other tabs use the older control adapters
- // accordion. We need to filter the list of valid T2I adapters according to the tab.
+ // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper.
const activeTabName = activeTabNameSelector(state);
-
- if (activeTabName === 'txt2img') {
- // Add only the cnets that are used in control layers
- // Collect all ControlNet ids for enabled ControlNet layers
- const layerControlNetIds = state.controlLayers.present.layers
- .filter(isControlAdapterLayer)
- .filter((l) => l.isEnabled)
- .map((l) => l.controlNetId);
- return intersectionWith(validControlNets, layerControlNetIds, (a, b) => a.id === b);
- } else {
- // Else, we want to exclude the cnets that are used in control layers
- // Collect all ControlNet ids for all ControlNet layers
- const layerControlNetIds = state.controlLayers.present.layers
- .filter(isControlAdapterLayer)
- .map((l) => l.controlNetId);
- return differenceWith(validControlNets, layerControlNetIds, (a, b) => a.id === b);
- }
-};
-
-export const addControlNetToLinearGraph = async (
- state: RootState,
- graph: NonNullableGraph,
- baseNodeId: string
-): Promise => {
- const controlNets = getControlNets(state);
- const controlNetMetadata: CoreMetadataInvocation['controlnets'] = [];
+ assert(activeTabName !== 'txt2img', 'Tried to use addControlNetToLinearGraph on txt2img tab');
if (controlNets.length) {
// Even though denoise_latents' control input is collection or scalar, keep it simple and always use a collect
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts
index 2c53fb3827..12ba4e12a8 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts
@@ -1,10 +1,8 @@
import type { RootState } from 'app/store/store';
import { selectValidIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice';
import type { IPAdapterConfig } from 'features/controlAdapters/store/types';
-import { isIPAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice';
import type { ImageField } from 'features/nodes/types/common';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
-import { differenceWith, intersectionWith } from 'lodash-es';
import type {
CollectInvocation,
CoreMetadataInvocation,
@@ -17,48 +15,21 @@ import { assert } from 'tsafe';
import { IP_ADAPTER_COLLECT } from './constants';
import { upsertMetadata } from './metadata';
-const getIPAdapters = (state: RootState) => {
- // Start with the valid IP adapters
- const validIPAdapters = selectValidIPAdapters(state.controlAdapters).filter(({ model, controlImage, isEnabled }) => {
- const hasModel = Boolean(model);
- const doesBaseMatch = model?.base === state.generation.model?.base;
- const hasControlImage = controlImage;
- return isEnabled && hasModel && doesBaseMatch && hasControlImage;
- });
-
- // Masked IP adapters are handled in the graph helper for regional control - skip them here
- const maskedIPAdapterIds = state.controlLayers.present.layers
- .filter(isRegionalGuidanceLayer)
- .map((l) => l.ipAdapterIds)
- .flat();
- const nonMaskedIPAdapters = differenceWith(validIPAdapters, maskedIPAdapterIds, (a, b) => a.id === b);
-
- // txt2img tab has special handling - it uses layers exclusively, while the other tabs use the older control adapters
- // accordion. We need to filter the list of valid IP adapters according to the tab.
- const activeTabName = activeTabNameSelector(state);
-
- if (activeTabName === 'txt2img') {
- // If we are on the t2i tab, we only want to add the IP adapters that are used in unmasked IP Adapter layers
- // Collect all IP Adapter ids for enabled IP adapter layers
- const layerIPAdapterIds = state.controlLayers.present.layers
- .filter(isIPAdapterLayer)
- .filter((l) => l.isEnabled)
- .map((l) => l.ipAdapterId);
- return intersectionWith(nonMaskedIPAdapters, layerIPAdapterIds, (a, b) => a.id === b);
- } else {
- // Else, we want to exclude the IP adapters that are used in IP Adapter layers
- // Collect all IP Adapter ids for enabled IP adapter layers
- const layerIPAdapterIds = state.controlLayers.present.layers.filter(isIPAdapterLayer).map((l) => l.ipAdapterId);
- return differenceWith(nonMaskedIPAdapters, layerIPAdapterIds, (a, b) => a.id === b);
- }
-};
-
export const addIPAdapterToLinearGraph = async (
state: RootState,
graph: NonNullableGraph,
baseNodeId: string
): Promise => {
- const ipAdapters = getIPAdapters(state);
+ // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper.
+ const activeTabName = activeTabNameSelector(state);
+ assert(activeTabName !== 'txt2img', 'Tried to use addT2IAdaptersToLinearGraph on txt2img tab');
+
+ const ipAdapters = selectValidIPAdapters(state.controlAdapters).filter(({ model, controlImage, isEnabled }) => {
+ const hasModel = Boolean(model);
+ const doesBaseMatch = model?.base === state.generation.model?.base;
+ const hasControlImage = controlImage;
+ return isEnabled && hasModel && doesBaseMatch && hasControlImage;
+ });
if (ipAdapters.length) {
// Even though denoise_latents' ip adapter input is collection or scalar, keep it simple and always use a collect
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts
index 1632449724..ddd87256f4 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts
@@ -1,10 +1,8 @@
import type { RootState } from 'app/store/store';
import { selectValidT2IAdapters } from 'features/controlAdapters/store/controlAdaptersSlice';
import type { ControlAdapterProcessorType, T2IAdapterConfig } from 'features/controlAdapters/store/types';
-import { isControlAdapterLayer } from 'features/controlLayers/store/controlLayersSlice';
import type { ImageField } from 'features/nodes/types/common';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
-import { differenceWith, intersectionWith } from 'lodash-es';
import type {
CollectInvocation,
CoreMetadataInvocation,
@@ -17,9 +15,16 @@ import { assert } from 'tsafe';
import { T2I_ADAPTER_COLLECT } from './constants';
import { upsertMetadata } from './metadata';
-const getT2IAdapters = (state: RootState) => {
- // Start with the valid controlnets
- const validT2IAdapters = selectValidT2IAdapters(state.controlAdapters).filter(
+export const addT2IAdaptersToLinearGraph = async (
+ state: RootState,
+ graph: NonNullableGraph,
+ baseNodeId: string
+): Promise => {
+ // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper.
+ const activeTabName = activeTabNameSelector(state);
+ assert(activeTabName !== 'txt2img', 'Tried to use addT2IAdaptersToLinearGraph on txt2img tab');
+
+ const t2iAdapters = selectValidT2IAdapters(state.controlAdapters).filter(
({ model, processedControlImage, processorType, controlImage, isEnabled }) => {
const hasModel = Boolean(model);
const doesBaseMatch = model?.base === state.generation.model?.base;
@@ -29,34 +34,6 @@ const getT2IAdapters = (state: RootState) => {
}
);
- // txt2img tab has special handling - it uses layers exclusively, while the other tabs use the older control adapters
- // accordion. We need to filter the list of valid T2I adapters according to the tab.
- const activeTabName = activeTabNameSelector(state);
-
- if (activeTabName === 'txt2img') {
- // Add only the T2Is that are used in control layers
- // Collect all ids for enabled control adapter layers
- const layerControlAdapterIds = state.controlLayers.present.layers
- .filter(isControlAdapterLayer)
- .filter((l) => l.isEnabled)
- .map((l) => l.controlNetId);
- return intersectionWith(validT2IAdapters, layerControlAdapterIds, (a, b) => a.id === b);
- } else {
- // Else, we want to exclude the T2Is that are used in control layers
- const layerControlAdapterIds = state.controlLayers.present.layers
- .filter(isControlAdapterLayer)
- .map((l) => l.controlNetId);
- return differenceWith(validT2IAdapters, layerControlAdapterIds, (a, b) => a.id === b);
- }
-};
-
-export const addT2IAdaptersToLinearGraph = async (
- state: RootState,
- graph: NonNullableGraph,
- baseNodeId: string
-): Promise => {
- const t2iAdapters = getT2IAdapters(state);
-
if (t2iAdapters.length) {
// Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect
const t2iAdapterCollectNode: CollectInvocation = {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts
index 010fb9c5e4..9134ef9de7 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts
@@ -4,13 +4,10 @@ import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetch
import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph';
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
-import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
-import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph';
import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph';
import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph';
import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph';
import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph';
-import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph';
import { addVAEToGraph } from './addVAEToGraph';
import { addWatermarkerToGraph } from './addWatermarkerToGraph';
import {
@@ -264,14 +261,6 @@ export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise
// add LoRA support
await addSDXLLoRAsToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId);
- // add controlnet, mutating `graph`
- await addControlNetToLinearGraph(state, graph, SDXL_DENOISE_LATENTS);
-
- // add IP Adapter
- await addIPAdapterToLinearGraph(state, graph, SDXL_DENOISE_LATENTS);
-
- await addT2IAdaptersToLinearGraph(state, graph, SDXL_DENOISE_LATENTS);
-
await addControlLayersToGraph(state, graph, SDXL_DENOISE_LATENTS);
// NSFW & watermark - must be last thing added to graph
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts
index ea59d7e41d..340a24bca4 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts
@@ -5,13 +5,10 @@ import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLay
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
-import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
import { addHrfToGraph } from './addHrfToGraph';
-import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph';
import { addLoRAsToGraph } from './addLoRAsToGraph';
import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph';
import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph';
-import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph';
import { addVAEToGraph } from './addVAEToGraph';
import { addWatermarkerToGraph } from './addWatermarkerToGraph';
import {
@@ -246,14 +243,6 @@ export const buildLinearTextToImageGraph = async (state: RootState): Promise