refactor(ui): wire up CA logic across (wip)

This commit is contained in:
psychedelicious 2024-05-01 18:36:08 +10:00 committed by Kent Keirsey
parent 424a27eeda
commit 0e55488ff6
34 changed files with 852 additions and 756 deletions

View File

@ -16,7 +16,6 @@ import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listen
import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet';
import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged';
import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery';
import { addControlLayersToControlAdapterBridge } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess';
import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed';
import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas';
@ -158,5 +157,3 @@ addUpscaleRequestedListener(startAppListening);
addDynamicPromptsListener(startAppListening);
addSetDefaultSettingsListener(startAppListening);
addControlLayersToControlAdapterBridge(startAppListening);

View File

@ -1,144 +0,0 @@
import { createAction } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
import { controlAdapterAdded, controlAdapterRemoved } from 'features/controlAdapters/store/controlAdaptersSlice';
import type { ControlNetConfig, IPAdapterConfig } from 'features/controlAdapters/store/types';
import { isControlAdapterProcessorType } from 'features/controlAdapters/store/types';
import {
caLayerAdded,
ipaLayerAdded,
layerDeleted,
rgLayerAdded,
rgLayerIPAdapterAdded,
rgLayerIPAdapterDeleted,
} from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types';
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
import { isControlNetModelConfig, isIPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
export const guidanceLayerAdded = createAction<Layer['type']>('controlLayers/guidanceLayerAdded');
export const guidanceLayerDeleted = createAction<string>('controlLayers/guidanceLayerDeleted');
export const allLayersDeleted = createAction('controlLayers/allLayersDeleted');
export const guidanceLayerIPAdapterAdded = createAction<string>('controlLayers/guidanceLayerIPAdapterAdded');
export const guidanceLayerIPAdapterDeleted = createAction<{ layerId: string; ipAdapterId: string }>(
'controlLayers/guidanceLayerIPAdapterDeleted'
);
export const addControlLayersToControlAdapterBridge = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: guidanceLayerAdded,
effect: (action, { dispatch, getState }) => {
const type = action.payload;
const layerId = uuidv4();
if (type === 'regional_guidance_layer') {
dispatch(rgLayerAdded({ layerId }));
return;
}
const state = getState();
const baseModel = state.generation.model?.base;
const modelConfigs = modelsApi.endpoints.getModelConfigs.select(undefined)(state).data;
if (type === 'ip_adapter_layer') {
const ipAdapterId = uuidv4();
const overrides: Partial<IPAdapterConfig> = {
id: ipAdapterId,
};
// Find and select the first matching model
if (modelConfigs) {
const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isIPAdapterModelConfig);
overrides.model = models.find((m) => m.base === baseModel) ?? null;
}
dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides }));
dispatch(ipaLayerAdded({ layerId, ipAdapterId }));
return;
}
if (type === 'control_adapter_layer') {
const controlNetId = uuidv4();
const overrides: Partial<ControlNetConfig> = {
id: controlNetId,
};
// Find and select the first matching model
if (modelConfigs) {
const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isControlNetModelConfig);
const model = models.find((m) => m.base === baseModel) ?? null;
overrides.model = model;
const defaultPreprocessor = model?.default_settings?.preprocessor;
overrides.processorType = isControlAdapterProcessorType(defaultPreprocessor) ? defaultPreprocessor : 'none';
overrides.processorNode = CONTROLNET_PROCESSORS[overrides.processorType].buildDefaults(baseModel);
}
dispatch(controlAdapterAdded({ type: 'controlnet', overrides }));
dispatch(caLayerAdded({ layerId, controlNetId }));
return;
}
},
});
startAppListening({
actionCreator: guidanceLayerDeleted,
effect: (action, { getState, dispatch }) => {
const layerId = action.payload;
const state = getState();
const layer = state.controlLayers.present.layers.find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
if (layer.type === 'ip_adapter_layer') {
dispatch(controlAdapterRemoved({ id: layer.ipAdapterId }));
} else if (layer.type === 'control_adapter_layer') {
dispatch(controlAdapterRemoved({ id: layer.controlNetId }));
} else if (layer.type === 'regional_guidance_layer') {
for (const ipAdapterId of layer.ipAdapterIds) {
dispatch(controlAdapterRemoved({ id: ipAdapterId }));
}
}
dispatch(layerDeleted(layerId));
},
});
startAppListening({
actionCreator: allLayersDeleted,
effect: (action, { dispatch, getOriginalState }) => {
const state = getOriginalState();
for (const layer of state.controlLayers.present.layers) {
dispatch(guidanceLayerDeleted(layer.id));
}
},
});
startAppListening({
actionCreator: guidanceLayerIPAdapterAdded,
effect: (action, { dispatch, getState }) => {
const layerId = action.payload;
const ipAdapterId = uuidv4();
const overrides: Partial<IPAdapterConfig> = {
id: ipAdapterId,
};
// Find and select the first matching model
const state = getState();
const baseModel = state.generation.model?.base;
const modelConfigs = modelsApi.endpoints.getModelConfigs.select(undefined)(state).data;
if (modelConfigs) {
const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isIPAdapterModelConfig);
overrides.model = models.find((m) => m.base === baseModel) ?? null;
}
dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides }));
dispatch(rgLayerIPAdapterAdded({ layerId, ipAdapterId }));
},
});
startAppListening({
actionCreator: guidanceLayerIPAdapterDeleted,
effect: (action, { dispatch }) => {
const { layerId, ipAdapterId } = action.payload;
dispatch(controlAdapterRemoved({ id: ipAdapterId }));
dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId }));
},
});
};

View File

