mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
refactor(ui): wire up CA logic across (wip)
This commit is contained in:
parent
424a27eeda
commit
0e55488ff6
@ -16,7 +16,6 @@ import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listen
|
|||||||
import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet';
|
import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet';
|
||||||
import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged';
|
import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged';
|
||||||
import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery';
|
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 { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess';
|
||||||
import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed';
|
import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed';
|
||||||
import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas';
|
import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas';
|
||||||
@ -158,5 +157,3 @@ addUpscaleRequestedListener(startAppListening);
|
|||||||
addDynamicPromptsListener(startAppListening);
|
addDynamicPromptsListener(startAppListening);
|
||||||
|
|
||||||
addSetDefaultSettingsListener(startAppListening);
|
addSetDefaultSettingsListener(startAppListening);
|
||||||
|
|
||||||
addControlLayersToControlAdapterBridge(startAppListening);
|
|
||||||
|
@ -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 }));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
@ -101,33 +101,35 @@ const selector = createMemoizedSelector(
|
|||||||
|
|
||||||
if (activeTabName === 'txt2img') {
|
if (activeTabName === 'txt2img') {
|
||||||
// Special handling for control layers on txt2img
|
// Special handling for control layers on txt2img
|
||||||
const enabledControlLayersAdapterIds = controlLayers.present.layers
|
const enabledControlLayersAdapterIds = []
|
||||||
.filter((l) => l.isEnabled)
|
// const enabledControlLayersAdapterIds = controlLayers.present.layers
|
||||||
.flatMap((layer) => {
|
// .filter((l) => l.isEnabled)
|
||||||
if (layer.type === 'regional_guidance_layer') {
|
// .flatMap((layer) => {
|
||||||
return layer.ipAdapterIds;
|
// if (layer.type === 'regional_guidance_layer') {
|
||||||
}
|
// return layer.ipAdapterIds;
|
||||||
if (layer.type === 'control_adapter_layer') {
|
// }
|
||||||
return [layer.controlNetId];
|
// if (layer.type === 'control_adapter_layer') {
|
||||||
}
|
// return [layer.controlNetId];
|
||||||
if (layer.type === 'ip_adapter_layer') {
|
// }
|
||||||
return [layer.ipAdapterId];
|
// if (layer.type === 'ip_adapter_layer') {
|
||||||
}
|
// return [layer.ipAdapterId];
|
||||||
});
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
enabledControlAdapters = enabledControlAdapters.filter((ca) => enabledControlLayersAdapterIds.includes(ca.id));
|
enabledControlAdapters = enabledControlAdapters.filter((ca) => enabledControlLayersAdapterIds.includes(ca.id));
|
||||||
} else {
|
} else {
|
||||||
const allControlLayerAdapterIds = controlLayers.present.layers.flatMap((layer) => {
|
const allControlLayerAdapterIds = []
|
||||||
if (layer.type === 'regional_guidance_layer') {
|
// const allControlLayerAdapterIds = controlLayers.present.layers.flatMap((layer) => {
|
||||||
return layer.ipAdapterIds;
|
// if (layer.type === 'regional_guidance_layer') {
|
||||||
}
|
// return layer.ipAdapterIds;
|
||||||
if (layer.type === 'control_adapter_layer') {
|
// }
|
||||||
return [layer.controlNetId];
|
// if (layer.type === 'control_adapter_layer') {
|
||||||
}
|
// return [layer.controlNetId];
|
||||||
if (layer.type === 'ip_adapter_layer') {
|
// }
|
||||||
return [layer.ipAdapterId];
|
// if (layer.type === 'ip_adapter_layer') {
|
||||||
}
|
// return [layer.ipAdapterId];
|
||||||
});
|
// }
|
||||||
|
// });
|
||||||
enabledControlAdapters = enabledControlAdapters.filter((ca) => !allControlLayerAdapterIds.includes(ca.id));
|
enabledControlAdapters = enabledControlAdapters.filter((ca) => !allControlLayerAdapterIds.includes(ca.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
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 { 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 { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
@ -8,14 +9,10 @@ import { PiPlusBold } from 'react-icons/pi';
|
|||||||
export const AddLayerButton = memo(() => {
|
export const AddLayerButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const addRegionalGuidanceLayer = useCallback(() => {
|
const [addCALayer, isAddCALayerDisabled] = useAddCALayer();
|
||||||
dispatch(guidanceLayerAdded('regional_guidance_layer'));
|
const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer();
|
||||||
}, [dispatch]);
|
const addRGLayer = useCallback(() => {
|
||||||
const addControlAdapterLayer = useCallback(() => {
|
dispatch(rgLayerAdded());
|
||||||
dispatch(guidanceLayerAdded('control_adapter_layer'));
|
|
||||||
}, [dispatch]);
|
|
||||||
const addIPAdapterLayer = useCallback(() => {
|
|
||||||
dispatch(guidanceLayerAdded('ip_adapter_layer'));
|
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -24,13 +21,13 @@ export const AddLayerButton = memo(() => {
|
|||||||
{t('controlLayers.addLayer')}
|
{t('controlLayers.addLayer')}
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidanceLayer}>
|
<MenuItem icon={<PiPlusBold />} onClick={addRGLayer}>
|
||||||
{t('controlLayers.regionalGuidanceLayer')}
|
{t('controlLayers.regionalGuidanceLayer')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<PiPlusBold />} onClick={addControlAdapterLayer}>
|
<MenuItem icon={<PiPlusBold />} onClick={addCALayer} isDisabled={isAddCALayerDisabled}>
|
||||||
{t('controlLayers.globalControlAdapterLayer')}
|
{t('controlLayers.globalControlAdapterLayer')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapterLayer}>
|
<MenuItem icon={<PiPlusBold />} onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}>
|
||||||
{t('controlLayers.globalIPAdapterLayer')}
|
{t('controlLayers.globalIPAdapterLayer')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Button, Flex } from '@invoke-ai/ui-library';
|
import { Button, Flex } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
import {
|
import {
|
||||||
isRegionalGuidanceLayer,
|
isRegionalGuidanceLayer,
|
||||||
rgLayerNegativePromptChanged,
|
rgLayerNegativePromptChanged,
|
||||||
@ -19,6 +19,7 @@ type AddPromptButtonProps = {
|
|||||||
export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
|
export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId);
|
||||||
const selectValidActions = useMemo(
|
const selectValidActions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
@ -38,9 +39,6 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
|
|||||||
const addNegativePrompt = useCallback(() => {
|
const addNegativePrompt = useCallback(() => {
|
||||||
dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' }));
|
dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' }));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
const addIPAdapter = useCallback(() => {
|
|
||||||
dispatch(guidanceLayerIPAdapterAdded(layerId));
|
|
||||||
}, [dispatch, layerId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" p={2} justifyContent="space-between">
|
<Flex w="full" p={2} justifyContent="space-between">
|
||||||
@ -62,7 +60,13 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
|
|||||||
>
|
>
|
||||||
{t('common.negativePrompt')}
|
{t('common.negativePrompt')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
leftIcon={<PiPlusBold />}
|
||||||
|
onClick={addIPAdapter}
|
||||||
|
isDisabled={isAddIPAdapterDisabled}
|
||||||
|
>
|
||||||
{t('common.ipAdapter')}
|
{t('common.ipAdapter')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
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 { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
||||||
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
|
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
|
||||||
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
||||||
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import {
|
import { layerSelected, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
isControlAdapterLayer,
|
import { memo, useCallback } from 'react';
|
||||||
layerSelected,
|
|
||||||
selectControlLayersSlice,
|
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
|
||||||
import { assert } from 'tsafe';
|
|
||||||
|
|
||||||
import CALayerOpacity from './CALayerOpacity';
|
import CALayerOpacity from './CALayerOpacity';
|
||||||
|
|
||||||
@ -22,19 +16,7 @@ type Props = {
|
|||||||
|
|
||||||
export const CALayer = memo(({ layerId }: Props) => {
|
export const CALayer = memo(({ layerId }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selector = useMemo(
|
const isSelected = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).isSelected);
|
||||||
() =>
|
|
||||||
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 onClickCapture = useCallback(() => {
|
const onClickCapture = useCallback(() => {
|
||||||
// Must be capture so that the layer is selected before deleting/resetting/etc
|
// Must be capture so that the layer is selected before deleting/resetting/etc
|
||||||
dispatch(layerSelected(layerId));
|
dispatch(layerSelected(layerId));
|
||||||
@ -61,7 +43,7 @@ export const CALayer = memo(({ layerId }: Props) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
||||||
<ControlAdapterLayerConfig id={controlNetId} />
|
<CALayerConfig layerId={layerId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,33 +1,101 @@
|
|||||||
import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
|
import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { CALayerModelCombobox } from 'features/controlLayers/components/CALayer/CALayerModelCombobox';
|
import { ControlAdapterModelCombobox } from 'features/controlLayers/components/CALayer/ControlAdapterModelCombobox';
|
||||||
import { selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
|
import {
|
||||||
import { memo } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { PiCaretUpBold } from 'react-icons/pi';
|
import { PiCaretUpBold } from 'react-icons/pi';
|
||||||
import { useToggle } from 'react-use';
|
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 { CALayerImagePreview } from './CALayerImagePreview';
|
||||||
import { CALayerProcessor } from './CALayerProcessor';
|
import { CALayerProcessor } from './CALayerProcessor';
|
||||||
import { CALayerProcessorCombobox } from './CALayerProcessorCombobox';
|
import { CALayerProcessorCombobox } from './CALayerProcessorCombobox';
|
||||||
import { CALayerWeight } from './CALayerWeight';
|
import { ControlAdapterBeginEndStepPct } from './ControlAdapterBeginEndStepPct';
|
||||||
|
import { ControlAdapterControlModeSelect } from './ControlAdapterControlModeSelect';
|
||||||
|
import { ControlAdapterWeight } from './ControlAdapterWeight';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CALayerConfig = memo(({ layerId }: Props) => {
|
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 { t } = useTranslation();
|
||||||
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
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 (
|
return (
|
||||||
<Flex flexDir="column" gap={4} position="relative" w="full">
|
<Flex flexDir="column" gap={4} position="relative" w="full">
|
||||||
<Flex gap={3} alignItems="center" w="full">
|
<Flex gap={3} alignItems="center" w="full">
|
||||||
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
|
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
|
||||||
<CALayerModelCombobox layerId={layerId} />
|
<ControlAdapterModelCombobox modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -49,18 +117,29 @@ export const CALayerConfig = memo(({ layerId }: Props) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap={4} w="full" alignItems="center">
|
<Flex gap={4} w="full" alignItems="center">
|
||||||
<Flex flexDir="column" gap={3} w="full">
|
<Flex flexDir="column" gap={3} w="full">
|
||||||
{caType === 'controlnet' && <CALayerControlMode layerId={layerId} />}
|
{controlAdapter.type === 'controlnet' && (
|
||||||
<CALayerWeight layerId={layerId} />
|
<ControlAdapterControlModeSelect controlMode={controlAdapter.controlMode} onChange={onChangeControlMode} />
|
||||||
<CALayerBeginEndStepPct layerId={layerId} />
|
)}
|
||||||
|
<ControlAdapterWeight weight={controlAdapter.weight} onChange={onChangeWeight} />
|
||||||
|
<ControlAdapterBeginEndStepPct
|
||||||
|
beginEndStepPct={controlAdapter.beginEndStepPct}
|
||||||
|
onChange={onChangeBeginEndStepPct}
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
|
<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>
|
||||||
</Flex>
|
</Flex>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<>
|
<>
|
||||||
<CALayerProcessorCombobox layerId={layerId} />
|
<CALayerProcessorCombobox config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
|
||||||
<CALayerProcessor layerId={layerId} />
|
<CALayerProcessor config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -7,13 +7,8 @@ import IAIDndImage from 'common/components/IAIDndImage';
|
|||||||
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
|
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
|
||||||
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
||||||
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
|
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import {
|
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
caLayerImageChanged,
|
import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters';
|
||||||
heightChanged,
|
|
||||||
selectCALayer,
|
|
||||||
selectControlLayersSlice,
|
|
||||||
widthChanged,
|
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import type { ControlLayerDropData, ImageDraggableData } from 'features/dnd/types';
|
import type { ControlLayerDropData, ImageDraggableData } from 'features/dnd/types';
|
||||||
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||||
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||||
@ -27,10 +22,14 @@ import {
|
|||||||
useGetImageDTOQuery,
|
useGetImageDTOQuery,
|
||||||
useRemoveImageFromBoardMutation,
|
useRemoveImageFromBoardMutation,
|
||||||
} from 'services/api/endpoints/images';
|
} from 'services/api/endpoints/images';
|
||||||
import type { ControlLayerAction } from 'services/api/types';
|
import type { ControlLayerAction, ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
type Props = {
|
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(
|
const selectPendingControlImages = createMemoizedSelector(
|
||||||
@ -38,23 +37,9 @@ const selectPendingControlImages = createMemoizedSelector(
|
|||||||
(controlAdapters) => controlAdapters.pendingControlImages
|
(controlAdapters) => controlAdapters.pendingControlImages
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CALayerImagePreview = memo(({ layerId }: Props) => {
|
export const CALayerImagePreview = memo(({ image, processedImage, onChangeImage, hasProcessor, layerId }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
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 autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||||
const isConnected = useAppSelector((s) => s.system.isConnected);
|
const isConnected = useAppSelector((s) => s.system.isConnected);
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
@ -64,17 +49,19 @@ export const CALayerImagePreview = memo(({ layerId }: Props) => {
|
|||||||
|
|
||||||
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
|
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(
|
const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery(
|
||||||
processedImageName ?? skipToken
|
processedImage?.imageName ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
|
const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
|
||||||
const [addToBoard] = useAddImageToBoardMutation();
|
const [addToBoard] = useAddImageToBoardMutation();
|
||||||
const [removeFromBoard] = useRemoveImageFromBoardMutation();
|
const [removeFromBoard] = useRemoveImageFromBoardMutation();
|
||||||
const handleResetControlImage = useCallback(() => {
|
const handleResetControlImage = useCallback(() => {
|
||||||
dispatch(caLayerImageChanged({ layerId, imageDTO: null }));
|
onChangeImage(null);
|
||||||
}, [layerId, dispatch]);
|
}, [onChangeImage]);
|
||||||
|
|
||||||
const handleSaveControlImage = useCallback(async () => {
|
const handleSaveControlImage = useCallback(async () => {
|
||||||
if (!processedControlImage) {
|
if (!processedControlImage) {
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { stopPropagation } from 'common/util/stopPropagation';
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
|
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 type { ChangeEvent } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -34,7 +34,7 @@ const CALayerOpacity = ({ layerId }: Props) => {
|
|||||||
const { opacity, isFilterEnabled } = useLayerOpacity(layerId);
|
const { opacity, isFilterEnabled } = useLayerOpacity(layerId);
|
||||||
const onChangeOpacity = useCallback(
|
const onChangeOpacity = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(layerOpacityChanged({ layerId, opacity: v / 100 }));
|
dispatch(caLayerOpacityChanged({ layerId, opacity: v / 100 }));
|
||||||
},
|
},
|
||||||
[dispatch, layerId]
|
[dispatch, layerId]
|
||||||
);
|
);
|
||||||
|
@ -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 type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { CannyProcessor } from './processors/CannyProcessor';
|
import { CannyProcessor } from './processors/CannyProcessor';
|
||||||
import { ColorMapProcessor } from './processors/ColorMapProcessor';
|
import { ColorMapProcessor } from './processors/ColorMapProcessor';
|
||||||
@ -16,19 +14,11 @@ import { MlsdImageProcessor } from './processors/MlsdImageProcessor';
|
|||||||
import { PidiProcessor } from './processors/PidiProcessor';
|
import { PidiProcessor } from './processors/PidiProcessor';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
config: ProcessorConfig | null;
|
||||||
|
onChange: (config: ProcessorConfig | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CALayerProcessor = memo(({ layerId }: Props) => {
|
export const CALayerProcessor = memo(({ config, onChange }: 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]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
||||||
import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library';
|
import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
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 { 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 { CONTROLNET_PROCESSORS, isProcessorType } from 'features/controlLayers/util/controlAdapters';
|
||||||
import { configSelector } from 'features/system/store/configSelectors';
|
import { configSelector } from 'features/system/store/configSelectors';
|
||||||
import { includes, map } from 'lodash-es';
|
import { includes, map } from 'lodash-es';
|
||||||
@ -13,7 +13,8 @@ import { PiXBold } from 'react-icons/pi';
|
|||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
config: ProcessorConfig | null;
|
||||||
|
onChange: (config: ProcessorConfig | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectDisabledProcessors = createMemoizedSelector(
|
const selectDisabledProcessors = createMemoizedSelector(
|
||||||
@ -21,49 +22,30 @@ const selectDisabledProcessors = createMemoizedSelector(
|
|||||||
(config) => config.sd.disabledControlNetProcessors
|
(config) => config.sd.disabledControlNetProcessors
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CALayerProcessorCombobox = memo(({ layerId }: Props) => {
|
export const CALayerProcessorCombobox = memo(({ config, onChange }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const disabledProcessors = useAppSelector(selectDisabledProcessors);
|
const disabledProcessors = useAppSelector(selectDisabledProcessors);
|
||||||
const processorType = useAppSelector(
|
|
||||||
(s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.processorConfig?.type ?? null
|
|
||||||
);
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return map(CONTROLNET_PROCESSORS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter(
|
return map(CONTROLNET_PROCESSORS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter(
|
||||||
(o) => !includes(disabledProcessors, o.value)
|
(o) => !includes(disabledProcessors, o.value)
|
||||||
);
|
);
|
||||||
}, [disabledProcessors, t]);
|
}, [disabledProcessors, t]);
|
||||||
|
|
||||||
const onChange = useCallback<ComboboxOnChange>(
|
const _onChange = useCallback<ComboboxOnChange>(
|
||||||
(v) => {
|
(v) => {
|
||||||
if (!v) {
|
if (!v) {
|
||||||
dispatch(
|
onChange(null);
|
||||||
caLayerProcessorConfigChanged({
|
} else {
|
||||||
layerId,
|
|
||||||
processorConfig: null,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
assert(isProcessorType(v.value));
|
assert(isProcessorType(v.value));
|
||||||
dispatch(
|
onChange(CONTROLNET_PROCESSORS[v.value].buildDefaults());
|
||||||
caLayerProcessorConfigChanged({
|
}
|
||||||
layerId,
|
|
||||||
processorConfig: CONTROLNET_PROCESSORS[v.value].buildDefaults(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch, layerId]
|
[onChange]
|
||||||
);
|
);
|
||||||
const clearProcessor = useCallback(() => {
|
const clearProcessor = useCallback(() => {
|
||||||
dispatch(
|
onChange(null);
|
||||||
caLayerProcessorConfigChanged({
|
}, [onChange]);
|
||||||
layerId,
|
const value = useMemo(() => options.find((o) => o.value === config?.type) ?? null, [options, config?.type]);
|
||||||
processorConfig: null,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [dispatch, layerId]);
|
|
||||||
const value = useMemo(() => options.find((o) => o.value === processorType) ?? null, [options, processorType]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -71,7 +53,7 @@ export const CALayerProcessorCombobox = memo(({ layerId }: Props) => {
|
|||||||
<FormLabel>{t('controlnet.processor')}</FormLabel>
|
<FormLabel>{t('controlnet.processor')}</FormLabel>
|
||||||
</InformationalPopover>
|
</InformationalPopover>
|
||||||
<Flex gap={4}>
|
<Flex gap={4}>
|
||||||
<Combobox value={value} options={options} onChange={onChange} />
|
<Combobox value={value} options={options} onChange={_onChange} />
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('controlnet.processor')}
|
aria-label={t('controlnet.processor')}
|
||||||
onClick={clearProcessor}
|
onClick={clearProcessor}
|
||||||
|
@ -1,44 +1,21 @@
|
|||||||
import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { caLayerBeginEndStepPctChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
beginEndStepPct: [number, number];
|
||||||
|
onChange: (beginEndStepPct: [number, number]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatPct = (v: number) => `${Math.round(v * 100)}%`;
|
const formatPct = (v: number) => `${Math.round(v * 100)}%`;
|
||||||
const ariaLabel = ['Begin Step %', 'End Step %'];
|
const ariaLabel = ['Begin Step %', 'End Step %'];
|
||||||
|
|
||||||
export const CALayerBeginEndStepPct = memo(({ layerId }: Props) => {
|
export const ControlAdapterBeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { t } = useTranslation();
|
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(() => {
|
const onReset = useCallback(() => {
|
||||||
dispatch(
|
onChange([0, 1]);
|
||||||
caLayerBeginEndStepPctChanged({
|
}, [onChange]);
|
||||||
layerId,
|
|
||||||
beginEndStepPct: [0, 1],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [dispatch, layerId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl orientation="horizontal">
|
<FormControl orientation="horizontal">
|
||||||
@ -63,4 +40,4 @@ export const CALayerBeginEndStepPct = memo(({ layerId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CALayerBeginEndStepPct.displayName = 'CALayerBeginEndStepPct';
|
ControlAdapterBeginEndStepPct.displayName = 'ControlAdapterBeginEndStepPct';
|
@ -1,26 +1,19 @@
|
|||||||
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
||||||
import { Combobox, FormControl, FormLabel } 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 { 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 { isControlMode } from 'features/controlLayers/util/controlAdapters';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
type Props = {
|
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 { 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(
|
const CONTROL_MODE_DATA = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ label: t('controlnet.balanced'), value: 'balanced' },
|
{ label: t('controlnet.balanced'), value: 'balanced' },
|
||||||
@ -34,14 +27,9 @@ export const CALayerControlMode = memo(({ layerId }: Props) => {
|
|||||||
const handleControlModeChange = useCallback<ComboboxOnChange>(
|
const handleControlModeChange = useCallback<ComboboxOnChange>(
|
||||||
(v) => {
|
(v) => {
|
||||||
assert(isControlMode(v?.value));
|
assert(isControlMode(v?.value));
|
||||||
dispatch(
|
onChange(v.value);
|
||||||
caLayerControlModeChanged({
|
|
||||||
layerId,
|
|
||||||
controlMode: v.value,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[layerId, dispatch]
|
[onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
@ -69,4 +57,4 @@ export const CALayerControlMode = memo(({ layerId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CALayerControlMode.displayName = 'CALayerControlMode';
|
ControlAdapterControlModeSelect.displayName = 'ControlAdapterControlModeSelect';
|
@ -1,39 +1,30 @@
|
|||||||
import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
|
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 { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
|
||||||
import { caLayerModelChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType';
|
import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType';
|
||||||
import type { AnyModelConfig, ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
import type { AnyModelConfig, ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
type Props = {
|
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 { 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 currentBaseModel = useAppSelector((s) => s.generation.model?.base);
|
||||||
|
|
||||||
const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels();
|
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(
|
const _onChange = useCallback(
|
||||||
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null) => {
|
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null) => {
|
||||||
if (!modelConfig) {
|
if (!modelConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(
|
onChangeModel(modelConfig);
|
||||||
caLayerModelChanged({
|
|
||||||
layerId,
|
|
||||||
modelConfig,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch, layerId]
|
[onChangeModel]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getIsDisabled = useCallback(
|
const getIsDisabled = useCallback(
|
||||||
@ -68,4 +59,4 @@ export const CALayerModelCombobox = memo(({ layerId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CALayerModelCombobox.displayName = 'CALayerModelCombobox';
|
ControlAdapterModelCombobox.displayName = 'ControlAdapterModelCombobox';
|
@ -1,21 +1,19 @@
|
|||||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
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 { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { caLayerWeightChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice';
|
import { memo } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
weight: number;
|
||||||
|
onChange: (weight: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatValue = (v: number) => v.toFixed(2);
|
const formatValue = (v: number) => v.toFixed(2);
|
||||||
const marks = [0, 1, 2];
|
const marks = [0, 1, 2];
|
||||||
|
|
||||||
export const CALayerWeight = memo(({ layerId }: Props) => {
|
export const ControlAdapterWeight = memo(({ weight, onChange }: Props) => {
|
||||||
const { t } = useTranslation();
|
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 initial = useAppSelector((s) => s.config.sd.ca.weight.initial);
|
||||||
const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin);
|
const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin);
|
||||||
const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax);
|
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 coarseStep = useAppSelector((s) => s.config.sd.ca.weight.coarseStep);
|
||||||
const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep);
|
const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep);
|
||||||
|
|
||||||
const onChange = useCallback(
|
|
||||||
(weight: number) => {
|
|
||||||
dispatch(caLayerWeightChanged({ layerId, weight }));
|
|
||||||
},
|
|
||||||
[dispatch, layerId]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl orientation="horizontal">
|
<FormControl orientation="horizontal">
|
||||||
<InformationalPopover feature="controlNetWeight">
|
<InformationalPopover feature="controlNetWeight">
|
||||||
@ -61,4 +52,4 @@ export const CALayerWeight = memo(({ layerId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CALayerWeight.displayName = 'CALayerWeight';
|
ControlAdapterWeight.displayName = 'ControlAdapterWeight';
|
@ -1,6 +1,6 @@
|
|||||||
import { Button } from '@invoke-ai/ui-library';
|
import { Button } from '@invoke-ai/ui-library';
|
||||||
import { allLayersDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
@ -8,12 +8,19 @@ import { PiTrashSimpleBold } from 'react-icons/pi';
|
|||||||
export const DeleteAllLayersButton = memo(() => {
|
export const DeleteAllLayersButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const isDisabled = useAppSelector((s) => s.controlLayers.present.layers.length === 0);
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(allLayersDeleted());
|
dispatch(allLayersDeleted());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={onClick} leftIcon={<PiTrashSimpleBold />} variant="ghost" colorScheme="error">
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
leftIcon={<PiTrashSimpleBold />}
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="error"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
>
|
||||||
{t('controlLayers.deleteAll')}
|
{t('controlLayers.deleteAll')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -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';
|
|
@ -1,29 +1,15 @@
|
|||||||
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { IPALayerConfig } from 'features/controlLayers/components/IPALayer/IPALayerConfig';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig';
|
|
||||||
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
||||||
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
||||||
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import { isIPAdapterLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
import { memo } from 'react';
|
||||||
import { memo, useMemo } from 'react';
|
|
||||||
import { assert } from 'tsafe';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IPALayer = memo(({ layerId }: Props) => {
|
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 });
|
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} bg="base.800" borderRadius="base" p="1px" px={2}>
|
<Flex gap={2} bg="base.800" borderRadius="base" p="1px" px={2}>
|
||||||
@ -36,7 +22,7 @@ export const IPALayer = memo(({ layerId }: Props) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
||||||
<ControlAdapterLayerConfig id={ipAdapterId} />
|
<IPALayerConfig layerId={layerId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -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';
|
@ -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';
|
@ -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 };
|
@ -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';
|
@ -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);
|
|
@ -1,7 +1,7 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { guidanceLayerDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { stopPropagation } from 'common/util/stopPropagation';
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
|
import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
@ -12,7 +12,7 @@ export const LayerDeleteButton = memo(({ layerId }: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const deleteLayer = useCallback(() => {
|
const deleteLayer = useCallback(() => {
|
||||||
dispatch(guidanceLayerDeleted(layerId));
|
dispatch(layerDeleted(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
|
||||||
import {
|
import {
|
||||||
isRegionalGuidanceLayer,
|
isRegionalGuidanceLayer,
|
||||||
rgLayerNegativePromptChanged,
|
rgLayerNegativePromptChanged,
|
||||||
@ -18,6 +18,7 @@ type Props = { layerId: string };
|
|||||||
export const LayerMenuRGActions = memo(({ layerId }: Props) => {
|
export const LayerMenuRGActions = memo(({ layerId }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId);
|
||||||
const selectValidActions = useMemo(
|
const selectValidActions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
@ -37,9 +38,6 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => {
|
|||||||
const addNegativePrompt = useCallback(() => {
|
const addNegativePrompt = useCallback(() => {
|
||||||
dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' }));
|
dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' }));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
const addIPAdapter = useCallback(() => {
|
|
||||||
dispatch(guidanceLayerIPAdapterAdded(layerId));
|
|
||||||
}, [dispatch, layerId]);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={addPositivePrompt} isDisabled={!validActions.canAddPositivePrompt} icon={<PiPlusBold />}>
|
<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 />}>
|
<MenuItem onClick={addNegativePrompt} isDisabled={!validActions.canAddNegativePrompt} icon={<PiPlusBold />}>
|
||||||
{t('controlLayers.addNegativePrompt')}
|
{t('controlLayers.addNegativePrompt')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={addIPAdapter} icon={<PiPlusBold />}>
|
<MenuItem onClick={addIPAdapter} icon={<PiPlusBold />} isDisabled={isAddIPAdapterDisabled}>
|
||||||
{t('controlLayers.addIPAdapter')}
|
{t('controlLayers.addIPAdapter')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
|
@ -38,7 +38,7 @@ export const RGLayer = memo(({ layerId }: Props) => {
|
|||||||
color: rgbColorToString(layer.previewColor),
|
color: rgbColorToString(layer.previewColor),
|
||||||
hasPositivePrompt: layer.positivePrompt !== null,
|
hasPositivePrompt: layer.positivePrompt !== null,
|
||||||
hasNegativePrompt: layer.negativePrompt !== null,
|
hasNegativePrompt: layer.negativePrompt !== null,
|
||||||
hasIPAdapters: layer.ipAdapterIds.length > 0,
|
hasIPAdapters: layer.ipAdapters.length > 0,
|
||||||
isSelected: layerId === controlLayers.present.selectedLayerId,
|
isSelected: layerId === controlLayers.present.selectedLayerId,
|
||||||
autoNegative: layer.autoNegative,
|
autoNegative: layer.autoNegative,
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { Divider, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
|
import { Divider, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { guidanceLayerIPAdapterDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig';
|
import {
|
||||||
import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
isRegionalGuidanceLayer,
|
||||||
|
rgLayerIPAdapterDeleted,
|
||||||
|
selectControlLayersSlice,
|
||||||
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
@ -18,19 +20,19 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => {
|
|||||||
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId);
|
const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId);
|
||||||
assert(layer, `Layer ${layerId} not found`);
|
assert(layer, `Layer ${layerId} not found`);
|
||||||
return layer.ipAdapterIds;
|
return layer.ipAdapters;
|
||||||
}),
|
}),
|
||||||
[layerId]
|
[layerId]
|
||||||
);
|
);
|
||||||
const ipAdapterIds = useAppSelector(selectIPAdapterIds);
|
const ipAdapters = useAppSelector(selectIPAdapterIds);
|
||||||
|
|
||||||
if (ipAdapterIds.length === 0) {
|
if (ipAdapters.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ipAdapterIds.map((id, index) => (
|
{ipAdapters.map(({ id }, index) => (
|
||||||
<Flex flexDir="column" key={id}>
|
<Flex flexDir="column" key={id}>
|
||||||
{index > 0 && (
|
{index > 0 && (
|
||||||
<Flex pb={3}>
|
<Flex pb={3}>
|
||||||
@ -55,7 +57,7 @@ type IPAdapterListItemProps = {
|
|||||||
const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber }: IPAdapterListItemProps) => {
|
const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber }: IPAdapterListItemProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onDeleteIPAdapter = useCallback(() => {
|
const onDeleteIPAdapter = useCallback(() => {
|
||||||
dispatch(guidanceLayerIPAdapterDeleted({ layerId, ipAdapterId }));
|
dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId }));
|
||||||
}, [dispatch, ipAdapterId, layerId]);
|
}, [dispatch, ipAdapterId, layerId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -72,7 +74,7 @@ const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber }
|
|||||||
colorScheme="error"
|
colorScheme="error"
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<ControlAdapterLayerConfig id={ipAdapterId} />
|
{/* <ControlAdapterLayerConfig id={ipAdapterId} /> */}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -9,7 +9,7 @@ import {
|
|||||||
$lastMouseDownPos,
|
$lastMouseDownPos,
|
||||||
$tool,
|
$tool,
|
||||||
brushSizeChanged,
|
brushSizeChanged,
|
||||||
rfLayerLineAdded,
|
rgLayerLineAdded,
|
||||||
rgLayerPointsAdded,
|
rgLayerPointsAdded,
|
||||||
rgLayerRectAdded,
|
rgLayerRectAdded,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
@ -71,7 +71,7 @@ export const useMouseEvents = () => {
|
|||||||
}
|
}
|
||||||
if (tool === 'brush' || tool === 'eraser') {
|
if (tool === 'brush' || tool === 'eraser') {
|
||||||
dispatch(
|
dispatch(
|
||||||
rfLayerLineAdded({
|
rgLayerLineAdded({
|
||||||
layerId: selectedLayerId,
|
layerId: selectedLayerId,
|
||||||
points: [pos.x, pos.y, pos.x, pos.y],
|
points: [pos.x, pos.y, pos.x, pos.y],
|
||||||
tool,
|
tool,
|
||||||
@ -181,7 +181,7 @@ export const useMouseEvents = () => {
|
|||||||
}
|
}
|
||||||
if (tool === 'brush' || tool === 'eraser') {
|
if (tool === 'brush' || tool === 'eraser') {
|
||||||
dispatch(
|
dispatch(
|
||||||
rfLayerLineAdded({
|
rgLayerLineAdded({
|
||||||
layerId: selectedLayerId,
|
layerId: selectedLayerId,
|
||||||
points: [pos.x, pos.y, pos.x, pos.y],
|
points: [pos.x, pos.y, pos.x, pos.y],
|
||||||
tool,
|
tool,
|
||||||
|
@ -13,7 +13,7 @@ const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlL
|
|||||||
.filter((l) => l.isEnabled)
|
.filter((l) => l.isEnabled)
|
||||||
.filter((l) => {
|
.filter((l) => {
|
||||||
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
||||||
const hasAtLeastOneImagePrompt = l.ipAdapterIds.length > 0;
|
const hasAtLeastOneImagePrompt = l.ipAdapters.length > 0;
|
||||||
return hasTextPrompt || hasAtLeastOneImagePrompt;
|
return hasTextPrompt || hasAtLeastOneImagePrompt;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import { isEqual, partition } from 'lodash-es';
|
|||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import type { RgbColor } from 'react-colorful';
|
import type { RgbColor } from 'react-colorful';
|
||||||
import type { UndoableOptions } from 'redux-undo';
|
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 { assert } from 'tsafe';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@ -88,11 +88,19 @@ export const selectCALayer = (state: ControlLayersState, layerId: string): Contr
|
|||||||
assert(isControlAdapterLayer(layer));
|
assert(isControlAdapterLayer(layer));
|
||||||
return 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);
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
assert(isIPAdapterLayer(layer));
|
assert(isIPAdapterLayer(layer));
|
||||||
return 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 selectRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => {
|
||||||
const layer = state.layers.find((l) => l.id === layerId);
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
assert(isRegionalGuidanceLayer(layer));
|
assert(isRegionalGuidanceLayer(layer));
|
||||||
@ -199,6 +207,10 @@ export const controlLayersSlice = createSlice({
|
|||||||
state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId);
|
state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId);
|
||||||
state.selectedLayerId = state.layers[0]?.id ?? null;
|
state.selectedLayerId = state.layers[0]?.id ?? null;
|
||||||
},
|
},
|
||||||
|
allLayersDeleted: (state) => {
|
||||||
|
state.layers = [];
|
||||||
|
state.selectedLayerId = null;
|
||||||
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region CA Layers
|
//#region CA Layers
|
||||||
@ -272,19 +284,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
layer.controlAdapter.processorConfig = candidateProcessorConfig;
|
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 }>) => {
|
caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlMode }>) => {
|
||||||
const { layerId, controlMode } = action.payload;
|
const { layerId, controlMode } = action.payload;
|
||||||
const layer = selectCALayer(state, layerId);
|
const layer = selectCALayer(state, layerId);
|
||||||
@ -348,6 +347,21 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = selectIPALayer(state, layerId);
|
const layer = selectIPALayer(state, layerId);
|
||||||
layer.ipAdapter.method = method;
|
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: (
|
ipaLayerCLIPVisionModelChanged: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }>
|
action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }>
|
||||||
@ -358,8 +372,33 @@ export const controlLayersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#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
|
//#region RG Layers
|
||||||
rgLayerAdded: (state, action: PayloadAction<{ layerId: string }>) => {
|
rgLayerAdded: {
|
||||||
|
reducer: (state, action: PayloadAction<{ layerId: string }>) => {
|
||||||
const { layerId } = action.payload;
|
const { layerId } = action.payload;
|
||||||
const layer: RegionalGuidanceLayer = {
|
const layer: RegionalGuidanceLayer = {
|
||||||
id: getRGLayerId(layerId),
|
id: getRGLayerId(layerId),
|
||||||
@ -386,6 +425,8 @@ export const controlLayersSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
prepare: () => ({ payload: { layerId: uuidv4() } }),
|
||||||
|
},
|
||||||
rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
||||||
const { layerId, prompt } = action.payload;
|
const { layerId, prompt } = action.payload;
|
||||||
const layer = selectRGLayer(state, layerId);
|
const layer = selectRGLayer(state, layerId);
|
||||||
@ -396,16 +437,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = selectRGLayer(state, layerId);
|
const layer = selectRGLayer(state, layerId);
|
||||||
layer.negativePrompt = prompt;
|
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 }>) => {
|
rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => {
|
||||||
const { layerId, color } = action.payload;
|
const { layerId, color } = action.payload;
|
||||||
const layer = selectRGLayer(state, layerId);
|
const layer = selectRGLayer(state, layerId);
|
||||||
@ -483,6 +514,16 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = selectRGLayer(state, layerId);
|
const layer = selectRGLayer(state, layerId);
|
||||||
layer.autoNegative = autoNegative;
|
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: (
|
rgLayerIPAdapterImageChanged: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }>
|
action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }>
|
||||||
@ -657,13 +698,12 @@ export const {
|
|||||||
layerMovedToBack,
|
layerMovedToBack,
|
||||||
selectedLayerReset,
|
selectedLayerReset,
|
||||||
selectedLayerDeleted,
|
selectedLayerDeleted,
|
||||||
|
allLayersDeleted,
|
||||||
// CA Layers
|
// CA Layers
|
||||||
caLayerAdded,
|
caLayerAdded,
|
||||||
caLayerImageChanged,
|
caLayerImageChanged,
|
||||||
caLayerProcessedImageChanged,
|
caLayerProcessedImageChanged,
|
||||||
caLayerModelChanged,
|
caLayerModelChanged,
|
||||||
caLayerWeightChanged,
|
|
||||||
caLayerBeginEndStepPctChanged,
|
|
||||||
caLayerControlModeChanged,
|
caLayerControlModeChanged,
|
||||||
caLayerProcessorConfigChanged,
|
caLayerProcessorConfigChanged,
|
||||||
caLayerIsFilterEnabledChanged,
|
caLayerIsFilterEnabledChanged,
|
||||||
@ -674,18 +714,22 @@ export const {
|
|||||||
ipaLayerWeightChanged,
|
ipaLayerWeightChanged,
|
||||||
ipaLayerBeginEndStepPctChanged,
|
ipaLayerBeginEndStepPctChanged,
|
||||||
ipaLayerMethodChanged,
|
ipaLayerMethodChanged,
|
||||||
|
ipaLayerModelChanged,
|
||||||
ipaLayerCLIPVisionModelChanged,
|
ipaLayerCLIPVisionModelChanged,
|
||||||
|
// CA or IPA Layers
|
||||||
|
caOrIPALayerWeightChanged,
|
||||||
|
caOrIPALayerBeginEndStepPctChanged,
|
||||||
// RG Layers
|
// RG Layers
|
||||||
rgLayerAdded,
|
rgLayerAdded,
|
||||||
rgLayerPositivePromptChanged,
|
rgLayerPositivePromptChanged,
|
||||||
rgLayerNegativePromptChanged,
|
rgLayerNegativePromptChanged,
|
||||||
rgLayerIPAdapterAdded,
|
|
||||||
rgLayerIPAdapterDeleted,
|
|
||||||
rgLayerPreviewColorChanged,
|
rgLayerPreviewColorChanged,
|
||||||
rgLayerLineAdded,
|
rgLayerLineAdded,
|
||||||
rgLayerPointsAdded,
|
rgLayerPointsAdded,
|
||||||
rgLayerRectAdded,
|
rgLayerRectAdded,
|
||||||
rgLayerAutoNegativeChanged,
|
rgLayerAutoNegativeChanged,
|
||||||
|
rgLayerIPAdapterAdded,
|
||||||
|
rgLayerIPAdapterDeleted,
|
||||||
rgLayerIPAdapterImageChanged,
|
rgLayerIPAdapterImageChanged,
|
||||||
rgLayerIPAdapterWeightChanged,
|
rgLayerIPAdapterWeightChanged,
|
||||||
rgLayerIPAdapterBeginEndStepPctChanged,
|
rgLayerIPAdapterBeginEndStepPctChanged,
|
||||||
|
@ -72,7 +72,7 @@ export type ProcessorConfig =
|
|||||||
| PidiProcessorConfig
|
| PidiProcessorConfig
|
||||||
| ZoeDepthProcessorConfig;
|
| ZoeDepthProcessorConfig;
|
||||||
|
|
||||||
type ImageWithDims = {
|
export type ImageWithDims = {
|
||||||
imageName: string;
|
imageName: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
@ -273,7 +273,7 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = {
|
|||||||
type: 'zoe_depth_image_processor',
|
type: 'zoe_depth_image_processor',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
export const zProcessorType = z.enum([
|
export const zProcessorType = z.enum([
|
||||||
'canny_image_processor',
|
'canny_image_processor',
|
||||||
'color_map_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 => {
|
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 => {
|
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 => {
|
export const buildIPAdapter = (id: string, overrides?: Partial<IPAdapterConfig>): IPAdapterConfig => {
|
||||||
return merge(deepClone(initialIPAdapter), { id, overrides });
|
return merge(deepClone(initialIPAdapter), { id, ...overrides });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildControlAdapterProcessor = (
|
export const buildControlAdapterProcessor = (
|
||||||
|
@ -52,8 +52,7 @@ const STAGE_BG_DATAURL =
|
|||||||
|
|
||||||
const mapId = (object: { id: string }) => object.id;
|
const mapId = (object: { id: string }) => object.id;
|
||||||
|
|
||||||
const selectRenderableLayers = (n: Konva.Node) =>
|
const selectRenderableLayers = (n: Konva.Node) => n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME;
|
||||||
n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME;
|
|
||||||
|
|
||||||
const selectVectorMaskObjects = (node: Konva.Node) => {
|
const selectVectorMaskObjects = (node: Konva.Node) => {
|
||||||
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME;
|
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME;
|
||||||
@ -432,9 +431,9 @@ const updateControlNetLayerImageSource = async (
|
|||||||
konvaLayer: Konva.Layer,
|
konvaLayer: Konva.Layer,
|
||||||
reduxLayer: ControlAdapterLayer
|
reduxLayer: ControlAdapterLayer
|
||||||
) => {
|
) => {
|
||||||
if (reduxLayer.imageName) {
|
if (reduxLayer.controlAdapter.image) {
|
||||||
const imageName = reduxLayer.imageName;
|
const { imageName } = reduxLayer.controlAdapter.image;
|
||||||
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(reduxLayer.imageName));
|
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
|
||||||
const imageDTO = await req.unwrap();
|
const imageDTO = await req.unwrap();
|
||||||
req.unsubscribe();
|
req.unsubscribe();
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
@ -442,8 +441,7 @@ const updateControlNetLayerImageSource = async (
|
|||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
// Find the existing image or create a new one - must find using the name, bc the id may have just changed
|
// Find the existing image or create a new one - must find using the name, bc the id may have just changed
|
||||||
const konvaImage =
|
const konvaImage =
|
||||||
konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`) ??
|
konvaLayer.findOne<Konva.Image>(`.${CA_LAYER_IMAGE_NAME}`) ?? createControlNetLayerImage(konvaLayer, image);
|
||||||
createControlNetLayerImage(konvaLayer, image);
|
|
||||||
|
|
||||||
// Update the image's attributes
|
// Update the image's attributes
|
||||||
konvaImage.setAttrs({
|
konvaImage.setAttrs({
|
||||||
@ -502,11 +500,11 @@ const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay
|
|||||||
let imageSourceNeedsUpdate = false;
|
let imageSourceNeedsUpdate = false;
|
||||||
if (canvasImageSource instanceof HTMLImageElement) {
|
if (canvasImageSource instanceof HTMLImageElement) {
|
||||||
if (
|
if (
|
||||||
reduxLayer.imageName &&
|
reduxLayer.controlAdapter.image &&
|
||||||
canvasImageSource.id !== getCALayerImageId(reduxLayer.id, reduxLayer.imageName)
|
canvasImageSource.id !== getCALayerImageId(reduxLayer.id, reduxLayer.controlAdapter.image.imageName)
|
||||||
) {
|
) {
|
||||||
imageSourceNeedsUpdate = true;
|
imageSourceNeedsUpdate = true;
|
||||||
} else if (!reduxLayer.imageName) {
|
} else if (!reduxLayer.controlAdapter.image) {
|
||||||
imageSourceNeedsUpdate = true;
|
imageSourceNeedsUpdate = true;
|
||||||
}
|
}
|
||||||
} else if (!canvasImageSource) {
|
} else if (!canvasImageSource) {
|
||||||
|
@ -13,24 +13,17 @@ import {
|
|||||||
selectValidIPAdapters,
|
selectValidIPAdapters,
|
||||||
selectValidT2IAdapters,
|
selectValidT2IAdapters,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import { selectAllControlAdapterIds, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
|
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { Fragment, memo } from 'react';
|
import { Fragment, memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
const selector = createMemoizedSelector(
|
const selector = createMemoizedSelector([selectControlAdaptersSlice], (controlAdapters) => {
|
||||||
[selectControlAdaptersSlice, selectControlLayersSlice],
|
|
||||||
(controlAdapters, controlLayers) => {
|
|
||||||
const badges: string[] = [];
|
const badges: string[] = [];
|
||||||
let isError = false;
|
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;
|
const validIPAdapterCount = selectValidIPAdapters(controlAdapters).length;
|
||||||
if (enabledNonRegionalIPAdapterCount > 0) {
|
if (enabledNonRegionalIPAdapterCount > 0) {
|
||||||
@ -40,9 +33,7 @@ const selector = createMemoizedSelector(
|
|||||||
isError = true;
|
isError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabledControlNetCount = selectAllControlNets(controlAdapters)
|
const enabledControlNetCount = selectAllControlNets(controlAdapters).filter((ca) => ca.isEnabled).length;
|
||||||
.filter((ca) => !controlLayersAdapterIds.includes(ca.id))
|
|
||||||
.filter((ca) => ca.isEnabled).length;
|
|
||||||
const validControlNetCount = selectValidControlNets(controlAdapters).length;
|
const validControlNetCount = selectValidControlNets(controlAdapters).length;
|
||||||
if (enabledControlNetCount > 0) {
|
if (enabledControlNetCount > 0) {
|
||||||
badges.push(`${enabledControlNetCount} ControlNet`);
|
badges.push(`${enabledControlNetCount} ControlNet`);
|
||||||
@ -51,9 +42,7 @@ const selector = createMemoizedSelector(
|
|||||||
isError = true;
|
isError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters)
|
const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters).filter((ca) => ca.isEnabled).length;
|
||||||
.filter((ca) => !controlLayersAdapterIds.includes(ca.id))
|
|
||||||
.filter((ca) => ca.isEnabled).length;
|
|
||||||
const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length;
|
const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length;
|
||||||
if (enabledT2IAdapterCount > 0) {
|
if (enabledT2IAdapterCount > 0) {
|
||||||
badges.push(`${enabledT2IAdapterCount} T2I`);
|
badges.push(`${enabledT2IAdapterCount} T2I`);
|
||||||
@ -62,17 +51,14 @@ const selector = createMemoizedSelector(
|
|||||||
isError = true;
|
isError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const controlAdapterIds = selectControlAdapterIds(controlAdapters).filter(
|
const controlAdapterIds = selectControlAdapterIds(controlAdapters);
|
||||||
(id) => !controlLayersAdapterIds.includes(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
controlAdapterIds,
|
controlAdapterIds,
|
||||||
badges,
|
badges,
|
||||||
isError, // TODO: Add some visual indicator that the control adapters are in an error state
|
isError, // TODO: Add some visual indicator that the control adapters are in an error state
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ControlSettingsAccordion: React.FC = memo(() => {
|
export const ControlSettingsAccordion: React.FC = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
Loading…
Reference in New Issue
Block a user