From 9fe727c9f8059db719ade91370a2665e43a39be8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 14 Jun 2024 21:14:37 +1000 Subject: [PATCH] refactor(ui): canvas v2 (wip) --- .../src/app/store/createMemoizedSelector.ts | 5 +- invokeai/frontend/web/src/app/store/store.ts | 2 - .../frontend/web/src/app/types/invokeai.ts | 4 +- .../src/common/hooks/useFullscreenDropzone.ts | 7 +- .../web/src/common/hooks/useGlobalHotkeys.ts | 12 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 301 +++++----- .../src/common/util/colorCodeTransformers.ts | 12 +- .../components/AddLayerButton.tsx | 11 +- .../components/AddPromptButtons.tsx | 38 +- .../{BrushSize.tsx => BrushWidth.tsx} | 25 +- .../components/CALayer/CALayer.tsx | 48 -- .../CALayer/CALayerControlAdapterWrapper.tsx | 135 ----- .../BeginEndStepPct.tsx} | 4 +- .../Weight.tsx} | 4 +- .../CAControlModeSelect.tsx} | 8 +- .../components/ControlAdapter/CAEntity.tsx | 35 ++ .../ControlAdapter/CAHeaderItems.tsx | 109 ++++ .../CAImagePreview.tsx} | 43 +- .../CAModelCombobox.tsx} | 4 +- .../CAOpacityAndFilter.tsx} | 20 +- .../CAProcessorConfig.tsx} | 29 +- .../CAProcessorTypeSelect.tsx} | 8 +- .../components/ControlAdapter/CASettings.tsx | 157 +++++ .../processors/CannyProcessor.tsx | 0 .../processors/ColorMapProcessor.tsx | 0 .../processors/ContentShuffleProcessor.tsx | 0 .../processors/DWOpenposeProcessor.tsx | 0 .../processors/DepthAnythingProcessor.tsx | 0 .../processors/HedProcessor.tsx | 0 .../processors/LineartProcessor.tsx | 0 .../processors/MediapipeFaceProcessor.tsx | 0 .../processors/MidasDepthProcessor.tsx | 0 .../processors/MlsdImageProcessor.tsx | 0 .../processors/PidiProcessor.tsx | 0 .../processors/ProcessorWrapper.tsx | 0 .../processors/types.ts | 0 .../ControlAndIPAdapter/ControlAdapter.tsx | 123 ---- .../ControlAndIPAdapter/IPAdapter.tsx | 4 +- .../components/ControlLayersPanelContent.tsx | 6 +- .../components/ControlLayersToolbar.tsx | 4 +- .../controlLayers/components/EraserWidth.tsx | 56 ++ ...ushColorPicker.tsx => FillColorPicker.tsx} | 16 +- .../components/IILayer/IILayer.tsx | 12 +- .../components/IPALayer/IPALayer.tsx | 30 +- .../IPALayer/IPALayerIPAdapterWrapper.tsx | 4 +- ...eleteButton.tsx => EntityDeleteButton.tsx} | 16 +- ...lityToggle.tsx => EntityEnabledToggle.tsx} | 19 +- .../{LayerMenu.tsx => EntityMenu.tsx} | 4 +- .../LayerCommon/EntityMenuButton.tsx | 18 + .../components/LayerCommon/EntityTitle.tsx | 16 + .../LayerCommon/LayerMenuRGActions.tsx | 4 +- .../components/LayerCommon/LayerTitle.tsx | 33 -- .../components/RGLayer/RGLayer.tsx | 14 +- .../RGLayer/RGLayerIPAdapterWrapper.tsx | 8 +- .../components/RasterLayer/RasterLayer.tsx | 16 +- .../controlLayers/hooks/addLayerHooks.ts | 35 +- .../controlLayers/konva/renderers/caLayer.ts | 2 +- .../store/controlAdaptersSlice.ts | 51 +- .../controlLayers/store/controlLayersSlice.ts | 6 +- .../store/regionalGuidanceSlice.ts | 4 +- .../types.test.ts} | 2 +- .../src/features/controlLayers/store/types.ts | 504 +++++++++++++++- .../controlLayers/util/controlAdapters.ts | 546 ------------------ .../web/src/features/dnd/types/index.ts | 39 +- .../ControlSettingsAccordion.stories.tsx | 16 - .../ControlSettingsAccordion.tsx | 125 ---- .../ImageSettingsAccordion.tsx | 33 +- .../ImageSizeCanvas.tsx | 59 -- .../SettingsModal/useClearIntermediates.ts | 6 +- .../src/features/ui/components/InvokeTabs.tsx | 11 +- .../ParametersPanelCanvas.tsx | 59 -- .../ParametersPanelTextToImage.tsx | 6 +- .../ui/components/tabs/UnifiedCanvasTab.tsx | 51 -- .../web/src/features/ui/store/tabMap.tsx | 2 +- .../frontend/web/src/services/api/types.ts | 37 +- 75 files changed, 1301 insertions(+), 1717 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/{BrushSize.tsx => BrushWidth.tsx} (60%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx => Common/BeginEndStepPct.tsx} (87%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterWeight.tsx => Common/Weight.tsx} (93%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx => ControlAdapter/CAControlModeSelect.tsx} (82%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterImagePreview.tsx => ControlAdapter/CAImagePreview.tsx} (83%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterModelCombobox.tsx => ControlAdapter/CAModelCombobox.tsx} (92%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer/CALayerOpacity.tsx => ControlAdapter/CAOpacityAndFilter.tsx} (83%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx => ControlAdapter/CAProcessorConfig.tsx} (55%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx => ControlAdapter/CAProcessorTypeSelect.tsx} (88%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/CannyProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/ColorMapProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/ContentShuffleProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/DWOpenposeProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/DepthAnythingProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/HedProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/LineartProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/MediapipeFaceProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/MidasDepthProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/MlsdImageProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/PidiProcessor.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/ProcessorWrapper.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ControlAndIPAdapter => ControlAdapter}/processors/types.ts (100%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{BrushColorPicker.tsx => FillColorPicker.tsx} (67%) rename invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/{LayerDeleteButton.tsx => EntityDeleteButton.tsx} (50%) rename invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/{LayerVisibilityToggle.tsx => EntityEnabledToggle.tsx} (50%) rename invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/{LayerMenu.tsx => EntityMenu.tsx} (96%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx rename invokeai/frontend/web/src/features/controlLayers/{util/controlAdapters.test.ts => store/types.test.ts} (99%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts delete mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.stories.tsx delete mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx delete mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx diff --git a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts index 8e2559927a..eb09641845 100644 --- a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts +++ b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts @@ -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(); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 45c0d020e8..17a4205725 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -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 = { }; const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { - [canvasPersistConfig.name]: canvasPersistConfig, [galleryPersistConfig.name]: galleryPersistConfig, [generationPersistConfig.name]: generationPersistConfig, [nodesPersistConfig.name]: nodesPersistConfig, diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index ffc4e1960b..4d54485f61 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -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 diff --git a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index d8e7d70a8c..93c9d73932 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -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(false); - + const postUploadAction = useAppSelector(selectPostUploadAction); const [uploadImage] = useUploadImageMutation(); const fileRejectionCallback = useCallback( diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts index 9ba044199f..487622f9b7 100644 --- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -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')); }, diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 97042124b1..c47a285c54 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -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 = { - 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 = { + 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 }; diff --git a/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts b/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts index 835b2a3e35..85635f93b5 100644 --- a/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts +++ b/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts @@ -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})`; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 72d18c3d17..591f4a41a1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -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(() => { } onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> {t('controlLayers.globalIPAdapterLayer')} - } onClick={addIILayer} isDisabled={isAddIILayerDisabled}> - {t('controlLayers.globalInitialImageLayer')} - ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 71b1ca5f1e..8f312aba1d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -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 ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx similarity index 60% rename from invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx index 4c102ea0ff..b1b813f652 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/BrushWidth.tsx @@ -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 ( - {t('controlLayers.brushSize')} + {t('controlLayers.brushWidth')} { - + @@ -60,4 +53,4 @@ export const BrushSize = memo(() => { ); }); -BrushSize.displayName = 'BrushSize'; +BrushWidth.displayName = 'BrushSize'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx deleted file mode 100644 index 9d543446e9..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ /dev/null @@ -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 ( - - - - - - - - - - {isOpen && ( - - - - )} - - ); -}); - -CALayer.displayName = 'CALayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx deleted file mode 100644 index f5ccd49cb2..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ /dev/null @@ -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( - () => ({ - actionType: 'SET_CA_LAYER_IMAGE', - context: { - layerId, - }, - id: layerId, - }), - [layerId] - ); - - const postUploadAction = useMemo( - () => ({ - layerId, - type: 'SET_CA_LAYER_IMAGE', - }), - [layerId] - ); - - return ( - - ); -}); - -CALayerControlAdapterWrapper.displayName = 'CALayerControlAdapterWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Common/BeginEndStepPct.tsx similarity index 87% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Common/BeginEndStepPct.tsx index 9da9ce50a0..4f75c0bb97 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Common/BeginEndStepPct.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Common/Weight.tsx similarity index 93% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Common/Weight.tsx index 4bb7bb3911..d9f841fa26 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Common/Weight.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAControlModeSelect.tsx similarity index 82% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAControlModeSelect.tsx index 2c35ce51b6..9c8ecb896f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAControlModeSelect.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx new file mode 100644 index 0000000000..2840bd3962 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx @@ -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 ( + + + + + {disclosure.isOpen && ( + + + + )} + + ); +}); + +CAEntity.displayName = 'CAEntity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx new file mode 100644 index 0000000000..67b2c539d5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx @@ -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 ( + <> + + + + + + + + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + } color="error.300"> + {t('common.delete')} + + + + + + ); +}); + +CAHeaderItems.displayName = 'CAHeaderItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx similarity index 83% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx index e840885050..34baca75fe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAImagePreview.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx similarity index 92% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx index 535f3067a4..9b6a5ad9db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAModelCombobox.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx similarity index 83% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx index 94f7cdf5fe..7703754044 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx @@ -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) => { - dispatch(caLayerIsFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked })); + dispatch(caFilterChanged({ id, filter: e.target.checked ? 'LightnessToAlphaFilter' : 'none' })); }, - [dispatch, layerId] + [dispatch, id] ); return ( @@ -93,6 +93,6 @@ const CALayerOpacity = ({ layerId }: Props) => { ); -}; +}); -export default memo(CALayerOpacity); +CAOpacityAndFilter.displayName = 'CAOpacityAndFilter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx similarity index 55% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx index 034dc5454e..e2f009e48f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorConfig.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect.tsx similarity index 88% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect.tsx index 5598b81787..70e9113c55 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAProcessorTypeSelect.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx new file mode 100644 index 0000000000..d892070158 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx @@ -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(() => ({ actionType: 'SET_CA_IMAGE', context: { id }, id }), [id]); + const postUploadAction = useMemo(() => ({ id, type: 'SET_CA_IMAGE' }), [id]); + + return ( + + + + + + + + } + /> + + + + {controlAdapter.controlMode && ( + + )} + + + + + + + + {isExpanded && ( + <> + + + + + + + )} + + ); +}); + +CASettings.displayName = 'CASettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/CannyProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/HedProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/LineartProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/PidiProcessor.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ProcessorWrapper.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/ProcessorWrapper.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/types.ts rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/processors/types.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx deleted file mode 100644 index 2a7b21352e..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx +++ /dev/null @@ -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 ( - - - - - - - - } - /> - - - - {controlAdapter.type === 'controlnet' && ( - - )} - - - - - - - - {isExpanded && ( - <> - - - - - - - )} - - ); - } -); - -ControlAdapter.displayName = 'ControlAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx index 86ed77ce36..75a1fa0c6b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx @@ -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( - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 27ef318c1a..50e39c43e3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -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 ; } if (type === 'control_adapter_layer') { - return ; + return ; } if (type === 'ip_adapter_layer') { - return ; + return ; } if (type === 'initial_image_layer') { return ; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 7f140e2be6..15da096680 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -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(() => { - {withBrushSize && } + {withBrushSize && } {withBrushColor && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx b/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx new file mode 100644 index 0000000000..d976fa8470 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/EraserWidth.tsx @@ -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 ( + + {t('controlLayers.eraserWidth')} + + + + + + + + + + + + + ); +}); + +EraserWidth.displayName = 'EraserWidth'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx similarity index 67% rename from invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx index ec8e535b40..3550f67f13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/FillColorPicker.tsx @@ -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(() => { - + ); }); -BrushColorPicker.displayName = 'BrushColorPicker'; +FillColorPicker.displayName = 'BrushColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx index 202b830e47..34ef2ce0ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -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 ( - - + + - + {isOpen && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx index d227f73045..e4b89dfe21 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -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 ( - - + + - + {isOpen && ( - + )} ); }); -IPALayer.displayName = 'IPALayer'; +IPAEntity.displayName = 'IPAEntity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx index 7d7ba7e1b5..7620fa57b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx @@ -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( + const droppableData = useMemo( () => ({ actionType: 'SET_IPA_LAYER_IMAGE', context: { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx similarity index 50% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx index 0cd7d83dfe..441c839587 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx @@ -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 ( { aria-label={t('common.delete')} tooltip={t('common.delete')} icon={} - onClick={deleteLayer} + onClick={onDelete} onDoubleClick={stopPropagation} // double click expands the layer /> ); }); -LayerDeleteButton.displayName = 'LayerDeleteButton'; +EntityDeleteButton.displayName = 'EntityDeleteButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx similarity index 50% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx index 227d74c35a..ca4855a64f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx @@ -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 ( { tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')} variant="outline" icon={isEnabled ? : undefined} - onClick={onClick} + onClick={onToggle} colorScheme="base" onDoubleClick={stopPropagation} // double click expands the layer /> ); }); -LayerIsEnabledToggle.displayName = 'LayerVisibilityToggle'; +EntityEnabledToggle.displayName = 'EntityEnabledToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx index aabad5ed63..a10079f160 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx new file mode 100644 index 0000000000..51887ed5e1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx @@ -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 ( + } + onDoubleClick={stopPropagation} // double click expands the layer + /> + ); +}); + +EntityMenuButton.displayName = 'EntityMenuButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx new file mode 100644 index 0000000000..31fd5902b8 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx @@ -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 ( + + {title} + + ); +}); + +EntityTitle.displayName = 'EntityTitle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx index d04da5e4a1..335a0d32cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx @@ -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) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx deleted file mode 100644 index 053fbd234e..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx +++ /dev/null @@ -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 ( - - {title} - - ); -}); - -LayerTitle.displayName = 'LayerTitle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index 4df5670f0e..5d5ce7fb02 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -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 ( - - + + {autoNegative === 'invert' && ( @@ -62,12 +62,12 @@ export const RGLayer = memo(({ layerId }: Props) => { )} - + {isOpen && ( - {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } {hasPositivePrompt && } {hasNegativePrompt && } {hasIPAdapters && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx index 802ad2bb3d..2fa392e0d0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx @@ -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( + const droppableData = useMemo( () => ({ actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', context: { @@ -90,7 +90,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu [ipAdapterId, layerId] ); - const postUploadAction = useMemo( + const postUploadAction = useMemo( () => ({ type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', layerId, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx index 921122fb33..82851dbb0b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx @@ -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 ( - - + + - + {isOpen && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 5b9e799063..c52944b88c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -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; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts index 7b85bcfce3..8301a97573 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/caLayer.ts @@ -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; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts index f53de4eb5a..6a98523bef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts @@ -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, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index ee2072eaf6..0dd87536c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -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) => { state.tool.selectedBuffer = action.payload; }, + entitySelected: (state, action: PayloadAction) => { + 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; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts index 7d177fabbc..182693abcc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.test.ts similarity index 99% rename from invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts rename to invokeai/frontend/web/src/features/controlLayers/store/types.test.ts index 22f54d622c..6d95cac121 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.test.ts @@ -24,7 +24,7 @@ import type { ProcessorConfig, ProcessorTypeV2, ZoeDepthProcessorConfig, -} from './controlAdapters'; +} from './types'; describe('Control Adapter Types', () => { test('ProcessorType', () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index fc4b010067..8388ed0530 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -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; + +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; +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; +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; +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; + +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; + +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; + +const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']); +export type DepthAnythingModelSize = z.infer; +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; + +const zHedProcessorConfig = z.object({ + id: zId, + type: z.literal('hed_image_processor'), + scribble: z.boolean(), +}); +export type HedProcessorConfig = z.infer; + +const zLineartAnimeProcessorConfig = z.object({ + id: zId, + type: z.literal('lineart_anime_image_processor'), +}); +export type LineartAnimeProcessorConfig = z.infer; + +const zLineartProcessorConfig = z.object({ + id: zId, + type: z.literal('lineart_image_processor'), + coarse: z.boolean(), +}); +export type LineartProcessorConfig = z.infer; + +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; + +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; + +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; + +const zNormalbaeProcessorConfig = z.object({ + id: zId, + type: z.literal('normalbae_image_processor'), +}); +export type NormalbaeProcessorConfig = z.infer; + +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; + +const zPidiProcessorConfig = z.object({ + id: zId, + type: z.literal('pidi_image_processor'), + safe: z.boolean(), + scribble: z.boolean(), +}); +export type PidiProcessorConfig = z.infer; + +const zZoeDepthProcessorConfig = z.object({ + id: zId, + type: z.literal('zoe_depth_image_processor'), +}); +export type ZoeDepthProcessorConfig = z.infer; + +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; + +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; +export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success; + +type ProcessorData = { + type: T; + labelTKey: string; + descriptionTKey: string; + buildDefaults(baseModel?: BaseModelType): Extract; + buildNode(image: ImageWithDims, config: Extract): Extract; +}; + +const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); + +type CAProcessorsData = { + [key in ProcessorTypeV2]: ProcessorData; +}; +/** + * 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; + 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; -const zFilter = z.enum(['none', LightnessToAlphaFilter.name]); +const zFilter = z.enum(['none', 'LightnessToAlphaFilter']); export type Filter = z.infer; 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 => { + return merge(deepClone(initialControlNetV2), { id, ...overrides }); +}; + +export const buildT2IAdapter = (id: string, overrides?: Partial): ControlAdapterConfig => { + return merge(deepClone(initialT2IAdapterV2), { id, ...overrides }); +}; + +export const buildIPAdapter = (id: string, overrides?: Partial): 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; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts deleted file mode 100644 index 892d2c5eac..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ /dev/null @@ -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; - -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; - -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; - -const zDepthAnythingModelSize = z.enum(['large', 'base', 'small', 'small_v2']); -export type DepthAnythingModelSize = z.infer; -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; - -const zHedProcessorConfig = z.object({ - id: zId, - type: z.literal('hed_image_processor'), - scribble: z.boolean(), -}); -export type HedProcessorConfig = z.infer; - -const zLineartAnimeProcessorConfig = z.object({ - id: zId, - type: z.literal('lineart_anime_image_processor'), -}); -export type LineartAnimeProcessorConfig = z.infer; - -const zLineartProcessorConfig = z.object({ - id: zId, - type: z.literal('lineart_image_processor'), - coarse: z.boolean(), -}); -export type LineartProcessorConfig = z.infer; - -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; - -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; - -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; - -const zNormalbaeProcessorConfig = z.object({ - id: zId, - type: z.literal('normalbae_image_processor'), -}); -export type NormalbaeProcessorConfig = z.infer; - -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; - -const zPidiProcessorConfig = z.object({ - id: zId, - type: z.literal('pidi_image_processor'), - safe: z.boolean(), - scribble: z.boolean(), -}); -export type PidiProcessorConfig = z.infer; - -const zZoeDepthProcessorConfig = z.object({ - id: zId, - type: z.literal('zoe_depth_image_processor'), -}); -export type ZoeDepthProcessorConfig = z.infer; - -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; - -export const zImageWithDims = z.object({ - name: z.string(), - width: z.number().int().positive(), - height: z.number().int().positive(), -}); -export type ImageWithDims = z.infer; - -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; -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; - -export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({ - type: z.literal('t2i_adapter'), - model: zModelIdentifierField.nullable(), -}); -export type T2IAdapterConfigV2 = z.infer; - -export const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); -export type CLIPVisionModelV2 = z.infer; -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; -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; - -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; -export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success; - -type ProcessorData = { - type: T; - labelTKey: string; - descriptionTKey: string; - buildDefaults(baseModel?: BaseModelType): Extract; - buildNode(image: ImageWithDims, config: Extract): Extract; -}; - -const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); - -type CAProcessorsData = { - [key in ProcessorTypeV2]: ProcessorData; -}; -/** - * 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 = { - 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 = { - 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 = { - 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 => { - return merge(deepClone(initialControlNetV2), { id, ...overrides }); -}; - -export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfigV2 => { - return merge(deepClone(initialT2IAdapterV2), { id, ...overrides }); -}; - -export const buildIPAdapter = (id: string, overrides?: Partial): 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', - }; -}; diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 1d99f4a415..692454ea50 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -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 = { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.stories.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.stories.tsx deleted file mode 100644 index 8909a826c3..0000000000 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import { ControlSettingsAccordion } from './ControlSettingsAccordion'; - -const meta: Meta = { - title: 'Feature/ControlSettingsAccordion', - tags: ['autodocs'], - component: ControlSettingsAccordion, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: () => , -}; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx deleted file mode 100644 index eef997a11b..0000000000 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx +++ /dev/null @@ -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 ( - - - - - - - - {controlAdapterIds.map((id, i) => ( - - - - - ))} - - - ); -}); - -ControlSettingsAccordion.displayName = 'ControlAdaptersSettingsAccordion'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 61453be0ba..499a0a307c 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -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(() => { > - {activeTabName === 'canvas' ? : } - {activeTabName === 'canvas' && } + + diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx deleted file mode 100644 index 1dc8f49b78..0000000000 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx +++ /dev/null @@ -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 ( - } - widthComponent={} - previewComponent={} - onChangeAspectRatioState={onChangeAspectRatioState} - onChangeWidth={onChangeWidth} - onChangeHeight={onChangeHeight} - /> - ); -}); - -ImageSizeCanvas.displayName = 'ImageSizeCanvas'; diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts b/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts index f392acb521..6302a16ba5 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/useClearIntermediates.ts @@ -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 }), diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index b98d713b80..c5bef7f6db 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -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 = { content: , parametersPanel: , }, - canvas: { - id: 'canvas', - translationKey: 'ui.tabs.canvas', - icon: , - content: , - parametersPanel: , - }, upscaling: { id: 'upscaling', translationKey: 'ui.tabs.upscaling', diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx deleted file mode 100644 index d98a736e3b..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelCanvas.tsx +++ /dev/null @@ -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 ( - - - - - - {isMenuOpen && ( - - - - - - )} - - - - - - - - {isSDXL && } - - - - - - - ); -}; - -export default memo(ParametersPanelCanvas); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 6a98798f90..bf2b915090 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -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 = () => { - {activeTabName !== 'generation' && } - {activeTabName === 'canvas' && } + {isSDXL && } diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx deleted file mode 100644 index db2156fbde..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx +++ /dev/null @@ -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 ( - - - - {isValidDrop(droppableData, active?.data.current) && ( - - )} - - ); -}; - -export default memo(UnifiedCanvasTab); diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.tsx b/invokeai/frontend/web/src/features/ui/store/tabMap.tsx index 5cf97b2d3e..b41c3e1414 100644 --- a/invokeai/frontend/web/src/features/ui/store/tabMap.tsx +++ b/invokeai/frontend/web/src/features/ui/store/tabMap.tsx @@ -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]; diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index bfe42f3f9a..83aaa4924b 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -167,44 +167,28 @@ export type OutputFields = 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;