refactor(ui): canvas v2 (wip)

This commit is contained in:
psychedelicious 2024-06-14 21:14:37 +10:00
parent d135c48319
commit 8533f207dc
75 changed files with 1301 additions and 1717 deletions

View File

@ -1,5 +1,6 @@
import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'; import { createDraftSafeSelectorCreator, createSelector, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit';
import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors'; import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors';
import type { RootState } from 'app/store/store';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
/** /**
@ -19,3 +20,5 @@ export const getSelectorsOptions: GetSelectorsOptions = {
argsMemoize: lruMemoize, argsMemoize: lruMemoize,
}), }),
}; };
export const createAppSelector = createSelector.withTypes<RootState>();

View File

@ -4,7 +4,6 @@ import { logger } from 'app/logging/logger';
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver'; import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
import type { JSONObject } from 'common/types'; import type { JSONObject } from 'common/types';
import { canvasPersistConfig } from 'features/canvas/store/canvasSlice';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import { import {
controlAdaptersV2PersistConfig, controlAdaptersV2PersistConfig,
@ -104,7 +103,6 @@ export type PersistConfig<T = any> = {
}; };
const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[canvasPersistConfig.name]: canvasPersistConfig,
[galleryPersistConfig.name]: galleryPersistConfig, [galleryPersistConfig.name]: galleryPersistConfig,
[generationPersistConfig.name]: generationPersistConfig, [generationPersistConfig.name]: generationPersistConfig,
[nodesPersistConfig.name]: nodesPersistConfig, [nodesPersistConfig.name]: nodesPersistConfig,

View File

@ -1,4 +1,4 @@
import type { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; import type { ProcessorTypeV2 } from 'features/controlLayers/store/types';
import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas'; import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas';
import type { InvokeTabName } from 'features/ui/store/tabMap'; import type { InvokeTabName } from 'features/ui/store/tabMap';
import type { O } from 'ts-toolbelt'; import type { O } from 'ts-toolbelt';
@ -83,7 +83,7 @@ export type AppConfig = {
sd: { sd: {
defaultModel?: string; defaultModel?: string;
disabledControlNetModels: string[]; disabledControlNetModels: string[];
disabledControlNetProcessors: (keyof typeof CONTROLNET_PROCESSORS)[]; disabledControlNetProcessors: ProcessorTypeV2;
// Core parameters // Core parameters
iterations: NumericalParameterConfig; iterations: NumericalParameterConfig;
width: NumericalParameterConfig; // initial value comes from model width: NumericalParameterConfig; // initial value comes from model

View File

@ -17,10 +17,6 @@ const accept: Accept = {
const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => { const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => {
let postUploadAction: PostUploadAction = { type: 'TOAST' }; let postUploadAction: PostUploadAction = { type: 'TOAST' };
if (activeTabName === 'canvas') {
postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' };
}
if (activeTabName === 'upscaling') { if (activeTabName === 'upscaling') {
postUploadAction = { type: 'SET_UPSCALE_INITIAL_IMAGE' }; postUploadAction = { type: 'SET_UPSCALE_INITIAL_IMAGE' };
} }
@ -30,10 +26,9 @@ const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (ac
export const useFullscreenDropzone = () => { export const useFullscreenDropzone = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const postUploadAction = useAppSelector(selectPostUploadAction);
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false); const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
const postUploadAction = useAppSelector(selectPostUploadAction);
const [uploadImage] = useUploadImageMutation(); const [uploadImage] = useUploadImageMutation();
const fileRejectionCallback = useCallback( const fileRejectionCallback = useCallback(

View File

@ -74,14 +74,6 @@ export const useGlobalHotkeys = () => {
useHotkeys( useHotkeys(
'2', '2',
() => {
dispatch(setActiveTab('canvas'));
},
[dispatch]
);
useHotkeys(
'3',
() => { () => {
dispatch(setActiveTab('workflows')); dispatch(setActiveTab('workflows'));
}, },
@ -89,7 +81,7 @@ export const useGlobalHotkeys = () => {
); );
useHotkeys( useHotkeys(
'4', '3',
() => { () => {
if (isModelManagerEnabled) { if (isModelManagerEnabled) {
dispatch(setActiveTab('models')); dispatch(setActiveTab('models'));
@ -99,7 +91,7 @@ export const useGlobalHotkeys = () => {
); );
useHotkeys( useHotkeys(
isModelManagerEnabled ? '5' : '4', isModelManagerEnabled ? '4' : '3',
() => { () => {
dispatch(setActiveTab('queue')); dispatch(setActiveTab('queue'));
}, },

View File

@ -1,13 +1,12 @@
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice';
selectControlAdapterAll,
selectControlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import type { LayerData } from 'features/controlLayers/store/types'; import { selectIPAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice';
import { selectLayersSlice } from 'features/controlLayers/store/layersSlice';
import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice';
import type { CanvasEntity } from 'features/controlLayers/store/types';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
@ -24,43 +23,49 @@ import { forEach, upperFirst } from 'lodash-es';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow'; import { getConnectedEdges } from 'reactflow';
const LAYER_TYPE_TO_TKEY: Record<LayerData['type'], string> = { const LAYER_TYPE_TO_TKEY: Record<CanvasEntity['type'], string> = {
initial_image_layer: 'controlLayers.globalInitialImage', control_adapter: 'controlLayers.globalControlAdapter',
control_adapter_layer: 'controlLayers.globalControlAdapter', ip_adapter: 'controlLayers.globalIPAdapter',
ip_adapter_layer: 'controlLayers.globalIPAdapter', regional_guidance: 'controlLayers.regionalGuidance',
regional_guidance_layer: 'controlLayers.regionalGuidance', layer: 'controlLayers.raster',
raster_layer: 'controlLayers.raster', inpaint_mask: 'controlLayers.inpaintMask',
}; };
const createSelector = (templates: Templates) => const createSelector = (templates: Templates) =>
createMemoizedSelector( createMemoizedSelector(
[ [
selectControlAdaptersSlice,
selectGenerationSlice, selectGenerationSlice,
selectSystemSlice, selectSystemSlice,
selectNodesSlice, selectNodesSlice,
selectWorkflowSettingsSlice, selectWorkflowSettingsSlice,
selectDynamicPromptsSlice, selectDynamicPromptsSlice,
selectCanvasV2Slice, selectCanvasV2Slice,
selectLayersSlice,
selectControlAdaptersV2Slice,
selectRegionalGuidanceSlice,
selectIPAdaptersSlice,
activeTabNameSelector, activeTabNameSelector,
selectUpscalelice, selectUpscalelice,
selectConfigSlice, selectConfigSlice,
], ],
( (
controlAdapters,
generation, generation,
system, system,
nodes, nodes,
workflowSettings, workflowSettings,
dynamicPrompts, dynamicPrompts,
controlLayers, canvasV2,
layersState,
controlAdaptersState,
regionalGuidanceState,
ipAdaptersState,
activeTabName, activeTabName,
upscale, upscale,
config config
) => { ) => {
const { model } = generation; const { model } = generation;
const { size } = canvasV2; const { size } = canvasV2;
const { positivePrompt } = canvasV2; const { positivePrompt } = canvasV2.prompts;
const { isConnected } = system; const { isConnected } = system;
@ -115,6 +120,26 @@ const createSelector = (templates: Templates) =>
}); });
}); });
} }
} else if (activeTabName === 'upscaling') {
if (!upscale.upscaleInitialImage) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
} else if (config.maxUpscaleDimension) {
const { width, height } = upscale.upscaleInitialImage;
const { scale } = upscale;
const maxPixels = config.maxUpscaleDimension ** 2;
const upscaledPixels = width * scale * height * scale;
if (upscaledPixels > maxPixels) {
reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') });
}
}
if (!upscale.upscaleModel) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') });
}
if (!upscale.tileControlnetModel) {
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
}
} else { } else {
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) { if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') }); reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
@ -124,140 +149,128 @@ const createSelector = (templates: Templates) =>
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
} }
if (activeTabName === 'generation') { controlAdaptersState.controlAdapters
// Handling for generation tab .filter((ca) => ca.isEnabled)
canvasV2.layers .forEach((ca, i) => {
.filter((l) => l.isEnabled) const layerLiteral = i18n.t('controlLayers.layers_one');
.forEach((l, i) => { const layerNumber = i + 1;
const layerLiteral = i18n.t('controlLayers.layers_one'); const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ca.type]);
const layerNumber = i + 1; const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]); const problems: string[] = [];
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; // Must have model
const problems: string[] = []; if (!ca.model) {
if (l.type === 'control_adapter_layer') { problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
// Must have model
if (!l.controlAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
}
// Model base must match
if (l.controlAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
}
// Must have a control image OR, if it has a processor, it must have a processed image
if (!l.controlAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected'));
} else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed'));
}
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
if (l.controlAdapter.type === 't2i_adapter') {
const multiple = model?.base === 'sdxl' ? 32 : 64;
if (size.width % multiple !== 0 || size.height % multiple !== 0) {
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
}
}
}
if (l.type === 'ip_adapter_layer') {
// Must have model
if (!l.ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (l.ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!l.ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
}
if (l.type === 'initial_image_layer') {
// Must have an image
if (!l.image) {
problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected'));
}
}
if (l.type === 'regional_guidance_layer') {
// Must have a region
if (l.objects.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
}
// Must have at least 1 prompt or IP Adapter
if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
}
l.ipAdapters.forEach((ipAdapter) => {
// Must have model
if (!ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (ipAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!ipAdapter.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
});
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
} else if (activeTabName === 'upscaling') {
if (!upscale.upscaleInitialImage) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
} else if (config.maxUpscaleDimension) {
const { width, height } = upscale.upscaleInitialImage;
const { scale } = upscale;
const maxPixels = config.maxUpscaleDimension ** 2;
const upscaledPixels = width * scale * height * scale;
if (upscaledPixels > maxPixels) {
reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') });
} }
} // Model base must match
if (!upscale.upscaleModel) { if (ca.model?.base !== model?.base) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') }); problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
} }
if (!upscale.tileControlnetModel) { // Must have a control image OR, if it has a processor, it must have a processed image
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') }); if (!ca.image) {
} problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected'));
} else { } else if (ca.processorConfig && !ca.processedImage) {
// Handling for all other tabs problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed'));
selectControlAdapterAll(controlAdapters) }
.filter((ca) => ca.isEnabled) // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
.forEach((ca, i) => { if (!ca.controlMode) {
if (!ca.isEnabled) { const multiple = model?.base === 'sdxl' ? 32 : 64;
return; if (size.width % multiple !== 0 || size.height % multiple !== 0) {
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
} }
}
if (!ca.model) { if (problems.length) {
reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) }); const content = upperFirst(problems.join(', '));
} else if (ca.model.base !== model?.base) { reasons.push({ prefix, content });
// This should never happen, just a sanity check }
reasons.push({ });
content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }),
}); ipAdaptersState.ipAdapters
.filter((ipa) => ipa.isEnabled)
.forEach((ipa, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ipa.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have model
if (!ipa.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
}
// Model base must match
if (ipa.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
}
// Must have an image
if (!ipa.image) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
regionalGuidanceState.regions
.filter((rg) => rg.isEnabled)
.forEach((rg, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[rg.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have a region
if (rg.objects.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
}
// Must have at least 1 prompt or IP Adapter
if (rg.positivePrompt === null && rg.negativePrompt === null && rg.ipAdapters.length === 0) {
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
}
rg.ipAdapters.forEach((ipAdapter) => {
// Must have model
if (!ipAdapter.model) {
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
} }
// Model base must match
if ( if (ipAdapter.model?.base !== model?.base) {
!ca.controlImage || problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none') }
) { // Must have an image
reasons.push({ if (!ipAdapter.image) {
content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }), problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
});
} }
}); });
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
layersState.layers
.filter((l) => l.isEnabled)
.forEach((l, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// if (l.type === 'initial_image_layer') {
// // Must have an image
// if (!l.image) {
// problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected'));
// }
// }
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
} }
return { isReady: !reasons.length, reasons }; return { isReady: !reasons.length, reasons };

View File

@ -1,4 +1,4 @@
import type { RgbaColor } from 'react-colorful'; import type { RgbaColor, RgbColor } from 'react-colorful';
export function rgbaToHex(color: RgbaColor, alpha: boolean = false): string { export function rgbaToHex(color: RgbaColor, alpha: boolean = false): string {
const hex = ((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1); const hex = ((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1);
@ -15,3 +15,13 @@ export function hexToRGBA(hex: string, alpha: number) {
const b = parseInt(hex.substring(4, 6), 16); const b = parseInt(hex.substring(4, 6), 16);
return { r, g, b, a: alpha }; return { r, g, b, a: alpha };
} }
export const rgbaColorToString = (color: RgbaColor): string => {
const { r, g, b, a } = color;
return `rgba(${r}, ${g}, ${b}, ${a})`;
};
export const rgbColorToString = (color: RgbColor): string => {
const { r, g, b } = color;
return `rgba(${r}, ${g}, ${b})`;
};

View File

@ -1,7 +1,8 @@
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import { layerAdded, regionalGuidanceAdded } from 'features/controlLayers/store/controlLayersSlice'; import { layerAdded } from 'features/controlLayers/store/layersSlice';
import { rgAdded } from 'features/controlLayers/store/regionalGuidanceSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi'; import { PiPlusBold } from 'react-icons/pi';
@ -11,9 +12,8 @@ export const AddLayerButton = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [addCALayer, isAddCALayerDisabled] = useAddCALayer(); const [addCALayer, isAddCALayerDisabled] = useAddCALayer();
const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer();
const [addIILayer, isAddIILayerDisabled] = useAddIILayer();
const addRGLayer = useCallback(() => { const addRGLayer = useCallback(() => {
dispatch(regionalGuidanceAdded()); dispatch(rgAdded());
}, [dispatch]); }, [dispatch]);
const addRasterLayer = useCallback(() => { const addRasterLayer = useCallback(() => {
dispatch(layerAdded()); dispatch(layerAdded());
@ -42,9 +42,6 @@ export const AddLayerButton = memo(() => {
<MenuItem icon={<PiPlusBold />} onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> <MenuItem icon={<PiPlusBold />} onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}>
{t('controlLayers.globalIPAdapterLayer')} {t('controlLayers.globalIPAdapterLayer')}
</MenuItem> </MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIILayer} isDisabled={isAddIILayerDisabled}>
{t('controlLayers.globalInitialImageLayer')}
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
); );

View File

@ -1,44 +1,42 @@
import { Button, Flex } from '@invoke-ai/ui-library'; import { Button, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks';
import { import {
regionalGuidanceNegativePromptChanged, rgNegativePromptChanged,
regionalGuidancePositivePromptChanged, rgPositivePromptChanged,
selectCanvasV2Slice, selectRegionalGuidanceSlice,
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/regionalGuidanceSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi'; import { PiPlusBold } from 'react-icons/pi';
import { assert } from 'tsafe';
type AddPromptButtonProps = { type AddPromptButtonProps = {
layerId: string; id: string;
}; };
export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { export const AddPromptButtons = ({ id }: AddPromptButtonProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id);
const selectValidActions = useMemo( const selectValidActions = useMemo(
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { createMemoizedSelector(selectRegionalGuidanceSlice, (regionalGuidanceState) => {
const layer = canvasV2.layers.find((l) => l.id === layerId); const rg = regionalGuidanceState.regions.find((rg) => rg.id === id);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return { return {
canAddPositivePrompt: layer.positivePrompt === null, canAddPositivePrompt: rg?.positivePrompt === null,
canAddNegativePrompt: layer.negativePrompt === null, canAddNegativePrompt: rg?.negativePrompt === null,
}; };
}), }),
[layerId] [id]
); );
const validActions = useAppSelector(selectValidActions); const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => { const addPositivePrompt = useCallback(() => {
dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: '' })); dispatch(rgPositivePromptChanged({ id, prompt: '' }));
}, [dispatch, layerId]); }, [dispatch, id]);
const addNegativePrompt = useCallback(() => { const addNegativePrompt = useCallback(() => {
dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: '' })); dispatch(rgNegativePromptChanged({ id, prompt: '' }));
}, [dispatch, layerId]); }, [dispatch, id]);
return ( return (
<Flex w="full" p={2} justifyContent="space-between"> <Flex w="full" p={2} justifyContent="space-between">

View File

@ -10,33 +10,33 @@ import {
PopoverTrigger, PopoverTrigger,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { brushSizeChanged, initialControlLayersState } from 'features/controlLayers/store/controlLayersSlice'; import { brushWidthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const marks = [0, 100, 200, 300]; const marks = [0, 100, 200, 300];
const formatPx = (v: number | string) => `${v} px`; const formatPx = (v: number | string) => `${v} px`;
export const BrushSize = memo(() => { export const BrushWidth = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const brushSize = useAppSelector((s) => s.canvasV2.brushSize); const width = useAppSelector((s) => s.canvasV2.tool.brush.width);
const onChange = useCallback( const onChange = useCallback(
(v: number) => { (v: number) => {
dispatch(brushSizeChanged(Math.round(v))); dispatch(brushWidthChanged(Math.round(v)));
}, },
[dispatch] [dispatch]
); );
return ( return (
<FormControl w="min-content" gap={2}> <FormControl w="min-content" gap={2}>
<FormLabel m={0}>{t('controlLayers.brushSize')}</FormLabel> <FormLabel m={0}>{t('controlLayers.brushWidth')}</FormLabel>
<Popover isLazy> <Popover isLazy>
<PopoverTrigger> <PopoverTrigger>
<CompositeNumberInput <CompositeNumberInput
min={1} min={1}
max={600} max={600}
defaultValue={initialControlLayersState.brushSize} defaultValue={50}
value={brushSize} value={width}
onChange={onChange} onChange={onChange}
w={24} w={24}
format={formatPx} format={formatPx}
@ -45,14 +45,7 @@ export const BrushSize = memo(() => {
<PopoverContent w={200} py={2} px={4}> <PopoverContent w={200} py={2} px={4}>
<PopoverArrow /> <PopoverArrow />
<PopoverBody> <PopoverBody>
<CompositeSlider <CompositeSlider min={1} max={300} defaultValue={50} value={width} onChange={onChange} marks={marks} />
min={1}
max={300}
defaultValue={initialControlLayersState.brushSize}
value={brushSize}
onChange={onChange}
marks={marks}
/>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
@ -60,4 +53,4 @@ export const BrushSize = memo(() => {
); );
}); });
BrushSize.displayName = 'BrushSize'; BrushWidth.displayName = 'BrushSize';

View File

@ -1,48 +0,0 @@
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CALayerControlAdapterWrapper } from 'features/controlLayers/components/CALayer/CALayerControlAdapterWrapper';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
import { isControlAdapterLayer } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import CALayerOpacity from './CALayerOpacity';
type Props = {
layerId: string;
};
export const CALayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector(
(s) => selectLayerOrThrow(s.canvasV2, layerId, isControlAdapterLayer).isSelected
);
const onClick = useCallback(() => {
dispatch(layerSelected(layerId));
}, [dispatch, layerId]);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} />
<LayerTitle type="control_adapter_layer" />
<Spacer />
<CALayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<CALayerControlAdapterWrapper layerId={layerId} />
</Flex>
)}
</LayerWrapper>
);
});
CALayer.displayName = 'CALayer';

View File

@ -1,135 +0,0 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ControlAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapter';
import {
caOrIPALayerBeginEndStepPctChanged,
caOrIPALayerWeightChanged,
controlAdapterControlModeChanged,
controlAdapterImageChanged,
controlAdapterModelChanged,
controlAdapterProcessedImageChanged,
controlAdapterProcessorConfigChanged,
selectLayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import { isControlAdapterLayer } from 'features/controlLayers/store/types';
import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import type { CALayerImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
import type {
CALayerImagePostUploadAction,
ControlNetModelConfig,
ImageDTO,
T2IAdapterModelConfig,
} from 'services/api/types';
type Props = {
layerId: string;
};
export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const controlAdapter = useAppSelector(
(s) => selectLayerOrThrow(s.canvasV2, layerId, isControlAdapterLayer).controlAdapter
);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
dispatch(
caOrIPALayerBeginEndStepPctChanged({
layerId,
beginEndStepPct,
})
);
},
[dispatch, layerId]
);
const onChangeControlMode = useCallback(
(controlMode: ControlModeV2) => {
dispatch(
controlAdapterControlModeChanged({
layerId,
controlMode,
})
);
},
[dispatch, layerId]
);
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(caOrIPALayerWeightChanged({ layerId, weight }));
},
[dispatch, layerId]
);
const onChangeProcessorConfig = useCallback(
(processorConfig: ProcessorConfig | null) => {
dispatch(controlAdapterProcessorConfigChanged({ layerId, processorConfig }));
},
[dispatch, layerId]
);
const onChangeModel = useCallback(
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => {
dispatch(
controlAdapterModelChanged({
layerId,
modelConfig,
})
);
},
[dispatch, layerId]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(controlAdapterImageChanged({ layerId, imageDTO }));
},
[dispatch, layerId]
);
const onErrorLoadingImage = useCallback(() => {
dispatch(controlAdapterImageChanged({ layerId, imageDTO: null }));
}, [dispatch, layerId]);
const onErrorLoadingProcessedImage = useCallback(() => {
dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null }));
}, [dispatch, layerId]);
const droppableData = useMemo<CALayerImageDropData>(
() => ({
actionType: 'SET_CA_LAYER_IMAGE',
context: {
layerId,
},
id: layerId,
}),
[layerId]
);
const postUploadAction = useMemo<CALayerImagePostUploadAction>(
() => ({
layerId,
type: 'SET_CA_LAYER_IMAGE',
}),
[layerId]
);
return (
<ControlAdapter
controlAdapter={controlAdapter}
onChangeBeginEndStepPct={onChangeBeginEndStepPct}
onChangeControlMode={onChangeControlMode}
onChangeWeight={onChangeWeight}
onChangeProcessorConfig={onChangeProcessorConfig}
onChangeModel={onChangeModel}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
onErrorLoadingImage={onErrorLoadingImage}
onErrorLoadingProcessedImage={onErrorLoadingProcessedImage}
/>
);
});
CALayerControlAdapterWrapper.displayName = 'CALayerControlAdapterWrapper';

View File

@ -11,7 +11,7 @@ type Props = {
const formatPct = (v: number) => `${Math.round(v * 100)}%`; const formatPct = (v: number) => `${Math.round(v * 100)}%`;
const ariaLabel = ['Begin Step %', 'End Step %']; const ariaLabel = ['Begin Step %', 'End Step %'];
export const ControlAdapterBeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => { export const BeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const onReset = useCallback(() => { const onReset = useCallback(() => {
onChange([0, 1]); onChange([0, 1]);
@ -40,4 +40,4 @@ export const ControlAdapterBeginEndStepPct = memo(({ beginEndStepPct, onChange }
); );
}); });
ControlAdapterBeginEndStepPct.displayName = 'ControlAdapterBeginEndStepPct'; BeginEndStepPct.displayName = 'BeginEndStepPct';

View File

@ -12,7 +12,7 @@ type Props = {
const formatValue = (v: number) => v.toFixed(2); const formatValue = (v: number) => v.toFixed(2);
const marks = [0, 1, 2]; const marks = [0, 1, 2];
export const ControlAdapterWeight = memo(({ weight, onChange }: Props) => { export const Weight = memo(({ weight, onChange }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const initial = useAppSelector((s) => s.config.sd.ca.weight.initial); const initial = useAppSelector((s) => s.config.sd.ca.weight.initial);
const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin); const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin);
@ -52,4 +52,4 @@ export const ControlAdapterWeight = memo(({ weight, onChange }: Props) => {
); );
}); });
ControlAdapterWeight.displayName = 'ControlAdapterWeight'; Weight.displayName = 'Weight';

View File

@ -1,8 +1,8 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { ControlModeV2 } from 'features/controlLayers/util/controlAdapters'; import type { ControlModeV2} from 'features/controlLayers/store/types';
import { isControlModeV2 } from 'features/controlLayers/util/controlAdapters'; import { isControlModeV2 } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
@ -12,7 +12,7 @@ type Props = {
onChange: (controlMode: ControlModeV2) => void; onChange: (controlMode: ControlModeV2) => void;
}; };
export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => { export const CAControlModeSelect = memo(({ controlMode, onChange }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const CONTROL_MODE_DATA = useMemo( const CONTROL_MODE_DATA = useMemo(
() => [ () => [
@ -57,4 +57,4 @@ export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }:
); );
}); });
ControlAdapterControlModeSelect.displayName = 'ControlAdapterControlModeSelect'; CAControlModeSelect.displayName = 'CAControlModeSelect';

View File

@ -0,0 +1,35 @@
import { Flex, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CAHeaderItems } from 'features/controlLayers/components/ControlAdapter/CAHeaderItems';
import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { entitySelected } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
type Props = {
id: string;
};
export const CAEntity = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const disclosure = useDisclosure({ defaultIsOpen: true });
const onClick = useCallback(() => {
dispatch(entitySelected({ id, type: 'control_adapter' }));
}, [dispatch, id]);
return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={disclosure.onToggle}>
<CAHeaderItems id={id} />
</Flex>
{disclosure.isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<CASettings id={id} />
</Flex>
)}
</LayerWrapper>
);
});
CAEntity.displayName = 'CAEntity';

View File

@ -0,0 +1,109 @@
import { Menu, MenuItem, MenuList, Spacer } from '@invoke-ai/ui-library';
import { createAppSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter';
import { EntityDeleteButton } from 'features/controlLayers/components/LayerCommon/EntityDeleteButton';
import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/EntityEnabledToggle';
import { EntityMenuButton } from 'features/controlLayers/components/LayerCommon/EntityMenuButton';
import { EntityTitle } from 'features/controlLayers/components/LayerCommon/EntityTitle';
import {
caDeleted,
caIsEnabledToggled,
caMovedBackwardOne,
caMovedForwardOne,
caMovedToBack,
caMovedToFront,
selectCA,
selectControlAdaptersV2Slice,
} from 'features/controlLayers/store/controlAdaptersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowDownBold,
PiArrowLineDownBold,
PiArrowLineUpBold,
PiArrowUpBold,
PiTrashSimpleBold,
} from 'react-icons/pi';
import { assert } from 'tsafe';
type Props = {
id: string;
};
const selectValidActions = createAppSelector(
[selectControlAdaptersV2Slice, (caState, id: string) => id],
(caState, id) => {
const ca = selectCA(caState, id);
assert(ca, `CA with id ${id} not found`);
const caIndex = caState.controlAdapters.indexOf(ca);
const caCount = caState.controlAdapters.length;
return {
canMoveForward: caIndex < caCount - 1,
canMoveBackward: caIndex > 0,
canMoveToFront: caIndex < caCount - 1,
canMoveToBack: caIndex > 0,
};
}
);
export const CAHeaderItems = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const validActions = useAppSelector((s) => selectValidActions(s, id));
const isEnabled = useAppSelector((s) => {
const ca = selectCA(s.controlAdaptersV2, id);
assert(ca, `CA with id ${id} not found`);
return ca.isEnabled;
});
const onToggle = useCallback(() => {
dispatch(caIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(caDeleted({ id }));
}, [dispatch, id]);
const moveForwardOne = useCallback(() => {
dispatch(caMovedForwardOne({ id }));
}, [dispatch, id]);
const moveToFront = useCallback(() => {
dispatch(caMovedToFront({ id }));
}, [dispatch, id]);
const moveBackwardOne = useCallback(() => {
dispatch(caMovedBackwardOne({ id }));
}, [dispatch, id]);
const moveToBack = useCallback(() => {
dispatch(caMovedToBack({ id }));
}, [dispatch, id]);
return (
<>
<EntityEnabledToggle isEnabled={isEnabled} onToggle={onToggle} />
<EntityTitle title={t('controlLayers.globalControlAdapter')} />
<Spacer />
<CAOpacityAndFilter id={id} />
<Menu>
<EntityMenuButton />
<MenuList>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={moveForwardOne} isDisabled={!validActions.canMoveForward} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={moveBackwardOne} isDisabled={!validActions.canMoveBackward} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
<MenuItem onClick={onDelete} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
</MenuList>
</Menu>
<EntityDeleteButton onDelete={onDelete} />
</>
);
});
CAHeaderItems.displayName = 'CAHeaderItems';

View File

@ -3,13 +3,11 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { ControlNetConfigV2, T2IAdapterConfigV2 } from 'features/controlLayers/util/controlAdapters'; import type { ControlAdapterData } from 'features/controlLayers/store/types';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi'; import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi';
@ -22,7 +20,7 @@ import {
import type { ImageDTO, PostUploadAction } from 'services/api/types'; import type { ImageDTO, PostUploadAction } from 'services/api/types';
type Props = { type Props = {
controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; controlAdapter: ControlAdapterData;
onChangeImage: (imageDTO: ImageDTO | null) => void; onChangeImage: (imageDTO: ImageDTO | null) => void;
droppableData: TypesafeDroppableData; droppableData: TypesafeDroppableData;
postUploadAction: PostUploadAction; postUploadAction: PostUploadAction;
@ -30,7 +28,7 @@ type Props = {
onErrorLoadingProcessedImage: () => void; onErrorLoadingProcessedImage: () => void;
}; };
export const ControlAdapterImagePreview = memo( export const CAImagePreview = memo(
({ ({
controlAdapter, controlAdapter,
onChangeImage, onChangeImage,
@ -43,7 +41,6 @@ export const ControlAdapterImagePreview = memo(
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const isConnected = useAppSelector((s) => s.system.isConnected); const isConnected = useAppSelector((s) => s.system.isConnected);
const activeTabName = useAppSelector(activeTabNameSelector);
const optimalDimension = useAppSelector(selectOptimalDimension); const optimalDimension = useAppSelector(selectOptimalDimension);
const shift = useShiftModifier(); const shift = useShiftModifier();
@ -88,27 +85,21 @@ export const ControlAdapterImagePreview = memo(
return; return;
} }
if (activeTabName === 'canvas') { const options = { updateAspectRatio: true, clamp: true };
dispatch(
setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)
);
} else {
const options = { updateAspectRatio: true, clamp: true };
if (shift) { if (shift) {
const { width, height } = controlImage; const { width, height } = controlImage;
dispatch(widthChanged({ width, ...options })); dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options })); dispatch(heightChanged({ height, ...options }));
} else { } else {
const { width, height } = calculateNewSize( const { width, height } = calculateNewSize(
controlImage.width / controlImage.height, controlImage.width / controlImage.height,
optimalDimension * optimalDimension optimalDimension * optimalDimension
); );
dispatch(widthChanged({ width, ...options })); dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options })); dispatch(heightChanged({ height, ...options }));
}
} }
}, [controlImage, activeTabName, dispatch, optimalDimension, shift]); }, [controlImage, dispatch, optimalDimension, shift]);
const handleMouseEnter = useCallback(() => { const handleMouseEnter = useCallback(() => {
setIsMouseOverImage(true); setIsMouseOverImage(true);
@ -235,4 +226,4 @@ export const ControlAdapterImagePreview = memo(
} }
); );
ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview'; CAImagePreview.displayName = 'CAImagePreview';

View File

@ -11,7 +11,7 @@ type Props = {
onChange: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; onChange: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void;
}; };
export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => { export const CAModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const currentBaseModel = useAppSelector((s) => s.generation.model?.base); const currentBaseModel = useAppSelector((s) => s.generation.model?.base);
const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels();
@ -60,4 +60,4 @@ export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeM
); );
}); });
ControlAdapterModelCombobox.displayName = 'ControlAdapterModelCombobox'; CAModelCombobox.displayName = 'CAModelCombobox';

View File

@ -15,34 +15,34 @@ import {
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { useCALayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; import { useCALayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
import { caLayerIsFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/controlAdaptersSlice';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi'; import { PiDropHalfFill } from 'react-icons/pi';
type Props = { type Props = {
layerId: string; id: string;
}; };
const marks = [0, 25, 50, 75, 100]; const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`; const formatPct = (v: number | string) => `${v} %`;
const CALayerOpacity = ({ layerId }: Props) => { export const CAOpacityAndFilter = memo(({ id }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { opacity, isFilterEnabled } = useCALayerOpacity(layerId); const { opacity, isFilterEnabled } = useCALayerOpacity(id);
const onChangeOpacity = useCallback( const onChangeOpacity = useCallback(
(v: number) => { (v: number) => {
dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); dispatch(caOpacityChanged({ id, opacity: v / 100 }));
}, },
[dispatch, layerId] [dispatch, id]
); );
const onChangeFilter = useCallback( const onChangeFilter = useCallback(
(e: ChangeEvent<HTMLInputElement>) => { (e: ChangeEvent<HTMLInputElement>) => {
dispatch(caLayerIsFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked })); dispatch(caFilterChanged({ id, filter: e.target.checked ? 'LightnessToAlphaFilter' : 'none' }));
}, },
[dispatch, layerId] [dispatch, id]
); );
return ( return (
<Popover isLazy> <Popover isLazy>
@ -93,6 +93,6 @@ const CALayerOpacity = ({ layerId }: Props) => {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
); );
}; });
export default memo(CALayerOpacity); CAOpacityAndFilter.displayName = 'CAOpacityAndFilter';

View File

@ -1,24 +1,23 @@
import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { CannyProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor';
import { ColorMapProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor';
import { ContentShuffleProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor';
import { DepthAnythingProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor';
import { DWOpenposeProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor';
import { HedProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor';
import { LineartProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor';
import { MediapipeFaceProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor';
import { MidasDepthProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor';
import { MlsdImageProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor';
import { PidiProcessor } from 'features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor';
import type { ProcessorConfig } from 'features/controlLayers/store/types';
import { memo } from 'react'; import { memo } from 'react';
import { CannyProcessor } from './processors/CannyProcessor';
import { ColorMapProcessor } from './processors/ColorMapProcessor';
import { ContentShuffleProcessor } from './processors/ContentShuffleProcessor';
import { DepthAnythingProcessor } from './processors/DepthAnythingProcessor';
import { DWOpenposeProcessor } from './processors/DWOpenposeProcessor';
import { HedProcessor } from './processors/HedProcessor';
import { LineartProcessor } from './processors/LineartProcessor';
import { MediapipeFaceProcessor } from './processors/MediapipeFaceProcessor';
import { MidasDepthProcessor } from './processors/MidasDepthProcessor';
import { MlsdImageProcessor } from './processors/MlsdImageProcessor';
import { PidiProcessor } from './processors/PidiProcessor';
type Props = { type Props = {
config: ProcessorConfig | null; config: ProcessorConfig | null;
onChange: (config: ProcessorConfig | null) => void; onChange: (config: ProcessorConfig | null) => void;
}; };
export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) => { export const CAProcessorConfig = memo(({ config, onChange }: Props) => {
if (!config) { if (!config) {
return null; return null;
} }
@ -82,4 +81,4 @@ export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props)
} }
}); });
ControlAdapterProcessorConfig.displayName = 'ControlAdapterProcessorConfig'; CAProcessorConfig.displayName = 'CAProcessorConfig';

View File

@ -3,8 +3,8 @@ import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/u
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import type {ProcessorConfig } from 'features/controlLayers/store/types';
import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/util/controlAdapters'; import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/store/types';
import { configSelector } from 'features/system/store/configSelectors'; import { configSelector } from 'features/system/store/configSelectors';
import { includes, map } from 'lodash-es'; import { includes, map } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
@ -22,7 +22,7 @@ const selectDisabledProcessors = createMemoizedSelector(
(config) => config.sd.disabledControlNetProcessors (config) => config.sd.disabledControlNetProcessors
); );
export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Props) => { export const CAProcessorTypeSelect = memo(({ config, onChange }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const disabledProcessors = useAppSelector(selectDisabledProcessors); const disabledProcessors = useAppSelector(selectDisabledProcessors);
const options = useMemo(() => { const options = useMemo(() => {
@ -67,4 +67,4 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro
); );
}); });
ControlAdapterProcessorTypeSelect.displayName = 'ControlAdapterProcessorTypeSelect'; CAProcessorTypeSelect.displayName = 'CAProcessorTypeSelect';

View File

@ -0,0 +1,157 @@
import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/Common/BeginEndStepPct';
import { Weight } from 'features/controlLayers/components/Common/Weight';
import { CAControlModeSelect } from 'features/controlLayers/components/ControlAdapter/CAControlModeSelect';
import { CAImagePreview } from 'features/controlLayers/components/ControlAdapter/CAImagePreview';
import { CAModelCombobox } from 'features/controlLayers/components/ControlAdapter/CAModelCombobox';
import { CAProcessorConfig } from 'features/controlLayers/components/ControlAdapter/CAProcessorConfig';
import { CAProcessorTypeSelect } from 'features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect';
import {
caBeginEndStepPctChanged,
caControlModeChanged,
caImageChanged,
caModelChanged,
caProcessedImageChanged,
caProcessorConfigChanged,
caWeightChanged,
} from 'features/controlLayers/store/controlAdaptersSlice';
import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/store/types';
import type { CAImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretUpBold } from 'react-icons/pi';
import { useToggle } from 'react-use';
import type {
CAImagePostUploadAction,
ControlNetModelConfig,
ImageDTO,
T2IAdapterModelConfig,
} from 'services/api/types';
import { assert } from 'tsafe';
type Props = {
id: string;
};
export const CASettings = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [isExpanded, toggleIsExpanded] = useToggle(false);
const controlAdapter = useAppSelector((s) => {
const ca = s.controlAdaptersV2.controlAdapters.find((ca) => ca.id === id);
assert(ca, `ControlAdapter with id ${id} not found`);
return ca;
});
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
dispatch(caBeginEndStepPctChanged({ id, beginEndStepPct }));
},
[dispatch, id]
);
const onChangeControlMode = useCallback(
(controlMode: ControlModeV2) => {
dispatch(caControlModeChanged({ id, controlMode }));
},
[dispatch, id]
);
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(caWeightChanged({ id, weight }));
},
[dispatch, id]
);
const onChangeProcessorConfig = useCallback(
(processorConfig: ProcessorConfig | null) => {
dispatch(caProcessorConfigChanged({ id, processorConfig }));
},
[dispatch, id]
);
const onChangeModel = useCallback(
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => {
dispatch(caModelChanged({ id, modelConfig }));
},
[dispatch, id]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(caImageChanged({ id, imageDTO }));
},
[dispatch, id]
);
const onErrorLoadingImage = useCallback(() => {
dispatch(caImageChanged({ id, imageDTO: null }));
}, [dispatch, id]);
const onErrorLoadingProcessedImage = useCallback(() => {
dispatch(caProcessedImageChanged({ id, imageDTO: null }));
}, [dispatch, id]);
const droppableData = useMemo<CAImageDropData>(() => ({ actionType: 'SET_CA_IMAGE', context: { id }, id }), [id]);
const postUploadAction = useMemo<CAImagePostUploadAction>(() => ({ id, type: 'SET_CA_IMAGE' }), [id]);
return (
<Flex flexDir="column" gap={3} position="relative" w="full">
<Flex gap={3} alignItems="center" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<CAModelCombobox modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
</Box>
<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={3} w="full">
<Flex flexDir="column" gap={3} w="full" h="full">
{controlAdapter.controlMode && (
<CAControlModeSelect controlMode={controlAdapter.controlMode} onChange={onChangeControlMode} />
)}
<Weight weight={controlAdapter.weight} onChange={onChangeWeight} />
<BeginEndStepPct beginEndStepPct={controlAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
</Flex>
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
<CAImagePreview
controlAdapter={controlAdapter}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
onErrorLoadingImage={onErrorLoadingImage}
onErrorLoadingProcessedImage={onErrorLoadingProcessedImage}
/>
</Flex>
</Flex>
{isExpanded && (
<>
<Divider />
<Flex flexDir="column" gap={3} w="full">
<CAProcessorTypeSelect config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
<CAProcessorConfig config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
</Flex>
</>
)}
</Flex>
);
});
CASettings.displayName = 'CASettings';

View File

@ -1,123 +0,0 @@
import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox';
import type {
ControlModeV2,
ControlNetConfigV2,
ProcessorConfig,
T2IAdapterConfigV2,
} from 'features/controlLayers/util/controlAdapters';
import type { TypesafeDroppableData } from 'features/dnd/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretUpBold } from 'react-icons/pi';
import { useToggle } from 'react-use';
import type { ControlNetModelConfig, ImageDTO, PostUploadAction, T2IAdapterModelConfig } from 'services/api/types';
import { ControlAdapterBeginEndStepPct } from './ControlAdapterBeginEndStepPct';
import { ControlAdapterControlModeSelect } from './ControlAdapterControlModeSelect';
import { ControlAdapterImagePreview } from './ControlAdapterImagePreview';
import { ControlAdapterProcessorConfig } from './ControlAdapterProcessorConfig';
import { ControlAdapterProcessorTypeSelect } from './ControlAdapterProcessorTypeSelect';
import { ControlAdapterWeight } from './ControlAdapterWeight';
type Props = {
controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2;
onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void;
onChangeControlMode: (controlMode: ControlModeV2) => void;
onChangeWeight: (weight: number) => void;
onChangeProcessorConfig: (processorConfig: ProcessorConfig | null) => void;
onChangeModel: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void;
onChangeImage: (imageDTO: ImageDTO | null) => void;
onErrorLoadingImage: () => void;
onErrorLoadingProcessedImage: () => void;
droppableData: TypesafeDroppableData;
postUploadAction: PostUploadAction;
};
export const ControlAdapter = memo(
({
controlAdapter,
onChangeBeginEndStepPct,
onChangeControlMode,
onChangeWeight,
onChangeProcessorConfig,
onChangeModel,
onChangeImage,
onErrorLoadingImage,
onErrorLoadingProcessedImage,
droppableData,
postUploadAction,
}: Props) => {
const { t } = useTranslation();
const [isExpanded, toggleIsExpanded] = useToggle(false);
return (
<Flex flexDir="column" gap={3} position="relative" w="full">
<Flex gap={3} alignItems="center" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<ControlAdapterModelCombobox modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
</Box>
<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={3} w="full">
<Flex flexDir="column" gap={3} w="full" h="full">
{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">
<ControlAdapterImagePreview
controlAdapter={controlAdapter}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
onErrorLoadingImage={onErrorLoadingImage}
onErrorLoadingProcessedImage={onErrorLoadingProcessedImage}
/>
</Flex>
</Flex>
{isExpanded && (
<>
<Divider />
<Flex flexDir="column" gap={3} w="full">
<ControlAdapterProcessorTypeSelect
config={controlAdapter.processorConfig}
onChange={onChangeProcessorConfig}
/>
<ControlAdapterProcessorConfig
config={controlAdapter.processorConfig}
onChange={onChangeProcessorConfig}
/>
</Flex>
</>
)}
</Flex>
);
}
);
ControlAdapter.displayName = 'ControlAdapter';

View File

@ -1,5 +1,5 @@
import { Box, Flex } from '@invoke-ai/ui-library'; import { Box, Flex } from '@invoke-ai/ui-library';
import { ControlAdapterBeginEndStepPct } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct'; import { BeginEndStepPct } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct';
import { ControlAdapterWeight } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight'; import { ControlAdapterWeight } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight';
import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview'; import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview';
import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod'; import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod';
@ -49,7 +49,7 @@ export const IPAdapter = memo(
<Flex flexDir="column" gap={3} w="full"> <Flex flexDir="column" gap={3} w="full">
<IPAdapterMethod method={ipAdapter.method} onChange={onChangeIPMethod} /> <IPAdapterMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
<ControlAdapterWeight weight={ipAdapter.weight} onChange={onChangeWeight} /> <ControlAdapterWeight weight={ipAdapter.weight} onChange={onChangeWeight} />
<ControlAdapterBeginEndStepPct <BeginEndStepPct
beginEndStepPct={ipAdapter.beginEndStepPct} beginEndStepPct={ipAdapter.beginEndStepPct}
onChange={onChangeBeginEndStepPct} onChange={onChangeBeginEndStepPct}
/> />

View File

@ -8,7 +8,7 @@ import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton
import { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; import { CALayer } from 'features/controlLayers/components/CALayer/CALayer';
import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton';
import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; import { IILayer } from 'features/controlLayers/components/IILayer/IILayer';
import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer'; import { IPAEntity } from 'features/controlLayers/components/IPALayer/IPALayer';
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
@ -58,10 +58,10 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => {
return <RGLayer key={id} layerId={id} />; return <RGLayer key={id} layerId={id} />;
} }
if (type === 'control_adapter_layer') { if (type === 'control_adapter_layer') {
return <CALayer key={id} layerId={id} />; return <CALayer key={id} id={id} />;
} }
if (type === 'ip_adapter_layer') { if (type === 'ip_adapter_layer') {
return <IPALayer key={id} layerId={id} />; return <IPAEntity key={id} layerId={id} />;
} }
if (type === 'initial_image_layer') { if (type === 'initial_image_layer') {
return <IILayer key={id} layerId={id} />; return <IILayer key={id} layerId={id} />;

View File

@ -2,7 +2,7 @@
import { Flex } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { BrushColorPicker } from 'features/controlLayers/components/BrushColorPicker'; import { BrushColorPicker } from 'features/controlLayers/components/BrushColorPicker';
import { BrushSize } from 'features/controlLayers/components/BrushSize'; import { BrushWidth } from 'features/controlLayers/components/BrushSize';
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
@ -28,7 +28,7 @@ export const ControlLayersToolbar = memo(() => {
</Flex> </Flex>
</Flex> </Flex>
<Flex flex={1} gap={2} justifyContent="center" alignItems="center"> <Flex flex={1} gap={2} justifyContent="center" alignItems="center">
{withBrushSize && <BrushSize />} {withBrushSize && <BrushWidth />}
{withBrushColor && <BrushColorPicker />} {withBrushColor && <BrushColorPicker />}
</Flex> </Flex>
<Flex flex={1} justifyContent="center"> <Flex flex={1} justifyContent="center">

View File

@ -0,0 +1,56 @@
import {
CompositeNumberInput,
CompositeSlider,
FormControl,
FormLabel,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { eraserWidthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const marks = [0, 100, 200, 300];
const formatPx = (v: number | string) => `${v} px`;
export const EraserWidth = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const width = useAppSelector((s) => s.canvasV2.tool.eraser.width);
const onChange = useCallback(
(v: number) => {
dispatch(eraserWidthChanged(Math.round(v)));
},
[dispatch]
);
return (
<FormControl w="min-content" gap={2}>
<FormLabel m={0}>{t('controlLayers.eraserWidth')}</FormLabel>
<Popover isLazy>
<PopoverTrigger>
<CompositeNumberInput
min={1}
max={600}
defaultValue={50}
value={width}
onChange={onChange}
w={24}
format={formatPx}
/>
</PopoverTrigger>
<PopoverContent w={200} py={2} px={4}>
<PopoverArrow />
<PopoverBody>
<CompositeSlider min={1} max={300} defaultValue={50} value={width} onChange={onChange} marks={marks} />
</PopoverBody>
</PopoverContent>
</Popover>
</FormControl>
);
});
EraserWidth.displayName = 'EraserWidth';

View File

@ -1,19 +1,19 @@
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker'; import IAIColorPicker from 'common/components/IAIColorPicker';
import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { brushColorChanged } from 'features/controlLayers/store/controlLayersSlice'; import { fillChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { RgbaColor } from 'features/controlLayers/store/types'; import type { RgbaColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const BrushColorPicker = memo(() => { export const FillColorPicker = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const brushColor = useAppSelector((s) => s.canvasV2.brushColor); const fill = useAppSelector((s) => s.canvasV2.tool.fill);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const onChange = useCallback( const onChange = useCallback(
(color: RgbaColor) => { (color: RgbaColor) => {
dispatch(brushColorChanged(color)); dispatch(fillChanged(color));
}, },
[dispatch] [dispatch]
); );
@ -25,7 +25,7 @@ export const BrushColorPicker = memo(() => {
aria-label={t('controlLayers.brushColor')} aria-label={t('controlLayers.brushColor')}
borderRadius="full" borderRadius="full"
borderWidth={1} borderWidth={1}
bg={rgbaColorToString(brushColor)} bg={rgbaColorToString(fill)}
w={8} w={8}
h={8} h={8}
cursor="pointer" cursor="pointer"
@ -34,11 +34,11 @@ export const BrushColorPicker = memo(() => {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent> <PopoverContent>
<PopoverBody minH={64}> <PopoverBody minH={64}>
<IAIColorPicker color={brushColor} onChange={onChange} withNumberInput /> <IAIColorPicker color={fill} onChange={onChange} withNumberInput />
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
); );
}); });
BrushColorPicker.displayName = 'BrushColorPicker'; FillColorPicker.displayName = 'BrushColorPicker';

View File

@ -2,10 +2,10 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview'; import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity'; import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { import {
iiLayerDenoisingStrengthChanged, iiLayerDenoisingStrengthChanged,
@ -67,11 +67,11 @@ export const IILayer = memo(({ layerId }: Props) => {
return ( return (
<LayerWrapper onClick={onClick} borderColor={layer.isSelected ? 'base.400' : 'base.800'}> <LayerWrapper onClick={onClick} borderColor={layer.isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} /> <EntityEnabledToggle layerId={layerId} />
<LayerTitle type="initial_image_layer" /> <EntityTitle type="initial_image_layer" />
<Spacer /> <Spacer />
<LayerOpacity layerId={layerId} /> <LayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} /> <EntityMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} /> <LayerDeleteButton layerId={layerId} />
</Flex> </Flex>
{isOpen && ( {isOpen && (

View File

@ -2,41 +2,39 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper'; import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; import { entitySelected } from 'features/controlLayers/store/controlLayersSlice';
import { isIPAdapterLayer } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
type Props = { type Props = {
layerId: string; id: string;
}; };
export const IPALayer = memo(({ layerId }: Props) => { export const IPAEntity = memo(({ id }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isSelected = useAppSelector( const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
(s) => selectLayerOrThrow(s.canvasV2, layerId, isIPAdapterLayer).isSelected
);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onClick = useCallback(() => { const onClick = useCallback(() => {
dispatch(layerSelected(layerId)); dispatch(entitySelected({ id, type: 'ip_adapter' }));
}, [dispatch, layerId]); }, [dispatch, id]);
return ( return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}> <LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} /> <EntityEnabledToggle id={id} />
<LayerTitle type="ip_adapter_layer" /> <EntityTitle type="ip_adapter" />
<Spacer /> <Spacer />
<LayerDeleteButton layerId={layerId} /> <LayerDeleteButton id={id} />
</Flex> </Flex>
{isOpen && ( {isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}> <Flex flexDir="column" gap={3} px={3} pb={3}>
<IPALayerIPAdapterWrapper layerId={layerId} /> <IPALayerIPAdapterWrapper id={id} />
</Flex> </Flex>
)} )}
</LayerWrapper> </LayerWrapper>
); );
}); });
IPALayer.displayName = 'IPALayer'; IPAEntity.displayName = 'IPAEntity';

View File

@ -11,7 +11,7 @@ import {
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import { isIPAdapterLayer } from 'features/controlLayers/store/types'; import { isIPAdapterLayer } from 'features/controlLayers/store/types';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import type { IPALayerImageDropData } from 'features/dnd/types'; import type { IPAImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types';
@ -72,7 +72,7 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => {
[dispatch, layerId] [dispatch, layerId]
); );
const droppableData = useMemo<IPALayerImageDropData>( const droppableData = useMemo<IPAImageDropData>(
() => ({ () => ({
actionType: 'SET_IPA_LAYER_IMAGE', actionType: 'SET_IPA_LAYER_IMAGE',
context: { context: {

View File

@ -1,19 +1,13 @@
import { IconButton } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice'; import { memo } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi'; import { PiTrashSimpleBold } from 'react-icons/pi';
type Props = { layerId: string }; type Props = { onDelete: () => void };
export const LayerDeleteButton = memo(({ layerId }: Props) => { export const EntityDeleteButton = memo(({ onDelete }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const deleteLayer = useCallback(() => {
dispatch(layerDeleted(layerId));
}, [dispatch, layerId]);
return ( return (
<IconButton <IconButton
size="sm" size="sm"
@ -21,10 +15,10 @@ export const LayerDeleteButton = memo(({ layerId }: Props) => {
aria-label={t('common.delete')} aria-label={t('common.delete')}
tooltip={t('common.delete')} tooltip={t('common.delete')}
icon={<PiTrashSimpleBold />} icon={<PiTrashSimpleBold />}
onClick={deleteLayer} onClick={onDelete}
onDoubleClick={stopPropagation} // double click expands the layer onDoubleClick={stopPropagation} // double click expands the layer
/> />
); );
}); });
LayerDeleteButton.displayName = 'LayerDeleteButton'; EntityDeleteButton.displayName = 'EntityDeleteButton';

View File

@ -1,23 +1,16 @@
import { IconButton } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { useLayerIsEnabled } from 'features/controlLayers/hooks/layerStateHooks'; import { memo } from 'react';
import { layerIsEnabledToggled } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiCheckBold } from 'react-icons/pi'; import { PiCheckBold } from 'react-icons/pi';
type Props = { type Props = {
layerId: string; isEnabled: boolean;
onToggle: () => void;
}; };
export const LayerIsEnabledToggle = memo(({ layerId }: Props) => { export const EntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useLayerIsEnabled(layerId);
const onClick = useCallback(() => {
dispatch(layerIsEnabledToggled(layerId));
}, [dispatch, layerId]);
return ( return (
<IconButton <IconButton
@ -26,11 +19,11 @@ export const LayerIsEnabledToggle = memo(({ layerId }: Props) => {
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')} tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
variant="outline" variant="outline"
icon={isEnabled ? <PiCheckBold /> : undefined} icon={isEnabled ? <PiCheckBold /> : undefined}
onClick={onClick} onClick={onToggle}
colorScheme="base" colorScheme="base"
onDoubleClick={stopPropagation} // double click expands the layer onDoubleClick={stopPropagation} // double click expands the layer
/> />
); );
}); });
LayerIsEnabledToggle.displayName = 'LayerVisibilityToggle'; EntityEnabledToggle.displayName = 'EntityEnabledToggle';

View File

@ -11,7 +11,7 @@ import { PiArrowCounterClockwiseBold, PiDotsThreeVerticalBold, PiTrashSimpleBold
type Props = { layerId: string }; type Props = { layerId: string };
export const LayerMenu = memo(({ layerId }: Props) => { export const EntityMenu = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const layerType = useLayerType(layerId); const layerType = useLayerType(layerId);
@ -68,4 +68,4 @@ export const LayerMenu = memo(({ layerId }: Props) => {
); );
}); });
LayerMenu.displayName = 'LayerMenu'; EntityMenu.displayName = 'EntityMenu';

View File

@ -0,0 +1,18 @@
import { IconButton, MenuButton } from '@invoke-ai/ui-library';
import { stopPropagation } from 'common/util/stopPropagation';
import { memo } from 'react';
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
export const EntityMenuButton = memo(() => {
return (
<MenuButton
as={IconButton}
aria-label="Layer menu"
size="sm"
icon={<PiDotsThreeVerticalBold />}
onDoubleClick={stopPropagation} // double click expands the layer
/>
);
});
EntityMenuButton.displayName = 'EntityMenuButton';

View File

@ -0,0 +1,16 @@
import { Text } from '@invoke-ai/ui-library';
import { memo } from 'react';
type Props = {
title: string;
};
export const EntityTitle = memo(({ title }: Props) => {
return (
<Text size="sm" fontWeight="semibold" userSelect="none" color="base.300">
{title}
</Text>
);
});
EntityTitle.displayName = 'EntityTitle';

View File

@ -1,7 +1,7 @@
import { MenuItem } from '@invoke-ai/ui-library'; import { MenuItem } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks';
import { import {
regionalGuidanceNegativePromptChanged, regionalGuidanceNegativePromptChanged,
regionalGuidancePositivePromptChanged, regionalGuidancePositivePromptChanged,
@ -18,7 +18,7 @@ type Props = { layerId: string };
export const LayerMenuRGActions = memo(({ layerId }: Props) => { export const LayerMenuRGActions = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(layerId);
const selectValidActions = useMemo( const selectValidActions = useMemo(
() => () =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {

View File

@ -1,33 +0,0 @@
import { Text } from '@invoke-ai/ui-library';
import type { LayerData } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
type: LayerData['type'];
};
export const LayerTitle = memo(({ type }: Props) => {
const { t } = useTranslation();
const title = useMemo(() => {
if (type === 'regional_guidance_layer') {
return t('controlLayers.regionalGuidance');
} else if (type === 'control_adapter_layer') {
return t('controlLayers.globalControlAdapter');
} else if (type === 'ip_adapter_layer') {
return t('controlLayers.globalIPAdapter');
} else if (type === 'initial_image_layer') {
return t('controlLayers.globalInitialImage');
} else if (type === 'raster_layer') {
return t('controlLayers.rasterLayer');
}
}, [t, type]);
return (
<Text size="sm" fontWeight="semibold" userSelect="none" color="base.300">
{title}
</Text>
);
});
LayerTitle.displayName = 'LayerTitle';

View File

@ -4,9 +4,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgbColorToString } from 'features/canvas/util/colorToString'; import { rgbColorToString } from 'features/canvas/util/colorToString';
import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { layerSelected, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
@ -52,8 +52,8 @@ export const RGLayer = memo(({ layerId }: Props) => {
return ( return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? color : 'base.800'}> <LayerWrapper onClick={onClick} borderColor={isSelected ? color : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} /> <EntityEnabledToggle layerId={layerId} />
<LayerTitle type="regional_guidance_layer" /> <EntityTitle type="regional_guidance_layer" />
<Spacer /> <Spacer />
{autoNegative === 'invert' && ( {autoNegative === 'invert' && (
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none"> <Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
@ -62,12 +62,12 @@ export const RGLayer = memo(({ layerId }: Props) => {
)} )}
<RGLayerColorPicker layerId={layerId} /> <RGLayerColorPicker layerId={layerId} />
<RGLayerSettingsPopover layerId={layerId} /> <RGLayerSettingsPopover layerId={layerId} />
<LayerMenu layerId={layerId} /> <EntityMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} /> <LayerDeleteButton layerId={layerId} />
</Flex> </Flex>
{isOpen && ( {isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}> <Flex flexDir="column" gap={3} px={3} pb={3}>
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons layerId={layerId} />} {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons id={layerId} />}
{hasPositivePrompt && <RGLayerPositivePrompt layerId={layerId} />} {hasPositivePrompt && <RGLayerPositivePrompt layerId={layerId} />}
{hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />} {hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />}
{hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />} {hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />}

View File

@ -12,10 +12,10 @@ import {
selectRGLayerIPAdapterOrThrow, selectRGLayerIPAdapterOrThrow,
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import type { RGLayerIPAdapterImageDropData } from 'features/dnd/types'; import type { RGIPAdapterImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { PiTrashSimpleBold } from 'react-icons/pi'; import { PiTrashSimpleBold } from 'react-icons/pi';
import type { ImageDTO, IPAdapterModelConfig, RGLayerIPAdapterImagePostUploadAction } from 'services/api/types'; import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types';
type Props = { type Props = {
layerId: string; layerId: string;
@ -78,7 +78,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
[dispatch, ipAdapterId, layerId] [dispatch, ipAdapterId, layerId]
); );
const droppableData = useMemo<RGLayerIPAdapterImageDropData>( const droppableData = useMemo<RGIPAdapterImageDropData>(
() => ({ () => ({
actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE',
context: { context: {
@ -90,7 +90,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
[ipAdapterId, layerId] [ipAdapterId, layerId]
); );
const postUploadAction = useMemo<RGLayerIPAdapterImagePostUploadAction>( const postUploadAction = useMemo<RGIPAdapterImagePostUploadAction>(
() => ({ () => ({
type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE',
layerId, layerId,

View File

@ -2,14 +2,14 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity'; import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
import { isRasterLayer } from 'features/controlLayers/store/types'; import { isRasterLayer } from 'features/controlLayers/store/types';
import type { RasterLayerImageDropData } from 'features/dnd/types'; import type { LayerImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
type Props = { type Props = {
@ -27,7 +27,7 @@ export const RasterLayer = memo(({ layerId }: Props) => {
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const droppableData = useMemo(() => { const droppableData = useMemo(() => {
const _droppableData: RasterLayerImageDropData = { const _droppableData: LayerImageDropData = {
id: layerId, id: layerId,
actionType: 'ADD_RASTER_LAYER_IMAGE', actionType: 'ADD_RASTER_LAYER_IMAGE',
context: { layerId }, context: { layerId },
@ -38,11 +38,11 @@ export const RasterLayer = memo(({ layerId }: Props) => {
return ( return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}> <LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} /> <EntityEnabledToggle layerId={layerId} />
<LayerTitle type="raster_layer" /> <EntityTitle type="raster_layer" />
<Spacer /> <Spacer />
<LayerOpacity layerId={layerId} /> <LayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} /> <EntityMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} /> <LayerDeleteButton layerId={layerId} />
</Flex> </Flex>
{isOpen && ( {isOpen && (

View File

@ -1,18 +1,14 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { import { caAdded } from 'features/controlLayers/store/controlAdaptersSlice';
controlAdapterAdded, import { ipaAdded } from 'features/controlLayers/store/ipAdaptersSlice';
iiLayerAdded, import { rgIPAdapterAdded } from 'features/controlLayers/store/regionalGuidanceSlice';
ipAdapterAdded,
regionalGuidanceIPAdapterAdded,
} from 'features/controlLayers/store/controlLayersSlice';
import { isInitialImageLayer } from 'features/controlLayers/store/types';
import { import {
buildControlNet, buildControlNet,
buildIPAdapter, buildIPAdapter,
buildT2IAdapter, buildT2IAdapter,
CA_PROCESSOR_DATA, CA_PROCESSOR_DATA,
isProcessorTypeV2, isProcessorTypeV2,
} from 'features/controlLayers/util/controlAdapters'; } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType'; import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType';
@ -46,7 +42,7 @@ export const useAddCALayer = () => {
processorConfig, processorConfig,
}); });
dispatch(controlAdapterAdded(controlAdapter)); dispatch(caAdded(controlAdapter));
}, [dispatch, model, baseModel]); }, [dispatch, model, baseModel]);
return [addCALayer, isDisabled] as const; return [addCALayer, isDisabled] as const;
@ -70,13 +66,13 @@ export const useAddIPALayer = () => {
const ipAdapter = buildIPAdapter(id, { const ipAdapter = buildIPAdapter(id, {
model: zModelIdentifierField.parse(model), model: zModelIdentifierField.parse(model),
}); });
dispatch(ipAdapterAdded(ipAdapter)); dispatch(ipaAdded(ipAdapter));
}, [dispatch, model]); }, [dispatch, model]);
return [addIPALayer, isDisabled] as const; return [addIPALayer, isDisabled] as const;
}; };
export const useAddIPAdapterToIPALayer = (layerId: string) => { export const useAddIPAdapterToRGLayer = (id: string) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const baseModel = useAppSelector((s) => s.generation.model?.base); const baseModel = useAppSelector((s) => s.generation.model?.base);
const [modelConfigs] = useIPAdapterModels(); const [modelConfigs] = useIPAdapterModels();
@ -90,22 +86,11 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => {
if (!model) { if (!model) {
return; return;
} }
const id = uuidv4(); const ipAdapter = buildIPAdapter(uuidv4(), {
const ipAdapter = buildIPAdapter(id, {
model: zModelIdentifierField.parse(model), model: zModelIdentifierField.parse(model),
}); });
dispatch(regionalGuidanceIPAdapterAdded({ layerId, ipAdapter })); dispatch(rgIPAdapterAdded({ id, ipAdapter: { ...ipAdapter, id: uuidv4(), type: 'ip_adapter', isEnabled: true } }));
}, [dispatch, model, layerId]); }, [model, dispatch, id]);
return [addIPAdapter, isDisabled] as const; return [addIPAdapter, isDisabled] as const;
}; };
export const useAddIILayer = () => {
const dispatch = useAppDispatch();
const isDisabled = useAppSelector((s) => Boolean(s.canvasV2.layers.find(isInitialImageLayer)));
const addIILayer = useCallback(() => {
dispatch(iiLayerAdded(null));
}, [dispatch]);
return [addIILayer, isDisabled] as const;
};

View File

@ -108,7 +108,7 @@ const updateCALayerImageAttrs = (stage: Konva.Stage, konvaImage: Konva.Image, ca
scaleX: 1, scaleX: 1,
scaleY: 1, scaleY: 1,
visible: ca.isEnabled, visible: ca.isEnabled,
filters: ca.filter === LightnessToAlphaFilter.name ? [LightnessToAlphaFilter] : [], filters: ca.filter === 'LightnessToAlphaFilter' ? [LightnessToAlphaFilter] : [],
}); });
needsCache = true; needsCache = true;
} }

View File

@ -2,15 +2,14 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store'; import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters';
import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { ControlAdapterConfig, ControlAdapterData, Filter } from './types'; import type { ControlAdapterConfig, ControlAdapterData, ControlModeV2, Filter, ProcessorConfig } from './types';
import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from './types';
type ControlAdaptersV2State = { type ControlAdaptersV2State = {
_version: 1; _version: 1;
@ -22,7 +21,7 @@ const initialState: ControlAdaptersV2State = {
controlAdapters: [], controlAdapters: [],
}; };
const selectCa = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); export const selectCA = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id);
export const controlAdaptersV2Slice = createSlice({ export const controlAdaptersV2Slice = createSlice({
name: 'controlAdaptersV2', name: 'controlAdaptersV2',
@ -40,7 +39,7 @@ export const controlAdaptersV2Slice = createSlice({
bboxNeedsUpdate: false, bboxNeedsUpdate: false,
isEnabled: true, isEnabled: true,
opacity: 1, opacity: 1,
filter: 'lightness_to_alpha', filter: 'LightnessToAlphaFilter',
processorPendingBatchId: null, processorPendingBatchId: null,
...config, ...config,
}); });
@ -52,17 +51,17 @@ export const controlAdaptersV2Slice = createSlice({
caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => { caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => {
state.controlAdapters.push(action.payload.data); state.controlAdapters.push(action.payload.data);
}, },
caIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id, isEnabled } = action.payload; const { id } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
ca.isEnabled = isEnabled; ca.isEnabled = !ca.isEnabled;
}, },
caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload; const { id, x, y } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -71,7 +70,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload; const { id, bbox } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -84,7 +83,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload; const { id, opacity } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -92,7 +91,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -100,7 +99,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { caMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -108,7 +107,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -116,7 +115,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { caMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -124,7 +123,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload; const { id, imageDTO } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -145,7 +144,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload; const { id, imageDTO } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -162,7 +161,7 @@ export const controlAdaptersV2Slice = createSlice({
}> }>
) => { ) => {
const { id, modelConfig } = action.payload; const { id, modelConfig } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -189,7 +188,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => {
const { id, controlMode } = action.payload; const { id, controlMode } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -200,7 +199,7 @@ export const controlAdaptersV2Slice = createSlice({
action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }> action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }>
) => { ) => {
const { id, processorConfig } = action.payload; const { id, processorConfig } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -211,7 +210,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => { caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => {
const { id, filter } = action.payload; const { id, filter } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -219,7 +218,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => { caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => {
const { id, batchId } = action.payload; const { id, batchId } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -227,7 +226,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
const { id, weight } = action.payload; const { id, weight } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -235,7 +234,7 @@ export const controlAdaptersV2Slice = createSlice({
}, },
caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => {
const { id, beginEndStepPct } = action.payload; const { id, beginEndStepPct } = action.payload;
const ca = selectCa(state, id); const ca = selectCA(state, id);
if (!ca) { if (!ca) {
return; return;
} }
@ -248,7 +247,7 @@ export const {
caAdded, caAdded,
caBboxChanged, caBboxChanged,
caDeleted, caDeleted,
caIsEnabledChanged, caIsEnabledToggled,
caMovedBackwardOne, caMovedBackwardOne,
caMovedForwardOne, caMovedForwardOne,
caMovedToBack, caMovedToBack,

View File

@ -11,7 +11,7 @@ import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/
import type { IRect, Vector2d } from 'konva/lib/types'; import type { IRect, Vector2d } from 'konva/lib/types';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
import type { CanvasEntity, CanvasV2State, RgbaColor, StageAttrs, Tool } from './types'; import type { CanvasEntity, CanvasEntityIdentifier, CanvasV2State, RgbaColor, StageAttrs, Tool } from './types';
import { DEFAULT_RGBA_COLOR } from './types'; import { DEFAULT_RGBA_COLOR } from './types';
const initialState: CanvasV2State = { const initialState: CanvasV2State = {
@ -110,6 +110,9 @@ export const canvasV2Slice = createSlice({
toolBufferChanged: (state, action: PayloadAction<Tool | null>) => { toolBufferChanged: (state, action: PayloadAction<Tool | null>) => {
state.tool.selectedBuffer = action.payload; state.tool.selectedBuffer = action.payload;
}, },
entitySelected: (state, action: PayloadAction<CanvasEntityIdentifier>) => {
state.selectedEntityIdentifier = action.payload;
},
}, },
extraReducers(builder) { extraReducers(builder) {
builder.addCase(modelChanged, (state, action) => { builder.addCase(modelChanged, (state, action) => {
@ -145,6 +148,7 @@ export const {
invertScrollChanged, invertScrollChanged,
toolChanged, toolChanged,
toolBufferChanged, toolBufferChanged,
entitySelected,
} = canvasV2Slice.actions; } = canvasV2Slice.actions;
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;

View File

@ -3,8 +3,8 @@ import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store'; import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; import { imageDTOToImageWithDims } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';

View File

@ -24,7 +24,7 @@ import type {
ProcessorConfig, ProcessorConfig,
ProcessorTypeV2, ProcessorTypeV2,
ZoeDepthProcessorConfig, ZoeDepthProcessorConfig,
} from './controlAdapters'; } from './types';
describe('Control Adapter Types', () => { describe('Control Adapter Types', () => {
test('ProcessorType', () => { test('ProcessorType', () => {

View File

@ -1,13 +1,4 @@
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters'; import { deepClone } from 'common/util/deepClone';
import {
zBeginEndStepPct,
zCLIPVisionModelV2,
zControlModeV2,
zId,
zImageWithDims,
zIPMethodV2,
zProcessorConfig,
} from 'features/controlLayers/util/controlAdapters';
import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type { import type {
@ -24,11 +15,442 @@ import {
zParameterPositivePrompt, zParameterPositivePrompt,
} from 'features/parameters/types/parameterSchemas'; } from 'features/parameters/types/parameterSchemas';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import type { ImageDTO } from 'services/api/types'; import { merge } from 'lodash-es';
import type {
AnyInvocation,
BaseModelType,
ControlNetModelConfig,
ImageDTO,
T2IAdapterModelConfig,
} from 'services/api/types';
import { z } from 'zod'; import { z } from 'zod';
export const zId = z.string().min(1);
export const zImageWithDims = z.object({
name: z.string(),
width: z.number().int().positive(),
height: z.number().int().positive(),
});
export type ImageWithDims = z.infer<typeof zImageWithDims>;
export const zBeginEndStepPct = z
.tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)])
.refine(([begin, end]) => begin < end, {
message: 'Begin must be less than end',
});
export const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
export type ControlModeV2 = z.infer<typeof zControlModeV2>;
export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success;
export const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']);
export type CLIPVisionModelV2 = z.infer<typeof zCLIPVisionModelV2>;
export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success;
export const zIPMethodV2 = z.enum(['full', 'style', 'composition']);
export type IPMethodV2 = z.infer<typeof zIPMethodV2>;
export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success;
const zCannyProcessorConfig = z.object({
id: zId,
type: z.literal('canny_image_processor'),
low_threshold: z.number().int().gte(0).lte(255),
high_threshold: z.number().int().gte(0).lte(255),
});
export type CannyProcessorConfig = z.infer<typeof zCannyProcessorConfig>;
const zColorMapProcessorConfig = z.object({
id: zId,
type: z.literal('color_map_image_processor'),
color_map_tile_size: z.number().int().gte(1),
});
export type ColorMapProcessorConfig = z.infer<typeof zColorMapProcessorConfig>;
const zContentShuffleProcessorConfig = z.object({
id: zId,
type: z.literal('content_shuffle_image_processor'),
w: z.number().int().gte(0),
h: z.number().int().gte(0),
f: z.number().int().gte(0),
});
export type ContentShuffleProcessorConfig = z.infer<typeof zContentShuffleProcessorConfig>;
const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']);
export type DepthAnythingModelSize = z.infer<typeof zDepthAnythingModelSize>;
export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize =>
zDepthAnythingModelSize.safeParse(v).success;
const zDepthAnythingProcessorConfig = z.object({
id: zId,
type: z.literal('depth_anything_image_processor'),
model_size: zDepthAnythingModelSize,
});
export type DepthAnythingProcessorConfig = z.infer<typeof zDepthAnythingProcessorConfig>;
const zHedProcessorConfig = z.object({
id: zId,
type: z.literal('hed_image_processor'),
scribble: z.boolean(),
});
export type HedProcessorConfig = z.infer<typeof zHedProcessorConfig>;
const zLineartAnimeProcessorConfig = z.object({
id: zId,
type: z.literal('lineart_anime_image_processor'),
});
export type LineartAnimeProcessorConfig = z.infer<typeof zLineartAnimeProcessorConfig>;
const zLineartProcessorConfig = z.object({
id: zId,
type: z.literal('lineart_image_processor'),
coarse: z.boolean(),
});
export type LineartProcessorConfig = z.infer<typeof zLineartProcessorConfig>;
const zMediapipeFaceProcessorConfig = z.object({
id: zId,
type: z.literal('mediapipe_face_processor'),
max_faces: z.number().int().gte(1),
min_confidence: z.number().gte(0).lte(1),
});
export type MediapipeFaceProcessorConfig = z.infer<typeof zMediapipeFaceProcessorConfig>;
const zMidasDepthProcessorConfig = z.object({
id: zId,
type: z.literal('midas_depth_image_processor'),
a_mult: z.number().gte(0),
bg_th: z.number().gte(0),
});
export type MidasDepthProcessorConfig = z.infer<typeof zMidasDepthProcessorConfig>;
const zMlsdProcessorConfig = z.object({
id: zId,
type: z.literal('mlsd_image_processor'),
thr_v: z.number().gte(0),
thr_d: z.number().gte(0),
});
export type MlsdProcessorConfig = z.infer<typeof zMlsdProcessorConfig>;
const zNormalbaeProcessorConfig = z.object({
id: zId,
type: z.literal('normalbae_image_processor'),
});
export type NormalbaeProcessorConfig = z.infer<typeof zNormalbaeProcessorConfig>;
const zDWOpenposeProcessorConfig = z.object({
id: zId,
type: z.literal('dw_openpose_image_processor'),
draw_body: z.boolean(),
draw_face: z.boolean(),
draw_hands: z.boolean(),
});
export type DWOpenposeProcessorConfig = z.infer<typeof zDWOpenposeProcessorConfig>;
const zPidiProcessorConfig = z.object({
id: zId,
type: z.literal('pidi_image_processor'),
safe: z.boolean(),
scribble: z.boolean(),
});
export type PidiProcessorConfig = z.infer<typeof zPidiProcessorConfig>;
const zZoeDepthProcessorConfig = z.object({
id: zId,
type: z.literal('zoe_depth_image_processor'),
});
export type ZoeDepthProcessorConfig = z.infer<typeof zZoeDepthProcessorConfig>;
export const zProcessorConfig = z.discriminatedUnion('type', [
zCannyProcessorConfig,
zColorMapProcessorConfig,
zContentShuffleProcessorConfig,
zDepthAnythingProcessorConfig,
zHedProcessorConfig,
zLineartAnimeProcessorConfig,
zLineartProcessorConfig,
zMediapipeFaceProcessorConfig,
zMidasDepthProcessorConfig,
zMlsdProcessorConfig,
zNormalbaeProcessorConfig,
zDWOpenposeProcessorConfig,
zPidiProcessorConfig,
zZoeDepthProcessorConfig,
]);
export type ProcessorConfig = z.infer<typeof zProcessorConfig>;
const zProcessorTypeV2 = z.enum([
'canny_image_processor',
'color_map_image_processor',
'content_shuffle_image_processor',
'depth_anything_image_processor',
'hed_image_processor',
'lineart_anime_image_processor',
'lineart_image_processor',
'mediapipe_face_processor',
'midas_depth_image_processor',
'mlsd_image_processor',
'normalbae_image_processor',
'dw_openpose_image_processor',
'pidi_image_processor',
'zoe_depth_image_processor',
]);
export type ProcessorTypeV2 = z.infer<typeof zProcessorTypeV2>;
export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success;
type ProcessorData<T extends ProcessorTypeV2> = {
type: T;
labelTKey: string;
descriptionTKey: string;
buildDefaults(baseModel?: BaseModelType): Extract<ProcessorConfig, { type: T }>;
buildNode(image: ImageWithDims, config: Extract<ProcessorConfig, { type: T }>): Extract<AnyInvocation, { type: T }>;
};
const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height);
type CAProcessorsData = {
[key in ProcessorTypeV2]: ProcessorData<key>;
};
/**
* A dict of ControlNet processors, including:
* - label translation key
* - description translation key
* - a builder to create default values for the config
* - a builder to create the node for the config
*
* TODO: Generate from the OpenAPI schema
*/
export const CA_PROCESSOR_DATA: CAProcessorsData = {
canny_image_processor: {
type: 'canny_image_processor',
labelTKey: 'controlnet.canny',
descriptionTKey: 'controlnet.cannyDescription',
buildDefaults: () => ({
id: 'canny_image_processor',
type: 'canny_image_processor',
low_threshold: 100,
high_threshold: 200,
}),
buildNode: (image, config) => ({
...config,
type: 'canny_image_processor',
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
color_map_image_processor: {
type: 'color_map_image_processor',
labelTKey: 'controlnet.colorMap',
descriptionTKey: 'controlnet.colorMapDescription',
buildDefaults: () => ({
id: 'color_map_image_processor',
type: 'color_map_image_processor',
color_map_tile_size: 64,
}),
buildNode: (image, config) => ({
...config,
type: 'color_map_image_processor',
image: { image_name: image.name },
}),
},
content_shuffle_image_processor: {
type: 'content_shuffle_image_processor',
labelTKey: 'controlnet.contentShuffle',
descriptionTKey: 'controlnet.contentShuffleDescription',
buildDefaults: (baseModel) => ({
id: 'content_shuffle_image_processor',
type: 'content_shuffle_image_processor',
h: baseModel === 'sdxl' ? 1024 : 512,
w: baseModel === 'sdxl' ? 1024 : 512,
f: baseModel === 'sdxl' ? 512 : 256,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
depth_anything_image_processor: {
type: 'depth_anything_image_processor',
labelTKey: 'controlnet.depthAnything',
descriptionTKey: 'controlnet.depthAnythingDescription',
buildDefaults: () => ({
id: 'depth_anything_image_processor',
type: 'depth_anything_image_processor',
model_size: 'small',
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
resolution: minDim(image),
}),
},
hed_image_processor: {
type: 'hed_image_processor',
labelTKey: 'controlnet.hed',
descriptionTKey: 'controlnet.hedDescription',
buildDefaults: () => ({
id: 'hed_image_processor',
type: 'hed_image_processor',
scribble: false,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
lineart_anime_image_processor: {
type: 'lineart_anime_image_processor',
labelTKey: 'controlnet.lineartAnime',
descriptionTKey: 'controlnet.lineartAnimeDescription',
buildDefaults: () => ({
id: 'lineart_anime_image_processor',
type: 'lineart_anime_image_processor',
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
lineart_image_processor: {
type: 'lineart_image_processor',
labelTKey: 'controlnet.lineart',
descriptionTKey: 'controlnet.lineartDescription',
buildDefaults: () => ({
id: 'lineart_image_processor',
type: 'lineart_image_processor',
coarse: false,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
mediapipe_face_processor: {
type: 'mediapipe_face_processor',
labelTKey: 'controlnet.mediapipeFace',
descriptionTKey: 'controlnet.mediapipeFaceDescription',
buildDefaults: () => ({
id: 'mediapipe_face_processor',
type: 'mediapipe_face_processor',
max_faces: 1,
min_confidence: 0.5,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
midas_depth_image_processor: {
type: 'midas_depth_image_processor',
labelTKey: 'controlnet.depthMidas',
descriptionTKey: 'controlnet.depthMidasDescription',
buildDefaults: () => ({
id: 'midas_depth_image_processor',
type: 'midas_depth_image_processor',
a_mult: 2,
bg_th: 0.1,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
mlsd_image_processor: {
type: 'mlsd_image_processor',
labelTKey: 'controlnet.mlsd',
descriptionTKey: 'controlnet.mlsdDescription',
buildDefaults: () => ({
id: 'mlsd_image_processor',
type: 'mlsd_image_processor',
thr_d: 0.1,
thr_v: 0.1,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
normalbae_image_processor: {
type: 'normalbae_image_processor',
labelTKey: 'controlnet.normalBae',
descriptionTKey: 'controlnet.normalBaeDescription',
buildDefaults: () => ({
id: 'normalbae_image_processor',
type: 'normalbae_image_processor',
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
dw_openpose_image_processor: {
type: 'dw_openpose_image_processor',
labelTKey: 'controlnet.dwOpenpose',
descriptionTKey: 'controlnet.dwOpenposeDescription',
buildDefaults: () => ({
id: 'dw_openpose_image_processor',
type: 'dw_openpose_image_processor',
draw_body: true,
draw_face: false,
draw_hands: false,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
image_resolution: minDim(image),
}),
},
pidi_image_processor: {
type: 'pidi_image_processor',
labelTKey: 'controlnet.pidi',
descriptionTKey: 'controlnet.pidiDescription',
buildDefaults: () => ({
id: 'pidi_image_processor',
type: 'pidi_image_processor',
scribble: false,
safe: false,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
zoe_depth_image_processor: {
type: 'zoe_depth_image_processor',
labelTKey: 'controlnet.depthZoe',
descriptionTKey: 'controlnet.depthZoeDescription',
buildDefaults: () => ({
id: 'zoe_depth_image_processor',
type: 'zoe_depth_image_processor',
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
}),
},
};
const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']); const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox']);
export type Tool = z.infer<typeof zTool>; export type Tool = z.infer<typeof zTool>;
const zDrawingTool = zTool.extract(['brush', 'eraser']); const zDrawingTool = zTool.extract(['brush', 'eraser']);
const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, { const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, {
@ -244,7 +666,7 @@ const zInpaintMaskData = z.object({
}); });
export type InpaintMaskData = z.infer<typeof zInpaintMaskData>; export type InpaintMaskData = z.infer<typeof zInpaintMaskData>;
const zFilter = z.enum(['none', LightnessToAlphaFilter.name]); const zFilter = z.enum(['none', 'LightnessToAlphaFilter']);
export type Filter = z.infer<typeof zFilter>; export type Filter = z.infer<typeof zFilter>;
const zControlAdapterData = z.object({ const zControlAdapterData = z.object({
@ -272,6 +694,64 @@ export type ControlAdapterConfig = Pick<
'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode' 'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode'
>; >;
export const initialControlNetV2: ControlAdapterConfig = {
model: null,
weight: 1,
beginEndStepPct: [0, 1],
controlMode: 'balanced',
image: null,
processedImage: null,
processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
};
export const initialT2IAdapterV2: ControlAdapterConfig = {
model: null,
weight: 1,
beginEndStepPct: [0, 1],
controlMode: null,
image: null,
processedImage: null,
processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
};
export const initialIPAdapterV2: IPAdapterConfig = {
image: null,
model: null,
beginEndStepPct: [0, 1],
method: 'full',
clipVisionModel: 'ViT-H',
weight: 1,
};
export const buildControlNet = (id: string, overrides?: Partial<ControlAdapterConfig>): ControlAdapterConfig => {
return merge(deepClone(initialControlNetV2), { id, ...overrides });
};
export const buildT2IAdapter = (id: string, overrides?: Partial<ControlAdapterConfig>): ControlAdapterConfig => {
return merge(deepClone(initialT2IAdapterV2), { id, ...overrides });
};
export const buildIPAdapter = (id: string, overrides?: Partial<IPAdapterConfig>): IPAdapterConfig => {
return merge(deepClone(initialIPAdapterV2), { id, ...overrides });
};
export const buildControlAdapterProcessorV2 = (
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig
): ProcessorConfig | null => {
const defaultPreprocessor = modelConfig.default_settings?.preprocessor;
if (!isProcessorTypeV2(defaultPreprocessor)) {
return null;
}
const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base);
return processorConfig;
};
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
name: image_name,
width,
height,
});
export type CanvasEntity = LayerData | IPAdapterData | ControlAdapterData | RegionalGuidanceData | InpaintMaskData; export type CanvasEntity = LayerData | IPAdapterData | ControlAdapterData | RegionalGuidanceData | InpaintMaskData;
export type CanvasEntityIdentifier = Pick<CanvasEntity, 'id' | 'type'>; export type CanvasEntityIdentifier = Pick<CanvasEntity, 'id' | 'type'>;

View File

@ -1,546 +0,0 @@
import { deepClone } from 'common/util/deepClone';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { merge, omit } from 'lodash-es';
import type {
AnyInvocation,
BaseModelType,
ControlNetModelConfig,
ImageDTO,
T2IAdapterModelConfig,
} from 'services/api/types';
import { z } from 'zod';
export const zId = z.string().min(1);
const zCannyProcessorConfig = z.object({
id: zId,
type: z.literal('canny_image_processor'),
low_threshold: z.number().int().gte(0).lte(255),
high_threshold: z.number().int().gte(0).lte(255),
});
export type CannyProcessorConfig = z.infer<typeof zCannyProcessorConfig>;
const zColorMapProcessorConfig = z.object({
id: zId,
type: z.literal('color_map_image_processor'),
color_map_tile_size: z.number().int().gte(1),
});
export type ColorMapProcessorConfig = z.infer<typeof zColorMapProcessorConfig>;
const zContentShuffleProcessorConfig = z.object({
id: zId,
type: z.literal('content_shuffle_image_processor'),
w: z.number().int().gte(0),
h: z.number().int().gte(0),
f: z.number().int().gte(0),
});
export type ContentShuffleProcessorConfig = z.infer<typeof zContentShuffleProcessorConfig>;
const zDepthAnythingModelSize = z.enum(['large', 'base', 'small', 'small_v2']);
export type DepthAnythingModelSize = z.infer<typeof zDepthAnythingModelSize>;
export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize =>
zDepthAnythingModelSize.safeParse(v).success;
const zDepthAnythingProcessorConfig = z.object({
id: zId,
type: z.literal('depth_anything_image_processor'),
model_size: zDepthAnythingModelSize,
});
export type DepthAnythingProcessorConfig = z.infer<typeof zDepthAnythingProcessorConfig>;
const zHedProcessorConfig = z.object({
id: zId,
type: z.literal('hed_image_processor'),
scribble: z.boolean(),
});
export type HedProcessorConfig = z.infer<typeof zHedProcessorConfig>;
const zLineartAnimeProcessorConfig = z.object({
id: zId,
type: z.literal('lineart_anime_image_processor'),
});
export type LineartAnimeProcessorConfig = z.infer<typeof zLineartAnimeProcessorConfig>;
const zLineartProcessorConfig = z.object({
id: zId,
type: z.literal('lineart_image_processor'),
coarse: z.boolean(),
});
export type LineartProcessorConfig = z.infer<typeof zLineartProcessorConfig>;
const zMediapipeFaceProcessorConfig = z.object({
id: zId,
type: z.literal('mediapipe_face_processor'),
max_faces: z.number().int().gte(1),
min_confidence: z.number().gte(0).lte(1),
});
export type MediapipeFaceProcessorConfig = z.infer<typeof zMediapipeFaceProcessorConfig>;
const zMidasDepthProcessorConfig = z.object({
id: zId,
type: z.literal('midas_depth_image_processor'),
a_mult: z.number().gte(0),
bg_th: z.number().gte(0),
});
export type MidasDepthProcessorConfig = z.infer<typeof zMidasDepthProcessorConfig>;
const zMlsdProcessorConfig = z.object({
id: zId,
type: z.literal('mlsd_image_processor'),
thr_v: z.number().gte(0),
thr_d: z.number().gte(0),
});
export type MlsdProcessorConfig = z.infer<typeof zMlsdProcessorConfig>;
const zNormalbaeProcessorConfig = z.object({
id: zId,
type: z.literal('normalbae_image_processor'),
});
export type NormalbaeProcessorConfig = z.infer<typeof zNormalbaeProcessorConfig>;
const zDWOpenposeProcessorConfig = z.object({
id: zId,
type: z.literal('dw_openpose_image_processor'),
draw_body: z.boolean(),
draw_face: z.boolean(),
draw_hands: z.boolean(),
});
export type DWOpenposeProcessorConfig = z.infer<typeof zDWOpenposeProcessorConfig>;
const zPidiProcessorConfig = z.object({
id: zId,
type: z.literal('pidi_image_processor'),
safe: z.boolean(),
scribble: z.boolean(),
});
export type PidiProcessorConfig = z.infer<typeof zPidiProcessorConfig>;
const zZoeDepthProcessorConfig = z.object({
id: zId,
type: z.literal('zoe_depth_image_processor'),
});
export type ZoeDepthProcessorConfig = z.infer<typeof zZoeDepthProcessorConfig>;
export const zProcessorConfig = z.discriminatedUnion('type', [
zCannyProcessorConfig,
zColorMapProcessorConfig,
zContentShuffleProcessorConfig,
zDepthAnythingProcessorConfig,
zHedProcessorConfig,
zLineartAnimeProcessorConfig,
zLineartProcessorConfig,
zMediapipeFaceProcessorConfig,
zMidasDepthProcessorConfig,
zMlsdProcessorConfig,
zNormalbaeProcessorConfig,
zDWOpenposeProcessorConfig,
zPidiProcessorConfig,
zZoeDepthProcessorConfig,
]);
export type ProcessorConfig = z.infer<typeof zProcessorConfig>;
export const zImageWithDims = z.object({
name: z.string(),
width: z.number().int().positive(),
height: z.number().int().positive(),
});
export type ImageWithDims = z.infer<typeof zImageWithDims>;
export const zBeginEndStepPct = z
.tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)])
.refine(([begin, end]) => begin < end, {
message: 'Begin must be less than end',
});
const zControlAdapterBase = z.object({
id: zId,
weight: z.number().gte(-1).lte(2),
image: zImageWithDims.nullable(),
processedImage: zImageWithDims.nullable(),
processorConfig: zProcessorConfig.nullable(),
processorPendingBatchId: z.string().nullable().default(null),
beginEndStepPct: zBeginEndStepPct,
});
export const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
export type ControlModeV2 = z.infer<typeof zControlModeV2>;
export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success;
export const zControlNetConfigV2 = zControlAdapterBase.extend({
type: z.literal('controlnet'),
model: zModelIdentifierField.nullable(),
controlMode: zControlModeV2,
});
export type ControlNetConfigV2 = z.infer<typeof zControlNetConfigV2>;
export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({
type: z.literal('t2i_adapter'),
model: zModelIdentifierField.nullable(),
});
export type T2IAdapterConfigV2 = z.infer<typeof zT2IAdapterConfigV2>;
export const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']);
export type CLIPVisionModelV2 = z.infer<typeof zCLIPVisionModelV2>;
export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success;
export const zIPMethodV2 = z.enum(['full', 'style', 'composition']);
export type IPMethodV2 = z.infer<typeof zIPMethodV2>;
export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success;
export const zIPAdapterConfigV2 = z.object({
id: zId,
type: z.literal('ip_adapter'),
weight: z.number().gte(-1).lte(2),
method: zIPMethodV2,
image: zImageWithDims.nullable(),
model: zModelIdentifierField.nullable(),
clipVisionModel: zCLIPVisionModelV2,
beginEndStepPct: zBeginEndStepPct,
});
export type IPAdapterConfigV2 = z.infer<typeof zIPAdapterConfigV2>;
const zProcessorTypeV2 = z.enum([
'canny_image_processor',
'color_map_image_processor',
'content_shuffle_image_processor',
'depth_anything_image_processor',
'hed_image_processor',
'lineart_anime_image_processor',
'lineart_image_processor',
'mediapipe_face_processor',
'midas_depth_image_processor',
'mlsd_image_processor',
'normalbae_image_processor',
'dw_openpose_image_processor',
'pidi_image_processor',
'zoe_depth_image_processor',
]);
export type ProcessorTypeV2 = z.infer<typeof zProcessorTypeV2>;
export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success;
type ProcessorData<T extends ProcessorTypeV2> = {
type: T;
labelTKey: string;
descriptionTKey: string;
buildDefaults(baseModel?: BaseModelType): Extract<ProcessorConfig, { type: T }>;
buildNode(image: ImageWithDims, config: Extract<ProcessorConfig, { type: T }>): Extract<AnyInvocation, { type: T }>;
};
const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height);
type CAProcessorsData = {
[key in ProcessorTypeV2]: ProcessorData<key>;
};
/**
* A dict of ControlNet processors, including:
* - label translation key
* - description translation key
* - a builder to create default values for the config
* - a builder to create the node for the config
*
* TODO: Generate from the OpenAPI schema
*/
export const CA_PROCESSOR_DATA: CAProcessorsData = {
canny_image_processor: {
type: 'canny_image_processor',
labelTKey: 'controlnet.canny',
descriptionTKey: 'controlnet.cannyDescription',
buildDefaults: () => ({
id: 'canny_image_processor',
type: 'canny_image_processor',
low_threshold: 100,
high_threshold: 200,
}),
buildNode: (image, config) => ({
...config,
type: 'canny_image_processor',
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
color_map_image_processor: {
type: 'color_map_image_processor',
labelTKey: 'controlnet.colorMap',
descriptionTKey: 'controlnet.colorMapDescription',
buildDefaults: () => ({
id: 'color_map_image_processor',
type: 'color_map_image_processor',
color_map_tile_size: 64,
}),
buildNode: (image, config) => ({
...config,
type: 'color_map_image_processor',
image: { image_name: image.name },
}),
},
content_shuffle_image_processor: {
type: 'content_shuffle_image_processor',
labelTKey: 'controlnet.contentShuffle',
descriptionTKey: 'controlnet.contentShuffleDescription',
buildDefaults: (baseModel) => ({
id: 'content_shuffle_image_processor',
type: 'content_shuffle_image_processor',
h: baseModel === 'sdxl' ? 1024 : 512,
w: baseModel === 'sdxl' ? 1024 : 512,
f: baseModel === 'sdxl' ? 512 : 256,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
depth_anything_image_processor: {
type: 'depth_anything_image_processor',
labelTKey: 'controlnet.depthAnything',
descriptionTKey: 'controlnet.depthAnythingDescription',
buildDefaults: () => ({
id: 'depth_anything_image_processor',
type: 'depth_anything_image_processor',
model_size: 'small_v2',
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
resolution: minDim(image),
}),
},
hed_image_processor: {
type: 'hed_image_processor',
labelTKey: 'controlnet.hed',
descriptionTKey: 'controlnet.hedDescription',
buildDefaults: () => ({
id: 'hed_image_processor',
type: 'hed_image_processor',
scribble: false,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
lineart_anime_image_processor: {
type: 'lineart_anime_image_processor',
labelTKey: 'controlnet.lineartAnime',
descriptionTKey: 'controlnet.lineartAnimeDescription',
buildDefaults: () => ({
id: 'lineart_anime_image_processor',
type: 'lineart_anime_image_processor',
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
lineart_image_processor: {
type: 'lineart_image_processor',
labelTKey: 'controlnet.lineart',
descriptionTKey: 'controlnet.lineartDescription',
buildDefaults: () => ({
id: 'lineart_image_processor',
type: 'lineart_image_processor',
coarse: false,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
mediapipe_face_processor: {
type: 'mediapipe_face_processor',
labelTKey: 'controlnet.mediapipeFace',
descriptionTKey: 'controlnet.mediapipeFaceDescription',
buildDefaults: () => ({
id: 'mediapipe_face_processor',
type: 'mediapipe_face_processor',
max_faces: 1,
min_confidence: 0.5,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
midas_depth_image_processor: {
type: 'midas_depth_image_processor',
labelTKey: 'controlnet.depthMidas',
descriptionTKey: 'controlnet.depthMidasDescription',
buildDefaults: () => ({
id: 'midas_depth_image_processor',
type: 'midas_depth_image_processor',
a_mult: 2,
bg_th: 0.1,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
mlsd_image_processor: {
type: 'mlsd_image_processor',
labelTKey: 'controlnet.mlsd',
descriptionTKey: 'controlnet.mlsdDescription',
buildDefaults: () => ({
id: 'mlsd_image_processor',
type: 'mlsd_image_processor',
thr_d: 0.1,
thr_v: 0.1,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
normalbae_image_processor: {
type: 'normalbae_image_processor',
labelTKey: 'controlnet.normalBae',
descriptionTKey: 'controlnet.normalBaeDescription',
buildDefaults: () => ({
id: 'normalbae_image_processor',
type: 'normalbae_image_processor',
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
dw_openpose_image_processor: {
type: 'dw_openpose_image_processor',
labelTKey: 'controlnet.dwOpenpose',
descriptionTKey: 'controlnet.dwOpenposeDescription',
buildDefaults: () => ({
id: 'dw_openpose_image_processor',
type: 'dw_openpose_image_processor',
draw_body: true,
draw_face: false,
draw_hands: false,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
image_resolution: minDim(image),
}),
},
pidi_image_processor: {
type: 'pidi_image_processor',
labelTKey: 'controlnet.pidi',
descriptionTKey: 'controlnet.pidiDescription',
buildDefaults: () => ({
id: 'pidi_image_processor',
type: 'pidi_image_processor',
scribble: false,
safe: false,
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
detect_resolution: minDim(image),
image_resolution: minDim(image),
}),
},
zoe_depth_image_processor: {
type: 'zoe_depth_image_processor',
labelTKey: 'controlnet.depthZoe',
descriptionTKey: 'controlnet.depthZoeDescription',
buildDefaults: () => ({
id: 'zoe_depth_image_processor',
type: 'zoe_depth_image_processor',
}),
buildNode: (image, config) => ({
...config,
image: { image_name: image.name },
}),
},
};
export const initialControlNetV2: Omit<ControlNetConfigV2, 'id'> = {
type: 'controlnet',
model: null,
weight: 1,
beginEndStepPct: [0, 1],
controlMode: 'balanced',
image: null,
processedImage: null,
processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
processorPendingBatchId: null,
};
export const initialT2IAdapterV2: Omit<T2IAdapterConfigV2, 'id'> = {
type: 't2i_adapter',
model: null,
weight: 1,
beginEndStepPct: [0, 1],
image: null,
processedImage: null,
processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
processorPendingBatchId: null,
};
export const initialIPAdapterV2: Omit<IPAdapterConfigV2, 'id'> = {
type: 'ip_adapter',
image: null,
model: null,
beginEndStepPct: [0, 1],
method: 'full',
clipVisionModel: 'ViT-H',
weight: 1,
};
export const buildControlNet = (id: string, overrides?: Partial<ControlNetConfigV2>): ControlNetConfigV2 => {
return merge(deepClone(initialControlNetV2), { id, ...overrides });
};
export const buildT2IAdapter = (id: string, overrides?: Partial<T2IAdapterConfigV2>): T2IAdapterConfigV2 => {
return merge(deepClone(initialT2IAdapterV2), { id, ...overrides });
};
export const buildIPAdapter = (id: string, overrides?: Partial<IPAdapterConfigV2>): IPAdapterConfigV2 => {
return merge(deepClone(initialIPAdapterV2), { id, ...overrides });
};
export const buildControlAdapterProcessorV2 = (
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig
): ProcessorConfig | null => {
const defaultPreprocessor = modelConfig.default_settings?.preprocessor;
if (!isProcessorTypeV2(defaultPreprocessor)) {
return null;
}
const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base);
return processorConfig;
};
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
name: image_name,
width,
height,
});
export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfigV2): ControlNetConfigV2 => {
return {
...deepClone(t2iAdapter),
type: 'controlnet',
controlMode: initialControlNetV2.controlMode,
};
};
export const controlNetToT2IAdapter = (controlNet: ControlNetConfigV2): T2IAdapterConfigV2 => {
return {
...omit(deepClone(controlNet), 'controlMode'),
type: 't2i_adapter',
};
};

View File

@ -22,39 +22,32 @@ export type CurrentImageDropData = BaseDropData & {
actionType: 'SET_CURRENT_IMAGE'; actionType: 'SET_CURRENT_IMAGE';
}; };
type ControlAdapterDropData = BaseDropData & { export type CAImageDropData = BaseDropData & {
actionType: 'SET_CONTROL_ADAPTER_IMAGE'; actionType: 'SET_CA_IMAGE';
context: { context: {
id: string; id: string;
}; };
}; };
export type CALayerImageDropData = BaseDropData & { export type IPAImageDropData = BaseDropData & {
actionType: 'SET_CA_LAYER_IMAGE'; actionType: 'SET_IPA_IMAGE';
context: { context: {
layerId: string; id: string;
}; };
}; };
export type IPALayerImageDropData = BaseDropData & { export type RGIPAdapterImageDropData = BaseDropData & {
actionType: 'SET_IPA_LAYER_IMAGE'; actionType: 'SET_RG_IP_ADAPTER_IMAGE';
context: { context: {
layerId: string; id: string;
};
};
export type RGLayerIPAdapterImageDropData = BaseDropData & {
actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE';
context: {
layerId: string;
ipAdapterId: string; ipAdapterId: string;
}; };
}; };
export type IILayerImageDropData = BaseDropData & { export type LayerImageDropData = BaseDropData & {
actionType: 'SET_II_LAYER_IMAGE'; actionType: 'ADD_LAYER_IMAGE';
context: { context: {
layerId: string; id: string;
}; };
}; };
@ -100,16 +93,14 @@ export type SelectForCompareDropData = BaseDropData & {
export type TypesafeDroppableData = export type TypesafeDroppableData =
| CurrentImageDropData | CurrentImageDropData
| ControlAdapterDropData
| CanvasInitialImageDropData
| NodesImageDropData | NodesImageDropData
| AddToBoardDropData | AddToBoardDropData
| RemoveFromBoardDropData | RemoveFromBoardDropData
| CALayerImageDropData | CAImageDropData
| IPALayerImageDropData | IPAImageDropData
| RGLayerIPAdapterImageDropData | RGIPAdapterImageDropData
| IILayerImageDropData
| SelectForCompareDropData | SelectForCompareDropData
| RasterLayerImageDropData
| UpscaleInitialImageDropData; | UpscaleInitialImageDropData;
type BaseDragData = { type BaseDragData = {

View File

@ -1,16 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ControlSettingsAccordion } from './ControlSettingsAccordion';
const meta: Meta<typeof ControlSettingsAccordion> = {
title: 'Feature/ControlSettingsAccordion',
tags: ['autodocs'],
component: ControlSettingsAccordion,
};
export default meta;
type Story = StoryObj<typeof ControlSettingsAccordion>;
export const Default: Story = {
render: () => <ControlSettingsAccordion />,
};

View File

@ -1,125 +0,0 @@
import { Button, ButtonGroup, Divider, Flex, StandaloneAccordion } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import ControlAdapterConfig from 'features/controlAdapters/components/ControlAdapterConfig';
import { useAddControlAdapter } from 'features/controlAdapters/hooks/useAddControlAdapter';
import {
selectAllControlNets,
selectAllIPAdapters,
selectAllT2IAdapters,
selectControlAdapterIds,
selectControlAdaptersSlice,
selectValidControlNets,
selectValidIPAdapters,
selectValidT2IAdapters,
} from 'features/controlAdapters/store/controlAdaptersSlice';
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], (controlAdapters) => {
const badges: string[] = [];
let isError = false;
const enabledNonRegionalIPAdapterCount = selectAllIPAdapters(controlAdapters).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) => 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();
const { controlAdapterIds, badges } = useAppSelector(selector);
const isControlNetEnabled = useFeatureStatus('controlNet');
const { isOpen, onToggle } = useStandaloneAccordionToggle({
id: 'control-settings',
defaultIsOpen: true,
});
const [addControlNet, isAddControlNetDisabled] = useAddControlAdapter('controlnet');
const [addIPAdapter, isAddIPAdapterDisabled] = useAddControlAdapter('ip_adapter');
const [addT2IAdapter, isAddT2IAdapterDisabled] = useAddControlAdapter('t2i_adapter');
if (!isControlNetEnabled) {
return null;
}
return (
<StandaloneAccordion label={t('accordions.control.title')} badges={badges} isOpen={isOpen} onToggle={onToggle}>
<Flex gap={2} p={4} flexDir="column" data-testid="control-accordion">
<ButtonGroup size="sm" w="full" justifyContent="space-between" variant="ghost" isAttached={false}>
<Button
tooltip={t('controlnet.addControlNet')}
leftIcon={<PiPlusBold />}
onClick={addControlNet}
data-testid="add controlnet"
flexGrow={1}
isDisabled={isAddControlNetDisabled}
>
{t('common.controlNet')}
</Button>
<Button
tooltip={t('controlnet.addIPAdapter')}
leftIcon={<PiPlusBold />}
onClick={addIPAdapter}
data-testid="add ip adapter"
flexGrow={1}
isDisabled={isAddIPAdapterDisabled}
>
{t('common.ipAdapter')}
</Button>
<Button
tooltip={t('controlnet.addT2IAdapter')}
leftIcon={<PiPlusBold />}
onClick={addT2IAdapter}
data-testid="add t2i adapter"
flexGrow={1}
isDisabled={isAddT2IAdapterDisabled}
>
{t('common.t2iAdapter')}
</Button>
</ButtonGroup>
{controlAdapterIds.map((id, i) => (
<Fragment key={id}>
<Divider />
<ControlAdapterConfig id={id} number={i + 1} />
</Fragment>
))}
</Flex>
</StandaloneAccordion>
);
});
ControlSettingsAccordion.displayName = 'ControlAdaptersSettingsAccordion';

View File

@ -2,7 +2,6 @@ import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library'; import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { HrfSettings } from 'features/hrf/components/HrfSettings'; import { HrfSettings } from 'features/hrf/components/HrfSettings';
import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; import { selectHrfSlice } from 'features/hrf/store/hrfSlice';
@ -20,34 +19,22 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ImageSizeCanvas } from './ImageSizeCanvas';
import { ImageSizeLinear } from './ImageSizeLinear'; import { ImageSizeLinear } from './ImageSizeLinear';
const selector = createMemoizedSelector( const selector = createMemoizedSelector(
[selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector], [selectGenerationSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector],
(generation, canvas, hrf, controlLayers, activeTabName) => { (generation, hrf, canvasV2, activeTabName) => {
const { shouldRandomizeSeed, model } = generation; const { shouldRandomizeSeed, model } = generation;
const { hrfEnabled } = hrf; const { hrfEnabled } = hrf;
const badges: string[] = []; const badges: string[] = [];
const isSDXL = model?.base === 'sdxl'; const isSDXL = model?.base === 'sdxl';
if (activeTabName === 'canvas') { const { aspectRatio, width, height } = canvasV2.size;
const { badges.push(`${width}×${height}`);
aspectRatio, badges.push(aspectRatio.id);
boundingBoxDimensions: { width, height },
} = canvas; if (aspectRatio.isLocked) {
badges.push(`${width}×${height}`); badges.push('locked');
badges.push(aspectRatio.id);
if (aspectRatio.isLocked) {
badges.push('locked');
}
} else {
const { aspectRatio, width, height } = canvasV2.size;
badges.push(`${width}×${height}`);
badges.push(aspectRatio.id);
if (aspectRatio.isLocked) {
badges.push('locked');
}
} }
if (!shouldRandomizeSeed) { if (!shouldRandomizeSeed) {
@ -86,8 +73,8 @@ export const ImageSettingsAccordion = memo(() => {
> >
<Flex px={4} pt={4} w="full" h="full" flexDir="column" data-testid="image-settings-accordion"> <Flex px={4} pt={4} w="full" h="full" flexDir="column" data-testid="image-settings-accordion">
<Flex flexDir="column" gap={4}> <Flex flexDir="column" gap={4}>
{activeTabName === 'canvas' ? <ImageSizeCanvas /> : <ImageSizeLinear />} <ImageSizeLinear />
{activeTabName === 'canvas' && <ParamImageToImageStrength />} <ParamImageToImageStrength />
</Flex> </Flex>
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}> <Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
<Flex gap={4} pb={4} flexDir="column"> <Flex gap={4} pb={4} flexDir="column">

View File

@ -1,59 +0,0 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { aspectRatioChanged, setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import ParamBoundingBoxHeight from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxHeight';
import ParamBoundingBoxWidth from 'features/parameters/components/Canvas/BoundingBox/ParamBoundingBoxWidth';
import { AspectRatioIconPreview } from 'features/parameters/components/ImageSize/AspectRatioIconPreview';
import { ImageSize } from 'features/parameters/components/ImageSize/ImageSize';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { memo, useCallback } from 'react';
export const ImageSizeCanvas = memo(() => {
const dispatch = useAppDispatch();
const { width, height } = useAppSelector((s) => s.canvas.boundingBoxDimensions);
const aspectRatioState = useAppSelector((s) => s.canvas.aspectRatio);
const optimalDimension = useAppSelector(selectOptimalDimension);
const onChangeWidth = useCallback(
(width: number) => {
if (width === 0) {
return;
}
dispatch(setBoundingBoxDimensions({ width }, optimalDimension));
},
[dispatch, optimalDimension]
);
const onChangeHeight = useCallback(
(height: number) => {
if (height === 0) {
return;
}
dispatch(setBoundingBoxDimensions({ height }, optimalDimension));
},
[dispatch, optimalDimension]
);
const onChangeAspectRatioState = useCallback(
(aspectRatioState: AspectRatioState) => {
dispatch(aspectRatioChanged(aspectRatioState));
},
[dispatch]
);
return (
<ImageSize
width={width}
height={height}
aspectRatioState={aspectRatioState}
heightComponent={<ParamBoundingBoxHeight />}
widthComponent={<ParamBoundingBoxWidth />}
previewComponent={<AspectRatioIconPreview />}
onChangeAspectRatioState={onChangeAspectRatioState}
onChangeWidth={onChangeWidth}
onChangeHeight={onChangeHeight}
/>
);
});
ImageSizeCanvas.displayName = 'ImageSizeCanvas';

View File

@ -1,6 +1,4 @@
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
import { toast } from 'features/toast/toast'; import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -40,8 +38,8 @@ export const useClearIntermediates = (shouldShowClearIntermediates: boolean): Us
_clearIntermediates() _clearIntermediates()
.unwrap() .unwrap()
.then((clearedCount) => { .then((clearedCount) => {
dispatch(controlAdaptersReset()); // dispatch(controlAdaptersReset());
dispatch(resetCanvas()); // dispatch(resetCanvas());
toast({ toast({
id: 'INTERMEDIATES_CLEARED', id: 'INTERMEDIATES_CLEARED',
title: t('settings.intermediatesCleared', { count: clearedCount }), title: t('settings.intermediatesCleared', { count: clearedCount }),

View File

@ -16,7 +16,6 @@ import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
import NodesTab from 'features/ui/components/tabs/NodesTab'; import NodesTab from 'features/ui/components/tabs/NodesTab';
import QueueTab from 'features/ui/components/tabs/QueueTab'; import QueueTab from 'features/ui/components/tabs/QueueTab';
import TextToImageTab from 'features/ui/components/tabs/TextToImageTab'; import TextToImageTab from 'features/ui/components/tabs/TextToImageTab';
import UnifiedCanvasTab from 'features/ui/components/tabs/UnifiedCanvasTab';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel'; import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel'; import { usePanel } from 'features/ui/hooks/usePanel';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
@ -30,11 +29,10 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MdZoomOutMap } from 'react-icons/md'; import { MdZoomOutMap } from 'react-icons/md';
import { PiFlowArrowBold } from 'react-icons/pi'; import { PiFlowArrowBold } from 'react-icons/pi';
import { RiBox2Line, RiBrushLine, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri'; import { RiBox2Line, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels';
import ParametersPanelCanvas from './ParametersPanels/ParametersPanelCanvas';
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale'; import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
import ResizeHandle from './tabs/ResizeHandle'; import ResizeHandle from './tabs/ResizeHandle';
import UpscalingTab from './tabs/UpscalingTab'; import UpscalingTab from './tabs/UpscalingTab';
@ -55,13 +53,6 @@ const TAB_DATA: Record<InvokeTabName, TabData> = {
content: <TextToImageTab />, content: <TextToImageTab />,
parametersPanel: <ParametersPanelTextToImage />, parametersPanel: <ParametersPanelTextToImage />,
}, },
canvas: {
id: 'canvas',
translationKey: 'ui.tabs.canvas',
icon: <RiBrushLine />,
content: <UnifiedCanvasTab />,
parametersPanel: <ParametersPanelCanvas />,
},
upscaling: { upscaling: {
id: 'upscaling', id: 'upscaling',
translationKey: 'ui.tabs.upscaling', translationKey: 'ui.tabs.upscaling',

View File

@ -1,59 +0,0 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
import { ControlSettingsAccordion } from 'features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion';
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
import { StylePresetMenu } from 'features/stylePresets/components/StylePresetMenu';
import { StylePresetMenuTrigger } from 'features/stylePresets/components/StylePresetMenuTrigger';
import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo } from 'react';
const overlayScrollbarsStyles: CSSProperties = {
height: '100%',
width: '100%',
};
const ParametersPanelCanvas = () => {
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
const isMenuOpen = useStore($isMenuOpen);
return (
<Flex w="full" h="full" flexDir="column" gap={2}>
<QueueControls />
<StylePresetMenuTrigger />
<Flex w="full" h="full" position="relative">
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
{isMenuOpen && (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<StylePresetMenu />
</Flex>
</OverlayScrollbarsComponent>
)}
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex gap={2} flexDirection="column" h="full" w="full">
<Prompts />
<ImageSettingsAccordion />
<GenerationSettingsAccordion />
<ControlSettingsAccordion />
<CompositingSettingsAccordion />
{isSDXL && <RefinerSettingsAccordion />}
<AdvancedSettingsAccordion />
</Flex>
</OverlayScrollbarsComponent>
</Box>
</Flex>
</Flex>
);
};
export default memo(ParametersPanelCanvas);

View File

@ -10,7 +10,6 @@ import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls'; import QueueControls from 'features/queue/components/QueueControls';
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion'; import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion'; import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
import { ControlSettingsAccordion } from 'features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion';
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion'; import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion'; import { ImageSettingsAccordion } from 'features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion';
import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion'; import { RefinerSettingsAccordion } from 'features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion';
@ -44,7 +43,7 @@ const ParametersPanelTextToImage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector); const activeTabName = useAppSelector(activeTabNameSelector);
const controlLayersCount = useAppSelector((s) => s.canvasV2.layers.length); const controlLayersCount = useAppSelector((s) => s.layers.layers.length);
const controlLayersTitle = useMemo(() => { const controlLayersTitle = useMemo(() => {
if (controlLayersCount === 0) { if (controlLayersCount === 0) {
return t('controlLayers.controlLayers'); return t('controlLayers.controlLayers');
@ -108,8 +107,7 @@ const ParametersPanelTextToImage = () => {
<Flex gap={2} flexDirection="column" h="full" w="full"> <Flex gap={2} flexDirection="column" h="full" w="full">
<ImageSettingsAccordion /> <ImageSettingsAccordion />
<GenerationSettingsAccordion /> <GenerationSettingsAccordion />
{activeTabName !== 'generation' && <ControlSettingsAccordion />} <CompositingSettingsAccordion />
{activeTabName === 'canvas' && <CompositingSettingsAccordion />}
{isSDXL && <RefinerSettingsAccordion />} {isSDXL && <RefinerSettingsAccordion />}
<AdvancedSettingsAccordion /> <AdvancedSettingsAccordion />
</Flex> </Flex>

View File

@ -1,51 +0,0 @@
import { Flex } from '@invoke-ai/ui-library';
import IAIDropOverlay from 'common/components/IAIDropOverlay';
import IAICanvas from 'features/canvas/components/IAICanvas';
import IAICanvasToolbar from 'features/canvas/components/IAICanvasToolbar/IAICanvasToolbar';
import { CANVAS_TAB_TESTID } from 'features/canvas/store/constants';
import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks';
import type { CanvasInitialImageDropData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const droppableData: CanvasInitialImageDropData = {
id: 'canvas-intial-image',
actionType: 'SET_CANVAS_INITIAL_IMAGE',
};
const UnifiedCanvasTab = () => {
const { t } = useTranslation();
const {
isOver,
setNodeRef: setDroppableRef,
active,
} = useDroppableTypesafe({
id: 'unifiedCanvas',
data: droppableData,
});
return (
<Flex
position="relative"
layerStyle="first"
ref={setDroppableRef}
flexDirection="column"
alignItems="center"
gap={4}
p={2}
borderRadius="base"
w="full"
h="full"
data-testid={CANVAS_TAB_TESTID}
>
<IAICanvasToolbar />
<IAICanvas />
{isValidDrop(droppableData, active?.data.current) && (
<IAIDropOverlay isOver={isOver} label={t('toast.setCanvasInitialImage')} />
)}
</Flex>
);
};
export default memo(UnifiedCanvasTab);

View File

@ -1,3 +1,3 @@
export const TAB_NUMBER_MAP = ['generation', 'canvas', 'upscaling', 'workflows', 'models', 'queue'] as const; export const TAB_NUMBER_MAP = ['generation', 'upscaling', 'workflows', 'models', 'queue'] as const;
export type InvokeTabName = (typeof TAB_NUMBER_MAP)[number]; export type InvokeTabName = (typeof TAB_NUMBER_MAP)[number];

View File

@ -195,44 +195,28 @@ export type OutputFields<T extends AnyInvocation> = Extract<
// Node Outputs // Node Outputs
export type ImageOutput = S['ImageOutput']; export type ImageOutput = S['ImageOutput'];
// Post-image upload actions, controls workflows when images are uploaded export type CAImagePostUploadAction = {
type: 'SET_CA_IMAGE';
type ControlAdapterAction = {
type: 'SET_CONTROL_ADAPTER_IMAGE';
id: string; id: string;
}; };
export type CALayerImagePostUploadAction = {
type: 'SET_CA_LAYER_IMAGE';
layerId: string;
};
export type IPALayerImagePostUploadAction = { export type IPALayerImagePostUploadAction = {
type: 'SET_IPA_LAYER_IMAGE'; type: 'SET_IPA_IMAGE';
layerId: string; id: string;
}; };
export type RGLayerIPAdapterImagePostUploadAction = { export type RGIPAdapterImagePostUploadAction = {
type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE'; type: 'SET_RG_IP_ADAPTER_IMAGE';
layerId: string; id: string;
ipAdapterId: string; ipAdapterId: string;
}; };
export type IILayerImagePostUploadAction = {
type: 'SET_II_LAYER_IMAGE';
layerId: string;
};
type NodesAction = { type NodesAction = {
type: 'SET_NODES_IMAGE'; type: 'SET_NODES_IMAGE';
nodeId: string; nodeId: string;
fieldName: string; fieldName: string;
}; };
type CanvasInitialImageAction = {
type: 'SET_CANVAS_INITIAL_IMAGE';
};
type UpscaleInitialImageAction = { type UpscaleInitialImageAction = {
type: 'SET_UPSCALE_INITIAL_IMAGE'; type: 'SET_UPSCALE_INITIAL_IMAGE';
}; };
@ -247,13 +231,10 @@ type AddToBatchAction = {
}; };
export type PostUploadAction = export type PostUploadAction =
| ControlAdapterAction
| NodesAction | NodesAction
| CanvasInitialImageAction
| ToastAction | ToastAction
| AddToBatchAction | AddToBatchAction
| CALayerImagePostUploadAction | CAImagePostUploadAction
| IPALayerImagePostUploadAction | IPALayerImagePostUploadAction
| RGLayerIPAdapterImagePostUploadAction | RGIPAdapterImagePostUploadAction
| IILayerImagePostUploadAction
| UpscaleInitialImageAction; | UpscaleInitialImageAction;