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