@ -101,33 +101,35 @@ const selector = createMemoizedSelector(
if (activeTabName === 'txt2img') {
// Special handling for control layers on txt2img
const enabledControlLayersAdapterIds = controlLayers.present.layers
.filter((l) => l.isEnabled)
.flatMap((layer) => {
if (layer.type === 'regional_guidance_layer') {
return layer.ipAdapterIds;
}
if (layer.type === 'control_adapter_layer') {
return [layer.controlNetId];
}
if (layer.type === 'ip_adapter_layer') {
return [layer.ipAdapterId];
}
});
const enabledControlLayersAdapterIds = []
// const enabledControlLayersAdapterIds = controlLayers.present.layers
// .filter((l) => l.isEnabled)
// .flatMap((layer) => {
// if (layer.type === 'regional_guidance_layer') {
// return layer.ipAdapterIds;
// }
// if (layer.type === 'control_adapter_layer') {
// return [layer.controlNetId];
// }
// if (layer.type === 'ip_adapter_layer') {
// return [layer.ipAdapterId];
// }
// });
enabledControlAdapters = enabledControlAdapters.filter((ca) => enabledControlLayersAdapterIds.includes(ca.id));
} else {
const allControlLayerAdapterIds = controlLayers.present.layers.flatMap((layer) => {
if (layer.type === 'regional_guidance_layer') {
return layer.ipAdapterIds;
}
if (layer.type === 'control_adapter_layer') {
return [layer.controlNetId];
}
if (layer.type === 'ip_adapter_layer') {
return [layer.ipAdapterId];
}
});
const allControlLayerAdapterIds = []
// const allControlLayerAdapterIds = controlLayers.present.layers.flatMap((layer) => {
// if (layer.type === 'regional_guidance_layer') {
// return layer.ipAdapterIds;
// }
// if (layer.type === 'control_adapter_layer') {
// return [layer.controlNetId];
// }
// if (layer.type === 'ip_adapter_layer') {
// return [layer.ipAdapterId];
// }
// });
enabledControlAdapters = enabledControlAdapters.filter((ca) => !allControlLayerAdapterIds.includes(ca.id));
}

View File

@ -1,6 +1,7 @@
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { guidanceLayerAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
@ -8,14 +9,10 @@ import { PiPlusBold } from 'react-icons/pi';
export const AddLayerButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const addRegionalGuidanceLayer = useCallback(() => {
dispatch(guidanceLayerAdded('regional_guidance_layer'));
}, [dispatch]);
const addControlAdapterLayer = useCallback(() => {
dispatch(guidanceLayerAdded('control_adapter_layer'));
}, [dispatch]);
const addIPAdapterLayer = useCallback(() => {
dispatch(guidanceLayerAdded('ip_adapter_layer'));
const [addCALayer, isAddCALayerDisabled] = useAddCALayer();
const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer();
const addRGLayer = useCallback(() => {
dispatch(rgLayerAdded());
}, [dispatch]);
return (
@ -24,13 +21,13 @@ export const AddLayerButton = memo(() => {
{t('controlLayers.addLayer')}
</MenuButton>
<MenuList>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidanceLayer}>
<MenuItem icon={<PiPlusBold />} onClick={addRGLayer}>
{t('controlLayers.regionalGuidanceLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addControlAdapterLayer}>
<MenuItem icon={<PiPlusBold />} onClick={addCALayer} isDisabled={isAddCALayerDisabled}>
{t('controlLayers.globalControlAdapterLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapterLayer}>
<MenuItem icon={<PiPlusBold />} onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}>
{t('controlLayers.globalIPAdapterLayer')}
</MenuItem>
</MenuList>

View File

@ -1,7 +1,7 @@
import { Button, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import {
isRegionalGuidanceLayer,
rgLayerNegativePromptChanged,
@ -19,6 +19,7 @@ type AddPromptButtonProps = {
export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId);
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
@ -38,9 +39,6 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
const addNegativePrompt = useCallback(() => {
dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' }));
}, [dispatch, layerId]);
const addIPAdapter = useCallback(() => {
dispatch(guidanceLayerIPAdapterAdded(layerId));
}, [dispatch, layerId]);
return (
<Flex w="full" p={2} justifyContent="space-between">
@ -62,7 +60,13 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
>
{t('common.negativePrompt')}
</Button>
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
<Button
size="sm"
variant="ghost"
leftIcon={<PiPlusBold />}
onClick={addIPAdapter}
isDisabled={isAddIPAdapterDisabled}
>
{t('common.ipAdapter')}
</Button>
</Flex>

View File

@ -1,18 +1,12 @@
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig';
import { CALayerConfig } from 'features/controlLayers/components/CALayer/CALayerConfig';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import {
isControlAdapterLayer,
layerSelected,
selectControlLayersSlice,
} from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback, useMemo } from 'react';
import { assert } from 'tsafe';
import { layerSelected, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import CALayerOpacity from './CALayerOpacity';
@ -22,19 +16,7 @@ type Props = {
export const CALayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isControlAdapterLayer(layer), `Layer ${layerId} not found or not a ControlNet layer`);
return {
controlNetId: layer.controlNetId,
isSelected: layerId === controlLayers.present.selectedLayerId,
};
}),
[layerId]
);
const { controlNetId, isSelected } = useAppSelector(selector);
const isSelected = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).isSelected);
const onClickCapture = useCallback(() => {
// Must be capture so that the layer is selected before deleting/resetting/etc
dispatch(layerSelected(layerId));
@ -61,7 +43,7 @@ export const CALayer = memo(({ layerId }: Props) => {
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<ControlAdapterLayerConfig id={controlNetId} />
<CALayerConfig layerId={layerId} />
</Flex>
)}
</Flex>

View File

@ -1,33 +1,101 @@
import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CALayerModelCombobox } from 'features/controlLayers/components/CALayer/CALayerModelCombobox';
import { selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
import { memo } from 'react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ControlAdapterModelCombobox } from 'features/controlLayers/components/CALayer/ControlAdapterModelCombobox';
import {
caLayerControlModeChanged,
caLayerImageChanged,
caLayerModelChanged,
caLayerProcessorConfigChanged,
caOrIPALayerBeginEndStepPctChanged,
caOrIPALayerWeightChanged,
selectCALayer,
} from 'features/controlLayers/store/controlLayersSlice';
import type { ControlMode, ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretUpBold } from 'react-icons/pi';
import { useToggle } from 'react-use';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
import { CALayerBeginEndStepPct } from './CALayerBeginEndStepPct';
import { CALayerControlMode } from './CALayerControlMode';
import { CALayerImagePreview } from './CALayerImagePreview';
import { CALayerProcessor } from './CALayerProcessor';
import { CALayerProcessorCombobox } from './CALayerProcessorCombobox';
import { CALayerWeight } from './CALayerWeight';
import { ControlAdapterBeginEndStepPct } from './ControlAdapterBeginEndStepPct';
import { ControlAdapterControlModeSelect } from './ControlAdapterControlModeSelect';
import { ControlAdapterWeight } from './ControlAdapterWeight';
type Props = {
layerId: string;
};
export const CALayerConfig = memo(({ layerId }: Props) => {
const caType = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.type);
const dispatch = useAppDispatch();
const controlAdapter = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter);
const { t } = useTranslation();
const [isExpanded, toggleIsExpanded] = useToggle(false);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
dispatch(
caOrIPALayerBeginEndStepPctChanged({
layerId,
beginEndStepPct,
})
);
},
[dispatch, layerId]
);
const onChangeControlMode = useCallback(
(controlMode: ControlMode) => {
dispatch(
caLayerControlModeChanged({
layerId,
controlMode,
})
);
},
[dispatch, layerId]
);
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(caOrIPALayerWeightChanged({ layerId, weight }));
},
[dispatch, layerId]
);
const onChangeProcessorConfig = useCallback(
(processorConfig: ProcessorConfig | null) => {
dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig }));
},
[dispatch, layerId]
);
const onChangeModel = useCallback(
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => {
dispatch(
caLayerModelChanged({
layerId,
modelConfig,
})
);
},
[dispatch, layerId]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(caLayerImageChanged({ layerId, imageDTO }));
},
[dispatch, layerId]
);
return (
<Flex flexDir="column" gap={4} position="relative" w="full">
<Flex gap={3} alignItems="center" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<CALayerModelCombobox layerId={layerId} />
<ControlAdapterModelCombobox modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
</Box>
<IconButton
@ -49,18 +117,29 @@ export const CALayerConfig = memo(({ layerId }: Props) => {
</Flex>
<Flex gap={4} w="full" alignItems="center">
<Flex flexDir="column" gap={3} w="full">
{caType === 'controlnet' && <CALayerControlMode layerId={layerId} />}
<CALayerWeight layerId={layerId} />
<CALayerBeginEndStepPct layerId={layerId} />
{controlAdapter.type === 'controlnet' && (
<ControlAdapterControlModeSelect controlMode={controlAdapter.controlMode} onChange={onChangeControlMode} />
)}
<ControlAdapterWeight weight={controlAdapter.weight} onChange={onChangeWeight} />
<ControlAdapterBeginEndStepPct
beginEndStepPct={controlAdapter.beginEndStepPct}
onChange={onChangeBeginEndStepPct}
/>
</Flex>
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
<CALayerImagePreview layerId={layerId} />
<CALayerImagePreview
image={controlAdapter.image}
processedImage={controlAdapter.processedImage}
onChangeImage={onChangeImage}
layerId={layerId}
hasProcessor={Boolean(controlAdapter.processorConfig)}
/>
</Flex>
</Flex>
{isExpanded && (
<>
<CALayerProcessorCombobox layerId={layerId} />
<CALayerProcessor layerId={layerId} />
<CALayerProcessorCombobox config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
<CALayerProcessor config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
</>
)}
</Flex>

View File

@ -7,13 +7,8 @@ import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
import {
caLayerImageChanged,
heightChanged,
selectCALayer,
selectControlLayersSlice,
widthChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters';
import type { ControlLayerDropData, ImageDraggableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
@ -27,10 +22,14 @@ import {
useGetImageDTOQuery,
useRemoveImageFromBoardMutation,
} from 'services/api/endpoints/images';
import type { ControlLayerAction } from 'services/api/types';
import type { ControlLayerAction, ImageDTO } from 'services/api/types';
type Props = {
layerId: string;
image: ImageWithDims | null;
processedImage: ImageWithDims | null;
onChangeImage: (imageDTO: ImageDTO | null) => void;
hasProcessor: boolean;
layerId: string; // required for the dnd/upload interactions
};
const selectPendingControlImages = createMemoizedSelector(
@ -38,23 +37,9 @@ const selectPendingControlImages = createMemoizedSelector(
(controlAdapters) => controlAdapters.pendingControlImages
);
export const CALayerImagePreview = memo(({ layerId }: Props) => {
export const CALayerImagePreview = memo(({ image, processedImage, onChangeImage, hasProcessor, layerId }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const layer = selectCALayer(controlLayers.present, layerId);
const { image, processedImage, processorConfig } = layer.controlAdapter;
return {
imageName: image?.imageName ?? null,
processedImageName: processedImage?.imageName ?? null,
hasProcessor: Boolean(processorConfig),
};
}),
[layerId]
);
const { imageName, processedImageName, hasProcessor } = useAppSelector(selector);
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const isConnected = useAppSelector((s) => s.system.isConnected);
const activeTabName = useAppSelector(activeTabNameSelector);
@ -64,17 +49,19 @@ export const CALayerImagePreview = memo(({ layerId }: Props) => {
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(imageName ?? skipToken);
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
image?.imageName ?? skipToken
);
const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery(
processedImageName ?? skipToken
processedImage?.imageName ?? skipToken
);
const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
const [addToBoard] = useAddImageToBoardMutation();
const [removeFromBoard] = useRemoveImageFromBoardMutation();
const handleResetControlImage = useCallback(() => {
dispatch(caLayerImageChanged({ layerId, imageDTO: null }));
}, [layerId, dispatch]);
onChangeImage(null);
}, [onChangeImage]);
const handleSaveControlImage = useCallback(async () => {
if (!processedControlImage) {

View File

@ -15,7 +15,7 @@ import {
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
import { caLayerIsFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice';
import { caLayerIsFilterEnabledChanged, caLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -34,7 +34,7 @@ const CALayerOpacity = ({ layerId }: Props) => {
const { opacity, isFilterEnabled } = useLayerOpacity(layerId);
const onChangeOpacity = useCallback(
(v: number) => {
dispatch(layerOpacityChanged({ layerId, opacity: v / 100 }));
dispatch(caLayerOpacityChanged({ layerId, opacity: v / 100 }));
},
[dispatch, layerId]
);

View File

@ -1,7 +1,5 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { caLayerProcessorConfigChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
import { memo } from 'react';
import { CannyProcessor } from './processors/CannyProcessor';
import { ColorMapProcessor } from './processors/ColorMapProcessor';
@ -16,19 +14,11 @@ import { MlsdImageProcessor } from './processors/MlsdImageProcessor';
import { PidiProcessor } from './processors/PidiProcessor';
type Props = {
layerId: string;
config: ProcessorConfig | null;
onChange: (config: ProcessorConfig | null) => void;
};
export const CALayerProcessor = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const config = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.processorConfig);
const onChange = useCallback(
(processorConfig: ProcessorConfig) => {
dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig }));
},
[dispatch, layerId]
);
export const CALayerProcessor = memo(({ config, onChange }: Props) => {
if (!config) {
return null;
}

View File

@ -1,9 +1,9 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { caLayerProcessorConfigChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS, isProcessorType } from 'features/controlLayers/util/controlAdapters';
import { configSelector } from 'features/system/store/configSelectors';
import { includes, map } from 'lodash-es';
@ -13,7 +13,8 @@ import { PiXBold } from 'react-icons/pi';
import { assert } from 'tsafe';
type Props = {
layerId: string;
config: ProcessorConfig | null;
onChange: (config: ProcessorConfig | null) => void;
};
const selectDisabledProcessors = createMemoizedSelector(
@ -21,49 +22,30 @@ const selectDisabledProcessors = createMemoizedSelector(
(config) => config.sd.disabledControlNetProcessors
);
export const CALayerProcessorCombobox = memo(({ layerId }: Props) => {
export const CALayerProcessorCombobox = memo(({ config, onChange }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const disabledProcessors = useAppSelector(selectDisabledProcessors);
const processorType = useAppSelector(
(s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.processorConfig?.type ?? null
);
const options = useMemo(() => {
return map(CONTROLNET_PROCESSORS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter(
(o) => !includes(disabledProcessors, o.value)
);
}, [disabledProcessors, t]);
const onChange = useCallback<ComboboxOnChange>(
const _onChange = useCallback<ComboboxOnChange>(
(v) => {
if (!v) {
dispatch(
caLayerProcessorConfigChanged({
layerId,
processorConfig: null,
})
);
return;
onChange(null);
} else {
assert(isProcessorType(v.value));
onChange(CONTROLNET_PROCESSORS[v.value].buildDefaults());
}
assert(isProcessorType(v.value));
dispatch(
caLayerProcessorConfigChanged({
layerId,
processorConfig: CONTROLNET_PROCESSORS[v.value].buildDefaults(),
})
);
},
[dispatch, layerId]
[onChange]
);
const clearProcessor = useCallback(() => {
dispatch(
caLayerProcessorConfigChanged({
layerId,
processorConfig: null,
})
);
}, [dispatch, layerId]);
const value = useMemo(() => options.find((o) => o.value === processorType) ?? null, [options, processorType]);
onChange(null);
}, [onChange]);
const value = useMemo(() => options.find((o) => o.value === config?.type) ?? null, [options, config?.type]);
return (
<FormControl>
@ -71,7 +53,7 @@ export const CALayerProcessorCombobox = memo(({ layerId }: Props) => {
<FormLabel>{t('controlnet.processor')}</FormLabel>
</InformationalPopover>
<Flex gap={4}>
<Combobox value={value} options={options} onChange={onChange} />
<Combobox value={value} options={options} onChange={_onChange} />
<IconButton
aria-label={t('controlnet.processor')}
onClick={clearProcessor}

View File

@ -1,44 +1,21 @@
import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { caLayerBeginEndStepPctChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
layerId: string;
beginEndStepPct: [number, number];
onChange: (beginEndStepPct: [number, number]) => void;
};
const formatPct = (v: number) => `${Math.round(v * 100)}%`;
const ariaLabel = ['Begin Step %', 'End Step %'];
export const CALayerBeginEndStepPct = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
export const ControlAdapterBeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => {
const { t } = useTranslation();
const beginEndStepPct = useAppSelector(
(s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.beginEndStepPct
);
const onChange = useCallback(
(v: [number, number]) => {
dispatch(
caLayerBeginEndStepPctChanged({
layerId,
beginEndStepPct: v,
})
);
},
[dispatch, layerId]
);
const onReset = useCallback(() => {
dispatch(
caLayerBeginEndStepPctChanged({
layerId,
beginEndStepPct: [0, 1],
})
);
}, [dispatch, layerId]);
onChange([0, 1]);
}, [onChange]);
return (
<FormControl orientation="horizontal">
@ -63,4 +40,4 @@ export const CALayerBeginEndStepPct = memo(({ layerId }: Props) => {
);
});
CALayerBeginEndStepPct.displayName = 'CALayerBeginEndStepPct';
ControlAdapterBeginEndStepPct.displayName = 'ControlAdapterBeginEndStepPct';

View File

@ -1,26 +1,19 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { caLayerControlModeChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
import type { ControlMode } from 'features/controlLayers/util/controlAdapters';
import { isControlMode } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
type Props = {
layerId: string;
controlMode: ControlMode;
onChange: (controlMode: ControlMode) => void;
};
export const CALayerControlMode = memo(({ layerId }: Props) => {
export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const controlMode = useAppSelector((s) => {
const ca = selectCALayer(s.controlLayers.present, layerId).controlAdapter;
assert(ca.type === 'controlnet');
return ca.controlMode;
});
const CONTROL_MODE_DATA = useMemo(
() => [
{ label: t('controlnet.balanced'), value: 'balanced' },
@ -34,14 +27,9 @@ export const CALayerControlMode = memo(({ layerId }: Props) => {
const handleControlModeChange = useCallback<ComboboxOnChange>(
(v) => {
assert(isControlMode(v?.value));
dispatch(
caLayerControlModeChanged({
layerId,
controlMode: v.value,
})
);
onChange(v.value);
},
[layerId, dispatch]
[onChange]
);
const value = useMemo(
@ -69,4 +57,4 @@ export const CALayerControlMode = memo(({ layerId }: Props) => {
);
});
CALayerControlMode.displayName = 'CALayerControlMode';
ControlAdapterControlModeSelect.displayName = 'ControlAdapterControlModeSelect';

View File

@ -1,39 +1,30 @@
import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { caLayerModelChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType';
import type { AnyModelConfig, ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
type Props = {
layerId: string;
modelKey: string | null;
onChange: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void;
};
export const CALayerModelCombobox = memo(({ layerId }: Props) => {
export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const caModelKey = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.model?.key);
const currentBaseModel = useAppSelector((s) => s.generation.model?.base);
const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels();
const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === caModelKey), [modelConfigs, caModelKey]);
const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]);
const _onChange = useCallback(
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null) => {
if (!modelConfig) {
return;
}
dispatch(
caLayerModelChanged({
layerId,
modelConfig,
})
);
onChangeModel(modelConfig);
},
[dispatch, layerId]
[onChangeModel]
);
const getIsDisabled = useCallback(
@ -68,4 +59,4 @@ export const CALayerModelCombobox = memo(({ layerId }: Props) => {
);
});
CALayerModelCombobox.displayName = 'CALayerModelCombobox';
ControlAdapterModelCombobox.displayName = 'ControlAdapterModelCombobox';

View File

@ -1,21 +1,19 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { caLayerWeightChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
layerId: string;
weight: number;
onChange: (weight: number) => void;
};
const formatValue = (v: number) => v.toFixed(2);
const marks = [0, 1, 2];
export const CALayerWeight = memo(({ layerId }: Props) => {
export const ControlAdapterWeight = memo(({ weight, onChange }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const weight = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.weight);
const initial = useAppSelector((s) => s.config.sd.ca.weight.initial);
const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin);
const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax);
@ -24,13 +22,6 @@ export const CALayerWeight = memo(({ layerId }: Props) => {
const coarseStep = useAppSelector((s) => s.config.sd.ca.weight.coarseStep);
const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep);
const onChange = useCallback(
(weight: number) => {
dispatch(caLayerWeightChanged({ layerId, weight }));
},
[dispatch, layerId]
);
return (
<FormControl orientation="horizontal">
<InformationalPopover feature="controlNetWeight">
@ -61,4 +52,4 @@ export const CALayerWeight = memo(({ layerId }: Props) => {
);
});
CALayerWeight.displayName = 'CALayerWeight';
ControlAdapterWeight.displayName = 'ControlAdapterWeight';

View File

@ -1,6 +1,6 @@
import { Button } from '@invoke-ai/ui-library';
import { allLayersDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
@ -8,12 +8,19 @@ import { PiTrashSimpleBold } from 'react-icons/pi';
export const DeleteAllLayersButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isDisabled = useAppSelector((s) => s.controlLayers.present.layers.length === 0);
const onClick = useCallback(() => {
dispatch(allLayersDeleted());
}, [dispatch]);
return (
<Button onClick={onClick} leftIcon={<PiTrashSimpleBold />} variant="ghost" colorScheme="error">
<Button
onClick={onClick}
leftIcon={<PiTrashSimpleBold />}
variant="ghost"
colorScheme="error"
isDisabled={isDisabled}
>
{t('controlLayers.deleteAll')}
</Button>
);

View File

@ -1,75 +0,0 @@
import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import ControlAdapterProcessorComponent from 'features/controlAdapters/components/ControlAdapterProcessorComponent';
import ControlAdapterShouldAutoConfig from 'features/controlAdapters/components/ControlAdapterShouldAutoConfig';
import ParamControlAdapterIPMethod from 'features/controlAdapters/components/parameters/ParamControlAdapterIPMethod';
import ParamControlAdapterProcessorSelect from 'features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect';
import { ParamControlAdapterBeginEnd } from 'features/controlLayers/components/CALayer/CALayerBeginEndStepPct';
import ParamControlAdapterControlMode from 'features/controlLayers/components/CALayer/CALayerControlMode';
import { CALayerImagePreview } from 'features/controlLayers/components/CALayer/CALayerImagePreview';
import ParamControlAdapterModel from 'features/controlLayers/components/CALayer/CALayerModelCombobox';
import ParamControlAdapterWeight from 'features/controlLayers/components/CALayer/CALayerWeight';
import { selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretUpBold } from 'react-icons/pi';
import { useToggle } from 'react-use';
type Props = {
layerId: string;
};
export const CALayerCAConfig = memo(({ layerId }: Props) => {
const caType = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.type);
const { t } = useTranslation();
const [isExpanded, toggleIsExpanded] = useToggle(false);
return (
<Flex flexDir="column" gap={4} position="relative" w="full">
<Flex gap={3} alignItems="center" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<ParamControlAdapterModel id={id} />{' '}
</Box>
{controlAdapterType !== 'ip_adapter' && (
<IconButton
size="sm"
tooltip={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
aria-label={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
onClick={toggleIsExpanded}
variant="ghost"
icon={
<Icon
boxSize={4}
as={PiCaretUpBold}
transform={isExpanded ? 'rotate(0deg)' : 'rotate(180deg)'}
transitionProperty="common"
transitionDuration="normal"
/>
}
/>
)}
</Flex>
<Flex gap={4} w="full" alignItems="center">
<Flex flexDir="column" gap={3} w="full">
{controlAdapterType === 'ip_adapter' && <ParamControlAdapterIPMethod id={id} />}
{controlAdapterType === 'controlnet' && <ParamControlAdapterControlMode id={id} />}
<ParamControlAdapterWeight id={id} />
<ParamControlAdapterBeginEnd id={id} />
</Flex>
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
<CALayerImagePreview id={id} isSmall />
</Flex>
</Flex>
{isExpanded && (
<>
<ControlAdapterShouldAutoConfig id={id} />
<ParamControlAdapterProcessorSelect id={id} />
<ControlAdapterProcessorComponent id={id} />
</>
)}
</Flex>
);
});
CALayerCAConfig.displayName = 'CALayerCAConfig';

View File

@ -1,29 +1,15 @@
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig';
import { IPALayerConfig } from 'features/controlLayers/components/IPALayer/IPALayerConfig';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { isIPAdapterLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useMemo } from 'react';
import { assert } from 'tsafe';
import { memo } from 'react';
type Props = {
layerId: string;
};
export const IPALayer = memo(({ layerId }: Props) => {
const selector = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
assert(isIPAdapterLayer(layer), `Layer ${layerId} not found or not an IP Adapter layer`);
return layer.ipAdapterId;
}),
[layerId]
);
const ipAdapterId = useAppSelector(selector);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
return (
<Flex gap={2} bg="base.800" borderRadius="base" p="1px" px={2}>
@ -36,7 +22,7 @@ export const IPALayer = memo(({ layerId }: Props) => {
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<ControlAdapterLayerConfig id={ipAdapterId} />
<IPALayerConfig layerId={layerId} />
</Flex>
)}
</Flex>

View File

@ -0,0 +1,105 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ControlAdapterBeginEndStepPct } from 'features/controlLayers/components/CALayer/ControlAdapterBeginEndStepPct';
import { ControlAdapterWeight } from 'features/controlLayers/components/CALayer/ControlAdapterWeight';
import { IPAdapterImagePreview } from 'features/controlLayers/components/IPALayer/IPAdapterImagePreview';
import { IPAdapterMethod } from 'features/controlLayers/components/IPALayer/IPAdapterMethod';
import { IPAdapterModelCombobox } from 'features/controlLayers/components/IPALayer/IPALayerModelCombobox';
import {
caOrIPALayerBeginEndStepPctChanged,
caOrIPALayerWeightChanged,
ipaLayerCLIPVisionModelChanged,
ipaLayerImageChanged,
ipaLayerMethodChanged,
ipaLayerModelChanged,
selectIPALayer,
} from 'features/controlLayers/store/controlLayersSlice';
import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
type Props = {
layerId: string;
};
export const IPALayerConfig = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const ipAdapter = useAppSelector((s) => selectIPALayer(s.controlLayers.present, layerId).ipAdapter);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
dispatch(
caOrIPALayerBeginEndStepPctChanged({
layerId,
beginEndStepPct,
})
);
},
[dispatch, layerId]
);
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(caOrIPALayerWeightChanged({ layerId, weight }));
},
[dispatch, layerId]
);
const onChangeIPMethod = useCallback(
(method: IPMethod) => {
dispatch(ipaLayerMethodChanged({ layerId, method }));
},
[dispatch, layerId]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig) => {
dispatch(ipaLayerModelChanged({ layerId, modelConfig }));
},
[dispatch, layerId]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModel) => {
dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel }));
},
[dispatch, layerId]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(ipaLayerImageChanged({ layerId, imageDTO }));
},
[dispatch, layerId]
);
return (
<Flex flexDir="column" gap={4} position="relative" w="full">
<Flex gap={3} alignItems="center" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<IPAdapterModelCombobox
modelKey={ipAdapter.model?.key ?? null}
onChangeModel={onChangeModel}
clipVisionModel={ipAdapter.clipVisionModel}
onChangeCLIPVisionModel={onChangeCLIPVisionModel}
/>
</Box>
</Flex>
<Flex gap={4} w="full" alignItems="center">
<Flex flexDir="column" gap={3} w="full">
<IPAdapterMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
<ControlAdapterWeight weight={ipAdapter.weight} onChange={onChangeWeight} />
<ControlAdapterBeginEndStepPct
beginEndStepPct={ipAdapter.beginEndStepPct}
onChange={onChangeBeginEndStepPct}
/>
</Flex>
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
<IPAdapterImagePreview image={ipAdapter.image} onChangeImage={onChangeImage} layerId={layerId} />
</Flex>
</Flex>
</Flex>
);
});
IPALayerConfig.displayName = 'IPALayerConfig';

View File

@ -0,0 +1,100 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import type { CLIPVisionModel } from 'features/controlLayers/util/controlAdapters';
import { isCLIPVisionModel } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useIPAdapterModels } from 'services/api/hooks/modelsByType';
import type { AnyModelConfig, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
const CLIP_VISION_OPTIONS = [
{ label: 'ViT-H', value: 'ViT-H' },
{ label: 'ViT-G', value: 'ViT-G' },
];
type Props = {
modelKey: string | null;
onChangeModel: (modelConfig: IPAdapterModelConfig) => void;
clipVisionModel: CLIPVisionModel;
onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void;
};
export const IPAdapterModelCombobox = memo(
({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => {
const { t } = useTranslation();
const currentBaseModel = useAppSelector((s) => s.generation.model?.base);
const [modelConfigs, { isLoading }] = useIPAdapterModels();
const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]);
const _onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig | null) => {
if (!modelConfig) {
return;
}
onChangeModel(modelConfig);
},
[onChangeModel]
);
const _onChangeCLIPVisionModel = useCallback<ComboboxOnChange>(
(v) => {
assert(isCLIPVisionModel(v?.value));
onChangeCLIPVisionModel(v.value);
},
[onChangeCLIPVisionModel]
);
const getIsDisabled = useCallback(
(model: AnyModelConfig): boolean => {
const isCompatible = currentBaseModel === model.base;
const hasMainModel = Boolean(currentBaseModel);
return !hasMainModel || !isCompatible;
},
[currentBaseModel]
);
const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({
modelConfigs,
onChange: _onChangeModel,
selectedModel,
getIsDisabled,
isLoading,
});
const clipVisionModelValue = useMemo(
() => CLIP_VISION_OPTIONS.find((o) => o.value === clipVisionModel),
[clipVisionModel]
);
return (
<Flex gap={4}>
<Tooltip label={selectedModel?.description}>
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} w="full">
<Combobox
options={options}
placeholder={t('controlnet.selectModel')}
value={value}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</FormControl>
</Tooltip>
{selectedModel?.format === 'checkpoint' && (
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} width="max-content" minWidth={28}>
<Combobox
options={CLIP_VISION_OPTIONS}
placeholder={t('controlnet.selectCLIPVisionModel')}
value={clipVisionModelValue}
onChange={_onChangeCLIPVisionModel}
/>
</FormControl>
)}
</Flex>
);
}
);
IPAdapterModelCombobox.displayName = 'IPALayerModelCombobox';

View File

@ -0,0 +1,119 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters';
import type { ControlLayerDropData, ImageDraggableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { ControlLayerAction, ImageDTO } from 'services/api/types';
type Props = {
image: ImageWithDims | null;
onChangeImage: (imageDTO: ImageDTO | null) => void;
layerId: string; // required for the dnd/upload interactions
};
export const IPAdapterImagePreview = memo(({ image, onChangeImage, layerId }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isConnected = useAppSelector((s) => s.system.isConnected);
const activeTabName = useAppSelector(activeTabNameSelector);
const optimalDimension = useAppSelector(selectOptimalDimension);
const shift = useShiftModifier();
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
image?.imageName ?? skipToken
);
const handleResetControlImage = useCallback(() => {
onChangeImage(null);
}, [onChangeImage]);
const handleSetControlImageToDimensions = useCallback(() => {
if (!controlImage) {
return;
}
if (activeTabName === 'unifiedCanvas') {
dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension));
} else {
if (shift) {
const { width, height } = controlImage;
dispatch(widthChanged({ width, updateAspectRatio: true }));
dispatch(heightChanged({ height, updateAspectRatio: true }));
} else {
const { width, height } = calculateNewSize(
controlImage.width / controlImage.height,
optimalDimension * optimalDimension
);
dispatch(widthChanged({ width, updateAspectRatio: true }));
dispatch(heightChanged({ height, updateAspectRatio: true }));
}
}
}, [controlImage, activeTabName, dispatch, optimalDimension, shift]);
const draggableData = useMemo<ImageDraggableData | undefined>(() => {
if (controlImage) {
return {
id: layerId,
payloadType: 'IMAGE_DTO',
payload: { imageDTO: controlImage },
};
}
}, [controlImage, layerId]);
const droppableData = useMemo<ControlLayerDropData>(
() => ({
id: layerId,
actionType: 'SET_CONTROL_LAYER_IMAGE',
context: { layerId },
}),
[layerId]
);
const postUploadAction = useMemo<ControlLayerAction>(() => ({ type: 'SET_CONTROL_LAYER_IMAGE', layerId }), [layerId]);
useEffect(() => {
if (isConnected && isErrorControlImage) {
handleResetControlImage();
}
}, [handleResetControlImage, isConnected, isErrorControlImage]);
return (
<Flex position="relative" w="full" h={36} alignItems="center" justifyContent="center">
<IAIDndImage
draggableData={draggableData}
droppableData={droppableData}
imageDTO={controlImage}
postUploadAction={postUploadAction}
/>
<>
<IAIDndImageIcon
onClick={handleResetControlImage}
icon={controlImage ? <PiArrowCounterClockwiseBold size={16} /> : undefined}
tooltip={t('controlnet.resetControlImage')}
/>
<IAIDndImageIcon
onClick={handleSetControlImageToDimensions}
icon={controlImage ? <PiRulerBold size={16} /> : undefined}
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
styleOverrides={setControlImageDimensionsStyleOverrides}
/>
</>
</Flex>
);
});
IPAdapterImagePreview.displayName = 'IPAdapterImagePreview';
const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 6 };

View File

@ -0,0 +1,44 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { IPMethod } from 'features/controlLayers/util/controlAdapters';
import { isIPMethod } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
type Props = {
method: IPMethod;
onChange: (method: IPMethod) => void;
};
export const IPAdapterMethod = memo(({ method, onChange }: Props) => {
const { t } = useTranslation();
const options: { label: string; value: IPMethod }[] = useMemo(
() => [
{ label: t('controlnet.full'), value: 'full' },
{ label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' },
{ label: `${t('controlnet.composition')} (${t('common.beta')})`, value: 'composition' },
],
[t]
);
const _onChange = useCallback<ComboboxOnChange>(
(v) => {
assert(isIPMethod(v?.value));
onChange(v.value);
},
[onChange]
);
const value = useMemo(() => options.find((o) => o.value === method), [options, method]);
return (
<FormControl>
<InformationalPopover feature="ipAdapterMethod">
<FormLabel>{t('controlnet.ipAdapterMethod')}</FormLabel>
</InformationalPopover>
<Combobox value={value} options={options} onChange={_onChange} />
</FormControl>
);
});
IPAdapterMethod.displayName = 'IPAdapterMethod';

View File

@ -1,136 +0,0 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { useControlAdapterCLIPVisionModel } from 'features/controlAdapters/hooks/useControlAdapterCLIPVisionModel';
import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled';
import { useControlAdapterModel } from 'features/controlAdapters/hooks/useControlAdapterModel';
import { useControlAdapterModels } from 'features/controlAdapters/hooks/useControlAdapterModels';
import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType';
import {
controlAdapterCLIPVisionModelChanged,
controlAdapterModelChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import type { CLIPVisionModel } from 'features/controlAdapters/store/types';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type {
AnyModelConfig,
ControlNetModelConfig,
IPAdapterModelConfig,
T2IAdapterModelConfig,
} from 'services/api/types';
type ParamControlAdapterModelProps = {
id: string;
};
const selectMainModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model);
const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => {
const isEnabled = useControlAdapterIsEnabled(id);
const controlAdapterType = useControlAdapterType(id);
const { modelConfig } = useControlAdapterModel(id);
const dispatch = useAppDispatch();
const currentBaseModel = useAppSelector((s) => s.generation.model?.base);
const currentCLIPVisionModel = useControlAdapterCLIPVisionModel(id);
const mainModel = useAppSelector(selectMainModel);
const { t } = useTranslation();
const [modelConfigs, { isLoading }] = useControlAdapterModels(controlAdapterType);
const _onChange = useCallback(
(modelConfig: ControlNetModelConfig | IPAdapterModelConfig | T2IAdapterModelConfig | null) => {
if (!modelConfig) {
return;
}
dispatch(
controlAdapterModelChanged({
id,
modelConfig,
})
);
},
[dispatch, id]
);
const onCLIPVisionModelChange = useCallback<ComboboxOnChange>(
(v) => {
if (!v?.value) {
return;
}
dispatch(controlAdapterCLIPVisionModelChanged({ id, clipVisionModel: v.value as CLIPVisionModel }));
},
[dispatch, id]
);
const selectedModel = useMemo(
() => (modelConfig && controlAdapterType ? { ...modelConfig, model_type: controlAdapterType } : null),
[controlAdapterType, modelConfig]
);
const getIsDisabled = useCallback(
(model: AnyModelConfig): boolean => {
const isCompatible = currentBaseModel === model.base;
const hasMainModel = Boolean(currentBaseModel);
return !hasMainModel || !isCompatible;
},
[currentBaseModel]
);
const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({
modelConfigs,
onChange: _onChange,
selectedModel,
getIsDisabled,
isLoading,
});
const clipVisionOptions = useMemo<ComboboxOption[]>(
() => [
{ label: 'ViT-H', value: 'ViT-H' },
{ label: 'ViT-G', value: 'ViT-G' },
],
[]
);
const clipVisionModel = useMemo(
() => clipVisionOptions.find((o) => o.value === currentCLIPVisionModel),
[clipVisionOptions, currentCLIPVisionModel]
);
return (
<Flex gap={4}>
<Tooltip label={selectedModel?.description}>
<FormControl isDisabled={!isEnabled} isInvalid={!value || mainModel?.base !== modelConfig?.base} w="full">
<Combobox
options={options}
placeholder={t('controlnet.selectModel')}
value={value}
onChange={onChange}
noOptionsMessage={noOptionsMessage}
/>
</FormControl>
</Tooltip>
{modelConfig?.type === 'ip_adapter' && modelConfig.format === 'checkpoint' && (
<FormControl
isDisabled={!isEnabled}
isInvalid={!value || mainModel?.base !== modelConfig?.base}
width="max-content"
minWidth={28}
>
<Combobox
options={clipVisionOptions}
placeholder={t('controlnet.selectCLIPVisionModel')}
value={clipVisionModel}
onChange={onCLIPVisionModelChange}
/>
</FormControl>
)}
</Flex>
);
};
export default memo(ParamControlAdapterModel);

View File

@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { guidanceLayerDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
@ -12,7 +12,7 @@ export const LayerDeleteButton = memo(({ layerId }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const deleteLayer = useCallback(() => {
dispatch(guidanceLayerDeleted(layerId));
dispatch(layerDeleted(layerId));
}, [dispatch, layerId]);
return (
<IconButton

View File

@ -1,7 +1,7 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import {
isRegionalGuidanceLayer,
rgLayerNegativePromptChanged,
@ -18,6 +18,7 @@ type Props = { layerId: string };
export const LayerMenuRGActions = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId);
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
@ -37,9 +38,6 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => {
const addNegativePrompt = useCallback(() => {
dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' }));
}, [dispatch, layerId]);
const addIPAdapter = useCallback(() => {
dispatch(guidanceLayerIPAdapterAdded(layerId));
}, [dispatch, layerId]);
return (
<>
<MenuItem onClick={addPositivePrompt} isDisabled={!validActions.canAddPositivePrompt} icon={<PiPlusBold />}>
@ -48,7 +46,7 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => {
<MenuItem onClick={addNegativePrompt} isDisabled={!validActions.canAddNegativePrompt} icon={<PiPlusBold />}>
{t('controlLayers.addNegativePrompt')}
</MenuItem>
<MenuItem onClick={addIPAdapter} icon={<PiPlusBold />}>
<MenuItem onClick={addIPAdapter} icon={<PiPlusBold />} isDisabled={isAddIPAdapterDisabled}>
{t('controlLayers.addIPAdapter')}
</MenuItem>
</>

View File

@ -38,7 +38,7 @@ export const RGLayer = memo(({ layerId }: Props) => {
color: rgbColorToString(layer.previewColor),
hasPositivePrompt: layer.positivePrompt !== null,
hasNegativePrompt: layer.negativePrompt !== null,
hasIPAdapters: layer.ipAdapterIds.length > 0,
hasIPAdapters: layer.ipAdapters.length > 0,
isSelected: layerId === controlLayers.present.selectedLayerId,
autoNegative: layer.autoNegative,
};

View File

@ -1,9 +1,11 @@
import { Divider, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { guidanceLayerIPAdapterDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig';
import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import {
isRegionalGuidanceLayer,
rgLayerIPAdapterDeleted,
selectControlLayersSlice,
} from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback, useMemo } from 'react';
import { PiTrashSimpleBold } from 'react-icons/pi';
import { assert } from 'tsafe';
@ -18,19 +20,19 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => {
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
return layer.ipAdapterIds;
return layer.ipAdapters;
}),
[layerId]
);
const ipAdapterIds = useAppSelector(selectIPAdapterIds);
const ipAdapters = useAppSelector(selectIPAdapterIds);
if (ipAdapterIds.length === 0) {
if (ipAdapters.length === 0) {
return null;
}
return (
<>
{ipAdapterIds.map((id, index) => (
{ipAdapters.map(({ id }, index) => (
<Flex flexDir="column" key={id}>
{index > 0 && (
<Flex pb={3}>
@ -55,7 +57,7 @@ type IPAdapterListItemProps = {
const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber }: IPAdapterListItemProps) => {
const dispatch = useAppDispatch();
const onDeleteIPAdapter = useCallback(() => {
dispatch(guidanceLayerIPAdapterDeleted({ layerId, ipAdapterId }));
dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId }));
}, [dispatch, ipAdapterId, layerId]);
return (
@ -72,7 +74,7 @@ const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber }
colorScheme="error"
/>
</Flex>
<ControlAdapterLayerConfig id={ipAdapterId} />
{/* <ControlAdapterLayerConfig id={ipAdapterId} /> */}
</Flex>
);
});

View File

@ -0,0 +1,95 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { caLayerAdded, ipaLayerAdded, rgLayerIPAdapterAdded } from 'features/controlLayers/store/controlLayersSlice';
import {
buildControlNet,
buildIPAdapter,
buildT2IAdapter,
CONTROLNET_PROCESSORS,
isProcessorType,
} from 'features/controlLayers/util/controlAdapters';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { useCallback, useMemo } from 'react';
import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType';
import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { v4 as uuidv4 } from 'uuid';
export const useAddCALayer = () => {
const dispatch = useAppDispatch();
const baseModel = useAppSelector((s) => s.generation.model?.base);
const [modelConfigs] = useControlNetAndT2IAdapterModels();
const model: ControlNetModelConfig | T2IAdapterModelConfig | null = useMemo(() => {
// prefer to use a model that matches the base model
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));
return compatibleModels[0] ?? modelConfigs[0] ?? null;
}, [baseModel, modelConfigs]);
const isDisabled = useMemo(() => !model, [model]);
const addCALayer = useCallback(() => {
if (!model) {
return;
}
const id = uuidv4();
const defaultPreprocessor = model.default_settings?.preprocessor;
const processorConfig = isProcessorType(defaultPreprocessor)
? CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(baseModel)
: null;
const builder = model.type === 'controlnet' ? buildControlNet : buildT2IAdapter;
const controlAdapter = builder(id, {
model: zModelIdentifierField.parse(model),
processorConfig,
});
dispatch(caLayerAdded(controlAdapter));
}, [dispatch, model, baseModel]);
return [addCALayer, isDisabled] as const;
};
export const useAddIPALayer = () => {
const dispatch = useAppDispatch();
const baseModel = useAppSelector((s) => s.generation.model?.base);
const [modelConfigs] = useIPAdapterModels();
const model: IPAdapterModelConfig | null = useMemo(() => {
// prefer to use a model that matches the base model
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));
return compatibleModels[0] ?? modelConfigs[0] ?? null;
}, [baseModel, modelConfigs]);
const isDisabled = useMemo(() => !model, [model]);
const addIPALayer = useCallback(() => {
if (!model) {
return;
}
const id = uuidv4();
const ipAdapter = buildIPAdapter(id, {
model: zModelIdentifierField.parse(model),
});
dispatch(ipaLayerAdded(ipAdapter));
}, [dispatch, model]);
return [addIPALayer, isDisabled] as const;
};
export const useAddIPAdapterToIPALayer = (layerId: string) => {
const dispatch = useAppDispatch();
const baseModel = useAppSelector((s) => s.generation.model?.base);
const [modelConfigs] = useIPAdapterModels();
const model: IPAdapterModelConfig | null = useMemo(() => {
// prefer to use a model that matches the base model
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));
return compatibleModels[0] ?? modelConfigs[0] ?? null;
}, [baseModel, modelConfigs]);
const isDisabled = useMemo(() => !model, [model]);
const addIPAdapter = useCallback(() => {
if (!model) {
return;
}
const id = uuidv4();
const ipAdapter = buildIPAdapter(id, {
model: zModelIdentifierField.parse(model),
});
dispatch(rgLayerIPAdapterAdded({ layerId, ipAdapter }));
}, [dispatch, model, layerId]);
return [addIPAdapter, isDisabled] as const;
};

View File

@ -9,7 +9,7 @@ import {
$lastMouseDownPos,
$tool,
brushSizeChanged,
rfLayerLineAdded,
rgLayerLineAdded,
rgLayerPointsAdded,
rgLayerRectAdded,
} from 'features/controlLayers/store/controlLayersSlice';
@ -71,7 +71,7 @@ export const useMouseEvents = () => {
}
if (tool === 'brush' || tool === 'eraser') {
dispatch(
rfLayerLineAdded({
rgLayerLineAdded({
layerId: selectedLayerId,
points: [pos.x, pos.y, pos.x, pos.y],
tool,
@ -181,7 +181,7 @@ export const useMouseEvents = () => {
}
if (tool === 'brush' || tool === 'eraser') {
dispatch(
rfLayerLineAdded({
rgLayerLineAdded({
layerId: selectedLayerId,
points: [pos.x, pos.y, pos.x, pos.y],
tool,

View File

@ -13,7 +13,7 @@ const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlL
.filter((l) => l.isEnabled)
.filter((l) => {
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
const hasAtLeastOneImagePrompt = l.ipAdapterIds.length > 0;
const hasAtLeastOneImagePrompt = l.ipAdapters.length > 0;
return hasTextPrompt || hasAtLeastOneImagePrompt;
});

View File

@ -25,7 +25,7 @@ import { isEqual, partition } from 'lodash-es';
import { atom } from 'nanostores';
import type { RgbColor } from 'react-colorful';
import type { UndoableOptions } from 'redux-undo';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
@ -88,11 +88,19 @@ export const selectCALayer = (state: ControlLayersState, layerId: string): Contr
assert(isControlAdapterLayer(layer));
return layer;
};
const selectIPALayer = (state: ControlLayersState, layerId: string): IPAdapterLayer => {
export const selectIPALayer = (state: ControlLayersState, layerId: string): IPAdapterLayer => {
const layer = state.layers.find((l) => l.id === layerId);
assert(isIPAdapterLayer(layer));
return layer;
};
export const selectCAOrIPALayer = (
state: ControlLayersState,
layerId: string
): ControlAdapterLayer | IPAdapterLayer => {
const layer = state.layers.find((l) => l.id === layerId);
assert(isControlAdapterLayer(layer) || isIPAdapterLayer(layer));
return layer;
};
const selectRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => {
const layer = state.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer));
@ -199,6 +207,10 @@ export const controlLayersSlice = createSlice({
state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId);
state.selectedLayerId = state.layers[0]?.id ?? null;
},
allLayersDeleted: (state) => {
state.layers = [];
state.selectedLayerId = null;
},
//#endregion
//#region CA Layers
@ -272,19 +284,6 @@ export const controlLayersSlice = createSlice({
layer.controlAdapter.processorConfig = candidateProcessorConfig;
}
},
caLayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => {
const { layerId, weight } = action.payload;
const layer = selectCALayer(state, layerId);
layer.controlAdapter.weight = weight;
},
caLayerBeginEndStepPctChanged: (
state,
action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }>
) => {
const { layerId, beginEndStepPct } = action.payload;
const layer = selectCALayer(state, layerId);
layer.controlAdapter.beginEndStepPct = beginEndStepPct;
},
caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlMode }>) => {
const { layerId, controlMode } = action.payload;
const layer = selectCALayer(state, layerId);
@ -348,6 +347,21 @@ export const controlLayersSlice = createSlice({
const layer = selectIPALayer(state, layerId);
layer.ipAdapter.method = method;
},
ipaLayerModelChanged: (
state,
action: PayloadAction<{
layerId: string;
modelConfig: IPAdapterModelConfig | null;
}>
) => {
const { layerId, modelConfig } = action.payload;
const layer = selectIPALayer(state, layerId);
if (!modelConfig) {
layer.ipAdapter.model = null;
return;
}
layer.ipAdapter.model = zModelIdentifierField.parse(modelConfig);
},
ipaLayerCLIPVisionModelChanged: (
state,
action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }>
@ -358,34 +372,61 @@ export const controlLayersSlice = createSlice({
},
//#endregion
//#region RG Layers
rgLayerAdded: (state, action: PayloadAction<{ layerId: string }>) => {
const { layerId } = action.payload;
const layer: RegionalGuidanceLayer = {
id: getRGLayerId(layerId),
type: 'regional_guidance_layer',
isEnabled: true,
bbox: null,
bboxNeedsUpdate: false,
maskObjects: [],
previewColor: getVectorMaskPreviewColor(state),
x: 0,
y: 0,
autoNegative: 'invert',
needsPixelBbox: false,
positivePrompt: '',
negativePrompt: null,
ipAdapters: [],
isSelected: true,
};
state.layers.push(layer);
state.selectedLayerId = layer.id;
for (const layer of state.layers.filter(isRenderableLayer)) {
if (layer.id !== layerId) {
layer.isSelected = false;
}
//#region CA or IPA Layers
caOrIPALayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => {
const { layerId, weight } = action.payload;
const layer = selectCAOrIPALayer(state, layerId);
if (layer.type === 'control_adapter_layer') {
layer.controlAdapter.weight = weight;
} else {
layer.ipAdapter.weight = weight;
}
},
caOrIPALayerBeginEndStepPctChanged: (
state,
action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }>
) => {
const { layerId, beginEndStepPct } = action.payload;
const layer = selectCAOrIPALayer(state, layerId);
if (layer.type === 'control_adapter_layer') {
layer.controlAdapter.beginEndStepPct = beginEndStepPct;
} else {
layer.ipAdapter.beginEndStepPct = beginEndStepPct;
}
},
//#endregion
//#region RG Layers
rgLayerAdded: {
reducer: (state, action: PayloadAction<{ layerId: string }>) => {
const { layerId } = action.payload;
const layer: RegionalGuidanceLayer = {
id: getRGLayerId(layerId),
type: 'regional_guidance_layer',
isEnabled: true,
bbox: null,
bboxNeedsUpdate: false,
maskObjects: [],
previewColor: getVectorMaskPreviewColor(state),
x: 0,
y: 0,
autoNegative: 'invert',
needsPixelBbox: false,
positivePrompt: '',
negativePrompt: null,
ipAdapters: [],
isSelected: true,
};
state.layers.push(layer);
state.selectedLayerId = layer.id;
for (const layer of state.layers.filter(isRenderableLayer)) {
if (layer.id !== layerId) {
layer.isSelected = false;
}
}
},
prepare: () => ({ payload: { layerId: uuidv4() } }),
},
rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
const { layerId, prompt } = action.payload;
const layer = selectRGLayer(state, layerId);
@ -396,16 +437,6 @@ export const controlLayersSlice = createSlice({
const layer = selectRGLayer(state, layerId);
layer.negativePrompt = prompt;
},
rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => {
const { layerId, ipAdapter } = action.payload;
const layer = selectRGLayer(state, layerId);
layer.ipAdapters.push(ipAdapter);
},
rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => {
const { layerId, ipAdapterId } = action.payload;
const layer = selectRGLayer(state, layerId);
layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId);
},
rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => {
const { layerId, color } = action.payload;
const layer = selectRGLayer(state, layerId);
@ -483,6 +514,16 @@ export const controlLayersSlice = createSlice({
const layer = selectRGLayer(state, layerId);
layer.autoNegative = autoNegative;
},
rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => {
const { layerId, ipAdapter } = action.payload;
const layer = selectRGLayer(state, layerId);
layer.ipAdapters.push(ipAdapter);
},
rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => {
const { layerId, ipAdapterId } = action.payload;
const layer = selectRGLayer(state, layerId);
layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId);
},
rgLayerIPAdapterImageChanged: (
state,
action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }>
@ -657,13 +698,12 @@ export const {
layerMovedToBack,
selectedLayerReset,
selectedLayerDeleted,
allLayersDeleted,
// CA Layers
caLayerAdded,
caLayerImageChanged,
caLayerProcessedImageChanged,
caLayerModelChanged,
caLayerWeightChanged,
caLayerBeginEndStepPctChanged,
caLayerControlModeChanged,
caLayerProcessorConfigChanged,
caLayerIsFilterEnabledChanged,
@ -674,18 +714,22 @@ export const {
ipaLayerWeightChanged,
ipaLayerBeginEndStepPctChanged,
ipaLayerMethodChanged,
ipaLayerModelChanged,
ipaLayerCLIPVisionModelChanged,
// CA or IPA Layers
caOrIPALayerWeightChanged,
caOrIPALayerBeginEndStepPctChanged,
// RG Layers
rgLayerAdded,
rgLayerPositivePromptChanged,
rgLayerNegativePromptChanged,
rgLayerIPAdapterAdded,
rgLayerIPAdapterDeleted,
rgLayerPreviewColorChanged,
rgLayerLineAdded,
rgLayerPointsAdded,
rgLayerRectAdded,
rgLayerAutoNegativeChanged,
rgLayerIPAdapterAdded,
rgLayerIPAdapterDeleted,
rgLayerIPAdapterImageChanged,
rgLayerIPAdapterWeightChanged,
rgLayerIPAdapterBeginEndStepPctChanged,

View File

@ -72,7 +72,7 @@ export type ProcessorConfig =
| PidiProcessorConfig
| ZoeDepthProcessorConfig;
type ImageWithDims = {
export type ImageWithDims = {
imageName: string;
width: number;
height: number;
@ -273,7 +273,7 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
type: 'zoe_depth_image_processor',
}),
},
}
};
export const zProcessorType = z.enum([
'canny_image_processor',
'color_map_image_processor',
@ -328,15 +328,15 @@ export const initialIPAdapter: Omit<IPAdapterConfig, 'id'> = {
};
export const buildControlNet = (id: string, overrides?: Partial<ControlNetConfig>): ControlNetConfig => {
return merge(deepClone(initialControlNet), { id, overrides });
return merge(deepClone(initialControlNet), { id, ...overrides });
};
export const buildT2IAdapter = (id: string, overrides?: Partial<T2IAdapterConfig>): T2IAdapterConfig => {
return merge(deepClone(initialT2IAdapter), { id, overrides });
return merge(deepClone(initialT2IAdapter), { id, ...overrides });
};
export const buildIPAdapter = (id: string, overrides?: Partial<IPAdapterConfig>): IPAdapterConfig => {
return merge(deepClone(initialIPAdapter), { id, overrides });
return merge(deepClone(initialIPAdapter), { id, ...overrides });
};
export const buildControlAdapterProcessor = (

View File

@ -52,8 +52,7 @@ const STAGE_BG_DATAURL =
const mapId = (object: { id: string }) => object.id;
const selectRenderableLayers = (n: Konva.Node) =>
n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME;
const selectRenderableLayers = (n: Konva.Node) => n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME;
const selectVectorMaskObjects = (node: Konva.Node) => {
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME;
@ -432,9 +431,9 @@ const updateControlNetLayerImageSource = async (
konvaLayer: Konva.Layer,
reduxLayer: ControlAdapterLayer
) => {
if (reduxLayer.imageName) {
const imageName = reduxLayer.imageName;
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(reduxLayer.imageName));
if (reduxLayer.controlAdapter.image) {
const { imageName } = reduxLayer.controlAdapter.image;
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
const imageDTO = await req.unwrap();
req.unsubscribe();
const image = new Image();
@ -442,8 +441,7 @@ const updateControlNetLayerImageSource = async (
image.onload = () => {
// Find the existing image or create a new one - must find using the name, bc the id may have just changed
const konvaImage =
konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`) ??
createControlNetLayerImage(konvaLayer, image);
konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`) ?? createControlNetLayerImage(konvaLayer, image);
// Update the image's attributes
konvaImage.setAttrs({
@ -502,11 +500,11 @@ const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay
let imageSourceNeedsUpdate = false;
if (canvasImageSource instanceof HTMLImageElement) {
if (
reduxLayer.imageName &&
canvasImageSource.id !== getCALayerImageId(reduxLayer.id, reduxLayer.imageName)
reduxLayer.controlAdapter.image &&
canvasImageSource.id !== getCALayerImageId(reduxLayer.id, reduxLayer.controlAdapter.image.imageName)
) {
imageSourceNeedsUpdate = true;
} else if (!reduxLayer.imageName) {
} else if (!reduxLayer.controlAdapter.image) {
imageSourceNeedsUpdate = true;
}
} else if (!canvasImageSource) {

View File

@ -13,66 +13,52 @@ import {
selectValidIPAdapters,
selectValidT2IAdapters,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { selectAllControlAdapterIds, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { Fragment, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
const selector = createMemoizedSelector(
[selectControlAdaptersSlice, selectControlLayersSlice],
(controlAdapters, controlLayers) => {
const badges: string[] = [];
let isError = false;
const selector = createMemoizedSelector([selectControlAdaptersSlice], (controlAdapters) => {
const badges: string[] = [];
let isError = false;
const controlLayersAdapterIds = selectAllControlAdapterIds(controlLayers.present);
const enabledNonRegionalIPAdapterCount = selectAllIPAdapters(controlAdapters).filter((ca) => ca.isEnabled).length;
const enabledNonRegionalIPAdapterCount = selectAllIPAdapters(controlAdapters)
.filter((ca) => !controlLayersAdapterIds.includes(ca.id))
.filter((ca) => ca.isEnabled).length;
const validIPAdapterCount = selectValidIPAdapters(controlAdapters).length;
if (enabledNonRegionalIPAdapterCount > 0) {
badges.push(`${enabledNonRegionalIPAdapterCount} IP`);
}
if (enabledNonRegionalIPAdapterCount > validIPAdapterCount) {
isError = true;
}
const enabledControlNetCount = selectAllControlNets(controlAdapters)
.filter((ca) => !controlLayersAdapterIds.includes(ca.id))
.filter((ca) => ca.isEnabled).length;
const validControlNetCount = selectValidControlNets(controlAdapters).length;
if (enabledControlNetCount > 0) {
badges.push(`${enabledControlNetCount} ControlNet`);
}
if (enabledControlNetCount > validControlNetCount) {
isError = true;
}
const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters)
.filter((ca) => !controlLayersAdapterIds.includes(ca.id))
.filter((ca) => ca.isEnabled).length;
const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length;
if (enabledT2IAdapterCount > 0) {
badges.push(`${enabledT2IAdapterCount} T2I`);
}
if (enabledT2IAdapterCount > validT2IAdapterCount) {
isError = true;
}
const controlAdapterIds = selectControlAdapterIds(controlAdapters).filter(
(id) => !controlLayersAdapterIds.includes(id)
);
return {
controlAdapterIds,
badges,
isError, // TODO: Add some visual indicator that the control adapters are in an error state
};
const validIPAdapterCount = selectValidIPAdapters(controlAdapters).length;
if (enabledNonRegionalIPAdapterCount > 0) {
badges.push(`${enabledNonRegionalIPAdapterCount} IP`);
}
);
if (enabledNonRegionalIPAdapterCount > validIPAdapterCount) {
isError = true;
}
const enabledControlNetCount = selectAllControlNets(controlAdapters).filter((ca) => ca.isEnabled).length;
const validControlNetCount = selectValidControlNets(controlAdapters).length;
if (enabledControlNetCount > 0) {
badges.push(`${enabledControlNetCount} ControlNet`);
}
if (enabledControlNetCount > validControlNetCount) {
isError = true;
}
const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters).filter((ca) => ca.isEnabled).length;
const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length;
if (enabledT2IAdapterCount > 0) {
badges.push(`${enabledT2IAdapterCount} T2I`);
}
if (enabledT2IAdapterCount > validT2IAdapterCount) {
isError = true;
}
const controlAdapterIds = selectControlAdapterIds(controlAdapters);
return {
controlAdapterIds,
badges,
isError, // TODO: Add some visual indicator that the control adapters are in an error state
};
});
export const ControlSettingsAccordion: React.FC = memo(() => {
const { t } = useTranslation();