refactor(ui): canvas v2 (wip)

This commit is contained in:
psychedelicious 2024-06-14 21:14:37 +10:00
parent 58c656224f
commit 9fe727c9f8
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 { RootState } from 'app/store/store';
import { isEqual } from 'lodash-es';
/**
@ -19,3 +20,5 @@ export const getSelectorsOptions: GetSelectorsOptions = {
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 { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
import type { JSONObject } from 'common/types';
import { canvasPersistConfig } from 'features/canvas/store/canvasSlice';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import {
controlAdaptersV2PersistConfig,
@ -104,7 +103,6 @@ export type PersistConfig<T = any> = {
};
const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[canvasPersistConfig.name]: canvasPersistConfig,
[galleryPersistConfig.name]: galleryPersistConfig,
[generationPersistConfig.name]: generationPersistConfig,
[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 { InvokeTabName } from 'features/ui/store/tabMap';
import type { O } from 'ts-toolbelt';
@ -83,7 +83,7 @@ export type AppConfig = {
sd: {
defaultModel?: string;
disabledControlNetModels: string[];
disabledControlNetProcessors: (keyof typeof CONTROLNET_PROCESSORS)[];
disabledControlNetProcessors: ProcessorTypeV2;
// Core parameters
iterations: NumericalParameterConfig;
width: NumericalParameterConfig; // initial value comes from model

View File

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

View File

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

View File

@ -1,13 +1,12 @@
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import {
selectControlAdapterAll,
selectControlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice';
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 { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
@ -24,43 +23,49 @@ import { forEach, upperFirst } from 'lodash-es';
import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow';
const LAYER_TYPE_TO_TKEY: Record<LayerData['type'], string> = {
initial_image_layer: 'controlLayers.globalInitialImage',
control_adapter_layer: 'controlLayers.globalControlAdapter',
ip_adapter_layer: 'controlLayers.globalIPAdapter',
regional_guidance_layer: 'controlLayers.regionalGuidance',
raster_layer: 'controlLayers.raster',
const LAYER_TYPE_TO_TKEY: Record<CanvasEntity['type'], string> = {
control_adapter: 'controlLayers.globalControlAdapter',
ip_adapter: 'controlLayers.globalIPAdapter',
regional_guidance: 'controlLayers.regionalGuidance',
layer: 'controlLayers.raster',
inpaint_mask: 'controlLayers.inpaintMask',
};
const createSelector = (templates: Templates) =>
createMemoizedSelector(
[
selectControlAdaptersSlice,
selectGenerationSlice,
selectSystemSlice,
selectNodesSlice,
selectWorkflowSettingsSlice,
selectDynamicPromptsSlice,
selectCanvasV2Slice,
selectLayersSlice,
selectControlAdaptersV2Slice,
selectRegionalGuidanceSlice,
selectIPAdaptersSlice,
activeTabNameSelector,
selectUpscalelice,
selectConfigSlice,
],
(
controlAdapters,
generation,
system,
nodes,
workflowSettings,
dynamicPrompts,
controlLayers,
canvasV2,
layersState,
controlAdaptersState,
regionalGuidanceState,
ipAdaptersState,
activeTabName,
upscale,
config
) => {
const { model } = generation;
const { size } = canvasV2;
const { positivePrompt } = canvasV2;
const { positivePrompt } = canvasV2.prompts;
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 {
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
@ -124,140 +149,128 @@ const createSelector = (templates: Templates) =>
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
}
if (activeTabName === 'generation') {
// Handling for generation tab
canvasV2.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 === 'control_adapter_layer') {
// 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') });
controlAdaptersState.controlAdapters
.filter((ca) => ca.isEnabled)
.forEach((ca, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ca.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have model
if (!ca.model) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
}
}
if (!upscale.upscaleModel) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') });
}
if (!upscale.tileControlnetModel) {
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
}
} else {
// Handling for all other tabs
selectControlAdapterAll(controlAdapters)
.filter((ca) => ca.isEnabled)
.forEach((ca, i) => {
if (!ca.isEnabled) {
return;
// Model base must match
if (ca.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 (!ca.image) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected'));
} else if (ca.processorConfig && !ca.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 (!ca.controlMode) {
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 (!ca.model) {
reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) });
} else if (ca.model.base !== model?.base) {
// This should never happen, just a sanity check
reasons.push({
content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }),
});
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
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'));
}
if (
!ca.controlImage ||
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
) {
reasons.push({
content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }),
});
// 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 });
}
});
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 };

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

View File

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

View File

@ -10,33 +10,33 @@ import {
PopoverTrigger,
} from '@invoke-ai/ui-library';
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 { useTranslation } from 'react-i18next';
const marks = [0, 100, 200, 300];
const formatPx = (v: number | string) => `${v} px`;
export const BrushSize = memo(() => {
export const BrushWidth = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const brushSize = useAppSelector((s) => s.canvasV2.brushSize);
const width = useAppSelector((s) => s.canvasV2.tool.brush.width);
const onChange = useCallback(
(v: number) => {
dispatch(brushSizeChanged(Math.round(v)));
dispatch(brushWidthChanged(Math.round(v)));
},
[dispatch]
);
return (
<FormControl w="min-content" gap={2}>
<FormLabel m={0}>{t('controlLayers.brushSize')}</FormLabel>
<FormLabel m={0}>{t('controlLayers.brushWidth')}</FormLabel>
<Popover isLazy>
<PopoverTrigger>
<CompositeNumberInput
min={1}
max={600}
defaultValue={initialControlLayersState.brushSize}
value={brushSize}
defaultValue={50}
value={width}
onChange={onChange}
w={24}
format={formatPx}
@ -45,14 +45,7 @@ export const BrushSize = memo(() => {
<PopoverContent w={200} py={2} px={4}>
<PopoverArrow />
<PopoverBody>
<CompositeSlider
min={1}
max={300}
defaultValue={initialControlLayersState.brushSize}
value={brushSize}
onChange={onChange}
marks={marks}
/>
<CompositeSlider min={1} max={300} defaultValue={50} value={width} onChange={onChange} marks={marks} />
</PopoverBody>
</PopoverContent>
</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 ariaLabel = ['Begin Step %', 'End Step %'];
export const ControlAdapterBeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => {
export const BeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => {
const { t } = useTranslation();
const onReset = useCallback(() => {
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 marks = [0, 1, 2];
export const ControlAdapterWeight = memo(({ weight, onChange }: Props) => {
export const Weight = memo(({ weight, onChange }: Props) => {
const { t } = useTranslation();
const initial = useAppSelector((s) => s.config.sd.ca.weight.initial);
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 { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { ControlModeV2 } from 'features/controlLayers/util/controlAdapters';
import { isControlModeV2 } from 'features/controlLayers/util/controlAdapters';
import type { ControlModeV2} from 'features/controlLayers/store/types';
import { isControlModeV2 } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
@ -12,7 +12,7 @@ type Props = {
onChange: (controlMode: ControlModeV2) => void;
};
export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => {
export const CAControlModeSelect = memo(({ controlMode, onChange }: Props) => {
const { t } = useTranslation();
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 IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { ControlNetConfigV2, T2IAdapterConfigV2 } from 'features/controlLayers/util/controlAdapters';
import type { ControlAdapterData } from 'features/controlLayers/store/types';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi';
@ -22,7 +20,7 @@ import {
import type { ImageDTO, PostUploadAction } from 'services/api/types';
type Props = {
controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2;
controlAdapter: ControlAdapterData;
onChangeImage: (imageDTO: ImageDTO | null) => void;
droppableData: TypesafeDroppableData;
postUploadAction: PostUploadAction;
@ -30,7 +28,7 @@ type Props = {
onErrorLoadingProcessedImage: () => void;
};
export const ControlAdapterImagePreview = memo(
export const CAImagePreview = memo(
({
controlAdapter,
onChangeImage,
@ -43,7 +41,6 @@ export const ControlAdapterImagePreview = memo(
const dispatch = useAppDispatch();
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const isConnected = useAppSelector((s) => s.system.isConnected);
const activeTabName = useAppSelector(activeTabNameSelector);
const optimalDimension = useAppSelector(selectOptimalDimension);
const shift = useShiftModifier();
@ -88,27 +85,21 @@ export const ControlAdapterImagePreview = memo(
return;
}
if (activeTabName === 'canvas') {
dispatch(
setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)
);
} else {
const options = { updateAspectRatio: true, clamp: true };
const options = { updateAspectRatio: true, clamp: true };
if (shift) {
const { width, height } = controlImage;
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(
controlImage.width / controlImage.height,
optimalDimension * optimalDimension
);
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
}
if (shift) {
const { width, height } = controlImage;
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(
controlImage.width / controlImage.height,
optimalDimension * optimalDimension
);
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
}
}, [controlImage, activeTabName, dispatch, optimalDimension, shift]);
}, [controlImage, dispatch, optimalDimension, shift]);
const handleMouseEnter = useCallback(() => {
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;
};
export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => {
export const CAModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => {
const { t } = useTranslation();
const currentBaseModel = useAppSelector((s) => s.generation.model?.base);
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 { stopPropagation } from 'common/util/stopPropagation';
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 { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi';
type Props = {
layerId: string;
id: string;
};
const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`;
const CALayerOpacity = ({ layerId }: Props) => {
export const CAOpacityAndFilter = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { opacity, isFilterEnabled } = useCALayerOpacity(layerId);
const { opacity, isFilterEnabled } = useCALayerOpacity(id);
const onChangeOpacity = useCallback(
(v: number) => {
dispatch(layerOpacityChanged({ layerId, opacity: v / 100 }));
dispatch(caOpacityChanged({ id, opacity: v / 100 }));
},
[dispatch, layerId]
[dispatch, id]
);
const onChangeFilter = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(caLayerIsFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked }));
dispatch(caFilterChanged({ id, filter: e.target.checked ? 'LightnessToAlphaFilter' : 'none' }));
},
[dispatch, layerId]
[dispatch, id]
);
return (
<Popover isLazy>
@ -93,6 +93,6 @@ const CALayerOpacity = ({ layerId }: Props) => {
</PopoverContent>
</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 { 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 = {
config: ProcessorConfig | null;
onChange: (config: ProcessorConfig | null) => void;
};
export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) => {
export const CAProcessorConfig = memo(({ config, onChange }: Props) => {
if (!config) {
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 { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/util/controlAdapters';
import type {ProcessorConfig } from 'features/controlLayers/store/types';
import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/store/types';
import { configSelector } from 'features/system/store/configSelectors';
import { includes, map } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
@ -22,7 +22,7 @@ const selectDisabledProcessors = createMemoizedSelector(
(config) => config.sd.disabledControlNetProcessors
);
export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Props) => {
export const CAProcessorTypeSelect = memo(({ config, onChange }: Props) => {
const { t } = useTranslation();
const disabledProcessors = useAppSelector(selectDisabledProcessors);
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 { ControlAdapterBeginEndStepPct } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct';
import { BeginEndStepPct } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct';
import { ControlAdapterWeight } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight';
import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview';
import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod';
@ -49,7 +49,7 @@ export const IPAdapter = memo(
<Flex flexDir="column" gap={3} w="full">
<IPAdapterMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
<ControlAdapterWeight weight={ipAdapter.weight} onChange={onChangeWeight} />
<ControlAdapterBeginEndStepPct
<BeginEndStepPct
beginEndStepPct={ipAdapter.beginEndStepPct}
onChange={onChangeBeginEndStepPct}
/>

View File

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

View File

@ -2,7 +2,7 @@
import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
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 { ToolChooser } from 'features/controlLayers/components/ToolChooser';
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
@ -28,7 +28,7 @@ export const ControlLayersToolbar = memo(() => {
</Flex>
</Flex>
<Flex flex={1} gap={2} justifyContent="center" alignItems="center">
{withBrushSize && <BrushSize />}
{withBrushSize && <BrushWidth />}
{withBrushColor && <BrushColorPicker />}
</Flex>
<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 { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { brushColorChanged } from 'features/controlLayers/store/controlLayersSlice';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { fillChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const BrushColorPicker = memo(() => {
export const FillColorPicker = memo(() => {
const { t } = useTranslation();
const brushColor = useAppSelector((s) => s.canvasV2.brushColor);
const fill = useAppSelector((s) => s.canvasV2.tool.fill);
const dispatch = useAppDispatch();
const onChange = useCallback(
(color: RgbaColor) => {
dispatch(brushColorChanged(color));
dispatch(fillChanged(color));
},
[dispatch]
);
@ -25,7 +25,7 @@ export const BrushColorPicker = memo(() => {
aria-label={t('controlLayers.brushColor')}
borderRadius="full"
borderWidth={1}
bg={rgbaColorToString(brushColor)}
bg={rgbaColorToString(fill)}
w={8}
h={8}
cursor="pointer"
@ -34,11 +34,11 @@ export const BrushColorPicker = memo(() => {
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<IAIColorPicker color={brushColor} onChange={onChange} withNumberInput />
<IAIColorPicker color={fill} onChange={onChange} withNumberInput />
</PopoverBody>
</PopoverContent>
</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 { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview';
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 { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import {
iiLayerDenoisingStrengthChanged,
@ -67,11 +67,11 @@ export const IILayer = memo(({ layerId }: Props) => {
return (
<LayerWrapper onClick={onClick} borderColor={layer.isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} />
<LayerTitle type="initial_image_layer" />
<EntityEnabledToggle layerId={layerId} />
<EntityTitle type="initial_image_layer" />
<Spacer />
<LayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} />
<EntityMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (

View File

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

View File

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

View File

@ -1,19 +1,13 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
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 dispatch = useAppDispatch();
const deleteLayer = useCallback(() => {
dispatch(layerDeleted(layerId));
}, [dispatch, layerId]);
return (
<IconButton
size="sm"
@ -21,10 +15,10 @@ export const LayerDeleteButton = memo(({ layerId }: Props) => {
aria-label={t('common.delete')}
tooltip={t('common.delete')}
icon={<PiTrashSimpleBold />}
onClick={deleteLayer}
onClick={onDelete}
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 { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { useLayerIsEnabled } from 'features/controlLayers/hooks/layerStateHooks';
import { layerIsEnabledToggled } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCheckBold } from 'react-icons/pi';
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 dispatch = useAppDispatch();
const isEnabled = useLayerIsEnabled(layerId);
const onClick = useCallback(() => {
dispatch(layerIsEnabledToggled(layerId));
}, [dispatch, layerId]);
return (
<IconButton
@ -26,11 +19,11 @@ export const LayerIsEnabledToggle = memo(({ layerId }: Props) => {
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
variant="outline"
icon={isEnabled ? <PiCheckBold /> : undefined}
onClick={onClick}
onClick={onToggle}
colorScheme="base"
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 };
export const LayerMenu = memo(({ layerId }: Props) => {
export const EntityMenu = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
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 { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks';
import {
regionalGuidanceNegativePromptChanged,
regionalGuidancePositivePromptChanged,
@ -18,7 +18,7 @@ type Props = { layerId: string };
export const LayerMenuRGActions = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId);
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(layerId);
const selectValidActions = useMemo(
() =>
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 { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons';
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 { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
@ -52,8 +52,8 @@ export const RGLayer = memo(({ layerId }: Props) => {
return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? color : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} />
<LayerTitle type="regional_guidance_layer" />
<EntityEnabledToggle layerId={layerId} />
<EntityTitle type="regional_guidance_layer" />
<Spacer />
{autoNegative === 'invert' && (
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
@ -62,12 +62,12 @@ export const RGLayer = memo(({ layerId }: Props) => {
)}
<RGLayerColorPicker layerId={layerId} />
<RGLayerSettingsPopover layerId={layerId} />
<LayerMenu layerId={layerId} />
<EntityMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<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} />}
{hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />}
{hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />}

View File

@ -12,10 +12,10 @@ import {
selectRGLayerIPAdapterOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
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 { 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 = {
layerId: string;
@ -78,7 +78,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
[dispatch, ipAdapterId, layerId]
);
const droppableData = useMemo<RGLayerIPAdapterImageDropData>(
const droppableData = useMemo<RGIPAdapterImageDropData>(
() => ({
actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE',
context: {
@ -90,7 +90,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
[ipAdapterId, layerId]
);
const postUploadAction = useMemo<RGLayerIPAdapterImagePostUploadAction>(
const postUploadAction = useMemo<RGIPAdapterImagePostUploadAction>(
() => ({
type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE',
layerId,

View File

@ -2,14 +2,14 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDroppable from 'common/components/IAIDroppable';
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 { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
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';
type Props = {
@ -27,7 +27,7 @@ export const RasterLayer = memo(({ layerId }: Props) => {
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const droppableData = useMemo(() => {
const _droppableData: RasterLayerImageDropData = {
const _droppableData: LayerImageDropData = {
id: layerId,
actionType: 'ADD_RASTER_LAYER_IMAGE',
context: { layerId },
@ -38,11 +38,11 @@ export const RasterLayer = memo(({ layerId }: Props) => {
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="raster_layer" />
<EntityEnabledToggle layerId={layerId} />
<EntityTitle type="raster_layer" />
<Spacer />
<LayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} />
<EntityMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/
import type { IRect, Vector2d } from 'konva/lib/types';
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';
const initialState: CanvasV2State = {
@ -110,6 +110,9 @@ export const canvasV2Slice = createSlice({
toolBufferChanged: (state, action: PayloadAction<Tool | null>) => {
state.tool.selectedBuffer = action.payload;
},
entitySelected: (state, action: PayloadAction<CanvasEntityIdentifier>) => {
state.selectedEntityIdentifier = action.payload;
},
},
extraReducers(builder) {
builder.addCase(modelChanged, (state, action) => {
@ -145,6 +148,7 @@ export const {
invertScrollChanged,
toolChanged,
toolBufferChanged,
entitySelected,
} = canvasV2Slice.actions;
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 { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import { imageDTOToImageWithDims } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
import type { IRect } from 'konva/lib/types';

View File

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

View File

@ -1,13 +1,4 @@
import { LightnessToAlphaFilter } from 'features/controlLayers/konva/filters';
import {
zBeginEndStepPct,
zCLIPVisionModelV2,
zControlModeV2,
zId,
zImageWithDims,
zIPMethodV2,
zProcessorConfig,
} from 'features/controlLayers/util/controlAdapters';
import { deepClone } from 'common/util/deepClone';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type {
@ -24,11 +15,442 @@ import {
zParameterPositivePrompt,
} from 'features/parameters/types/parameterSchemas';
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';
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']);
export type Tool = z.infer<typeof zTool>;
const zDrawingTool = zTool.extract(['brush', 'eraser']);
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>;
const zFilter = z.enum(['none', LightnessToAlphaFilter.name]);
const zFilter = z.enum(['none', 'LightnessToAlphaFilter']);
export type Filter = z.infer<typeof zFilter>;
const zControlAdapterData = z.object({
@ -272,6 +694,64 @@ export type ControlAdapterConfig = Pick<
'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 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';
};
type ControlAdapterDropData = BaseDropData & {
actionType: 'SET_CONTROL_ADAPTER_IMAGE';
export type CAImageDropData = BaseDropData & {
actionType: 'SET_CA_IMAGE';
context: {
id: string;
};
};
export type CALayerImageDropData = BaseDropData & {
actionType: 'SET_CA_LAYER_IMAGE';
export type IPAImageDropData = BaseDropData & {
actionType: 'SET_IPA_IMAGE';
context: {
layerId: string;
id: string;
};
};
export type IPALayerImageDropData = BaseDropData & {
actionType: 'SET_IPA_LAYER_IMAGE';
export type RGIPAdapterImageDropData = BaseDropData & {
actionType: 'SET_RG_IP_ADAPTER_IMAGE';
context: {
layerId: string;
};
};
export type RGLayerIPAdapterImageDropData = BaseDropData & {
actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE';
context: {
layerId: string;
id: string;
ipAdapterId: string;
};
};
export type IILayerImageDropData = BaseDropData & {
actionType: 'SET_II_LAYER_IMAGE';
export type LayerImageDropData = BaseDropData & {
actionType: 'ADD_LAYER_IMAGE';
context: {
layerId: string;
id: string;
};
};
@ -100,16 +93,14 @@ export type SelectForCompareDropData = BaseDropData & {
export type TypesafeDroppableData =
| CurrentImageDropData
| ControlAdapterDropData
| CanvasInitialImageDropData
| NodesImageDropData
| AddToBoardDropData
| RemoveFromBoardDropData
| CALayerImageDropData
| IPALayerImageDropData
| RGLayerIPAdapterImageDropData
| IILayerImageDropData
| CAImageDropData
| IPAImageDropData
| RGIPAdapterImageDropData
| SelectForCompareDropData
| RasterLayerImageDropData
| UpscaleInitialImageDropData;
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 { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { HrfSettings } from 'features/hrf/components/HrfSettings';
import { selectHrfSlice } from 'features/hrf/store/hrfSlice';
@ -20,34 +19,22 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { ImageSizeCanvas } from './ImageSizeCanvas';
import { ImageSizeLinear } from './ImageSizeLinear';
const selector = createMemoizedSelector(
[selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector],
(generation, canvas, hrf, controlLayers, activeTabName) => {
[selectGenerationSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector],
(generation, hrf, canvasV2, activeTabName) => {
const { shouldRandomizeSeed, model } = generation;
const { hrfEnabled } = hrf;
const badges: string[] = [];
const isSDXL = model?.base === 'sdxl';
if (activeTabName === 'canvas') {
const {
aspectRatio,
boundingBoxDimensions: { width, height },
} = canvas;
badges.push(`${width}×${height}`);
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');
}
const { aspectRatio, width, height } = canvasV2.size;
badges.push(`${width}×${height}`);
badges.push(aspectRatio.id);
if (aspectRatio.isLocked) {
badges.push('locked');
}
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 flexDir="column" gap={4}>
{activeTabName === 'canvas' ? <ImageSizeCanvas /> : <ImageSizeLinear />}
{activeTabName === 'canvas' && <ParamImageToImageStrength />}
<ImageSizeLinear />
<ParamImageToImageStrength />
</Flex>
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
<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 { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -40,8 +38,8 @@ export const useClearIntermediates = (shouldShowClearIntermediates: boolean): Us
_clearIntermediates()
.unwrap()
.then((clearedCount) => {
dispatch(controlAdaptersReset());
dispatch(resetCanvas());
// dispatch(controlAdaptersReset());
// dispatch(resetCanvas());
toast({
id: 'INTERMEDIATES_CLEARED',
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 QueueTab from 'features/ui/components/tabs/QueueTab';
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 { usePanel } from 'features/ui/hooks/usePanel';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
@ -30,11 +29,10 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { MdZoomOutMap } from 'react-icons/md';
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 { Panel, PanelGroup } from 'react-resizable-panels';
import ParametersPanelCanvas from './ParametersPanels/ParametersPanelCanvas';
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
import ResizeHandle from './tabs/ResizeHandle';
import UpscalingTab from './tabs/UpscalingTab';
@ -55,13 +53,6 @@ const TAB_DATA: Record<InvokeTabName, TabData> = {
content: <TextToImageTab />,
parametersPanel: <ParametersPanelTextToImage />,
},
canvas: {
id: 'canvas',
translationKey: 'ui.tabs.canvas',
icon: <RiBrushLine />,
content: <UnifiedCanvasTab />,
parametersPanel: <ParametersPanelCanvas />,
},
upscaling: {
id: '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 { 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';
@ -44,7 +43,7 @@ const ParametersPanelTextToImage = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector);
const controlLayersCount = useAppSelector((s) => s.canvasV2.layers.length);
const controlLayersCount = useAppSelector((s) => s.layers.layers.length);
const controlLayersTitle = useMemo(() => {
if (controlLayersCount === 0) {
return t('controlLayers.controlLayers');
@ -108,8 +107,7 @@ const ParametersPanelTextToImage = () => {
<Flex gap={2} flexDirection="column" h="full" w="full">
<ImageSettingsAccordion />
<GenerationSettingsAccordion />
{activeTabName !== 'generation' && <ControlSettingsAccordion />}
{activeTabName === 'canvas' && <CompositingSettingsAccordion />}
<CompositingSettingsAccordion />
{isSDXL && <RefinerSettingsAccordion />}
<AdvancedSettingsAccordion />
</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];

View File

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