From b6e6bdc19575ae34c9cdb3172b048fd78688730a Mon Sep 17 00:00:00 2001 From: l0stl0rd <82827604+l0stl0rd@users.noreply.github.com> Date: Thu, 28 Mar 2024 22:51:59 +0100 Subject: [PATCH 01/74] Update schedulers.py --- invokeai/backend/stable_diffusion/schedulers/schedulers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/invokeai/backend/stable_diffusion/schedulers/schedulers.py b/invokeai/backend/stable_diffusion/schedulers/schedulers.py index c824d94dca..cb22abd643 100644 --- a/invokeai/backend/stable_diffusion/schedulers/schedulers.py +++ b/invokeai/backend/stable_diffusion/schedulers/schedulers.py @@ -14,6 +14,7 @@ from diffusers import ( LMSDiscreteScheduler, PNDMScheduler, UniPCMultistepScheduler, + TCDScheduler, ) SCHEDULER_MAP = { @@ -40,4 +41,5 @@ SCHEDULER_MAP = { "dpmpp_sde_k": (DPMSolverSDEScheduler, {"use_karras_sigmas": True, "noise_sampler_seed": 0}), "unipc": (UniPCMultistepScheduler, {"cpu_only": True}), "lcm": (LCMScheduler, {}), + "TCD": (TCDScheduler, {}), } From 80e311a0693071d767c1a8d9f1c8652f8e1eb8fd Mon Sep 17 00:00:00 2001 From: l0stl0rd <82827604+l0stl0rd@users.noreply.github.com> Date: Thu, 28 Mar 2024 22:52:15 +0100 Subject: [PATCH 02/74] Update schedulers.py --- invokeai/backend/stable_diffusion/schedulers/schedulers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/backend/stable_diffusion/schedulers/schedulers.py b/invokeai/backend/stable_diffusion/schedulers/schedulers.py index cb22abd643..b60d0ba96f 100644 --- a/invokeai/backend/stable_diffusion/schedulers/schedulers.py +++ b/invokeai/backend/stable_diffusion/schedulers/schedulers.py @@ -41,5 +41,5 @@ SCHEDULER_MAP = { "dpmpp_sde_k": (DPMSolverSDEScheduler, {"use_karras_sigmas": True, "noise_sampler_seed": 0}), "unipc": (UniPCMultistepScheduler, {"cpu_only": True}), "lcm": (LCMScheduler, {}), - "TCD": (TCDScheduler, {}), + "TCD": (TCDScheduler, {}), } From 97579770e1b37a172b6de0ff72e995d9bb118611 Mon Sep 17 00:00:00 2001 From: l0stl0rd <82827604+l0stl0rd@users.noreply.github.com> Date: Fri, 29 Mar 2024 12:35:42 +0100 Subject: [PATCH 03/74] Update common.ts --- invokeai/frontend/web/src/features/nodes/types/v2/common.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/common.ts b/invokeai/frontend/web/src/features/nodes/types/v2/common.ts index 8613076132..f715f404f0 100644 --- a/invokeai/frontend/web/src/features/nodes/types/v2/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/v2/common.ts @@ -40,6 +40,7 @@ export const zSchedulerField = z.enum([ 'euler_a', 'kdpm_2_a', 'lcm', + 'TCD', ]); // #endregion From 23da3de915a31f8b6eaec570828b8b4e80966670 Mon Sep 17 00:00:00 2001 From: l0stl0rd <82827604+l0stl0rd@users.noreply.github.com> Date: Fri, 29 Mar 2024 12:39:08 +0100 Subject: [PATCH 04/74] Update constants.ts --- invokeai/frontend/web/src/features/parameters/types/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/parameters/types/constants.ts b/invokeai/frontend/web/src/features/parameters/types/constants.ts index 5bdfe9937b..0bc0e94aa1 100644 --- a/invokeai/frontend/web/src/features/parameters/types/constants.ts +++ b/invokeai/frontend/web/src/features/parameters/types/constants.ts @@ -75,4 +75,5 @@ export const SCHEDULER_OPTIONS: ComboboxOption[] = [ { value: 'euler_a', label: 'Euler Ancestral' }, { value: 'kdpm_2_a', label: 'KDPM 2 Ancestral' }, { value: 'lcm', label: 'LCM' }, + { value: 'TCD', label: 'TCD' }, ].sort((a, b) => a.label.localeCompare(b.label)); From 8c509295f93a6c99382202f05ad8b510431249b0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:02:45 +1100 Subject: [PATCH 05/74] chore: ruff --- invokeai/backend/stable_diffusion/schedulers/schedulers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/backend/stable_diffusion/schedulers/schedulers.py b/invokeai/backend/stable_diffusion/schedulers/schedulers.py index b60d0ba96f..a580ac84ff 100644 --- a/invokeai/backend/stable_diffusion/schedulers/schedulers.py +++ b/invokeai/backend/stable_diffusion/schedulers/schedulers.py @@ -13,8 +13,8 @@ from diffusers import ( LCMScheduler, LMSDiscreteScheduler, PNDMScheduler, - UniPCMultistepScheduler, TCDScheduler, + UniPCMultistepScheduler, ) SCHEDULER_MAP = { From 51e515b925b6f25d5d297b38987dc226e9cc20cf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:03:02 +1100 Subject: [PATCH 06/74] tidy: use lowercase for tcd scheduler identifier --- invokeai/backend/stable_diffusion/schedulers/schedulers.py | 2 +- invokeai/frontend/web/src/features/nodes/types/v2/common.ts | 2 +- .../frontend/web/src/features/parameters/types/constants.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/backend/stable_diffusion/schedulers/schedulers.py b/invokeai/backend/stable_diffusion/schedulers/schedulers.py index a580ac84ff..3a55d52d4a 100644 --- a/invokeai/backend/stable_diffusion/schedulers/schedulers.py +++ b/invokeai/backend/stable_diffusion/schedulers/schedulers.py @@ -41,5 +41,5 @@ SCHEDULER_MAP = { "dpmpp_sde_k": (DPMSolverSDEScheduler, {"use_karras_sigmas": True, "noise_sampler_seed": 0}), "unipc": (UniPCMultistepScheduler, {"cpu_only": True}), "lcm": (LCMScheduler, {}), - "TCD": (TCDScheduler, {}), + "tcd": (TCDScheduler, {}), } diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/common.ts b/invokeai/frontend/web/src/features/nodes/types/v2/common.ts index f715f404f0..671ed81d05 100644 --- a/invokeai/frontend/web/src/features/nodes/types/v2/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/v2/common.ts @@ -40,7 +40,7 @@ export const zSchedulerField = z.enum([ 'euler_a', 'kdpm_2_a', 'lcm', - 'TCD', + 'tcd', ]); // #endregion diff --git a/invokeai/frontend/web/src/features/parameters/types/constants.ts b/invokeai/frontend/web/src/features/parameters/types/constants.ts index 0bc0e94aa1..6d7b4f9248 100644 --- a/invokeai/frontend/web/src/features/parameters/types/constants.ts +++ b/invokeai/frontend/web/src/features/parameters/types/constants.ts @@ -75,5 +75,5 @@ export const SCHEDULER_OPTIONS: ComboboxOption[] = [ { value: 'euler_a', label: 'Euler Ancestral' }, { value: 'kdpm_2_a', label: 'KDPM 2 Ancestral' }, { value: 'lcm', label: 'LCM' }, - { value: 'TCD', label: 'TCD' }, + { value: 'tcd', label: 'TCD' }, ].sort((a, b) => a.label.localeCompare(b.label)); From 1d45ef529b350bd66b7075547f39261e0fa45e2b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:08:02 +1100 Subject: [PATCH 07/74] fix(ui): move tcd scheduler to current zod schemas It was in the v2 schemas which should be immutable and only used for migrations --- invokeai/frontend/web/src/features/nodes/types/common.ts | 1 + invokeai/frontend/web/src/features/nodes/types/v2/common.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts index 06d5ecd5c7..ac9b67f0e8 100644 --- a/invokeai/frontend/web/src/features/nodes/types/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/common.ts @@ -49,6 +49,7 @@ export const zSchedulerField = z.enum([ 'euler_a', 'kdpm_2_a', 'lcm', + 'tcd', ]); export type SchedulerField = z.infer; // #endregion diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/common.ts b/invokeai/frontend/web/src/features/nodes/types/v2/common.ts index 671ed81d05..8613076132 100644 --- a/invokeai/frontend/web/src/features/nodes/types/v2/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/v2/common.ts @@ -40,7 +40,6 @@ export const zSchedulerField = z.enum([ 'euler_a', 'kdpm_2_a', 'lcm', - 'tcd', ]); // #endregion From 07cb6c944e2747fa81430600a8350af0ccbfa4d2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:18:12 +1100 Subject: [PATCH 08/74] chore(ui): typegen --- invokeai/frontend/web/src/services/api/schema.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 2de0f89d13..fe70b2be07 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -3085,7 +3085,7 @@ export type components = { * @default euler * @enum {string} */ - scheduler?: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm"; + scheduler?: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm" | "tcd"; /** * UNet * @description UNet (scheduler, LoRAs) @@ -4112,7 +4112,7 @@ export type components = { * @description The nodes in this graph */ nodes: { - [key: string]: components["schemas"]["ControlNetInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"]; + [key: string]: components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"]; }; /** * Edges @@ -4149,7 +4149,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["FaceMaskOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["String2Output"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["LatentsCollectionOutput"]; + [key: string]: components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["String2Output"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["TileToPropertiesOutput"]; }; /** * Errors @@ -6790,7 +6790,7 @@ export type components = { * Scheduler * @description Default scheduler for this model */ - scheduler?: ("ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm") | null; + scheduler?: ("ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm" | "tcd") | null; /** * Steps * @description Default number of steps for this model @@ -8898,7 +8898,7 @@ export type components = { * @default euler * @enum {string} */ - scheduler?: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm"; + scheduler?: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm" | "tcd"; /** * type * @default scheduler @@ -8913,7 +8913,7 @@ export type components = { * @description Scheduler to use during inference * @enum {string} */ - scheduler: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm"; + scheduler: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm" | "tcd"; /** * type * @default scheduler_output From 38880cde5ce886f17382c0d1fad380d7e81035b9 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Wed, 1 May 2024 01:20:22 +0530 Subject: [PATCH 09/74] chore: update schema --- .../frontend/web/src/services/api/schema.ts | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 727fad6f81..27a3c670da 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -2883,6 +2883,11 @@ export type components = { * @description OPTIONAL: Only connect for specialized Inpainting models, masked_latents will be generated from the image with the VAE */ image?: components["schemas"]["ImageField"] | null; + /** + * [OPTIONAL] UNet + * @description OPTIONAL: If the Unet is a specialized Inpainting model, masked_latents will be generated from the image with the VAE + */ + unet?: components["schemas"]["UNetField"] | null; /** * [OPTIONAL] VAE * @description OPTIONAL: Only connect for specialized Inpainting models, masked_latents will be generated from the image with the VAE @@ -3157,7 +3162,7 @@ export type components = { * @default euler * @enum {string} */ - scheduler?: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm"; + scheduler?: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm" | "tcd"; /** * UNet * @description UNet (scheduler, LoRAs) @@ -4184,7 +4189,7 @@ export type components = { * @description The nodes in this graph */ nodes: { - [key: string]: components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"]; + [key: string]: components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["SDXLLoRALoaderInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["MaskFromIDInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["VAELoaderInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["HeuristicResizeInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["RectangleMaskInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["LoRALoaderInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["InvertTensorMaskInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["AlphaMaskToTensorInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["CLIPSkipInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["ImageMaskToTensorInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["NoiseInvocation"]; }; /** * Edges @@ -4221,7 +4226,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["String2Output"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["LoRALoaderOutput"]; + [key: string]: components["schemas"]["ImageOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["MaskOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["String2Output"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["GradientMaskOutput"] | components["schemas"]["SDXLLoRALoaderOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["LoRALoaderOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["CLIPSkipInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["CLIPOutput"]; }; /** * Errors @@ -4325,6 +4330,49 @@ export type components = { */ type: "hed_image_processor"; }; + /** + * Heuristic Resize + * @description Resize an image using a heuristic method. Preserves edge maps. + */ + HeuristicResizeInvocation: { + /** + * Id + * @description The id of this instance of an invocation. Must be unique among all instances of invocations. + */ + id: string; + /** + * Is Intermediate + * @description Whether or not this is an intermediate invocation. + * @default false + */ + is_intermediate?: boolean; + /** + * Use Cache + * @description Whether or not to use the cache + * @default true + */ + use_cache?: boolean; + /** @description The image to resize */ + image?: components["schemas"]["ImageField"]; + /** + * Width + * @description The width to resize to (px) + * @default 512 + */ + width?: number; + /** + * Height + * @description The height to resize to (px) + * @default 512 + */ + height?: number; + /** + * type + * @default heuristic_resize + * @constant + */ + type: "heuristic_resize"; + }; /** * HuggingFaceMetadata * @description Extended metadata fields provided by HuggingFace. @@ -7040,7 +7088,7 @@ export type components = { * Scheduler * @description Default scheduler for this model */ - scheduler?: ("ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm") | null; + scheduler?: ("ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm" | "tcd") | null; /** * Steps * @description Default number of steps for this model @@ -9235,7 +9283,7 @@ export type components = { * @default euler * @enum {string} */ - scheduler?: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm"; + scheduler?: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm" | "tcd"; /** * type * @default scheduler @@ -9250,7 +9298,7 @@ export type components = { * @description Scheduler to use during inference * @enum {string} */ - scheduler: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm"; + scheduler: "ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm" | "tcd"; /** * type * @default scheduler_output From 2ddb82200cf29744fd3dba98dc4997131f0262ce Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Wed, 1 May 2024 01:20:53 +0530 Subject: [PATCH 10/74] fix: Manually update eta(gamma) to 1.0 for TCDScheduler seems to work best with invoke at 4 steps --- invokeai/app/invocations/latent.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 4ad63f4f89..e35de458b5 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -3,7 +3,7 @@ import inspect import math from contextlib import ExitStack from functools import singledispatchmethod -from typing import Any, Iterator, List, Literal, Optional, Tuple, Union +from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple, Union import einops import numpy as np @@ -521,9 +521,10 @@ class DenoiseLatentsInvocation(BaseInvocation): ) if is_sdxl: - return SDXLConditioningInfo( - embeds=text_embedding, pooled_embeds=pooled_embedding, add_time_ids=add_time_ids - ), regions + return ( + SDXLConditioningInfo(embeds=text_embedding, pooled_embeds=pooled_embedding, add_time_ids=add_time_ids), + regions, + ) return BasicConditioningInfo(embeds=text_embedding), regions def get_conditioning_data( @@ -825,7 +826,7 @@ class DenoiseLatentsInvocation(BaseInvocation): denoising_start: float, denoising_end: float, seed: int, - ) -> Tuple[int, List[int], int]: + ) -> Tuple[int, List[int], int, Dict[str, Union[torch.Generator, float]]]: assert isinstance(scheduler, ConfigMixin) if scheduler.config.get("cpu_only", False): scheduler.set_timesteps(steps, device="cpu") @@ -853,13 +854,16 @@ class DenoiseLatentsInvocation(BaseInvocation): timesteps = timesteps[t_start_idx : t_start_idx + t_end_idx] num_inference_steps = len(timesteps) // scheduler.order - scheduler_step_kwargs = {} + scheduler_step_kwargs: Dict[str, Union[torch.Generator, float]] = {} scheduler_step_signature = inspect.signature(scheduler.step) + print(scheduler_step_signature.parameters) if "generator" in scheduler_step_signature.parameters: # At some point, someone decided that schedulers that accept a generator should use the original seed with # all bits flipped. I don't know the original rationale for this, but now we must keep it like this for # reproducibility. scheduler_step_kwargs = {"generator": torch.Generator(device=device).manual_seed(seed ^ 0xFFFFFFFF)} + if "eta" in scheduler_step_signature.parameters: + scheduler_step_kwargs = {"eta": 1.0} return num_inference_steps, timesteps, init_timestep, scheduler_step_kwargs From 1bdcbe328427596aa9f6161d348fed4d9184c0e3 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Wed, 1 May 2024 12:08:39 +0530 Subject: [PATCH 11/74] cleanup: use dict update to actually update the scheduler keyword args --- invokeai/app/invocations/latent.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index e35de458b5..56e15e7566 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -826,7 +826,7 @@ class DenoiseLatentsInvocation(BaseInvocation): denoising_start: float, denoising_end: float, seed: int, - ) -> Tuple[int, List[int], int, Dict[str, Union[torch.Generator, float]]]: + ) -> Tuple[int, List[int], int, Dict[str, Any]]: assert isinstance(scheduler, ConfigMixin) if scheduler.config.get("cpu_only", False): scheduler.set_timesteps(steps, device="cpu") @@ -854,16 +854,15 @@ class DenoiseLatentsInvocation(BaseInvocation): timesteps = timesteps[t_start_idx : t_start_idx + t_end_idx] num_inference_steps = len(timesteps) // scheduler.order - scheduler_step_kwargs: Dict[str, Union[torch.Generator, float]] = {} + scheduler_step_kwargs: Dict[str, Any] = {} scheduler_step_signature = inspect.signature(scheduler.step) - print(scheduler_step_signature.parameters) if "generator" in scheduler_step_signature.parameters: # At some point, someone decided that schedulers that accept a generator should use the original seed with # all bits flipped. I don't know the original rationale for this, but now we must keep it like this for # reproducibility. - scheduler_step_kwargs = {"generator": torch.Generator(device=device).manual_seed(seed ^ 0xFFFFFFFF)} + scheduler_step_kwargs.update({"generator": torch.Generator(device=device).manual_seed(seed ^ 0xFFFFFFFF)}) if "eta" in scheduler_step_signature.parameters: - scheduler_step_kwargs = {"eta": 1.0} + scheduler_step_kwargs.update({"eta": 1.0}) return num_inference_steps, timesteps, init_timestep, scheduler_step_kwargs From dce8b88aafa767d36983c238e1f988f10e149628 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Wed, 1 May 2024 12:30:06 +0530 Subject: [PATCH 12/74] fix: change eta only for TCD Scheduler --- invokeai/app/invocations/latent.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 56e15e7566..3d1439f7db 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -11,7 +11,6 @@ import numpy.typing as npt import torch import torchvision import torchvision.transforms as T -from diffusers import AutoencoderKL, AutoencoderTiny from diffusers.configuration_utils import ConfigMixin from diffusers.image_processor import VaeImageProcessor from diffusers.models.adapter import T2IAdapter @@ -21,9 +20,12 @@ from diffusers.models.attention_processor import ( LoRAXFormersAttnProcessor, XFormersAttnProcessor, ) +from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL +from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel -from diffusers.schedulers import DPMSolverSDEScheduler -from diffusers.schedulers import SchedulerMixin as Scheduler +from diffusers.schedulers.scheduling_dpmsolver_sde import DPMSolverSDEScheduler +from diffusers.schedulers.scheduling_tcd import TCDScheduler +from diffusers.schedulers.scheduling_utils import SchedulerMixin as Scheduler from PIL import Image, ImageFilter from pydantic import field_validator from torchvision.transforms.functional import resize as tv_resize @@ -861,7 +863,7 @@ class DenoiseLatentsInvocation(BaseInvocation): # all bits flipped. I don't know the original rationale for this, but now we must keep it like this for # reproducibility. scheduler_step_kwargs.update({"generator": torch.Generator(device=device).manual_seed(seed ^ 0xFFFFFFFF)}) - if "eta" in scheduler_step_signature.parameters: + if isinstance(scheduler, TCDScheduler): scheduler_step_kwargs.update({"eta": 1.0}) return num_inference_steps, timesteps, init_timestep, scheduler_step_kwargs From 3717321480e56b5226017977efe23555eb20e113 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 1 May 2024 07:39:31 +1000 Subject: [PATCH 13/74] tidy(ui): organize layer components --- .../CALayer.tsx} | 15 ++++++------ .../{ => CALayer}/CALayerOpacity.tsx | 0 .../components/ControlLayersPanelContent.tsx | 12 +++++----- .../IPALayer.tsx} | 10 ++++---- .../{ => LayerCommon}/LayerDeleteButton.tsx | 0 .../{ => LayerCommon}/LayerMenu.tsx | 4 ++-- .../LayerMenuArrangeActions.tsx | 0 .../{ => LayerCommon}/LayerMenuRGActions.tsx | 0 .../{ => LayerCommon}/LayerTitle.tsx | 0 .../LayerVisibilityToggle.tsx | 0 .../RGLayer.tsx} | 24 +++++++++---------- .../RGLayerAutoNegativeCheckbox.tsx | 0 .../{ => RGLayer}/RGLayerColorPicker.tsx | 0 .../{ => RGLayer}/RGLayerIPAdapterList.tsx | 0 .../{ => RGLayer}/RGLayerNegativePrompt.tsx | 2 +- .../{ => RGLayer}/RGLayerPositivePrompt.tsx | 2 +- .../RGLayerPromptDeleteButton.tsx | 0 .../{ => RGLayer}/RGLayerSettingsPopover.tsx | 2 +- 18 files changed, 36 insertions(+), 35 deletions(-) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayerListItem.tsx => CALayer/CALayer.tsx} (90%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => CALayer}/CALayerOpacity.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{IPLayerListItem.tsx => IPALayer/IPALayer.tsx} (90%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => LayerCommon}/LayerDeleteButton.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => LayerCommon}/LayerMenu.tsx (96%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => LayerCommon}/LayerMenuArrangeActions.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => LayerCommon}/LayerMenuRGActions.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => LayerCommon}/LayerTitle.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => LayerCommon}/LayerVisibilityToggle.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{RGLayerListItem.tsx => RGLayer/RGLayer.tsx} (81%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => RGLayer}/RGLayerAutoNegativeCheckbox.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => RGLayer}/RGLayerColorPicker.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => RGLayer}/RGLayerIPAdapterList.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => RGLayer}/RGLayerNegativePrompt.tsx (97%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => RGLayer}/RGLayerPositivePrompt.tsx (97%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => RGLayer}/RGLayerPromptDeleteButton.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{ => RGLayer}/RGLayerSettingsPopover.tsx (96%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayerListItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx similarity index 90% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayerListItem.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index f97546c4fe..f39de592db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayerListItem.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -1,12 +1,11 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import CALayerOpacity from 'features/controlLayers/components/CALayerOpacity'; import ControlAdapterLayerConfig from 'features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig'; -import { LayerDeleteButton } from 'features/controlLayers/components/LayerDeleteButton'; -import { LayerMenu } from 'features/controlLayers/components/LayerMenu'; -import { LayerTitle } from 'features/controlLayers/components/LayerTitle'; -import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerVisibilityToggle'; +import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; +import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; +import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; +import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { isControlAdapterLayer, layerSelected, @@ -15,11 +14,13 @@ import { import { memo, useCallback, useMemo } from 'react'; import { assert } from 'tsafe'; +import CALayerOpacity from './CALayerOpacity'; + type Props = { layerId: string; }; -export const CALayerListItem = memo(({ layerId }: Props) => { +export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const selector = useMemo( () => @@ -68,4 +69,4 @@ export const CALayerListItem = memo(({ layerId }: Props) => { ); }); -CALayerListItem.displayName = 'CALayerListItem'; +CALayer.displayName = 'CALayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayerOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index e2865be356..ffa2856116 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -4,10 +4,10 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; -import { CALayerListItem } from 'features/controlLayers/components/CALayerListItem'; +import { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; -import { IPLayerListItem } from 'features/controlLayers/components/IPLayerListItem'; -import { RGLayerListItem } from 'features/controlLayers/components/RGLayerListItem'; +import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer'; +import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import type { Layer } from 'features/controlLayers/store/types'; import { partition } from 'lodash-es'; @@ -46,13 +46,13 @@ type LayerWrapperProps = { const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { if (type === 'regional_guidance_layer') { - return ; + return ; } if (type === 'control_adapter_layer') { - return ; + return ; } if (type === 'ip_adapter_layer') { - return ; + return ; } }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPLayerListItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx similarity index 90% rename from invokeai/frontend/web/src/features/controlLayers/components/IPLayerListItem.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx index bdc54373a0..fbef07f39e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPLayerListItem.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -2,9 +2,9 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import ControlAdapterLayerConfig from 'features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig'; -import { LayerDeleteButton } from 'features/controlLayers/components/LayerDeleteButton'; -import { LayerTitle } from 'features/controlLayers/components/LayerTitle'; -import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerVisibilityToggle'; +import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; +import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; +import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { isIPAdapterLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useMemo } from 'react'; import { assert } from 'tsafe'; @@ -13,7 +13,7 @@ type Props = { layerId: string; }; -export const IPLayerListItem = memo(({ layerId }: Props) => { +export const IPALayer = memo(({ layerId }: Props) => { const selector = useMemo( () => createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { @@ -44,4 +44,4 @@ export const IPLayerListItem = memo(({ layerId }: Props) => { ); }); -IPLayerListItem.displayName = 'IPLayerListItem'; +IPALayer.displayName = 'IPALayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerDeleteButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerMenu.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx index e5c8cc0aac..b83f48188f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx @@ -1,8 +1,8 @@ import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { LayerMenuArrangeActions } from 'features/controlLayers/components/LayerMenuArrangeActions'; -import { LayerMenuRGActions } from 'features/controlLayers/components/LayerMenuRGActions'; +import { LayerMenuArrangeActions } from 'features/controlLayers/components/LayerCommon/LayerMenuArrangeActions'; +import { LayerMenuRGActions } from 'features/controlLayers/components/LayerCommon/LayerMenuRGActions'; import { useLayerType } from 'features/controlLayers/hooks/layerStateHooks'; import { layerDeleted, layerReset } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerMenuArrangeActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerMenuArrangeActions.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerMenuRGActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerMenuRGActions.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerTitle.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerVisibilityToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerVisibilityToggle.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerListItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx similarity index 81% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerListItem.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index 3c126cabaa..a3dbfb00e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerListItem.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -2,15 +2,11 @@ import { Badge, Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { rgbColorToString } from 'features/canvas/util/colorToString'; -import { LayerDeleteButton } from 'features/controlLayers/components/LayerDeleteButton'; -import { LayerMenu } from 'features/controlLayers/components/LayerMenu'; -import { LayerTitle } from 'features/controlLayers/components/LayerTitle'; -import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerVisibilityToggle'; -import { RGLayerColorPicker } from 'features/controlLayers/components/RGLayerColorPicker'; -import { RGLayerIPAdapterList } from 'features/controlLayers/components/RGLayerIPAdapterList'; -import { RGLayerNegativePrompt } from 'features/controlLayers/components/RGLayerNegativePrompt'; -import { RGLayerPositivePrompt } from 'features/controlLayers/components/RGLayerPositivePrompt'; -import RGLayerSettingsPopover from 'features/controlLayers/components/RGLayerSettingsPopover'; +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 { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { isRegionalGuidanceLayer, layerSelected, @@ -20,13 +16,17 @@ import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; -import { AddPromptButtons } from './AddPromptButtons'; +import { RGLayerColorPicker } from './RGLayerColorPicker'; +import { RGLayerIPAdapterList } from './RGLayerIPAdapterList'; +import { RGLayerNegativePrompt } from './RGLayerNegativePrompt'; +import { RGLayerPositivePrompt } from './RGLayerPositivePrompt'; +import RGLayerSettingsPopover from './RGLayerSettingsPopover'; type Props = { layerId: string; }; -export const RGLayerListItem = memo(({ layerId }: Props) => { +export const RGLayer = memo(({ layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const selector = useMemo( @@ -81,4 +81,4 @@ export const RGLayerListItem = memo(({ layerId }: Props) => { ); }); -RGLayerListItem.displayName = 'RGLayerListItem'; +RGLayer.displayName = 'RGLayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerAutoNegativeCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerAutoNegativeCheckbox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerColorPicker.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerIPAdapterList.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerNegativePrompt.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx index e869c8809a..7dfddbccf2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayerPromptDeleteButton'; +import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; import { useLayerNegativePrompt } from 'features/controlLayers/hooks/layerStateHooks'; import { maskLayerNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerPositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerPositivePrompt.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx index 6d508338c1..99fe834fd3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayerPromptDeleteButton'; +import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; import { useLayerPositivePrompt } from 'features/controlLayers/hooks/layerStateHooks'; import { maskLayerPositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerPromptDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerPromptDeleteButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerSettingsPopover.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerSettingsPopover.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerSettingsPopover.tsx index e270748b9b..9203069b3c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerSettingsPopover.tsx @@ -10,7 +10,7 @@ import { PopoverTrigger, } from '@invoke-ai/ui-library'; import { stopPropagation } from 'common/util/stopPropagation'; -import { RGLayerAutoNegativeCheckbox } from 'features/controlLayers/components/RGLayerAutoNegativeCheckbox'; +import { RGLayerAutoNegativeCheckbox } from 'features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiGearSixBold } from 'react-icons/pi'; From 121918352a8aa14bde231bbe0c0d27079a08c7d3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 1 May 2024 13:21:49 +1000 Subject: [PATCH 14/74] refactor(ui): add control layers separate control adapter implementation (wip) - Revise control adapter config types - Recreate all control adapter mutations in control layers slice - Bit of renaming along the way - typing 'RegionalGuidanceLayer' over and over again was getting tedious --- .../controlLayersToControlAdapterBridge.ts | 20 +- .../components/AddPromptButtons.tsx | 8 +- .../components/CALayer/CALayerOpacity.tsx | 4 +- .../LayerCommon/LayerMenuRGActions.tsx | 8 +- .../RGLayer/RGLayerAutoNegativeCheckbox.tsx | 4 +- .../components/RGLayer/RGLayerColorPicker.tsx | 4 +- .../RGLayer/RGLayerNegativePrompt.tsx | 4 +- .../RGLayer/RGLayerPositivePrompt.tsx | 4 +- .../RGLayer/RGLayerPromptDeleteButton.tsx | 8 +- .../controlLayers/hooks/mouseEventHooks.ts | 16 +- .../controlLayers/store/controlLayersSlice.ts | 577 +++++++++++------- .../src/features/controlLayers/store/types.ts | 11 +- .../src/features/controlLayers/util/bbox.ts | 4 +- .../controlLayers/util/controlAdapters.ts | 354 +++++++++++ .../controlLayers/util/getLayerBlobs.ts | 4 +- .../features/controlLayers/util/renderers.ts | 48 +- 16 files changed, 775 insertions(+), 303 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts index bc14277f88..81672758c9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts @@ -5,12 +5,12 @@ import { controlAdapterAdded, controlAdapterRemoved } from 'features/controlAdap import type { ControlNetConfig, IPAdapterConfig } from 'features/controlAdapters/store/types'; import { isControlAdapterProcessorType } from 'features/controlAdapters/store/types'; import { - controlAdapterLayerAdded, - ipAdapterLayerAdded, + caLayerAdded, + ipaLayerAdded, layerDeleted, - maskLayerIPAdapterAdded, - maskLayerIPAdapterDeleted, - regionalGuidanceLayerAdded, + rgLayerAdded, + rgLayerIPAdapterAdded, + rgLayerIPAdapterDeleted, } from 'features/controlLayers/store/controlLayersSlice'; import type { Layer } from 'features/controlLayers/store/types'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; @@ -33,7 +33,7 @@ export const addControlLayersToControlAdapterBridge = (startAppListening: AppSta const type = action.payload; const layerId = uuidv4(); if (type === 'regional_guidance_layer') { - dispatch(regionalGuidanceLayerAdded({ layerId })); + dispatch(rgLayerAdded({ layerId })); return; } @@ -53,7 +53,7 @@ export const addControlLayersToControlAdapterBridge = (startAppListening: AppSta overrides.model = models.find((m) => m.base === baseModel) ?? null; } dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides })); - dispatch(ipAdapterLayerAdded({ layerId, ipAdapterId })); + dispatch(ipaLayerAdded({ layerId, ipAdapterId })); return; } @@ -73,7 +73,7 @@ export const addControlLayersToControlAdapterBridge = (startAppListening: AppSta overrides.processorNode = CONTROLNET_PROCESSORS[overrides.processorType].buildDefaults(baseModel); } dispatch(controlAdapterAdded({ type: 'controlnet', overrides })); - dispatch(controlAdapterLayerAdded({ layerId, controlNetId })); + dispatch(caLayerAdded({ layerId, controlNetId })); return; } }, @@ -129,7 +129,7 @@ export const addControlLayersToControlAdapterBridge = (startAppListening: AppSta } dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides })); - dispatch(maskLayerIPAdapterAdded({ layerId, ipAdapterId })); + dispatch(rgLayerIPAdapterAdded({ layerId, ipAdapterId })); }, }); @@ -138,7 +138,7 @@ export const addControlLayersToControlAdapterBridge = (startAppListening: AppSta effect: (action, { dispatch }) => { const { layerId, ipAdapterId } = action.payload; dispatch(controlAdapterRemoved({ id: ipAdapterId })); - dispatch(maskLayerIPAdapterDeleted({ layerId, ipAdapterId })); + dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); }, }); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 88eac207b2..d943b33f60 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -4,8 +4,8 @@ import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddle import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { isRegionalGuidanceLayer, - maskLayerNegativePromptChanged, - maskLayerPositivePromptChanged, + rgLayerNegativePromptChanged, + rgLayerPositivePromptChanged, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; import { useCallback, useMemo } from 'react'; @@ -33,10 +33,10 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { ); const validActions = useAppSelector(selectValidActions); const addPositivePrompt = useCallback(() => { - dispatch(maskLayerPositivePromptChanged({ layerId, prompt: '' })); + dispatch(rgLayerPositivePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); const addNegativePrompt = useCallback(() => { - dispatch(maskLayerNegativePromptChanged({ layerId, prompt: '' })); + dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); const addIPAdapter = useCallback(() => { dispatch(guidanceLayerIPAdapterAdded(layerId)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx index a6107da1ec..3e73158343 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx @@ -15,7 +15,7 @@ import { import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; -import { isFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { caLayerIsFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -40,7 +40,7 @@ const CALayerOpacity = ({ layerId }: Props) => { ); const onChangeFilter = useCallback( (e: ChangeEvent) => { - dispatch(isFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked })); + dispatch(caLayerIsFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked })); }, [dispatch, layerId] ); 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 6c2bb4c26b..d488a0e31d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx @@ -4,8 +4,8 @@ import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddle import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { isRegionalGuidanceLayer, - maskLayerNegativePromptChanged, - maskLayerPositivePromptChanged, + rgLayerNegativePromptChanged, + rgLayerPositivePromptChanged, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback, useMemo } from 'react'; @@ -32,10 +32,10 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { ); const validActions = useAppSelector(selectValidActions); const addPositivePrompt = useCallback(() => { - dispatch(maskLayerPositivePromptChanged({ layerId, prompt: '' })); + dispatch(rgLayerPositivePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); const addNegativePrompt = useCallback(() => { - dispatch(maskLayerNegativePromptChanged({ layerId, prompt: '' })); + dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); const addIPAdapter = useCallback(() => { dispatch(guidanceLayerIPAdapterAdded(layerId)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx index 6f03d4b28d..89edb58d2f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx @@ -3,7 +3,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { isRegionalGuidanceLayer, - maskLayerAutoNegativeChanged, + rgLayerAutoNegativeChanged, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; import type { ChangeEvent } from 'react'; @@ -35,7 +35,7 @@ export const RGLayerAutoNegativeCheckbox = memo(({ layerId }: Props) => { const autoNegative = useAutoNegative(layerId); const onChange = useCallback( (e: ChangeEvent) => { - dispatch(maskLayerAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' })); + dispatch(rgLayerAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx index e76ab57a51..624047caf3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx @@ -6,7 +6,7 @@ import { stopPropagation } from 'common/util/stopPropagation'; import { rgbColorToString } from 'features/canvas/util/colorToString'; import { isRegionalGuidanceLayer, - maskLayerPreviewColorChanged, + rgLayerPreviewColorChanged, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback, useMemo } from 'react'; @@ -33,7 +33,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const onColorChange = useCallback( (color: RgbColor) => { - dispatch(maskLayerPreviewColorChanged({ layerId, color })); + dispatch(rgLayerPreviewColorChanged({ layerId, color })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx index 7dfddbccf2..ba02aa9242 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx @@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; import { useLayerNegativePrompt } from 'features/controlLayers/hooks/layerStateHooks'; -import { maskLayerNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { rgLayerNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -21,7 +21,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => { const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(maskLayerNegativePromptChanged({ layerId, prompt: v })); + dispatch(rgLayerNegativePromptChanged({ layerId, prompt: v })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx index 99fe834fd3..6f85ea077c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx @@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; import { useLayerPositivePrompt } from 'features/controlLayers/hooks/layerStateHooks'; -import { maskLayerPositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { rgLayerPositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -21,7 +21,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => { const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(maskLayerPositivePromptChanged({ layerId, prompt: v })); + dispatch(rgLayerPositivePromptChanged({ layerId, prompt: v })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx index 9a32bb68ad..62a4ddfaeb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx @@ -1,8 +1,8 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { - maskLayerNegativePromptChanged, - maskLayerPositivePromptChanged, + rgLayerNegativePromptChanged, + rgLayerPositivePromptChanged, } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,9 +18,9 @@ export const RGLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) => const dispatch = useAppDispatch(); const onClick = useCallback(() => { if (polarity === 'positive') { - dispatch(maskLayerPositivePromptChanged({ layerId, prompt: null })); + dispatch(rgLayerPositivePromptChanged({ layerId, prompt: null })); } else { - dispatch(maskLayerNegativePromptChanged({ layerId, prompt: null })); + dispatch(rgLayerNegativePromptChanged({ layerId, prompt: null })); } }, [dispatch, layerId, polarity]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts index bab7ef263f..bb744b0535 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts @@ -9,9 +9,9 @@ import { $lastMouseDownPos, $tool, brushSizeChanged, - maskLayerLineAdded, - maskLayerPointsAdded, - maskLayerRectAdded, + rfLayerLineAdded, + rgLayerPointsAdded, + rgLayerRectAdded, } from 'features/controlLayers/store/controlLayersSlice'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -71,7 +71,7 @@ export const useMouseEvents = () => { } if (tool === 'brush' || tool === 'eraser') { dispatch( - maskLayerLineAdded({ + rfLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool, @@ -94,7 +94,7 @@ export const useMouseEvents = () => { const tool = $tool.get(); if (pos && lastPos && selectedLayerId && tool === 'rect') { dispatch( - maskLayerRectAdded({ + rgLayerRectAdded({ layerId: selectedLayerId, rect: { x: Math.min(pos.x, lastPos.x), @@ -128,7 +128,7 @@ export const useMouseEvents = () => { } } lastCursorPosRef.current = [pos.x, pos.y]; - dispatch(maskLayerPointsAdded({ layerId: selectedLayerId, point: lastCursorPosRef.current })); + dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: lastCursorPosRef.current })); } }, [dispatch, selectedLayerId, tool] @@ -149,7 +149,7 @@ export const useMouseEvents = () => { $isMouseDown.get() && (tool === 'brush' || tool === 'eraser') ) { - dispatch(maskLayerPointsAdded({ layerId: selectedLayerId, point: [pos.x, pos.y] })); + dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: [pos.x, pos.y] })); } $isMouseOver.set(false); $isMouseDown.set(false); @@ -181,7 +181,7 @@ export const useMouseEvents = () => { } if (tool === 'brush' || tool === 'eraser') { dispatch( - maskLayerLineAdded({ + rfLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 6d351d4d0d..f2b567e41a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -3,12 +3,17 @@ import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; -import { roundToMultiple } from 'common/util/roundDownToMultiple'; -import { - controlAdapterImageChanged, - controlAdapterProcessedImageChanged, - isAnyControlAdapterAdded, -} from 'features/controlAdapters/store/controlAdaptersSlice'; +import type { + CLIPVisionModel, + ControlMode, + ControlNetConfig, + IPAdapterConfig, + IPMethod, + ProcessorConfig, + T2IAdapterConfig, +} from 'features/controlLayers/util/controlAdapters'; +import { buildControlAdapterProcessor, imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; @@ -20,6 +25,7 @@ import { isEqual, partition } from 'lodash-es'; import { atom } from 'nanostores'; import type { RgbColor } from 'react-colorful'; import type { UndoableOptions } from 'redux-undo'; +import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -47,7 +53,6 @@ export const initialControlLayersState: ControlLayersState = { positivePrompt2: '', negativePrompt2: '', shouldConcatPrompts: true, - initialImage: null, size: { width: 512, height: 512, @@ -82,76 +87,35 @@ const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => { const lastColor = vmLayers[vmLayers.length - 1]?.previewColor; return LayerColors.next(lastColor); }; +const getCALayer = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isControlAdapterLayer(layer)); + return layer; +}; +const getIPALayer = (state: ControlLayersState, layerId: string): IPAdapterLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isIPAdapterLayer(layer)); + return layer; +}; +const getRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isRegionalGuidanceLayer(layer)); + return layer; +}; +const getRGLayerIPAdapter = (state: ControlLayersState, layerId: string, ipAdapterId: string): IPAdapterConfig => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isRegionalGuidanceLayer(layer)); + const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); + assert(ipAdapter); + return ipAdapter; +}; export const controlLayersSlice = createSlice({ name: 'controlLayers', initialState: initialControlLayersState, reducers: { //#region All Layers - regionalGuidanceLayerAdded: (state, action: PayloadAction<{ layerId: string }>) => { - const { layerId } = action.payload; - const layer: RegionalGuidanceLayer = { - id: getRegionalGuidanceLayerId(layerId), - type: 'regional_guidance_layer', - isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, - maskObjects: [], - previewColor: getVectorMaskPreviewColor(state), - x: 0, - y: 0, - autoNegative: 'invert', - needsPixelBbox: false, - positivePrompt: '', - negativePrompt: null, - ipAdapterIds: [], - isSelected: true, - }; - state.layers.push(layer); - state.selectedLayerId = layer.id; - for (const layer of state.layers.filter(isRenderableLayer)) { - if (layer.id !== layerId) { - layer.isSelected = false; - } - } - return; - }, - ipAdapterLayerAdded: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { - const { layerId, ipAdapterId } = action.payload; - const layer: IPAdapterLayer = { - id: getIPAdapterLayerId(layerId), - type: 'ip_adapter_layer', - isEnabled: true, - ipAdapterId, - }; - state.layers.push(layer); - return; - }, - controlAdapterLayerAdded: (state, action: PayloadAction<{ layerId: string; controlNetId: string }>) => { - const { layerId, controlNetId } = action.payload; - const layer: ControlAdapterLayer = { - id: getControlNetLayerId(layerId), - type: 'control_adapter_layer', - controlNetId, - x: 0, - y: 0, - bbox: null, - bboxNeedsUpdate: false, - isEnabled: true, - imageName: null, - opacity: 1, - isSelected: true, - isFilterEnabled: true, - }; - state.layers.push(layer); - state.selectedLayerId = layer.id; - for (const layer of state.layers.filter(isRenderableLayer)) { - if (layer.id !== layerId) { - layer.isSelected = false; - } - } - return; - }, + layerSelected: (state, action: PayloadAction) => { for (const layer of state.layers.filter(isRenderableLayer)) { if (layer.id === action.payload) { @@ -245,7 +209,103 @@ export const controlLayersSlice = createSlice({ //#endregion //#region CA Layers - isFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => { + caLayerAdded: { + reducer: ( + state, + action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }> + ) => { + const { layerId, controlAdapter } = action.payload; + const layer: ControlAdapterLayer = { + id: getCALayerId(layerId), + type: 'control_adapter_layer', + x: 0, + y: 0, + bbox: null, + bboxNeedsUpdate: false, + isEnabled: true, + opacity: 1, + isSelected: true, + isFilterEnabled: true, + controlAdapter, + }; + state.layers.push(layer); + state.selectedLayerId = layer.id; + for (const layer of state.layers.filter(isRenderableLayer)) { + if (layer.id !== layerId) { + layer.isSelected = false; + } + } + }, + prepare: (controlAdapter: ControlNetConfig | T2IAdapterConfig) => ({ + payload: { layerId: uuidv4(), controlAdapter }, + }), + }, + caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { + const { layerId, imageDTO } = action.payload; + const layer = getCALayer(state, layerId); + layer.bbox = null; + layer.bboxNeedsUpdate = true; + layer.isEnabled = true; + layer.controlAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + caLayerProcessedImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { + const { layerId, imageDTO } = action.payload; + const layer = getCALayer(state, layerId); + layer.bbox = null; + layer.bboxNeedsUpdate = true; + layer.isEnabled = true; + layer.controlAdapter.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + caLayerModelChanged: ( + state, + action: PayloadAction<{ + layerId: string; + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; + }> + ) => { + const { layerId, modelConfig } = action.payload; + const layer = getCALayer(state, layerId); + if (!modelConfig) { + layer.controlAdapter.model = null; + return; + } + layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig); + const candidateProcessorConfig = buildControlAdapterProcessor(modelConfig); + if (candidateProcessorConfig?.type !== layer.controlAdapter.processorConfig?.type) { + // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth + // model. We need to use the new processor. + layer.controlAdapter.processedImage = null; + layer.controlAdapter.processorConfig = candidateProcessorConfig; + } + }, + caLayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { + const { layerId, weight } = action.payload; + const layer = getCALayer(state, layerId); + layer.controlAdapter.weight = weight; + }, + caLayerBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> + ) => { + const { layerId, beginEndStepPct } = action.payload; + const layer = getCALayer(state, layerId); + layer.controlAdapter.beginEndStepPct = beginEndStepPct; + }, + caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlMode }>) => { + const { layerId, controlMode } = action.payload; + const layer = getCALayer(state, layerId); + assert(layer.controlAdapter.type === 'controlnet'); + layer.controlAdapter.controlMode = controlMode; + }, + caLayerProcessorConfigChanged: ( + state, + action: PayloadAction<{ layerId: string; processorConfig: ProcessorConfig }> + ) => { + const { layerId, processorConfig } = action.payload; + const layer = getCALayer(state, layerId); + layer.controlAdapter.processorConfig = processorConfig; + }, + caLayerIsFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => { const { layerId, isFilterEnabled } = action.payload; const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); if (layer) { @@ -254,121 +314,217 @@ export const controlLayersSlice = createSlice({ }, //#endregion - //#region Mask Layers - maskLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { + //#region IP Adapter Layers + ipaLayerAdded: { + reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { + const { layerId, ipAdapter } = action.payload; + const layer: IPAdapterLayer = { + id: getIPALayerId(layerId), + type: 'ip_adapter_layer', + isEnabled: true, + ipAdapter, + }; + state.layers.push(layer); + }, + prepare: (ipAdapter: IPAdapterConfig) => ({ payload: { layerId: uuidv4(), ipAdapter } }), + }, + ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { + const { layerId, imageDTO } = action.payload; + const layer = getIPALayer(state, layerId); + layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + ipaLayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { + const { layerId, weight } = action.payload; + const layer = getIPALayer(state, layerId); + layer.ipAdapter.weight = weight; + }, + ipaLayerBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> + ) => { + const { layerId, beginEndStepPct } = action.payload; + const layer = getIPALayer(state, layerId); + layer.ipAdapter.beginEndStepPct = beginEndStepPct; + }, + ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethod }>) => { + const { layerId, method } = action.payload; + const layer = getIPALayer(state, layerId); + layer.ipAdapter.method = method; + }, + ipaLayerCLIPVisionModelChanged: ( + state, + action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }> + ) => { + const { layerId, clipVisionModel } = action.payload; + const layer = getIPALayer(state, layerId); + layer.ipAdapter.clipVisionModel = clipVisionModel; + }, + //#endregion + + //#region RG Layers + rgLayerAdded: (state, action: PayloadAction<{ layerId: string }>) => { + const { layerId } = action.payload; + const layer: RegionalGuidanceLayer = { + id: getRGLayerId(layerId), + type: 'regional_guidance_layer', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + maskObjects: [], + previewColor: getVectorMaskPreviewColor(state), + x: 0, + y: 0, + autoNegative: 'invert', + needsPixelBbox: false, + positivePrompt: '', + negativePrompt: null, + ipAdapters: [], + isSelected: true, + }; + state.layers.push(layer); + state.selectedLayerId = layer.id; + for (const layer of state.layers.filter(isRenderableLayer)) { + if (layer.id !== layerId) { + layer.isSelected = false; + } + } + }, + rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (layer?.type === 'regional_guidance_layer') { - layer.positivePrompt = prompt; - } + const layer = getRGLayer(state, layerId); + layer.positivePrompt = prompt; }, - maskLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { + rgLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (layer?.type === 'regional_guidance_layer') { - layer.negativePrompt = prompt; - } + const layer = getRGLayer(state, layerId); + layer.negativePrompt = prompt; }, - maskLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { + rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { + const { layerId, ipAdapter } = action.payload; + const layer = getRGLayer(state, layerId); + layer.ipAdapters.push(ipAdapter); + }, + rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { const { layerId, ipAdapterId } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (layer?.type === 'regional_guidance_layer') { - layer.ipAdapterIds.push(ipAdapterId); - } + const layer = getRGLayer(state, layerId); + layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); }, - maskLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { - const { layerId, ipAdapterId } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (layer?.type === 'regional_guidance_layer') { - layer.ipAdapterIds = layer.ipAdapterIds.filter((id) => id !== ipAdapterId); - } - }, - maskLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { + rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { const { layerId, color } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (layer?.type === 'regional_guidance_layer') { - layer.previewColor = color; - } + const layer = getRGLayer(state, layerId); + layer.previewColor = color; }, - maskLayerLineAdded: { + rgLayerLineAdded: { reducer: ( state, - action: PayloadAction< - { layerId: string; points: [number, number, number, number]; tool: DrawingTool }, - string, - { uuid: string } - > + action: PayloadAction<{ + layerId: string; + points: [number, number, number, number]; + tool: DrawingTool; + lineUuid: string; + }> ) => { - const { layerId, points, tool } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (layer?.type === 'regional_guidance_layer') { - const lineId = getRegionalGuidanceLayerLineId(layer.id, action.meta.uuid); - layer.maskObjects.push({ - type: 'vector_mask_line', - tool: tool, - id: lineId, - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - }); - layer.bboxNeedsUpdate = true; - if (!layer.needsPixelBbox && tool === 'eraser') { - layer.needsPixelBbox = true; - } + const { layerId, points, tool, lineUuid } = action.payload; + const layer = getRGLayer(state, layerId); + const lineId = getRGLayerLineId(layer.id, lineUuid); + layer.maskObjects.push({ + type: 'vector_mask_line', + tool: tool, + id: lineId, + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], + strokeWidth: state.brushSize, + }); + layer.bboxNeedsUpdate = true; + if (!layer.needsPixelBbox && tool === 'eraser') { + layer.needsPixelBbox = true; } }, prepare: (payload: { layerId: string; points: [number, number, number, number]; tool: DrawingTool }) => ({ - payload, - meta: { uuid: uuidv4() }, + payload: { ...payload, lineUuid: uuidv4() }, }), }, - maskLayerPointsAdded: (state, action: PayloadAction<{ layerId: string; point: [number, number] }>) => { + rgLayerPointsAdded: (state, action: PayloadAction<{ layerId: string; point: [number, number] }>) => { const { layerId, point } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (layer?.type === 'regional_guidance_layer') { - const lastLine = layer.maskObjects.findLast(isLine); - if (!lastLine) { - return; - } - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener - lastLine.points.push(point[0] - layer.x, point[1] - layer.y); - layer.bboxNeedsUpdate = true; + const layer = getRGLayer(state, layerId); + const lastLine = layer.maskObjects.findLast(isLine); + if (!lastLine) { + return; } + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener + lastLine.points.push(point[0] - layer.x, point[1] - layer.y); + layer.bboxNeedsUpdate = true; }, - maskLayerRectAdded: { - reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect }, string, { uuid: string }>) => { - const { layerId, rect } = action.payload; + rgLayerRectAdded: { + reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect; rectUuid: string }>) => { + const { layerId, rect, rectUuid } = action.payload; if (rect.height === 0 || rect.width === 0) { // Ignore zero-area rectangles return; } - const layer = state.layers.find((l) => l.id === layerId); - if (layer?.type === 'regional_guidance_layer') { - const id = getMaskedGuidnaceLayerRectId(layer.id, action.meta.uuid); - layer.maskObjects.push({ - type: 'vector_mask_rect', - id, - x: rect.x - layer.x, - y: rect.y - layer.y, - width: rect.width, - height: rect.height, - }); - layer.bboxNeedsUpdate = true; - } + const layer = getRGLayer(state, layerId); + const id = getRGLayerRectId(layer.id, rectUuid); + layer.maskObjects.push({ + type: 'vector_mask_rect', + id, + x: rect.x - layer.x, + y: rect.y - layer.y, + width: rect.width, + height: rect.height, + }); + layer.bboxNeedsUpdate = true; }, - prepare: (payload: { layerId: string; rect: IRect }) => ({ payload, meta: { uuid: uuidv4() } }), + prepare: (payload: { layerId: string; rect: IRect }) => ({ payload: { ...payload, rectUuid: uuidv4() } }), }, - maskLayerAutoNegativeChanged: ( + rgLayerAutoNegativeChanged: ( state, action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }> ) => { const { layerId, autoNegative } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (layer?.type === 'regional_guidance_layer') { - layer.autoNegative = autoNegative; - } + const layer = getRGLayer(state, layerId); + layer.autoNegative = autoNegative; + }, + rgLayerIPAdapterImageChanged: ( + state, + action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }> + ) => { + const { layerId, ipAdapterId, imageDTO } = action.payload; + const ipAdapter = getRGLayerIPAdapter(state, layerId, ipAdapterId); + ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + rgLayerIPAdapterWeightChanged: ( + state, + action: PayloadAction<{ layerId: string; ipAdapterId: string; weight: number }> + ) => { + const { layerId, ipAdapterId, weight } = action.payload; + const ipAdapter = getRGLayerIPAdapter(state, layerId, ipAdapterId); + ipAdapter.weight = weight; + }, + rgLayerIPAdapterBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ layerId: string; ipAdapterId: string; beginEndStepPct: [number, number] }> + ) => { + const { layerId, ipAdapterId, beginEndStepPct } = action.payload; + const ipAdapter = getRGLayerIPAdapter(state, layerId, ipAdapterId); + ipAdapter.beginEndStepPct = beginEndStepPct; + }, + rgLayerIPAdapterMethodChanged: ( + state, + action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethod }> + ) => { + const { layerId, ipAdapterId, method } = action.payload; + const ipAdapter = getRGLayerIPAdapter(state, layerId, ipAdapterId); + ipAdapter.method = method; + }, + rgLayerIPAdapterCLIPVisionModelChanged: ( + state, + action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModel }> + ) => { + const { layerId, ipAdapterId, clipVisionModel } = action.payload; + const ipAdapter = getRGLayerIPAdapter(state, layerId, ipAdapterId); + ipAdapter.clipVisionModel = clipVisionModel; }, //#endregion @@ -451,36 +607,14 @@ export const controlLayersSlice = createSlice({ state.size.height = height; }); - builder.addCase(controlAdapterImageChanged, (state, action) => { - const { id, controlImage } = action.payload; - const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.controlNetId === id); - if (layer) { - layer.bbox = null; - layer.bboxNeedsUpdate = true; - layer.isEnabled = true; - layer.imageName = controlImage?.image_name ?? null; - } - }); - - builder.addCase(controlAdapterProcessedImageChanged, (state, action) => { - const { id, processedControlImage } = action.payload; - const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.controlNetId === id); - if (layer) { - layer.bbox = null; - layer.bboxNeedsUpdate = true; - layer.isEnabled = true; - layer.imageName = processedControlImage?.image_name ?? null; - } - }); - - // TODO: This is a temp fix to reduce issues with T2I adapter having a different downscaling - // factor than the UNet. Hopefully we get an upstream fix in diffusers. - builder.addMatcher(isAnyControlAdapterAdded, (state, action) => { - if (action.payload.type === 't2i_adapter') { - state.size.width = roundToMultiple(state.size.width, 64); - state.size.height = roundToMultiple(state.size.height, 64); - } - }); + // // TODO: This is a temp fix to reduce issues with T2I adapter having a different downscaling + // // factor than the UNet. Hopefully we get an upstream fix in diffusers. + // builder.addMatcher(isAnyControlAdapterAdded, (state, action) => { + // if (action.payload.type === 't2i_adapter') { + // state.size.width = roundToMultiple(state.size.width, 64); + // state.size.height = roundToMultiple(state.size.height, 64); + // } + // }); }, }); @@ -529,22 +663,22 @@ export const { layerVisibilityToggled, selectedLayerReset, selectedLayerDeleted, - regionalGuidanceLayerAdded, - ipAdapterLayerAdded, - controlAdapterLayerAdded, + rgLayerAdded: regionalGuidanceLayerAdded, + ipaLayerAdded: ipAdapterLayerAdded, + caLayerAdded: controlAdapterLayerAdded, layerOpacityChanged, // CA layer actions - isFilterEnabledChanged, + caLayerIsFilterEnabledChanged: isFilterEnabledChanged, // Mask layer actions - maskLayerLineAdded, - maskLayerPointsAdded, - maskLayerRectAdded, - maskLayerNegativePromptChanged, - maskLayerPositivePromptChanged, - maskLayerIPAdapterAdded, - maskLayerIPAdapterDeleted, - maskLayerAutoNegativeChanged, - maskLayerPreviewColorChanged, + rgLayerLineAdded: maskLayerLineAdded, + rgLayerPointsAdded: maskLayerPointsAdded, + rgLayerRectAdded: maskLayerRectAdded, + rgLayerNegativePromptChanged: maskLayerNegativePromptChanged, + rgLayerPositivePromptChanged: maskLayerPositivePromptChanged, + rgLayerIPAdapterAdded: maskLayerIPAdapterAdded, + rgLayerIPAdapterDeleted: maskLayerIPAdapterDeleted, + rgLayerAutoNegativeChanged: maskLayerAutoNegativeChanged, + rgLayerPreviewColorChanged: maskLayerPreviewColorChanged, // Base layer actions positivePromptChanged, negativePromptChanged, @@ -561,20 +695,6 @@ export const { redo, } = controlLayersSlice.actions; -export const selectAllControlAdapterIds = (controlLayers: ControlLayersState) => - controlLayers.layers.flatMap((l) => { - if (l.type === 'control_adapter_layer') { - return [l.controlNetId]; - } - if (l.type === 'ip_adapter_layer') { - return [l.ipAdapterId]; - } - if (l.type === 'regional_guidance_layer') { - return l.ipAdapterIds; - } - return []; - }); - export const selectControlLayersSlice = (state: RootState) => state.controlLayers; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -600,24 +720,23 @@ export const BACKGROUND_RECT_ID = 'background_layer.rect'; export const NO_LAYERS_MESSAGE_LAYER_ID = 'no_layers_message'; // Names (aka classes) for Konva layers and objects -export const CONTROLNET_LAYER_NAME = 'control_adapter_layer'; -export const CONTROLNET_LAYER_IMAGE_NAME = 'control_adapter_layer.image'; -export const regional_guidance_layer_NAME = 'regional_guidance_layer'; -export const regional_guidance_layer_LINE_NAME = 'regional_guidance_layer.line'; -export const regional_guidance_layer_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group'; -export const regional_guidance_layer_RECT_NAME = 'regional_guidance_layer.rect'; +export const CA_LAYER_NAME = 'control_adapter_layer'; +export const CA_LAYER_IMAGE_NAME = 'control_adapter_layer.image'; +export const RG_LAYER_NAME = 'regional_guidance_layer'; +export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line'; +export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group'; +export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect'; export const LAYER_BBOX_NAME = 'layer.bbox'; // Getters for non-singleton layer and object IDs -const getRegionalGuidanceLayerId = (layerId: string) => `${regional_guidance_layer_NAME}_${layerId}`; -const getRegionalGuidanceLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`; -const getMaskedGuidnaceLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`; -export const getRegionalGuidanceLayerObjectGroupId = (layerId: string, groupId: string) => - `${layerId}.objectGroup_${groupId}`; +const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; +const getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`; +const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`; +export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`; -const getControlNetLayerId = (layerId: string) => `control_adapter_layer_${layerId}`; -export const getControlNetLayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; -const getIPAdapterLayerId = (layerId: string) => `ip_adapter_layer_${layerId}`; +const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`; +export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; +const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`; export const controlLayersPersistConfig: PersistConfig = { name: controlLayersSlice.name, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 58b25f967b..3d5ba672ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,3 +1,4 @@ +import type { ControlNetConfig, IPAdapterConfig,T2IAdapterConfig } from 'features/controlLayers/util/controlAdapters'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { ParameterAutoNegative, @@ -47,15 +48,14 @@ type RenderableLayerBase = LayerBase & { export type ControlAdapterLayer = RenderableLayerBase & { type: 'control_adapter_layer'; // technically, also t2i adapter layer - controlNetId: string; - imageName: string | null; opacity: number; isFilterEnabled: boolean; + controlAdapter: ControlNetConfig | T2IAdapterConfig; }; export type IPAdapterLayer = LayerBase & { - type: 'ip_adapter_layer'; // technically, also t2i adapter layer - ipAdapterId: string; + type: 'ip_adapter_layer'; + ipAdapter: IPAdapterConfig; }; export type RegionalGuidanceLayer = RenderableLayerBase & { @@ -63,7 +63,7 @@ export type RegionalGuidanceLayer = RenderableLayerBase & { maskObjects: (VectorMaskLine | VectorMaskRect)[]; positivePrompt: ParameterPositivePrompt | null; negativePrompt: ParameterNegativePrompt | null; // Up to one text prompt per mask - ipAdapterIds: string[]; // Any number of image prompts + ipAdapters: IPAdapterConfig[]; // Any number of image prompts previewColor: RgbColor; autoNegative: ParameterAutoNegative; needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object @@ -83,7 +83,6 @@ export type ControlLayersState = { positivePrompt2: ParameterPositiveStylePromptSDXL; negativePrompt2: ParameterNegativeStylePromptSDXL; shouldConcatPrompts: boolean; - initialImage: string | null; size: { width: ParameterWidth; height: ParameterHeight; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts index 3c2915e0ab..a4c7be6886 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts @@ -1,6 +1,6 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL'; -import { regional_guidance_layer_OBJECT_GROUP_NAME } from 'features/controlLayers/store/controlLayersSlice'; +import { RG_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/store/controlLayersSlice'; import Konva from 'konva'; import type { Layer as KonvaLayerType } from 'konva/lib/Layer'; import type { IRect } from 'konva/lib/types'; @@ -81,7 +81,7 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal offscreenStage.add(layerClone); for (const child of layerClone.getChildren()) { - if (child.name() === regional_guidance_layer_OBJECT_GROUP_NAME) { + if (child.name() === RG_LAYER_OBJECT_GROUP_NAME) { // We need to cache the group to ensure it composites out eraser strokes correctly child.opacity(1); child.cache(); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts new file mode 100644 index 0000000000..5a13ed2315 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -0,0 +1,354 @@ +import { deepClone } from 'common/util/deepClone'; +import type { + ParameterControlNetModel, + ParameterIPAdapterModel, + ParameterT2IAdapterModel, +} from 'features/parameters/types/parameterSchemas'; +import { merge } from 'lodash-es'; +import type { + BaseModelType, + CannyImageProcessorInvocation, + ColorMapImageProcessorInvocation, + ContentShuffleImageProcessorInvocation, + ControlNetInvocation, + ControlNetModelConfig, + DepthAnythingImageProcessorInvocation, + DWOpenposeImageProcessorInvocation, + HedImageProcessorInvocation, + ImageDTO, + LineartAnimeImageProcessorInvocation, + LineartImageProcessorInvocation, + MediapipeFaceProcessorInvocation, + MidasDepthImageProcessorInvocation, + MlsdImageProcessorInvocation, + NormalbaeImageProcessorInvocation, + PidiImageProcessorInvocation, + T2IAdapterModelConfig, + ZoeDepthImageProcessorInvocation, +} from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; +import { z } from 'zod'; + +const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']); +export type DepthAnythingModelSize = z.infer; +export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize => + zDepthAnythingModelSize.safeParse(v).success; + +export type CannyProcessorConfig = Required< + Pick +>; +export type ColorMapProcessorConfig = Required>; +export type ContentShuffleProcessorConfig = Required< + Pick +>; +export type DepthAnythingProcessorConfig = Required>; +export type HedProcessorConfig = Required>; +export type LineartAnimeProcessorConfig = Required>; +export type LineartProcessorConfig = Required>; +export type MediapipeFaceProcessorConfig = Required< + Pick +>; +export type MidasDepthProcessorConfig = Required>; +export type MlsdProcessorConfig = Required>; +export type NormalbaeProcessorConfig = Required>; +export type DWOpenposeProcessorConfig = Required< + Pick +>; +export type PidiProcessorConfig = Required>; +export type ZoeDepthProcessorConfig = Required>; + +export type ProcessorConfig = + | CannyProcessorConfig + | ColorMapProcessorConfig + | ContentShuffleProcessorConfig + | DepthAnythingProcessorConfig + | HedProcessorConfig + | LineartAnimeProcessorConfig + | LineartProcessorConfig + | MediapipeFaceProcessorConfig + | MidasDepthProcessorConfig + | MlsdProcessorConfig + | NormalbaeProcessorConfig + | DWOpenposeProcessorConfig + | PidiProcessorConfig + | ZoeDepthProcessorConfig; + +type ImageWithDims = { + imageName: string; + width: number; + height: number; +}; + +type ControlAdapterBase = { + id: string; + isEnabled: boolean; + weight: number; + image: ImageWithDims | null; + processedImage: ImageWithDims | null; + processorConfig: ProcessorConfig | null; + beginEndStepPct: [number, number]; +}; +export type ControlMode = NonNullable; + +export type ControlNetConfig = ControlAdapterBase & { + type: 'controlnet'; + model: ParameterControlNetModel | null; + controlMode: ControlMode; +}; + +export type T2IAdapterConfig = ControlAdapterBase & { + type: 't2i_adapter'; + model: ParameterT2IAdapterModel | null; +}; + +export type CLIPVisionModel = 'ViT-H' | 'ViT-G'; + +const zIPMethod = z.enum(['full', 'style', 'composition']); + +export type IPMethod = z.infer; +export const isIPMethod = (v: unknown): v is IPMethod => zIPMethod.safeParse(v).success; + +export type IPAdapterConfig = { + id: string; + type: 'ip_adapter'; + isEnabled: boolean; + weight: number; + method: IPMethod; + image: ImageWithDims | null; + model: ParameterIPAdapterModel | null; + clipVisionModel: CLIPVisionModel; + beginEndStepPct: [number, number]; +}; + +type ProcessorData = { + labelTKey: string; + descriptionTKey: string; + buildDefaults(baseModel?: BaseModelType): Extract; +}; + +type ControlNetProcessorsDict = { + [key in ProcessorConfig['type']]: ProcessorData; +}; +/** + * A dict of ControlNet processors, including: + * - label translation key + * - description translation key + * - a builder to create default values for the config + * + * TODO: Generate from the OpenAPI schema + */ +export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { + canny_image_processor: { + labelTKey: 'controlnet.canny', + descriptionTKey: 'controlnet.cannyDescription', + buildDefaults: () => ({ + id: `canny_image_processor_${uuidv4()}`, + type: 'canny_image_processor', + low_threshold: 100, + high_threshold: 200, + }), + }, + color_map_image_processor: { + labelTKey: 'controlnet.colorMap', + descriptionTKey: 'controlnet.colorMapDescription', + buildDefaults: () => ({ + id: `color_map_image_processor_${uuidv4()}`, + type: 'color_map_image_processor', + color_map_tile_size: 64, + }), + }, + content_shuffle_image_processor: { + labelTKey: 'controlnet.contentShuffle', + descriptionTKey: 'controlnet.contentShuffleDescription', + buildDefaults: (baseModel) => ({ + id: `content_shuffle_image_processor_${uuidv4()}`, + type: 'content_shuffle_image_processor', + h: baseModel === 'sdxl' ? 1024 : 512, + w: baseModel === 'sdxl' ? 1024 : 512, + f: baseModel === 'sdxl' ? 512 : 256, + }), + }, + depth_anything_image_processor: { + labelTKey: 'controlnet.depthAnything', + descriptionTKey: 'controlnet.depthAnythingDescription', + buildDefaults: () => ({ + id: `depth_anything_image_processor_${uuidv4()}`, + type: 'depth_anything_image_processor', + model_size: 'small', + }), + }, + hed_image_processor: { + labelTKey: 'controlnet.hed', + descriptionTKey: 'controlnet.hedDescription', + buildDefaults: () => ({ + id: `hed_image_processor_${uuidv4()}`, + type: 'hed_image_processor', + scribble: false, + }), + }, + lineart_anime_image_processor: { + labelTKey: 'controlnet.lineartAnime', + descriptionTKey: 'controlnet.lineartAnimeDescription', + buildDefaults: () => ({ + id: `lineart_anime_image_processor_${uuidv4()}`, + type: 'lineart_anime_image_processor', + }), + }, + lineart_image_processor: { + labelTKey: 'controlnet.lineart', + descriptionTKey: 'controlnet.lineartDescription', + buildDefaults: () => ({ + id: `lineart_image_processor_${uuidv4()}`, + type: 'lineart_image_processor', + coarse: false, + }), + }, + mediapipe_face_processor: { + labelTKey: 'controlnet.mediapipeFace', + descriptionTKey: 'controlnet.mediapipeFaceDescription', + buildDefaults: () => ({ + id: `mediapipe_face_processor_${uuidv4()}`, + type: 'mediapipe_face_processor', + max_faces: 1, + min_confidence: 0.5, + }), + }, + midas_depth_image_processor: { + labelTKey: 'controlnet.depthMidas', + descriptionTKey: 'controlnet.depthMidasDescription', + buildDefaults: () => ({ + id: `midas_depth_image_processor_${uuidv4()}`, + type: 'midas_depth_image_processor', + a_mult: 2, + bg_th: 0.1, + }), + }, + mlsd_image_processor: { + labelTKey: 'controlnet.mlsd', + descriptionTKey: 'controlnet.mlsdDescription', + buildDefaults: () => ({ + id: `mlsd_image_processor_${uuidv4()}`, + type: 'mlsd_image_processor', + thr_d: 0.1, + thr_v: 0.1, + }), + }, + normalbae_image_processor: { + labelTKey: 'controlnet.normalBae', + descriptionTKey: 'controlnet.normalBaeDescription', + buildDefaults: () => ({ + id: `normalbae_image_processor_${uuidv4()}`, + type: 'normalbae_image_processor', + }), + }, + dw_openpose_image_processor: { + labelTKey: 'controlnet.dwOpenpose', + descriptionTKey: 'controlnet.dwOpenposeDescription', + buildDefaults: () => ({ + id: `dw_openpose_image_processor_${uuidv4()}`, + type: 'dw_openpose_image_processor', + draw_body: true, + draw_face: false, + draw_hands: false, + }), + }, + pidi_image_processor: { + labelTKey: 'controlnet.pidi', + descriptionTKey: 'controlnet.pidiDescription', + buildDefaults: () => ({ + id: `pidi_image_processor_${uuidv4()}`, + type: 'pidi_image_processor', + scribble: false, + safe: false, + }), + }, + zoe_depth_image_processor: { + labelTKey: 'controlnet.depthZoe', + descriptionTKey: 'controlnet.depthZoeDescription', + buildDefaults: () => ({ + id: `zoe_depth_image_processor_${uuidv4()}`, + type: 'zoe_depth_image_processor', + }), + }, +}; +export const zProcessorType = 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 ProcessorType = z.infer; +export const isControlAdapterProcessorType = (v: unknown): v is ProcessorType => zProcessorType.safeParse(v).success; + +export const initialControlNet: Omit = { + type: 'controlnet', + isEnabled: true, + model: null, + weight: 1, + beginEndStepPct: [0, 0], + controlMode: 'balanced', + image: null, + processedImage: null, + processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), +}; + +export const initialT2IAdapter: Omit = { + type: 't2i_adapter', + isEnabled: true, + model: null, + weight: 1, + beginEndStepPct: [0, 0], + image: null, + processedImage: null, + processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), +}; + +export const initialIPAdapter: Omit = { + type: 'ip_adapter', + isEnabled: true, + image: null, + model: null, + beginEndStepPct: [0, 0], + method: 'full', + clipVisionModel: 'ViT-H', + weight: 1, +}; + +export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfig => { + return merge(deepClone(initialControlNet), { id, overrides }); +}; + +export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfig => { + return merge(deepClone(initialT2IAdapter), { id, overrides }); +}; + +export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfig => { + return merge(deepClone(initialIPAdapter), { id, overrides }); +}; + +export const buildControlAdapterProcessor = ( + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig +): ProcessorConfig | null => { + const defaultPreprocessor = modelConfig.default_settings?.preprocessor; + if (!isControlAdapterProcessorType(defaultPreprocessor)) { + return null; + } + const processorConfig = CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(modelConfig.base); + return processorConfig; +}; + +export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({ + imageName: image_name, + width, + height, +}); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/getLayerBlobs.ts b/invokeai/frontend/web/src/features/controlLayers/util/getLayerBlobs.ts index 1b0808c5f1..2ad3e0c90c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/getLayerBlobs.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/getLayerBlobs.ts @@ -1,7 +1,7 @@ import { getStore } from 'app/store/nanostores/store'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; -import { isRegionalGuidanceLayer, regional_guidance_layer_NAME } from 'features/controlLayers/store/controlLayersSlice'; +import { isRegionalGuidanceLayer, RG_LAYER_NAME } from 'features/controlLayers/store/controlLayersSlice'; import { renderers } from 'features/controlLayers/util/renderers'; import Konva from 'konva'; import { assert } from 'tsafe'; @@ -24,7 +24,7 @@ export const getRegionalPromptLayerBlobs = async ( const stage = new Konva.Stage({ container, width, height }); renderers.renderLayers(stage, reduxLayers, 1, 'brush'); - const konvaLayers = stage.find(`.${regional_guidance_layer_NAME}`); + const konvaLayers = stage.find(`.${RG_LAYER_NAME}`); const blobs: Record = {}; // First remove all layers diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index b2f04a88c1..cbe1410a95 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -5,20 +5,20 @@ import { $tool, BACKGROUND_LAYER_ID, BACKGROUND_RECT_ID, - CONTROLNET_LAYER_IMAGE_NAME, - CONTROLNET_LAYER_NAME, - getControlNetLayerImageId, + CA_LAYER_IMAGE_NAME, + CA_LAYER_NAME, + getCALayerImageId, getLayerBboxId, - getRegionalGuidanceLayerObjectGroupId, + getRGLayerObjectGroupId, isControlAdapterLayer, isRegionalGuidanceLayer, isRenderableLayer, LAYER_BBOX_NAME, NO_LAYERS_MESSAGE_LAYER_ID, - regional_guidance_layer_LINE_NAME, - regional_guidance_layer_NAME, - regional_guidance_layer_OBJECT_GROUP_NAME, - regional_guidance_layer_RECT_NAME, + RG_LAYER_LINE_NAME, + RG_LAYER_NAME, + RG_LAYER_OBJECT_GROUP_NAME, + RG_LAYER_RECT_NAME, TOOL_PREVIEW_BRUSH_BORDER_INNER_ID, TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID, TOOL_PREVIEW_BRUSH_FILL_ID, @@ -53,10 +53,10 @@ const STAGE_BG_DATAURL = const mapId = (object: { id: string }) => object.id; const selectRenderableLayers = (n: Konva.Node) => - n.name() === regional_guidance_layer_NAME || n.name() === CONTROLNET_LAYER_NAME; + n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME; const selectVectorMaskObjects = (node: Konva.Node) => { - return node.name() === regional_guidance_layer_LINE_NAME || node.name() === regional_guidance_layer_RECT_NAME; + return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; }; /** @@ -141,7 +141,7 @@ const renderToolPreview = ( isMouseOver: boolean, brushSize: number ) => { - const layerCount = stage.find(`.${regional_guidance_layer_NAME}`).length; + const layerCount = stage.find(`.${RG_LAYER_NAME}`).length; // Update the stage's pointer style if (layerCount === 0) { // We have no layers, so we should not render any tool @@ -233,7 +233,7 @@ const createRegionalGuidanceLayer = ( // This layer hasn't been added to the konva state yet const konvaLayer = new Konva.Layer({ id: reduxLayer.id, - name: regional_guidance_layer_NAME, + name: RG_LAYER_NAME, draggable: true, dragDistance: 0, }); @@ -265,8 +265,8 @@ const createRegionalGuidanceLayer = ( // The object group holds all of the layer's objects (e.g. lines and rects) const konvaObjectGroup = new Konva.Group({ - id: getRegionalGuidanceLayerObjectGroupId(reduxLayer.id, uuidv4()), - name: regional_guidance_layer_OBJECT_GROUP_NAME, + id: getRGLayerObjectGroupId(reduxLayer.id, uuidv4()), + name: RG_LAYER_OBJECT_GROUP_NAME, listening: false, }); konvaLayer.add(konvaObjectGroup); @@ -285,7 +285,7 @@ const createVectorMaskLine = (reduxObject: VectorMaskLine, konvaGroup: Konva.Gro const vectorMaskLine = new Konva.Line({ id: reduxObject.id, key: reduxObject.id, - name: regional_guidance_layer_LINE_NAME, + name: RG_LAYER_LINE_NAME, strokeWidth: reduxObject.strokeWidth, tension: 0, lineCap: 'round', @@ -307,7 +307,7 @@ const createVectorMaskRect = (reduxObject: VectorMaskRect, konvaGroup: Konva.Gro const vectorMaskRect = new Konva.Rect({ id: reduxObject.id, key: reduxObject.id, - name: regional_guidance_layer_RECT_NAME, + name: RG_LAYER_RECT_NAME, x: reduxObject.x, y: reduxObject.y, width: reduxObject.width, @@ -347,7 +347,7 @@ const renderRegionalGuidanceLayer = ( // Convert the color to a string, stripping the alpha - the object group will handle opacity. const rgbColor = rgbColorToString(reduxLayer.previewColor); - const konvaObjectGroup = konvaLayer.findOne(`.${regional_guidance_layer_OBJECT_GROUP_NAME}`); + const konvaObjectGroup = konvaLayer.findOne(`.${RG_LAYER_OBJECT_GROUP_NAME}`); assert(konvaObjectGroup, `Object group not found for layer ${reduxLayer.id}`); // We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required. @@ -411,7 +411,7 @@ const renderRegionalGuidanceLayer = ( const createControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLayer): Konva.Layer => { const konvaLayer = new Konva.Layer({ id: reduxLayer.id, - name: CONTROLNET_LAYER_NAME, + name: CA_LAYER_NAME, imageSmoothingEnabled: true, }); stage.add(konvaLayer); @@ -420,7 +420,7 @@ const createControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay const createControlNetLayerImage = (konvaLayer: Konva.Layer, image: HTMLImageElement): Konva.Image => { const konvaImage = new Konva.Image({ - name: CONTROLNET_LAYER_IMAGE_NAME, + name: CA_LAYER_IMAGE_NAME, image, }); konvaLayer.add(konvaImage); @@ -438,11 +438,11 @@ const updateControlNetLayerImageSource = async ( const imageDTO = await req.unwrap(); req.unsubscribe(); const image = new Image(); - const imageId = getControlNetLayerImageId(reduxLayer.id, imageName); + const imageId = getCALayerImageId(reduxLayer.id, imageName); image.onload = () => { // Find the existing image or create a new one - must find using the name, bc the id may have just changed const konvaImage = - konvaLayer.findOne(`.${CONTROLNET_LAYER_IMAGE_NAME}`) ?? + konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`) ?? createControlNetLayerImage(konvaLayer, image); // Update the image's attributes @@ -457,7 +457,7 @@ const updateControlNetLayerImageSource = async ( }; image.src = imageDTO.image_url; } else { - konvaLayer.findOne(`.${CONTROLNET_LAYER_IMAGE_NAME}`)?.destroy(); + konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`)?.destroy(); } }; @@ -497,13 +497,13 @@ const updateControlNetLayerImageAttrs = ( const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLayer) => { const konvaLayer = stage.findOne(`#${reduxLayer.id}`) ?? createControlNetLayer(stage, reduxLayer); - const konvaImage = konvaLayer.findOne(`.${CONTROLNET_LAYER_IMAGE_NAME}`); + const konvaImage = konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`); const canvasImageSource = konvaImage?.image(); let imageSourceNeedsUpdate = false; if (canvasImageSource instanceof HTMLImageElement) { if ( reduxLayer.imageName && - canvasImageSource.id !== getControlNetLayerImageId(reduxLayer.id, reduxLayer.imageName) + canvasImageSource.id !== getCALayerImageId(reduxLayer.id, reduxLayer.imageName) ) { imageSourceNeedsUpdate = true; } else if (!reduxLayer.imageName) { From 811e8a5a8b66de2f4fccc02c156be21c144269d6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 1 May 2024 13:35:58 +1000 Subject: [PATCH 15/74] refactor(ui): rename & export actions from CL slice --- .../controlLayers/store/controlLayersSlice.ts | 112 ++++++++++-------- 1 file changed, 63 insertions(+), 49 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index f2b567e41a..e3b9ff2944 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -114,8 +114,7 @@ export const controlLayersSlice = createSlice({ name: 'controlLayers', initialState: initialControlLayersState, reducers: { - //#region All Layers - + //#region Any Layer Type layerSelected: (state, action: PayloadAction) => { for (const layer of state.layers.filter(isRenderableLayer)) { if (layer.id === action.payload) { @@ -199,13 +198,6 @@ export const controlLayersSlice = createSlice({ state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId); state.selectedLayerId = state.layers[0]?.id ?? null; }, - layerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { - const { layerId, opacity } = action.payload; - const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); - if (layer) { - layer.opacity = opacity; - } - }, //#endregion //#region CA Layers @@ -307,10 +299,13 @@ export const controlLayersSlice = createSlice({ }, caLayerIsFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => { const { layerId, isFilterEnabled } = action.payload; - const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); - if (layer) { - layer.isFilterEnabled = isFilterEnabled; - } + const layer = getCALayer(state, layerId); + layer.isFilterEnabled = isFilterEnabled; + }, + caLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { + const { layerId, opacity } = action.payload; + const layer = getCALayer(state, layerId); + layer.opacity = opacity; }, //#endregion @@ -528,7 +523,7 @@ export const controlLayersSlice = createSlice({ }, //#endregion - //#region Base Layer + //#region Globals positivePromptChanged: (state, action: PayloadAction) => { state.positivePrompt = action.payload; }, @@ -565,9 +560,6 @@ export const controlLayersSlice = createSlice({ aspectRatioChanged: (state, action: PayloadAction) => { state.size.aspectRatio = action.payload; }, - //#endregion - - //#region General brushSizeChanged: (state, action: PayloadAction) => { state.brushSize = Math.round(action.payload); }, @@ -650,36 +642,54 @@ class LayerColors { } export const { - // All layer actions - layerDeleted, - layerMovedBackward, - layerMovedForward, - layerMovedToBack, - layerMovedToFront, - layerReset, + // Any Layer Type layerSelected, + layerVisibilityToggled, layerTranslated, layerBboxChanged, - layerVisibilityToggled, + layerReset, + layerDeleted, + layerMovedForward, + layerMovedToFront, + layerMovedBackward, + layerMovedToBack, selectedLayerReset, selectedLayerDeleted, - rgLayerAdded: regionalGuidanceLayerAdded, - ipaLayerAdded: ipAdapterLayerAdded, - caLayerAdded: controlAdapterLayerAdded, - layerOpacityChanged, - // CA layer actions - caLayerIsFilterEnabledChanged: isFilterEnabledChanged, - // Mask layer actions - rgLayerLineAdded: maskLayerLineAdded, - rgLayerPointsAdded: maskLayerPointsAdded, - rgLayerRectAdded: maskLayerRectAdded, - rgLayerNegativePromptChanged: maskLayerNegativePromptChanged, - rgLayerPositivePromptChanged: maskLayerPositivePromptChanged, - rgLayerIPAdapterAdded: maskLayerIPAdapterAdded, - rgLayerIPAdapterDeleted: maskLayerIPAdapterDeleted, - rgLayerAutoNegativeChanged: maskLayerAutoNegativeChanged, - rgLayerPreviewColorChanged: maskLayerPreviewColorChanged, - // Base layer actions + // CA Layers + caLayerAdded, + caLayerImageChanged, + caLayerProcessedImageChanged, + caLayerModelChanged, + caLayerWeightChanged, + caLayerBeginEndStepPctChanged, + caLayerControlModeChanged, + caLayerProcessorConfigChanged, + caLayerIsFilterEnabledChanged, + caLayerOpacityChanged, + // IPA Layers + ipaLayerAdded, + ipaLayerImageChanged, + ipaLayerWeightChanged, + ipaLayerBeginEndStepPctChanged, + ipaLayerMethodChanged, + ipaLayerCLIPVisionModelChanged, + // RG Layers + rgLayerAdded, + rgLayerPositivePromptChanged, + rgLayerNegativePromptChanged, + rgLayerIPAdapterAdded, + rgLayerIPAdapterDeleted, + rgLayerPreviewColorChanged, + rgLayerLineAdded, + rgLayerPointsAdded, + rgLayerRectAdded, + rgLayerAutoNegativeChanged, + rgLayerIPAdapterImageChanged, + rgLayerIPAdapterWeightChanged, + rgLayerIPAdapterBeginEndStepPctChanged, + rgLayerIPAdapterMethodChanged, + rgLayerIPAdapterCLIPVisionModelChanged, + // Globals positivePromptChanged, negativePromptChanged, positivePrompt2Changed, @@ -688,9 +698,9 @@ export const { widthChanged, heightChanged, aspectRatioChanged, - // General actions brushSizeChanged, globalMaskLayerOpacityChanged, + isEnabledChanged, undo, redo, } = controlLayersSlice.actions; @@ -750,9 +760,13 @@ const undoableGroupByMatcher = isAnyOf( layerTranslated, brushSizeChanged, globalMaskLayerOpacityChanged, - maskLayerPositivePromptChanged, - maskLayerNegativePromptChanged, - maskLayerPreviewColorChanged + positivePromptChanged, + negativePromptChanged, + positivePrompt2Changed, + negativePrompt2Changed, + rgLayerPositivePromptChanged, + rgLayerNegativePromptChanged, + rgLayerPreviewColorChanged ); // These are used to group actions into logical lines below (hate typos) @@ -764,13 +778,13 @@ export const controlLayersUndoableConfig: UndoableOptions { - // Lines are started with `maskLayerLineAdded` and may have any number of subsequent `maskLayerPointsAdded` events. + // Lines are started with `rgLayerLineAdded` and may have any number of subsequent `rgLayerPointsAdded` events. // We can use a double-buffer-esque trick to group each "logical" line as a single undoable action, without grouping // separate logical lines as a single undo action. - if (maskLayerLineAdded.match(action)) { + if (rgLayerLineAdded.match(action)) { return history.group === LINE_1 ? LINE_2 : LINE_1; } - if (maskLayerPointsAdded.match(action)) { + if (rgLayerPointsAdded.match(action)) { if (history.group === LINE_1 || history.group === LINE_2) { return history.group; } From 6007218a51b07cde17895a4bbd60bdf5bc9756e8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 1 May 2024 15:20:28 +1000 Subject: [PATCH 16/74] refactor(ui): add CA config components (wip) --- .../components/CALayer/CALayer.tsx | 2 +- .../CALayer/CALayerBeginEndStepPct.tsx | 66 ++++++++++++++ .../components/CALayer/CALayerConfig.tsx | 70 +++++++++++++++ .../CALayerControlMode.tsx} | 48 +++++----- .../CALayerImagePreview.tsx} | 81 +++++++++-------- .../CALayer/CALayerModelCombobox.tsx | 71 +++++++++++++++ .../CALayer/CALayerProcessorCombobox.tsx | 86 ++++++++++++++++++ .../CALayerWeight.tsx} | 32 +++---- .../ControlAdapterLayerConfig copy.tsx} | 27 +++--- .../components/IPALayer/IPALayer.tsx | 2 +- .../ParamControlAdapterModel.tsx | 0 .../RGLayer/RGLayerIPAdapterList.tsx | 2 +- .../ParamControlAdapterBeginEnd.tsx | 89 ------------------- .../controlLayers/store/controlLayersSlice.ts | 78 ++++++++-------- .../util/controlAdapters.test.ts | 14 +++ .../controlLayers/util/controlAdapters.ts | 17 ++-- .../web/src/features/dnd/types/index.ts | 10 ++- .../src/services/api/hooks/modelsByType.ts | 2 + .../frontend/web/src/services/api/types.ts | 8 +- 19 files changed, 475 insertions(+), 230 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerBeginEndStepPct.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{controlAdapterOverrides/ParamControlAdapterControlMode.tsx => CALayer/CALayerControlMode.tsx} (56%) rename invokeai/frontend/web/src/features/controlLayers/components/{controlAdapterOverrides/ControlAdapterImagePreview.tsx => CALayer/CALayerImagePreview.tsx} (78%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerModelCombobox.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{controlAdapterOverrides/ParamControlAdapterWeight.tsx => CALayer/CALayerWeight.tsx} (68%) rename invokeai/frontend/web/src/features/controlLayers/components/{controlAdapterOverrides/ControlAdapterLayerConfig.tsx => IPALayer/ControlAdapterLayerConfig copy.tsx} (72%) rename invokeai/frontend/web/src/features/controlLayers/components/{controlAdapterOverrides => IPALayer}/ParamControlAdapterModel.tsx (100%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterBeginEnd.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index f39de592db..7d582236bf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -1,7 +1,7 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterLayerConfig from 'features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig'; +import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerBeginEndStepPct.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerBeginEndStepPct.tsx new file mode 100644 index 0000000000..3d11dbdab2 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerBeginEndStepPct.tsx @@ -0,0 +1,66 @@ +import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { caLayerBeginEndStepPctChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + layerId: string; +}; + +const formatPct = (v: number) => `${Math.round(v * 100)}%`; +const ariaLabel = ['Begin Step %', 'End Step %']; + +export const CALayerBeginEndStepPct = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const beginEndStepPct = useAppSelector( + (s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.beginEndStepPct + ); + + const onChange = useCallback( + (v: [number, number]) => { + dispatch( + caLayerBeginEndStepPctChanged({ + layerId, + beginEndStepPct: v, + }) + ); + }, + [dispatch, layerId] + ); + + const onReset = useCallback(() => { + dispatch( + caLayerBeginEndStepPctChanged({ + layerId, + beginEndStepPct: [0, 1], + }) + ); + }, [dispatch, layerId]); + + return ( + + + {t('controlnet.beginEndStepPercentShort')} + + + + ); +}); + +CALayerBeginEndStepPct.displayName = 'CALayerBeginEndStepPct'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx new file mode 100644 index 0000000000..024ffe791b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx @@ -0,0 +1,70 @@ +import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import ControlAdapterProcessorComponent from 'features/controlAdapters/components/ControlAdapterProcessorComponent'; +import { CALayerModelCombobox } from 'features/controlLayers/components/CALayer/CALayerModelCombobox'; +import { selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretUpBold } from 'react-icons/pi'; +import { useToggle } from 'react-use'; + +import { CALayerBeginEndStepPct } from './CALayerBeginEndStepPct'; +import { CALayerControlMode } from './CALayerControlMode'; +import { CALayerImagePreview } from './CALayerImagePreview'; +import { CALayerProcessorCombobox } from './CALayerProcessorCombobox'; +import { CALayerWeight } from './CALayerWeight'; + +type Props = { + layerId: string; +}; + +export const CALayerConfig = memo(({ layerId }: Props) => { + const caType = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.type); + const { t } = useTranslation(); + const [isExpanded, toggleIsExpanded] = useToggle(false); + + return ( + + + + + + + + } + /> + + + + {caType === 'controlnet' && } + + + + + + + + {isExpanded && ( + <> + + + + )} + + ); +}); + +CALayerConfig.displayName = 'CALayerConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterControlMode.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlMode.tsx similarity index 56% rename from invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterControlMode.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlMode.tsx index 6b5d34c106..c60d22a3a0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterControlMode.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlMode.tsx @@ -1,23 +1,25 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterControlMode } from 'features/controlAdapters/hooks/useControlAdapterControlMode'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { controlAdapterControlModeChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlMode } from 'features/controlAdapters/store/types'; +import { caLayerControlModeChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import { isControlMode } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { assert } from 'tsafe'; type Props = { - id: string; + layerId: string; }; -const ParamControlAdapterControlMode = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const controlMode = useControlAdapterControlMode(id); - const dispatch = useAppDispatch(); +export const CALayerControlMode = memo(({ layerId }: Props) => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const controlMode = useAppSelector((s) => { + const ca = selectCALayer(s.controlLayers.present, layerId).controlAdapter; + assert(ca.type === 'controlnet'); + return ca.controlMode; + }); const CONTROL_MODE_DATA = useMemo( () => [ @@ -31,17 +33,15 @@ const ParamControlAdapterControlMode = ({ id }: Props) => { const handleControlModeChange = useCallback( (v) => { - if (!v) { - return; - } + assert(isControlMode(v?.value)); dispatch( - controlAdapterControlModeChanged({ - id, - controlMode: v.value as ControlMode, + caLayerControlModeChanged({ + layerId, + controlMode: v.value, }) ); }, - [id, dispatch] + [layerId, dispatch] ); const value = useMemo( @@ -54,13 +54,19 @@ const ParamControlAdapterControlMode = ({ id }: Props) => { } return ( - + {t('controlnet.control')} - + ); -}; +}); -export default memo(ParamControlAdapterControlMode); +CALayerControlMode.displayName = 'CALayerControlMode'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx similarity index 78% rename from invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterImagePreview.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx index b3094e5599..209725f94d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx @@ -6,15 +6,15 @@ 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 { useControlAdapterControlImage } from 'features/controlAdapters/hooks/useControlAdapterControlImage'; -import { useControlAdapterProcessedControlImage } from 'features/controlAdapters/hooks/useControlAdapterProcessedControlImage'; -import { useControlAdapterProcessorType } from 'features/controlAdapters/hooks/useControlAdapterProcessorType'; +import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; import { - controlAdapterImageChanged, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; + caLayerImageChanged, + heightChanged, + selectCALayer, + selectControlLayersSlice, + widthChanged, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlLayerDropData, ImageDraggableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; @@ -27,11 +27,10 @@ import { useGetImageDTOQuery, useRemoveImageFromBoardMutation, } from 'services/api/endpoints/images'; -import type { PostUploadAction } from 'services/api/types'; +import type { ControlLayerAction } from 'services/api/types'; type Props = { - id: string; - isSmall?: boolean; + layerId: string; }; const selectPendingControlImages = createMemoizedSelector( @@ -39,12 +38,23 @@ const selectPendingControlImages = createMemoizedSelector( (controlAdapters) => controlAdapters.pendingControlImages ); -const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { +export const CALayerImagePreview = memo(({ layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const controlImageName = useControlAdapterControlImage(id); - const processedControlImageName = useControlAdapterProcessedControlImage(id); - const processorType = useControlAdapterProcessorType(id); + const selector = useMemo( + () => + createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + const layer = selectCALayer(controlLayers.present, layerId); + const { image, processedImage, processorConfig } = layer.controlAdapter; + return { + imageName: image?.imageName ?? null, + processedImageName: processedImage?.imageName ?? null, + hasProcessor: Boolean(processorConfig), + }; + }), + [layerId] + ); + const { imageName, processedImageName, hasProcessor } = useAppSelector(selector); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const isConnected = useAppSelector((s) => s.system.isConnected); const activeTabName = useAppSelector(activeTabNameSelector); @@ -54,20 +64,17 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { const [isMouseOverImage, setIsMouseOverImage] = useState(false); - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - controlImageName ?? skipToken - ); - + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(imageName ?? skipToken); const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - processedControlImageName ?? skipToken + processedImageName ?? skipToken ); const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); const [addToBoard] = useAddImageToBoardMutation(); const [removeFromBoard] = useRemoveImageFromBoardMutation(); const handleResetControlImage = useCallback(() => { - dispatch(controlAdapterImageChanged({ id, controlImage: null })); - }, [id, dispatch]); + dispatch(caLayerImageChanged({ layerId, imageDTO: null })); + }, [layerId, dispatch]); const handleSaveControlImage = useCallback(async () => { if (!processedControlImage) { @@ -120,33 +127,33 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { setIsMouseOverImage(false); }, []); - const draggableData = useMemo(() => { + const draggableData = useMemo(() => { if (controlImage) { return { - id, + id: layerId, payloadType: 'IMAGE_DTO', payload: { imageDTO: controlImage }, }; } - }, [controlImage, id]); + }, [controlImage, layerId]); - const droppableData = useMemo( + const droppableData = useMemo( () => ({ - id, - actionType: 'SET_CONTROL_ADAPTER_IMAGE', - context: { id }, + id: layerId, + actionType: 'SET_CONTROL_LAYER_IMAGE', + context: { layerId }, }), - [id] + [layerId] ); - const postUploadAction = useMemo(() => ({ type: 'SET_CONTROL_ADAPTER_IMAGE', id }), [id]); + const postUploadAction = useMemo(() => ({ type: 'SET_CONTROL_LAYER_IMAGE', layerId }), [layerId]); const shouldShowProcessedImage = controlImage && processedControlImage && !isMouseOverImage && - !pendingControlImages.includes(id) && - processorType !== 'none'; + !pendingControlImages.includes(layerId) && + hasProcessor; useEffect(() => { if (isConnected && (isErrorControlImage || isErrorProcessedControlImage)) { @@ -160,7 +167,7 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { onMouseLeave={handleMouseLeave} position="relative" w="full" - h={isSmall ? 36 : 366} // magic no touch + h={36} alignItems="center" justifyContent="center" > @@ -211,7 +218,7 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { /> - {pendingControlImages.includes(id) && ( + {pendingControlImages.includes(layerId) && ( { )} ); -}; +}); -export default memo(ControlAdapterImagePreview); +CALayerImagePreview.displayName = 'CALayerImagePreview'; const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 }; const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerModelCombobox.tsx new file mode 100644 index 0000000000..8e1e5c6891 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerModelCombobox.tsx @@ -0,0 +1,71 @@ +import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import { caLayerModelChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType'; +import type { AnyModelConfig, ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; + +type Props = { + layerId: string; +}; + +export const CALayerModelCombobox = memo(({ layerId }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const caModelKey = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.model?.key); + const currentBaseModel = useAppSelector((s) => s.generation.model?.base); + + const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); + const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === caModelKey), [modelConfigs, caModelKey]); + + const _onChange = useCallback( + (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null) => { + if (!modelConfig) { + return; + } + dispatch( + caLayerModelChanged({ + layerId, + modelConfig, + }) + ); + }, + [dispatch, layerId] + ); + + const getIsDisabled = useCallback( + (model: AnyModelConfig): boolean => { + const isCompatible = currentBaseModel === model.base; + const hasMainModel = Boolean(currentBaseModel); + return !hasMainModel || !isCompatible; + }, + [currentBaseModel] + ); + + const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ + modelConfigs, + onChange: _onChange, + selectedModel, + getIsDisabled, + isLoading, + }); + + return ( + + + + + + ); +}); + +CALayerModelCombobox.displayName = 'CALayerModelCombobox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx new file mode 100644 index 0000000000..c0f4cca2a5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx @@ -0,0 +1,86 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { caLayerProcessorConfigChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import { CONTROLNET_PROCESSORS, isProcessorType } from 'features/controlLayers/util/controlAdapters'; +import { configSelector } from 'features/system/store/configSelectors'; +import { includes, map } from 'lodash-es'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiXBold } from 'react-icons/pi'; +import { assert } from 'tsafe'; + +type Props = { + layerId: string; +}; + +const selectDisabledProcessors = createMemoizedSelector( + configSelector, + (config) => config.sd.disabledControlNetProcessors +); + +export const CALayerProcessorCombobox = memo(({ layerId }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const disabledProcessors = useAppSelector(selectDisabledProcessors); + const processorType = useAppSelector( + (s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.processorConfig?.type ?? null + ); + const options = useMemo(() => { + return map(CONTROLNET_PROCESSORS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( + (o) => !includes(disabledProcessors, o.value) + ); + }, [disabledProcessors, t]); + + const onChange = useCallback( + (v) => { + if (!v) { + dispatch( + caLayerProcessorConfigChanged({ + layerId, + processorConfig: null, + }) + ); + return; + } + assert(isProcessorType(v.value)); + dispatch( + caLayerProcessorConfigChanged({ + layerId, + processorConfig: CONTROLNET_PROCESSORS[v.value].buildDefaults(), + }) + ); + }, + [dispatch, layerId] + ); + const clearProcessor = useCallback(() => { + dispatch( + caLayerProcessorConfigChanged({ + layerId, + processorConfig: null, + }) + ); + }, [dispatch, layerId]); + const value = useMemo(() => options.find((o) => o.value === processorType) ?? null, [options, processorType]); + + return ( + + + {t('controlnet.processor')} + + + + } + variant="ghost" + /> + + + ); +}); + +CALayerProcessorCombobox.displayName = 'CALayerProcessorCombobox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterWeight.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerWeight.tsx similarity index 68% rename from invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterWeight.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerWeight.tsx index 5e456fc792..b8738fd352 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterWeight.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerWeight.tsx @@ -1,24 +1,21 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterWeight } from 'features/controlAdapters/hooks/useControlAdapterWeight'; -import { controlAdapterWeightChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isNil } from 'lodash-es'; +import { caLayerWeightChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -type ParamControlAdapterWeightProps = { - id: string; +type Props = { + layerId: string; }; const formatValue = (v: number) => v.toFixed(2); +const marks = [0, 1, 2]; -const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => { +export const CALayerWeight = memo(({ layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isEnabled = useControlAdapterIsEnabled(id); - const weight = useControlAdapterWeight(id); + const weight = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.weight); const initial = useAppSelector((s) => s.config.sd.ca.weight.initial); const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax); @@ -29,18 +26,13 @@ const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => { const onChange = useCallback( (weight: number) => { - dispatch(controlAdapterWeightChanged({ id, weight })); + dispatch(caLayerWeightChanged({ layerId, weight })); }, - [dispatch, id] + [dispatch, layerId] ); - if (isNil(weight)) { - // should never happen - return null; - } - return ( - + {t('controlnet.weight')} @@ -67,8 +59,6 @@ const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => { /> ); -}; +}); -export default memo(ParamControlAdapterWeight); - -const marks = [0, 1, 2]; +CALayerWeight.displayName = 'CALayerWeight'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx similarity index 72% rename from invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx index 29a3502d37..aa518c6dd4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx @@ -1,23 +1,26 @@ import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; import ControlAdapterProcessorComponent from 'features/controlAdapters/components/ControlAdapterProcessorComponent'; import ControlAdapterShouldAutoConfig from 'features/controlAdapters/components/ControlAdapterShouldAutoConfig'; import ParamControlAdapterIPMethod from 'features/controlAdapters/components/parameters/ParamControlAdapterIPMethod'; import ParamControlAdapterProcessorSelect from 'features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect'; -import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType'; +import { ParamControlAdapterBeginEnd } from 'features/controlLayers/components/CALayer/CALayerBeginEndStepPct'; +import ParamControlAdapterControlMode from 'features/controlLayers/components/CALayer/CALayerControlMode'; +import { CALayerImagePreview } from 'features/controlLayers/components/CALayer/CALayerImagePreview'; +import ParamControlAdapterModel from 'features/controlLayers/components/CALayer/CALayerModelCombobox'; +import ParamControlAdapterWeight from 'features/controlLayers/components/CALayer/CALayerWeight'; +import { selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCaretUpBold } from 'react-icons/pi'; import { useToggle } from 'react-use'; -import ControlAdapterImagePreview from './ControlAdapterImagePreview'; -import { ParamControlAdapterBeginEnd } from './ParamControlAdapterBeginEnd'; -import ParamControlAdapterControlMode from './ParamControlAdapterControlMode'; -import ParamControlAdapterModel from './ParamControlAdapterModel'; -import ParamControlAdapterWeight from './ParamControlAdapterWeight'; +type Props = { + layerId: string; +}; -const ControlAdapterLayerConfig = (props: { id: string }) => { - const { id } = props; - const controlAdapterType = useControlAdapterType(id); +export const CALayerCAConfig = memo(({ layerId }: Props) => { + const caType = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.type); const { t } = useTranslation(); const [isExpanded, toggleIsExpanded] = useToggle(false); @@ -55,7 +58,7 @@ const ControlAdapterLayerConfig = (props: { id: string }) => { - + {isExpanded && ( @@ -67,6 +70,6 @@ const ControlAdapterLayerConfig = (props: { id: string }) => { )} ); -}; +}); -export default memo(ControlAdapterLayerConfig); +CALayerCAConfig.displayName = 'CALayerCAConfig'; 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 fbef07f39e..2dee5d95a6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -1,7 +1,7 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterLayerConfig from 'features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig'; +import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ParamControlAdapterModel.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterModel.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ParamControlAdapterModel.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx index 464bd41897..3a1f6d79cd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx @@ -2,7 +2,7 @@ import { Divider, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { guidanceLayerIPAdapterDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterLayerConfig from 'features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig'; +import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig'; import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback, useMemo } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterBeginEnd.tsx b/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterBeginEnd.tsx deleted file mode 100644 index e4bc07e0b4..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterBeginEnd.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterBeginEndStepPct } from 'features/controlAdapters/hooks/useControlAdapterBeginEndStepPct'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { - controlAdapterBeginStepPctChanged, - controlAdapterEndStepPctChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const formatPct = (v: number) => `${Math.round(v * 100)}%`; - -export const ParamControlAdapterBeginEnd = memo(({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const stepPcts = useControlAdapterBeginEndStepPct(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const onChange = useCallback( - (v: [number, number]) => { - dispatch( - controlAdapterBeginStepPctChanged({ - id, - beginStepPct: v[0], - }) - ); - dispatch( - controlAdapterEndStepPctChanged({ - id, - endStepPct: v[1], - }) - ); - }, - [dispatch, id] - ); - - const onReset = useCallback(() => { - dispatch( - controlAdapterBeginStepPctChanged({ - id, - beginStepPct: 0, - }) - ); - dispatch( - controlAdapterEndStepPctChanged({ - id, - endStepPct: 1, - }) - ); - }, [dispatch, id]); - - const value = useMemo<[number, number]>(() => [stepPcts?.beginStepPct ?? 0, stepPcts?.endStepPct ?? 1], [stepPcts]); - - if (!stepPcts) { - return null; - } - - return ( - - - {t('controlnet.beginEndStepPercentShort')} - - - - ); -}); - -ParamControlAdapterBeginEnd.displayName = 'ParamControlAdapterBeginEnd'; - -const ariaLabel = ['Begin Step %', 'End Step %']; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index e3b9ff2944..f8c55b7223 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -82,33 +82,34 @@ const resetLayer = (layer: Layer) => { // TODO } }; -const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => { - const vmLayers = state.layers.filter(isRegionalGuidanceLayer); - const lastColor = vmLayers[vmLayers.length - 1]?.previewColor; - return LayerColors.next(lastColor); -}; -const getCALayer = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { + +export const selectCALayer = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isControlAdapterLayer(layer)); return layer; }; -const getIPALayer = (state: ControlLayersState, layerId: string): IPAdapterLayer => { +const selectIPALayer = (state: ControlLayersState, layerId: string): IPAdapterLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isIPAdapterLayer(layer)); return layer; }; -const getRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { +const selectRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer)); return layer; }; -const getRGLayerIPAdapter = (state: ControlLayersState, layerId: string, ipAdapterId: string): IPAdapterConfig => { +const selectRGLayerIPAdapter = (state: ControlLayersState, layerId: string, ipAdapterId: string): IPAdapterConfig => { const layer = state.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer)); const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); assert(ipAdapter); return ipAdapter; }; +const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => { + const rgLayers = state.layers.filter(isRegionalGuidanceLayer); + const lastColor = rgLayers[rgLayers.length - 1]?.previewColor; + return LayerColors.next(lastColor); +}; export const controlLayersSlice = createSlice({ name: 'controlLayers', @@ -234,15 +235,16 @@ export const controlLayersSlice = createSlice({ }, caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; - const layer = getCALayer(state, layerId); + const layer = selectCALayer(state, layerId); layer.bbox = null; layer.bboxNeedsUpdate = true; layer.isEnabled = true; layer.controlAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + layer.controlAdapter.processedImage = null; }, caLayerProcessedImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; - const layer = getCALayer(state, layerId); + const layer = selectCALayer(state, layerId); layer.bbox = null; layer.bboxNeedsUpdate = true; layer.isEnabled = true; @@ -256,7 +258,7 @@ export const controlLayersSlice = createSlice({ }> ) => { const { layerId, modelConfig } = action.payload; - const layer = getCALayer(state, layerId); + const layer = selectCALayer(state, layerId); if (!modelConfig) { layer.controlAdapter.model = null; return; @@ -272,7 +274,7 @@ export const controlLayersSlice = createSlice({ }, caLayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { const { layerId, weight } = action.payload; - const layer = getCALayer(state, layerId); + const layer = selectCALayer(state, layerId); layer.controlAdapter.weight = weight; }, caLayerBeginEndStepPctChanged: ( @@ -280,31 +282,31 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> ) => { const { layerId, beginEndStepPct } = action.payload; - const layer = getCALayer(state, layerId); + const layer = selectCALayer(state, layerId); layer.controlAdapter.beginEndStepPct = beginEndStepPct; }, caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlMode }>) => { const { layerId, controlMode } = action.payload; - const layer = getCALayer(state, layerId); + const layer = selectCALayer(state, layerId); assert(layer.controlAdapter.type === 'controlnet'); layer.controlAdapter.controlMode = controlMode; }, caLayerProcessorConfigChanged: ( state, - action: PayloadAction<{ layerId: string; processorConfig: ProcessorConfig }> + action: PayloadAction<{ layerId: string; processorConfig: ProcessorConfig | null }> ) => { const { layerId, processorConfig } = action.payload; - const layer = getCALayer(state, layerId); + const layer = selectCALayer(state, layerId); layer.controlAdapter.processorConfig = processorConfig; }, caLayerIsFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => { const { layerId, isFilterEnabled } = action.payload; - const layer = getCALayer(state, layerId); + const layer = selectCALayer(state, layerId); layer.isFilterEnabled = isFilterEnabled; }, caLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { const { layerId, opacity } = action.payload; - const layer = getCALayer(state, layerId); + const layer = selectCALayer(state, layerId); layer.opacity = opacity; }, //#endregion @@ -325,12 +327,12 @@ export const controlLayersSlice = createSlice({ }, ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; - const layer = getIPALayer(state, layerId); + const layer = selectIPALayer(state, layerId); layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, ipaLayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { const { layerId, weight } = action.payload; - const layer = getIPALayer(state, layerId); + const layer = selectIPALayer(state, layerId); layer.ipAdapter.weight = weight; }, ipaLayerBeginEndStepPctChanged: ( @@ -338,12 +340,12 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> ) => { const { layerId, beginEndStepPct } = action.payload; - const layer = getIPALayer(state, layerId); + const layer = selectIPALayer(state, layerId); layer.ipAdapter.beginEndStepPct = beginEndStepPct; }, ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethod }>) => { const { layerId, method } = action.payload; - const layer = getIPALayer(state, layerId); + const layer = selectIPALayer(state, layerId); layer.ipAdapter.method = method; }, ipaLayerCLIPVisionModelChanged: ( @@ -351,7 +353,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }> ) => { const { layerId, clipVisionModel } = action.payload; - const layer = getIPALayer(state, layerId); + const layer = selectIPALayer(state, layerId); layer.ipAdapter.clipVisionModel = clipVisionModel; }, //#endregion @@ -386,27 +388,27 @@ export const controlLayersSlice = createSlice({ }, rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; - const layer = getRGLayer(state, layerId); + const layer = selectRGLayer(state, layerId); layer.positivePrompt = prompt; }, rgLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; - const layer = getRGLayer(state, layerId); + const layer = selectRGLayer(state, layerId); layer.negativePrompt = prompt; }, rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { const { layerId, ipAdapter } = action.payload; - const layer = getRGLayer(state, layerId); + const layer = selectRGLayer(state, layerId); layer.ipAdapters.push(ipAdapter); }, rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { const { layerId, ipAdapterId } = action.payload; - const layer = getRGLayer(state, layerId); + const layer = selectRGLayer(state, layerId); layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); }, rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { const { layerId, color } = action.payload; - const layer = getRGLayer(state, layerId); + const layer = selectRGLayer(state, layerId); layer.previewColor = color; }, rgLayerLineAdded: { @@ -420,7 +422,7 @@ export const controlLayersSlice = createSlice({ }> ) => { const { layerId, points, tool, lineUuid } = action.payload; - const layer = getRGLayer(state, layerId); + const layer = selectRGLayer(state, layerId); const lineId = getRGLayerLineId(layer.id, lineUuid); layer.maskObjects.push({ type: 'vector_mask_line', @@ -442,7 +444,7 @@ export const controlLayersSlice = createSlice({ }, rgLayerPointsAdded: (state, action: PayloadAction<{ layerId: string; point: [number, number] }>) => { const { layerId, point } = action.payload; - const layer = getRGLayer(state, layerId); + const layer = selectRGLayer(state, layerId); const lastLine = layer.maskObjects.findLast(isLine); if (!lastLine) { return; @@ -459,7 +461,7 @@ export const controlLayersSlice = createSlice({ // Ignore zero-area rectangles return; } - const layer = getRGLayer(state, layerId); + const layer = selectRGLayer(state, layerId); const id = getRGLayerRectId(layer.id, rectUuid); layer.maskObjects.push({ type: 'vector_mask_rect', @@ -478,7 +480,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }> ) => { const { layerId, autoNegative } = action.payload; - const layer = getRGLayer(state, layerId); + const layer = selectRGLayer(state, layerId); layer.autoNegative = autoNegative; }, rgLayerIPAdapterImageChanged: ( @@ -486,7 +488,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }> ) => { const { layerId, ipAdapterId, imageDTO } = action.payload; - const ipAdapter = getRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, rgLayerIPAdapterWeightChanged: ( @@ -494,7 +496,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; ipAdapterId: string; weight: number }> ) => { const { layerId, ipAdapterId, weight } = action.payload; - const ipAdapter = getRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); ipAdapter.weight = weight; }, rgLayerIPAdapterBeginEndStepPctChanged: ( @@ -502,7 +504,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; ipAdapterId: string; beginEndStepPct: [number, number] }> ) => { const { layerId, ipAdapterId, beginEndStepPct } = action.payload; - const ipAdapter = getRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); ipAdapter.beginEndStepPct = beginEndStepPct; }, rgLayerIPAdapterMethodChanged: ( @@ -510,7 +512,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethod }> ) => { const { layerId, ipAdapterId, method } = action.payload; - const ipAdapter = getRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); ipAdapter.method = method; }, rgLayerIPAdapterCLIPVisionModelChanged: ( @@ -518,7 +520,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModel }> ) => { const { layerId, ipAdapterId, clipVisionModel } = action.payload; - const ipAdapter = getRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); ipAdapter.clipVisionModel = clipVisionModel; }, //#endregion diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts new file mode 100644 index 0000000000..b8ef50f4c7 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts @@ -0,0 +1,14 @@ +import type { S } from 'services/api/types'; +import type { Equals } from 'tsafe'; +import { assert } from 'tsafe'; +import { describe, test } from 'vitest'; + +import type { CLIPVisionModel, ControlMode, IPMethod, ProcessorConfig, ProcessorType } from './controlAdapters'; + +describe('Control Adapter Types', () => { + test('ProcessorType', () => assert>()); + test('IP Adapter Method', () => assert, IPMethod>>()); + test('CLIP Vision Model', () => + assert, CLIPVisionModel>>()); + test('Control Mode', () => assert, ControlMode>>()); +}); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 5a13ed2315..a388d65e94 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -10,7 +10,6 @@ import type { CannyImageProcessorInvocation, ColorMapImageProcessorInvocation, ContentShuffleImageProcessorInvocation, - ControlNetInvocation, ControlNetModelConfig, DepthAnythingImageProcessorInvocation, DWOpenposeImageProcessorInvocation, @@ -88,7 +87,10 @@ type ControlAdapterBase = { processorConfig: ProcessorConfig | null; beginEndStepPct: [number, number]; }; -export type ControlMode = NonNullable; + +const zControlMode = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); +export type ControlMode = z.infer; +export const isControlMode = (v: unknown): v is ControlMode => zControlMode.safeParse(v).success; export type ControlNetConfig = ControlAdapterBase & { type: 'controlnet'; @@ -101,10 +103,11 @@ export type T2IAdapterConfig = ControlAdapterBase & { model: ParameterT2IAdapterModel | null; }; -export type CLIPVisionModel = 'ViT-H' | 'ViT-G'; +const zCLIPVisionModel = z.enum(['ViT-H', 'ViT-G']); +export type CLIPVisionModel = z.infer; +export const isCLIPVisionModel = (v: unknown): v is CLIPVisionModel => zCLIPVisionModel.safeParse(v).success; const zIPMethod = z.enum(['full', 'style', 'composition']); - export type IPMethod = z.infer; export const isIPMethod = (v: unknown): v is IPMethod => zIPMethod.safeParse(v).success; @@ -270,7 +273,7 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { type: 'zoe_depth_image_processor', }), }, -}; +} export const zProcessorType = z.enum([ 'canny_image_processor', 'color_map_image_processor', @@ -288,7 +291,7 @@ export const zProcessorType = z.enum([ 'zoe_depth_image_processor', ]); export type ProcessorType = z.infer; -export const isControlAdapterProcessorType = (v: unknown): v is ProcessorType => zProcessorType.safeParse(v).success; +export const isProcessorType = (v: unknown): v is ProcessorType => zProcessorType.safeParse(v).success; export const initialControlNet: Omit = { type: 'controlnet', @@ -340,7 +343,7 @@ export const buildControlAdapterProcessor = ( modelConfig: ControlNetModelConfig | T2IAdapterModelConfig ): ProcessorConfig | null => { const defaultPreprocessor = modelConfig.default_settings?.preprocessor; - if (!isControlAdapterProcessorType(defaultPreprocessor)) { + if (!isProcessorType(defaultPreprocessor)) { return null; } const processorConfig = CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(modelConfig.base); diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index b2b7820762..739f15c882 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -33,6 +33,13 @@ type ControlAdapterDropData = BaseDropData & { }; }; +export type ControlLayerDropData = BaseDropData & { + actionType: 'SET_CONTROL_LAYER_IMAGE'; + context: { + layerId: string; + }; +}; + export type CanvasInitialImageDropData = BaseDropData & { actionType: 'SET_CANVAS_INITIAL_IMAGE'; }; @@ -61,7 +68,8 @@ export type TypesafeDroppableData = | CanvasInitialImageDropData | NodesImageDropData | AddToBoardDropData - | RemoveFromBoardDropData; + | RemoveFromBoardDropData + | ControlLayerDropData; type BaseDragData = { id: string; diff --git a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts index 2d04b9dc46..a42df8f600 100644 --- a/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts +++ b/invokeai/frontend/web/src/services/api/hooks/modelsByType.ts @@ -4,6 +4,7 @@ import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/ import type { AnyModelConfig } from 'services/api/types'; import { isControlNetModelConfig, + isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig, isLoRAModelConfig, isNonRefinerMainModelConfig, @@ -35,6 +36,7 @@ export const useNonSDXLMainModels = buildModelsHook(isNonSDXLMainModelConfig); export const useRefinerModels = buildModelsHook(isRefinerMainModelModelConfig); export const useSDXLModels = buildModelsHook(isSDXLMainModelModelConfig); export const useLoRAModels = buildModelsHook(isLoRAModelConfig); +export const useControlNetAndT2IAdapterModels = buildModelsHook(isControlNetOrT2IAdapterModelConfig); export const useControlNetModels = buildModelsHook(isControlNetModelConfig); export const useT2IAdapterModels = buildModelsHook(isT2IAdapterModelConfig); export const useIPAdapterModels = buildModelsHook(isIPAdapterModelConfig); diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 7dde5fb624..95701c7484 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -177,6 +177,11 @@ type ControlAdapterAction = { id: string; }; +export type ControlLayerAction = { + type: 'SET_CONTROL_LAYER_IMAGE'; + layerId: string; +}; + type InitialImageAction = { type: 'SET_INITIAL_IMAGE'; }; @@ -206,4 +211,5 @@ export type PostUploadAction = | NodesAction | CanvasInitialImageAction | ToastAction - | AddToBatchAction; + | AddToBatchAction + | ControlLayerAction; From 424a27eedab95a0464657a73c38941e0d15d634a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 1 May 2024 16:04:05 +1000 Subject: [PATCH 17/74] refactor(ui): add CA processor config components (wip) --- .../components/CALayer/CALayerConfig.tsx | 4 +- .../components/CALayer/CALayerProcessor.tsx | 95 +++++++++++++++++++ .../CALayer/processors/CannyProcessor.tsx | 67 +++++++++++++ .../CALayer/processors/ColorMapProcessor.tsx | 47 +++++++++ .../processors/ContentShuffleProcessor.tsx | 79 +++++++++++++++ .../processors/DWOpenposeProcessor.tsx | 62 ++++++++++++ .../processors/DepthAnythingProcessor.tsx | 52 ++++++++++ .../CALayer/processors/HedProcessor.tsx | 32 +++++++ .../CALayer/processors/LineartProcessor.tsx | 32 +++++++ .../processors/MediapipeFaceProcessor.tsx | 73 ++++++++++++++ .../processors/MidasDepthProcessor.tsx | 76 +++++++++++++++ .../CALayer/processors/MlsdImageProcessor.tsx | 76 +++++++++++++++ .../CALayer/processors/PidiProcessor.tsx | 43 +++++++++ .../CALayer/processors/ProcessorWrapper.tsx | 15 +++ .../components/CALayer/processors/types.ts | 6 ++ .../controlLayers/store/controlLayersSlice.ts | 2 +- .../util/controlAdapters.test.ts | 11 ++- 17 files changed, 768 insertions(+), 4 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/CannyProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ColorMapProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ContentShuffleProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DWOpenposeProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DepthAnythingProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/HedProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/LineartProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MediapipeFaceProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MidasDepthProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MlsdImageProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/PidiProcessor.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ProcessorWrapper.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/types.ts diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx index 024ffe791b..7627fe4364 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx @@ -1,6 +1,5 @@ import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterProcessorComponent from 'features/controlAdapters/components/ControlAdapterProcessorComponent'; import { CALayerModelCombobox } from 'features/controlLayers/components/CALayer/CALayerModelCombobox'; import { selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; import { memo } from 'react'; @@ -11,6 +10,7 @@ import { useToggle } from 'react-use'; import { CALayerBeginEndStepPct } from './CALayerBeginEndStepPct'; import { CALayerControlMode } from './CALayerControlMode'; import { CALayerImagePreview } from './CALayerImagePreview'; +import { CALayerProcessor } from './CALayerProcessor'; import { CALayerProcessorCombobox } from './CALayerProcessorCombobox'; import { CALayerWeight } from './CALayerWeight'; @@ -60,7 +60,7 @@ export const CALayerConfig = memo(({ layerId }: Props) => { {isExpanded && ( <> - + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx new file mode 100644 index 0000000000..05271010ba --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx @@ -0,0 +1,95 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { caLayerProcessorConfigChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; + +import { 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 = { + layerId: string; +}; + +export const CALayerProcessor = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const config = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.processorConfig); + const onChange = useCallback( + (processorConfig: ProcessorConfig) => { + dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); + }, + [dispatch, layerId] + ); + + if (!config) { + return null; + } + + if (config.type === 'canny_image_processor') { + return ; + } + + if (config.type === 'color_map_image_processor') { + return ; + } + + if (config.type === 'depth_anything_image_processor') { + return ; + } + + if (config.type === 'hed_image_processor') { + return ; + } + + if (config.type === 'lineart_image_processor') { + return ; + } + + if (config.type === 'content_shuffle_image_processor') { + return ; + } + + if (config.type === 'lineart_anime_image_processor') { + // No configurable options for this processor + return null; + } + + if (config.type === 'mediapipe_face_processor') { + return ; + } + + if (config.type === 'midas_depth_image_processor') { + return ; + } + + if (config.type === 'mlsd_image_processor') { + return ; + } + + if (config.type === 'normalbae_image_processor') { + // No configurable options for this processor + return null; + } + + if (config.type === 'dw_openpose_image_processor') { + return ; + } + + if (config.type === 'pidi_image_processor') { + return ; + } + + if (config.type === 'zoe_depth_image_processor') { + return null; + } +}); + +CALayerProcessor.displayName = 'CALayerProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/CannyProcessor.tsx new file mode 100644 index 0000000000..5ae1e2cc0e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/CannyProcessor.tsx @@ -0,0 +1,67 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import { type CannyProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; +const DEFAULTS = CONTROLNET_PROCESSORS['canny_image_processor'].buildDefaults(); + +export const CannyProcessor = ({ onChange, config }: Props) => { + const { t } = useTranslation(); + const handleLowThresholdChanged = useCallback( + (v: number) => { + onChange({ ...config, low_threshold: v }); + }, + [onChange, config] + ); + const handleHighThresholdChanged = useCallback( + (v: number) => { + onChange({ ...config, high_threshold: v }); + }, + [onChange, config] + ); + + return ( + + + {t('controlnet.lowThreshold')} + + + + + {t('controlnet.highThreshold')} + + + + + ); +}; + +CannyProcessor.displayName = 'CannyProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ColorMapProcessor.tsx new file mode 100644 index 0000000000..e867ecfe12 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ColorMapProcessor.tsx @@ -0,0 +1,47 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import { type ColorMapProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; +const DEFAULTS = CONTROLNET_PROCESSORS['color_map_image_processor'].buildDefaults(); + +export const ColorMapProcessor = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + const handleColorMapTileSizeChanged = useCallback( + (v: number) => { + onChange({ ...config, color_map_tile_size: v }); + }, + [config, onChange] + ); + + return ( + + + {t('controlnet.colorMapTileSize')} + + + + + ); +}); + +ColorMapProcessor.displayName = 'ColorMapProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ContentShuffleProcessor.tsx new file mode 100644 index 0000000000..19c75045b4 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ContentShuffleProcessor.tsx @@ -0,0 +1,79 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ContentShuffleProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; +const DEFAULTS = CONTROLNET_PROCESSORS['content_shuffle_image_processor'].buildDefaults(); + +export const ContentShuffleProcessor = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleWChanged = useCallback( + (v: number) => { + onChange({ ...config, w: v }); + }, + [config, onChange] + ); + + const handleHChanged = useCallback( + (v: number) => { + onChange({ ...config, h: v }); + }, + [config, onChange] + ); + + const handleFChanged = useCallback( + (v: number) => { + onChange({ ...config, f: v }); + }, + [config, onChange] + ); + + return ( + + + {t('controlnet.w')} + + + + + {t('controlnet.h')} + + + + + {t('controlnet.f')} + + + + + ); +}); + +ContentShuffleProcessor.displayName = 'ContentShuffleProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DWOpenposeProcessor.tsx new file mode 100644 index 0000000000..4d6776a913 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DWOpenposeProcessor.tsx @@ -0,0 +1,62 @@ +import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { DWOpenposeProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; +const DEFAULTS = CONTROLNET_PROCESSORS['dw_openpose_image_processor'].buildDefaults(); + +export const DWOpenposeProcessor = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleDrawBodyChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, draw_body: e.target.checked }); + }, + [config, onChange] + ); + + const handleDrawFaceChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, draw_face: e.target.checked }); + }, + [config, onChange] + ); + + const handleDrawHandsChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, draw_hands: e.target.checked }); + }, + [config, onChange] + ); + + return ( + + + + {t('controlnet.body')} + + + + {t('controlnet.face')} + + + + {t('controlnet.hands')} + + + + + ); +}); + +DWOpenposeProcessor.displayName = 'DWOpenposeProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DepthAnythingProcessor.tsx new file mode 100644 index 0000000000..90c8b32e69 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DepthAnythingProcessor.tsx @@ -0,0 +1,52 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { CONTROLNET_PROCESSORS, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; +const DEFAULTS = CONTROLNET_PROCESSORS['depth_anything_image_processor'].buildDefaults(); + +export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + const handleModelSizeChange = useCallback( + (v) => { + if (!isDepthAnythingModelSize(v?.value)) { + return; + } + onChange({ ...config, model_size: v.value }); + }, + [config, onChange] + ); + + const options: { label: string; value: DepthAnythingModelSize }[] = useMemo( + () => [ + { label: t('controlnet.small'), value: 'small' }, + { label: t('controlnet.base'), value: 'base' }, + { label: t('controlnet.large'), value: 'large' }, + ], + [t] + ); + + const value = useMemo(() => options.filter((o) => o.value === config.model_size)[0], [options, config.model_size]); + + return ( + + + {t('controlnet.modelSize')} + + + + ); +}); + +DepthAnythingProcessor.displayName = 'DepthAnythingProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/HedProcessor.tsx new file mode 100644 index 0000000000..3708287450 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/HedProcessor.tsx @@ -0,0 +1,32 @@ +import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { HedProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; + +export const HedProcessor = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleScribbleChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, scribble: e.target.checked }); + }, + [config, onChange] + ); + + return ( + + + {t('controlnet.scribble')} + + + + ); +}); + +HedProcessor.displayName = 'HedProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/LineartProcessor.tsx new file mode 100644 index 0000000000..ef18e9d61f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/LineartProcessor.tsx @@ -0,0 +1,32 @@ +import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { LineartProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; + +export const LineartProcessor = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleCoarseChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, coarse: e.target.checked }); + }, + [config, onChange] + ); + + return ( + + + {t('controlnet.coarse')} + + + + ); +}); + +LineartProcessor.displayName = 'LineartProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MediapipeFaceProcessor.tsx new file mode 100644 index 0000000000..e3d67f91bb --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MediapipeFaceProcessor.tsx @@ -0,0 +1,73 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import { CONTROLNET_PROCESSORS, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; +const DEFAULTS = CONTROLNET_PROCESSORS['mediapipe_face_processor'].buildDefaults(); + +export const MediapipeFaceProcessor = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleMaxFacesChanged = useCallback( + (v: number) => { + onChange({ ...config, max_faces: v }); + }, + [config, onChange] + ); + + const handleMinConfidenceChanged = useCallback( + (v: number) => { + onChange({ ...config, min_confidence: v }); + }, + [config, onChange] + ); + + return ( + + + {t('controlnet.maxFaces')} + + + + + {t('controlnet.minConfidence')} + + + + + ); +}); + +MediapipeFaceProcessor.displayName = 'MediapipeFaceProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MidasDepthProcessor.tsx new file mode 100644 index 0000000000..36f008d6be --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MidasDepthProcessor.tsx @@ -0,0 +1,76 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { MidasDepthProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; +const DEFAULTS = CONTROLNET_PROCESSORS['midas_depth_image_processor'].buildDefaults(); + +export const MidasDepthProcessor = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleAMultChanged = useCallback( + (v: number) => { + onChange({ ...config, a_mult: v }); + }, + [config, onChange] + ); + + const handleBgThChanged = useCallback( + (v: number) => { + onChange({ ...config, bg_th: v }); + }, + [config, onChange] + ); + + return ( + + + {t('controlnet.amult')} + + + + + {t('controlnet.bgth')} + + + + + ); +}); + +MidasDepthProcessor.displayName = 'MidasDepthProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MlsdImageProcessor.tsx new file mode 100644 index 0000000000..69dc1ce4d9 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MlsdImageProcessor.tsx @@ -0,0 +1,76 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { MlsdProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; +const DEFAULTS = CONTROLNET_PROCESSORS['mlsd_image_processor'].buildDefaults(); + +export const MlsdImageProcessor = memo(({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleThrDChanged = useCallback( + (v: number) => { + onChange({ ...config, thr_d: v }); + }, + [config, onChange] + ); + + const handleThrVChanged = useCallback( + (v: number) => { + onChange({ ...config, thr_v: v }); + }, + [config, onChange] + ); + + return ( + + + {t('controlnet.w')} + + + + + {t('controlnet.h')} + + + + + ); +}); + +MlsdImageProcessor.displayName = 'MlsdImageProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/PidiProcessor.tsx new file mode 100644 index 0000000000..e4c894ef45 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/PidiProcessor.tsx @@ -0,0 +1,43 @@ +import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { PidiProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ChangeEvent } from 'react'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; + +export const PidiProcessor = ({ onChange, config }: Props) => { + const { t } = useTranslation(); + + const handleScribbleChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, scribble: e.target.checked }); + }, + [config, onChange] + ); + + const handleSafeChanged = useCallback( + (e: ChangeEvent) => { + onChange({ ...config, safe: e.target.checked }); + }, + [config, onChange] + ); + + return ( + + + {t('controlnet.scribble')} + + + + {t('controlnet.safe')} + + + + ); +}; + +PidiProcessor.displayName = 'PidiProcessor'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ProcessorWrapper.tsx new file mode 100644 index 0000000000..0b99887b53 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ProcessorWrapper.tsx @@ -0,0 +1,15 @@ +import { Flex } from '@invoke-ai/ui-library'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +type Props = PropsWithChildren; + +const ProcessorWrapper = (props: Props) => { + return ( + + {props.children} + + ); +}; + +export default memo(ProcessorWrapper); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/types.ts new file mode 100644 index 0000000000..48a0942678 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/types.ts @@ -0,0 +1,6 @@ +import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; + +export type ProcessorComponentProps = { + onChange: (config: T) => void; + config: T; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index f8c55b7223..8ea3bb5bee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -240,7 +240,7 @@ export const controlLayersSlice = createSlice({ layer.bboxNeedsUpdate = true; layer.isEnabled = true; layer.controlAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - layer.controlAdapter.processedImage = null; + layer.controlAdapter.processedImage = null; }, caLayerProcessedImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts index b8ef50f4c7..656b759faa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts @@ -3,7 +3,14 @@ import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; import { describe, test } from 'vitest'; -import type { CLIPVisionModel, ControlMode, IPMethod, ProcessorConfig, ProcessorType } from './controlAdapters'; +import type { + CLIPVisionModel, + ControlMode, + DepthAnythingModelSize, + IPMethod, + ProcessorConfig, + ProcessorType, +} from './controlAdapters'; describe('Control Adapter Types', () => { test('ProcessorType', () => assert>()); @@ -11,4 +18,6 @@ describe('Control Adapter Types', () => { test('CLIP Vision Model', () => assert, CLIPVisionModel>>()); test('Control Mode', () => assert, ControlMode>>()); + test('DepthAnything Model Size', () => + assert, DepthAnythingModelSize>>()); }); From 0e55488ff6aa6969dfd92019999040ae2632ee00 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 1 May 2024 18:36:08 +1000 Subject: [PATCH 18/74] refactor(ui): wire up CA logic across (wip) --- .../middleware/listenerMiddleware/index.ts | 3 - .../controlLayersToControlAdapterBridge.ts | 144 ---------------- .../src/common/hooks/useIsReadyToEnqueue.ts | 50 +++--- .../components/AddLayerButton.tsx | 21 +-- .../components/AddPromptButtons.tsx | 14 +- .../components/CALayer/CALayer.tsx | 28 +--- .../components/CALayer/CALayerConfig.tsx | 109 +++++++++++-- .../CALayer/CALayerImagePreview.tsx | 43 ++--- .../components/CALayer/CALayerOpacity.tsx | 4 +- .../components/CALayer/CALayerProcessor.tsx | 18 +- .../CALayer/CALayerProcessorCombobox.tsx | 48 ++---- ....tsx => ControlAdapterBeginEndStepPct.tsx} | 35 +--- ...sx => ControlAdapterControlModeSelect.tsx} | 26 +-- ...ox.tsx => ControlAdapterModelCombobox.tsx} | 25 +-- ...yerWeight.tsx => ControlAdapterWeight.tsx} | 21 +-- .../components/DeleteAllLayersButton.tsx | 13 +- .../ControlAdapterLayerConfig copy.tsx | 75 --------- .../components/IPALayer/IPALayer.tsx | 20 +-- .../components/IPALayer/IPALayerConfig.tsx | 105 ++++++++++++ .../IPALayer/IPALayerModelCombobox.tsx | 100 ++++++++++++ .../IPALayer/IPAdapterImagePreview.tsx | 119 ++++++++++++++ .../components/IPALayer/IPAdapterMethod.tsx | 44 +++++ .../IPALayer/ParamControlAdapterModel.tsx | 136 ---------------- .../LayerCommon/LayerDeleteButton.tsx | 4 +- .../LayerCommon/LayerMenuRGActions.tsx | 8 +- .../components/RGLayer/RGLayer.tsx | 2 +- .../RGLayer/RGLayerIPAdapterList.tsx | 20 ++- .../controlLayers/hooks/addLayerHooks.ts | 95 +++++++++++ .../controlLayers/hooks/mouseEventHooks.ts | 6 +- .../hooks/useControlLayersTitle.ts | 2 +- .../controlLayers/store/controlLayersSlice.ts | 154 +++++++++++------- .../controlLayers/util/controlAdapters.ts | 10 +- .../features/controlLayers/util/renderers.ts | 18 +- .../ControlSettingsAccordion.tsx | 88 +++++----- 34 files changed, 852 insertions(+), 756 deletions(-) delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts rename invokeai/frontend/web/src/features/controlLayers/components/CALayer/{CALayerBeginEndStepPct.tsx => ControlAdapterBeginEndStepPct.tsx} (54%) rename invokeai/frontend/web/src/features/controlLayers/components/CALayer/{CALayerControlMode.tsx => ControlAdapterControlModeSelect.tsx} (68%) rename invokeai/frontend/web/src/features/controlLayers/components/CALayer/{CALayerModelCombobox.tsx => ControlAdapterModelCombobox.tsx} (72%) rename invokeai/frontend/web/src/features/controlLayers/components/CALayer/{CALayerWeight.tsx => ControlAdapterWeight.tsx} (72%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ParamControlAdapterModel.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index ac039c2df6..cd0c1290e9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -16,7 +16,6 @@ import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listen import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet'; import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged'; import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery'; -import { addControlLayersToControlAdapterBridge } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess'; import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed'; import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas'; @@ -158,5 +157,3 @@ addUpscaleRequestedListener(startAppListening); addDynamicPromptsListener(startAppListening); addSetDefaultSettingsListener(startAppListening); - -addControlLayersToControlAdapterBridge(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts deleted file mode 100644 index 81672758c9..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import { controlAdapterAdded, controlAdapterRemoved } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlNetConfig, IPAdapterConfig } from 'features/controlAdapters/store/types'; -import { isControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import { - caLayerAdded, - ipaLayerAdded, - layerDeleted, - rgLayerAdded, - rgLayerIPAdapterAdded, - rgLayerIPAdapterDeleted, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; -import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; -import { isControlNetModelConfig, isIPAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; - -export const guidanceLayerAdded = createAction('controlLayers/guidanceLayerAdded'); -export const guidanceLayerDeleted = createAction('controlLayers/guidanceLayerDeleted'); -export const allLayersDeleted = createAction('controlLayers/allLayersDeleted'); -export const guidanceLayerIPAdapterAdded = createAction('controlLayers/guidanceLayerIPAdapterAdded'); -export const guidanceLayerIPAdapterDeleted = createAction<{ layerId: string; ipAdapterId: string }>( - 'controlLayers/guidanceLayerIPAdapterDeleted' -); - -export const addControlLayersToControlAdapterBridge = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: guidanceLayerAdded, - effect: (action, { dispatch, getState }) => { - const type = action.payload; - const layerId = uuidv4(); - if (type === 'regional_guidance_layer') { - dispatch(rgLayerAdded({ layerId })); - return; - } - - const state = getState(); - const baseModel = state.generation.model?.base; - const modelConfigs = modelsApi.endpoints.getModelConfigs.select(undefined)(state).data; - - if (type === 'ip_adapter_layer') { - const ipAdapterId = uuidv4(); - const overrides: Partial = { - id: ipAdapterId, - }; - - // Find and select the first matching model - if (modelConfigs) { - const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isIPAdapterModelConfig); - overrides.model = models.find((m) => m.base === baseModel) ?? null; - } - dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides })); - dispatch(ipaLayerAdded({ layerId, ipAdapterId })); - return; - } - - if (type === 'control_adapter_layer') { - const controlNetId = uuidv4(); - const overrides: Partial = { - id: controlNetId, - }; - - // Find and select the first matching model - if (modelConfigs) { - const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isControlNetModelConfig); - const model = models.find((m) => m.base === baseModel) ?? null; - overrides.model = model; - const defaultPreprocessor = model?.default_settings?.preprocessor; - overrides.processorType = isControlAdapterProcessorType(defaultPreprocessor) ? defaultPreprocessor : 'none'; - overrides.processorNode = CONTROLNET_PROCESSORS[overrides.processorType].buildDefaults(baseModel); - } - dispatch(controlAdapterAdded({ type: 'controlnet', overrides })); - dispatch(caLayerAdded({ layerId, controlNetId })); - return; - } - }, - }); - - startAppListening({ - actionCreator: guidanceLayerDeleted, - effect: (action, { getState, dispatch }) => { - const layerId = action.payload; - const state = getState(); - const layer = state.controlLayers.present.layers.find((l) => l.id === layerId); - assert(layer, `Layer ${layerId} not found`); - - if (layer.type === 'ip_adapter_layer') { - dispatch(controlAdapterRemoved({ id: layer.ipAdapterId })); - } else if (layer.type === 'control_adapter_layer') { - dispatch(controlAdapterRemoved({ id: layer.controlNetId })); - } else if (layer.type === 'regional_guidance_layer') { - for (const ipAdapterId of layer.ipAdapterIds) { - dispatch(controlAdapterRemoved({ id: ipAdapterId })); - } - } - dispatch(layerDeleted(layerId)); - }, - }); - - startAppListening({ - actionCreator: allLayersDeleted, - effect: (action, { dispatch, getOriginalState }) => { - const state = getOriginalState(); - for (const layer of state.controlLayers.present.layers) { - dispatch(guidanceLayerDeleted(layer.id)); - } - }, - }); - - startAppListening({ - actionCreator: guidanceLayerIPAdapterAdded, - effect: (action, { dispatch, getState }) => { - const layerId = action.payload; - const ipAdapterId = uuidv4(); - const overrides: Partial = { - id: ipAdapterId, - }; - - // Find and select the first matching model - const state = getState(); - const baseModel = state.generation.model?.base; - const modelConfigs = modelsApi.endpoints.getModelConfigs.select(undefined)(state).data; - if (modelConfigs) { - const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isIPAdapterModelConfig); - overrides.model = models.find((m) => m.base === baseModel) ?? null; - } - - dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides })); - dispatch(rgLayerIPAdapterAdded({ layerId, ipAdapterId })); - }, - }); - - startAppListening({ - actionCreator: guidanceLayerIPAdapterDeleted, - effect: (action, { dispatch }) => { - const { layerId, ipAdapterId } = action.payload; - dispatch(controlAdapterRemoved({ id: ipAdapterId })); - dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); - }, - }); -}; diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index d765e987eb..b5650209a4 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -101,33 +101,35 @@ const selector = createMemoizedSelector( if (activeTabName === 'txt2img') { // Special handling for control layers on txt2img - const enabledControlLayersAdapterIds = controlLayers.present.layers - .filter((l) => l.isEnabled) - .flatMap((layer) => { - if (layer.type === 'regional_guidance_layer') { - return layer.ipAdapterIds; - } - if (layer.type === 'control_adapter_layer') { - return [layer.controlNetId]; - } - if (layer.type === 'ip_adapter_layer') { - return [layer.ipAdapterId]; - } - }); + const enabledControlLayersAdapterIds = [] + // const enabledControlLayersAdapterIds = controlLayers.present.layers + // .filter((l) => l.isEnabled) + // .flatMap((layer) => { + // if (layer.type === 'regional_guidance_layer') { + // return layer.ipAdapterIds; + // } + // if (layer.type === 'control_adapter_layer') { + // return [layer.controlNetId]; + // } + // if (layer.type === 'ip_adapter_layer') { + // return [layer.ipAdapterId]; + // } + // }); enabledControlAdapters = enabledControlAdapters.filter((ca) => enabledControlLayersAdapterIds.includes(ca.id)); } else { - const allControlLayerAdapterIds = controlLayers.present.layers.flatMap((layer) => { - if (layer.type === 'regional_guidance_layer') { - return layer.ipAdapterIds; - } - if (layer.type === 'control_adapter_layer') { - return [layer.controlNetId]; - } - if (layer.type === 'ip_adapter_layer') { - return [layer.ipAdapterId]; - } - }); + const allControlLayerAdapterIds = [] + // const allControlLayerAdapterIds = controlLayers.present.layers.flatMap((layer) => { + // if (layer.type === 'regional_guidance_layer') { + // return layer.ipAdapterIds; + // } + // if (layer.type === 'control_adapter_layer') { + // return [layer.controlNetId]; + // } + // if (layer.type === 'ip_adapter_layer') { + // return [layer.ipAdapterId]; + // } + // }); enabledControlAdapters = enabledControlAdapters.filter((ca) => !allControlLayerAdapterIds.includes(ca.id)); } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index b521153239..3eb97dddff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,6 +1,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { guidanceLayerAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { useAppDispatch } from 'app/store/storeHooks'; +import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; +import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -8,14 +9,10 @@ import { PiPlusBold } from 'react-icons/pi'; export const AddLayerButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const addRegionalGuidanceLayer = useCallback(() => { - dispatch(guidanceLayerAdded('regional_guidance_layer')); - }, [dispatch]); - const addControlAdapterLayer = useCallback(() => { - dispatch(guidanceLayerAdded('control_adapter_layer')); - }, [dispatch]); - const addIPAdapterLayer = useCallback(() => { - dispatch(guidanceLayerAdded('ip_adapter_layer')); + const [addCALayer, isAddCALayerDisabled] = useAddCALayer(); + const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); + const addRGLayer = useCallback(() => { + dispatch(rgLayerAdded()); }, [dispatch]); return ( @@ -24,13 +21,13 @@ export const AddLayerButton = memo(() => { {t('controlLayers.addLayer')} - } onClick={addRegionalGuidanceLayer}> + } onClick={addRGLayer}> {t('controlLayers.regionalGuidanceLayer')} - } onClick={addControlAdapterLayer}> + } onClick={addCALayer} isDisabled={isAddCALayerDisabled}> {t('controlLayers.globalControlAdapterLayer')} - } onClick={addIPAdapterLayer}> + } onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> {t('controlLayers.globalIPAdapterLayer')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index d943b33f60..26d9c8ce69 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -1,7 +1,7 @@ import { Button, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { isRegionalGuidanceLayer, rgLayerNegativePromptChanged, @@ -19,6 +19,7 @@ type AddPromptButtonProps = { export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const selectValidActions = useMemo( () => createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { @@ -38,9 +39,6 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { const addNegativePrompt = useCallback(() => { dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); - const addIPAdapter = useCallback(() => { - dispatch(guidanceLayerIPAdapterAdded(layerId)); - }, [dispatch, layerId]); return ( @@ -62,7 +60,13 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { > {t('common.negativePrompt')} - diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index 7d582236bf..864e48c1d2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -1,18 +1,12 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig'; +import { CALayerConfig } from 'features/controlLayers/components/CALayer/CALayerConfig'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; -import { - isControlAdapterLayer, - layerSelected, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { assert } from 'tsafe'; +import { layerSelected, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; import CALayerOpacity from './CALayerOpacity'; @@ -22,19 +16,7 @@ type Props = { export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const selector = useMemo( - () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); - assert(isControlAdapterLayer(layer), `Layer ${layerId} not found or not a ControlNet layer`); - return { - controlNetId: layer.controlNetId, - isSelected: layerId === controlLayers.present.selectedLayerId, - }; - }), - [layerId] - ); - const { controlNetId, isSelected } = useAppSelector(selector); + const isSelected = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).isSelected); const onClickCapture = useCallback(() => { // Must be capture so that the layer is selected before deleting/resetting/etc dispatch(layerSelected(layerId)); @@ -61,7 +43,7 @@ export const CALayer = memo(({ layerId }: Props) => { {isOpen && ( - + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx index 7627fe4364..c998c30f14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx @@ -1,33 +1,101 @@ import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { CALayerModelCombobox } from 'features/controlLayers/components/CALayer/CALayerModelCombobox'; -import { selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; -import { memo } from 'react'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { ControlAdapterModelCombobox } from 'features/controlLayers/components/CALayer/ControlAdapterModelCombobox'; +import { + caLayerControlModeChanged, + caLayerImageChanged, + caLayerModelChanged, + caLayerProcessorConfigChanged, + caOrIPALayerBeginEndStepPctChanged, + caOrIPALayerWeightChanged, + selectCALayer, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlMode, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCaretUpBold } from 'react-icons/pi'; import { useToggle } from 'react-use'; +import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; -import { CALayerBeginEndStepPct } from './CALayerBeginEndStepPct'; -import { CALayerControlMode } from './CALayerControlMode'; import { CALayerImagePreview } from './CALayerImagePreview'; import { CALayerProcessor } from './CALayerProcessor'; import { CALayerProcessorCombobox } from './CALayerProcessorCombobox'; -import { CALayerWeight } from './CALayerWeight'; +import { ControlAdapterBeginEndStepPct } from './ControlAdapterBeginEndStepPct'; +import { ControlAdapterControlModeSelect } from './ControlAdapterControlModeSelect'; +import { ControlAdapterWeight } from './ControlAdapterWeight'; type Props = { layerId: string; }; export const CALayerConfig = memo(({ layerId }: Props) => { - const caType = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.type); + const dispatch = useAppDispatch(); + const controlAdapter = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter); const { t } = useTranslation(); const [isExpanded, toggleIsExpanded] = useToggle(false); + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch( + caOrIPALayerBeginEndStepPctChanged({ + layerId, + beginEndStepPct, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeControlMode = useCallback( + (controlMode: ControlMode) => { + dispatch( + caLayerControlModeChanged({ + layerId, + controlMode, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(caOrIPALayerWeightChanged({ layerId, weight })); + }, + [dispatch, layerId] + ); + + const onChangeProcessorConfig = useCallback( + (processorConfig: ProcessorConfig | null) => { + dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); + }, + [dispatch, layerId] + ); + + const onChangeModel = useCallback( + (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { + dispatch( + caLayerModelChanged({ + layerId, + modelConfig, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(caLayerImageChanged({ layerId, imageDTO })); + }, + [dispatch, layerId] + ); + return ( - + { - {caType === 'controlnet' && } - - + {controlAdapter.type === 'controlnet' && ( + + )} + + - + {isExpanded && ( <> - - + + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx index 209725f94d..c20b408730 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx @@ -7,13 +7,8 @@ import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { - caLayerImageChanged, - heightChanged, - selectCALayer, - selectControlLayersSlice, - widthChanged, -} from 'features/controlLayers/store/controlLayersSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; import type { ControlLayerDropData, ImageDraggableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; @@ -27,10 +22,14 @@ import { useGetImageDTOQuery, useRemoveImageFromBoardMutation, } from 'services/api/endpoints/images'; -import type { ControlLayerAction } from 'services/api/types'; +import type { ControlLayerAction, ImageDTO } from 'services/api/types'; type Props = { - layerId: string; + image: ImageWithDims | null; + processedImage: ImageWithDims | null; + onChangeImage: (imageDTO: ImageDTO | null) => void; + hasProcessor: boolean; + layerId: string; // required for the dnd/upload interactions }; const selectPendingControlImages = createMemoizedSelector( @@ -38,23 +37,9 @@ const selectPendingControlImages = createMemoizedSelector( (controlAdapters) => controlAdapters.pendingControlImages ); -export const CALayerImagePreview = memo(({ layerId }: Props) => { +export const CALayerImagePreview = memo(({ image, processedImage, onChangeImage, hasProcessor, layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const selector = useMemo( - () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = selectCALayer(controlLayers.present, layerId); - const { image, processedImage, processorConfig } = layer.controlAdapter; - return { - imageName: image?.imageName ?? null, - processedImageName: processedImage?.imageName ?? null, - hasProcessor: Boolean(processorConfig), - }; - }), - [layerId] - ); - const { imageName, processedImageName, hasProcessor } = useAppSelector(selector); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const isConnected = useAppSelector((s) => s.system.isConnected); const activeTabName = useAppSelector(activeTabNameSelector); @@ -64,17 +49,19 @@ export const CALayerImagePreview = memo(({ layerId }: Props) => { const [isMouseOverImage, setIsMouseOverImage] = useState(false); - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(imageName ?? skipToken); + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( + image?.imageName ?? skipToken + ); const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - processedImageName ?? skipToken + processedImage?.imageName ?? skipToken ); const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); const [addToBoard] = useAddImageToBoardMutation(); const [removeFromBoard] = useRemoveImageFromBoardMutation(); const handleResetControlImage = useCallback(() => { - dispatch(caLayerImageChanged({ layerId, imageDTO: null })); - }, [layerId, dispatch]); + onChangeImage(null); + }, [onChangeImage]); const handleSaveControlImage = useCallback(async () => { if (!processedControlImage) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx index 3e73158343..31c8d81853 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx @@ -15,7 +15,7 @@ import { import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; -import { caLayerIsFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { caLayerIsFilterEnabledChanged, caLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -34,7 +34,7 @@ const CALayerOpacity = ({ layerId }: Props) => { const { opacity, isFilterEnabled } = useLayerOpacity(layerId); const onChangeOpacity = useCallback( (v: number) => { - dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); + dispatch(caLayerOpacityChanged({ layerId, opacity: v / 100 })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx index 05271010ba..b5ae89f53a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx @@ -1,7 +1,5 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { caLayerProcessorConfigChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { CannyProcessor } from './processors/CannyProcessor'; import { ColorMapProcessor } from './processors/ColorMapProcessor'; @@ -16,19 +14,11 @@ import { MlsdImageProcessor } from './processors/MlsdImageProcessor'; import { PidiProcessor } from './processors/PidiProcessor'; type Props = { - layerId: string; + config: ProcessorConfig | null; + onChange: (config: ProcessorConfig | null) => void; }; -export const CALayerProcessor = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const config = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.processorConfig); - const onChange = useCallback( - (processorConfig: ProcessorConfig) => { - dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); - }, - [dispatch, layerId] - ); - +export const CALayerProcessor = memo(({ config, onChange }: Props) => { if (!config) { return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx index c0f4cca2a5..a01487af44 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx @@ -1,9 +1,9 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { caLayerProcessorConfigChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { CONTROLNET_PROCESSORS, isProcessorType } from 'features/controlLayers/util/controlAdapters'; import { configSelector } from 'features/system/store/configSelectors'; import { includes, map } from 'lodash-es'; @@ -13,7 +13,8 @@ import { PiXBold } from 'react-icons/pi'; import { assert } from 'tsafe'; type Props = { - layerId: string; + config: ProcessorConfig | null; + onChange: (config: ProcessorConfig | null) => void; }; const selectDisabledProcessors = createMemoizedSelector( @@ -21,49 +22,30 @@ const selectDisabledProcessors = createMemoizedSelector( (config) => config.sd.disabledControlNetProcessors ); -export const CALayerProcessorCombobox = memo(({ layerId }: Props) => { +export const CALayerProcessorCombobox = memo(({ config, onChange }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); const disabledProcessors = useAppSelector(selectDisabledProcessors); - const processorType = useAppSelector( - (s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.processorConfig?.type ?? null - ); const options = useMemo(() => { return map(CONTROLNET_PROCESSORS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( (o) => !includes(disabledProcessors, o.value) ); }, [disabledProcessors, t]); - const onChange = useCallback( + const _onChange = useCallback( (v) => { if (!v) { - dispatch( - caLayerProcessorConfigChanged({ - layerId, - processorConfig: null, - }) - ); - return; + onChange(null); + } else { + assert(isProcessorType(v.value)); + onChange(CONTROLNET_PROCESSORS[v.value].buildDefaults()); } - assert(isProcessorType(v.value)); - dispatch( - caLayerProcessorConfigChanged({ - layerId, - processorConfig: CONTROLNET_PROCESSORS[v.value].buildDefaults(), - }) - ); }, - [dispatch, layerId] + [onChange] ); const clearProcessor = useCallback(() => { - dispatch( - caLayerProcessorConfigChanged({ - layerId, - processorConfig: null, - }) - ); - }, [dispatch, layerId]); - const value = useMemo(() => options.find((o) => o.value === processorType) ?? null, [options, processorType]); + onChange(null); + }, [onChange]); + const value = useMemo(() => options.find((o) => o.value === config?.type) ?? null, [options, config?.type]); return ( @@ -71,7 +53,7 @@ export const CALayerProcessorCombobox = memo(({ layerId }: Props) => { {t('controlnet.processor')} - + void; }; const formatPct = (v: number) => `${Math.round(v * 100)}%`; const ariaLabel = ['Begin Step %', 'End Step %']; -export const CALayerBeginEndStepPct = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); +export const ControlAdapterBeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => { const { t } = useTranslation(); - const beginEndStepPct = useAppSelector( - (s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.beginEndStepPct - ); - - const onChange = useCallback( - (v: [number, number]) => { - dispatch( - caLayerBeginEndStepPctChanged({ - layerId, - beginEndStepPct: v, - }) - ); - }, - [dispatch, layerId] - ); - const onReset = useCallback(() => { - dispatch( - caLayerBeginEndStepPctChanged({ - layerId, - beginEndStepPct: [0, 1], - }) - ); - }, [dispatch, layerId]); + onChange([0, 1]); + }, [onChange]); return ( @@ -63,4 +40,4 @@ export const CALayerBeginEndStepPct = memo(({ layerId }: Props) => { ); }); -CALayerBeginEndStepPct.displayName = 'CALayerBeginEndStepPct'; +ControlAdapterBeginEndStepPct.displayName = 'ControlAdapterBeginEndStepPct'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlMode.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterControlModeSelect.tsx similarity index 68% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlMode.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterControlModeSelect.tsx index c60d22a3a0..34f4c85467 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlMode.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterControlModeSelect.tsx @@ -1,26 +1,19 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { caLayerControlModeChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlMode } from 'features/controlLayers/util/controlAdapters'; import { isControlMode } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; type Props = { - layerId: string; + controlMode: ControlMode; + onChange: (controlMode: ControlMode) => void; }; -export const CALayerControlMode = memo(({ layerId }: Props) => { +export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const controlMode = useAppSelector((s) => { - const ca = selectCALayer(s.controlLayers.present, layerId).controlAdapter; - assert(ca.type === 'controlnet'); - return ca.controlMode; - }); - const CONTROL_MODE_DATA = useMemo( () => [ { label: t('controlnet.balanced'), value: 'balanced' }, @@ -34,14 +27,9 @@ export const CALayerControlMode = memo(({ layerId }: Props) => { const handleControlModeChange = useCallback( (v) => { assert(isControlMode(v?.value)); - dispatch( - caLayerControlModeChanged({ - layerId, - controlMode: v.value, - }) - ); + onChange(v.value); }, - [layerId, dispatch] + [onChange] ); const value = useMemo( @@ -69,4 +57,4 @@ export const CALayerControlMode = memo(({ layerId }: Props) => { ); }); -CALayerControlMode.displayName = 'CALayerControlMode'; +ControlAdapterControlModeSelect.displayName = 'ControlAdapterControlModeSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterModelCombobox.tsx similarity index 72% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerModelCombobox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterModelCombobox.tsx index 8e1e5c6891..a4b1d6b744 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterModelCombobox.tsx @@ -1,39 +1,30 @@ import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { caLayerModelChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType'; import type { AnyModelConfig, ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; type Props = { - layerId: string; + modelKey: string | null; + onChange: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; }; -export const CALayerModelCombobox = memo(({ layerId }: Props) => { +export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - - const caModelKey = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.model?.key); const currentBaseModel = useAppSelector((s) => s.generation.model?.base); - const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); - const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === caModelKey), [modelConfigs, caModelKey]); + const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); const _onChange = useCallback( (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null) => { if (!modelConfig) { return; } - dispatch( - caLayerModelChanged({ - layerId, - modelConfig, - }) - ); + onChangeModel(modelConfig); }, - [dispatch, layerId] + [onChangeModel] ); const getIsDisabled = useCallback( @@ -68,4 +59,4 @@ export const CALayerModelCombobox = memo(({ layerId }: Props) => { ); }); -CALayerModelCombobox.displayName = 'CALayerModelCombobox'; +ControlAdapterModelCombobox.displayName = 'ControlAdapterModelCombobox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerWeight.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterWeight.tsx similarity index 72% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerWeight.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterWeight.tsx index b8738fd352..4bb7bb3911 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerWeight.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterWeight.tsx @@ -1,21 +1,19 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { caLayerWeightChanged, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; type Props = { - layerId: string; + weight: number; + onChange: (weight: number) => void; }; const formatValue = (v: number) => v.toFixed(2); const marks = [0, 1, 2]; -export const CALayerWeight = memo(({ layerId }: Props) => { +export const ControlAdapterWeight = memo(({ weight, onChange }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const weight = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.weight); const initial = useAppSelector((s) => s.config.sd.ca.weight.initial); const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin); const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax); @@ -24,13 +22,6 @@ export const CALayerWeight = memo(({ layerId }: Props) => { const coarseStep = useAppSelector((s) => s.config.sd.ca.weight.coarseStep); const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep); - const onChange = useCallback( - (weight: number) => { - dispatch(caLayerWeightChanged({ layerId, weight })); - }, - [dispatch, layerId] - ); - return ( @@ -61,4 +52,4 @@ export const CALayerWeight = memo(({ layerId }: Props) => { ); }); -CALayerWeight.displayName = 'CALayerWeight'; +ControlAdapterWeight.displayName = 'ControlAdapterWeight'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index c55864afa5..dad102b470 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -1,6 +1,6 @@ import { Button } from '@invoke-ai/ui-library'; -import { allLayersDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; @@ -8,12 +8,19 @@ import { PiTrashSimpleBold } from 'react-icons/pi'; export const DeleteAllLayersButton = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const isDisabled = useAppSelector((s) => s.controlLayers.present.layers.length === 0); const onClick = useCallback(() => { dispatch(allLayersDeleted()); }, [dispatch]); return ( - ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx deleted file mode 100644 index aa518c6dd4..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ControlAdapterLayerConfig copy.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterProcessorComponent from 'features/controlAdapters/components/ControlAdapterProcessorComponent'; -import ControlAdapterShouldAutoConfig from 'features/controlAdapters/components/ControlAdapterShouldAutoConfig'; -import ParamControlAdapterIPMethod from 'features/controlAdapters/components/parameters/ParamControlAdapterIPMethod'; -import ParamControlAdapterProcessorSelect from 'features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect'; -import { ParamControlAdapterBeginEnd } from 'features/controlLayers/components/CALayer/CALayerBeginEndStepPct'; -import ParamControlAdapterControlMode from 'features/controlLayers/components/CALayer/CALayerControlMode'; -import { CALayerImagePreview } from 'features/controlLayers/components/CALayer/CALayerImagePreview'; -import ParamControlAdapterModel from 'features/controlLayers/components/CALayer/CALayerModelCombobox'; -import ParamControlAdapterWeight from 'features/controlLayers/components/CALayer/CALayerWeight'; -import { selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiCaretUpBold } from 'react-icons/pi'; -import { useToggle } from 'react-use'; - -type Props = { - layerId: string; -}; - -export const CALayerCAConfig = memo(({ layerId }: Props) => { - const caType = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter.type); - const { t } = useTranslation(); - const [isExpanded, toggleIsExpanded] = useToggle(false); - - return ( - - - - {' '} - - - {controlAdapterType !== 'ip_adapter' && ( - - } - /> - )} - - - - {controlAdapterType === 'ip_adapter' && } - {controlAdapterType === 'controlnet' && } - - - - - - - - {isExpanded && ( - <> - - - - - )} - - ); -}); - -CALayerCAConfig.displayName = 'CALayerCAConfig'; 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 2dee5d95a6..71b06e6830 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -1,29 +1,15 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig'; +import { IPALayerConfig } from 'features/controlLayers/components/IPALayer/IPALayerConfig'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; -import { isIPAdapterLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useMemo } from 'react'; -import { assert } from 'tsafe'; +import { memo } from 'react'; type Props = { layerId: string; }; export const IPALayer = memo(({ layerId }: Props) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); - assert(isIPAdapterLayer(layer), `Layer ${layerId} not found or not an IP Adapter layer`); - return layer.ipAdapterId; - }), - [layerId] - ); - const ipAdapterId = useAppSelector(selector); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( @@ -36,7 +22,7 @@ export const IPALayer = memo(({ layerId }: Props) => { {isOpen && ( - + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx new file mode 100644 index 0000000000..f1b035da1c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx @@ -0,0 +1,105 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { ControlAdapterBeginEndStepPct } from 'features/controlLayers/components/CALayer/ControlAdapterBeginEndStepPct'; +import { ControlAdapterWeight } from 'features/controlLayers/components/CALayer/ControlAdapterWeight'; +import { IPAdapterImagePreview } from 'features/controlLayers/components/IPALayer/IPAdapterImagePreview'; +import { IPAdapterMethod } from 'features/controlLayers/components/IPALayer/IPAdapterMethod'; +import { IPAdapterModelCombobox } from 'features/controlLayers/components/IPALayer/IPALayerModelCombobox'; +import { + caOrIPALayerBeginEndStepPctChanged, + caOrIPALayerWeightChanged, + ipaLayerCLIPVisionModelChanged, + ipaLayerImageChanged, + ipaLayerMethodChanged, + ipaLayerModelChanged, + selectIPALayer, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; +import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; + +type Props = { + layerId: string; +}; + +export const IPALayerConfig = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const ipAdapter = useAppSelector((s) => selectIPALayer(s.controlLayers.present, layerId).ipAdapter); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch( + caOrIPALayerBeginEndStepPctChanged({ + layerId, + beginEndStepPct, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(caOrIPALayerWeightChanged({ layerId, weight })); + }, + [dispatch, layerId] + ); + + const onChangeIPMethod = useCallback( + (method: IPMethod) => { + dispatch(ipaLayerMethodChanged({ layerId, method })); + }, + [dispatch, layerId] + ); + + const onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig) => { + dispatch(ipaLayerModelChanged({ layerId, modelConfig })); + }, + [dispatch, layerId] + ); + + const onChangeCLIPVisionModel = useCallback( + (clipVisionModel: CLIPVisionModel) => { + dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel })); + }, + [dispatch, layerId] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(ipaLayerImageChanged({ layerId, imageDTO })); + }, + [dispatch, layerId] + ); + + return ( + + + + + + + + + + + + + + + + + + ); +}); + +IPALayerConfig.displayName = 'IPALayerConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx new file mode 100644 index 0000000000..facd46aed1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx @@ -0,0 +1,100 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import type { CLIPVisionModel } from 'features/controlLayers/util/controlAdapters'; +import { isCLIPVisionModel } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useIPAdapterModels } from 'services/api/hooks/modelsByType'; +import type { AnyModelConfig, IPAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; + +const CLIP_VISION_OPTIONS = [ + { label: 'ViT-H', value: 'ViT-H' }, + { label: 'ViT-G', value: 'ViT-G' }, +]; + +type Props = { + modelKey: string | null; + onChangeModel: (modelConfig: IPAdapterModelConfig) => void; + clipVisionModel: CLIPVisionModel; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void; +}; + +export const IPAdapterModelCombobox = memo( + ({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { + const { t } = useTranslation(); + const currentBaseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs, { isLoading }] = useIPAdapterModels(); + const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); + + const _onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig | null) => { + if (!modelConfig) { + return; + } + onChangeModel(modelConfig); + }, + [onChangeModel] + ); + + const _onChangeCLIPVisionModel = useCallback( + (v) => { + assert(isCLIPVisionModel(v?.value)); + onChangeCLIPVisionModel(v.value); + }, + [onChangeCLIPVisionModel] + ); + + const getIsDisabled = useCallback( + (model: AnyModelConfig): boolean => { + const isCompatible = currentBaseModel === model.base; + const hasMainModel = Boolean(currentBaseModel); + return !hasMainModel || !isCompatible; + }, + [currentBaseModel] + ); + + const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ + modelConfigs, + onChange: _onChangeModel, + selectedModel, + getIsDisabled, + isLoading, + }); + + const clipVisionModelValue = useMemo( + () => CLIP_VISION_OPTIONS.find((o) => o.value === clipVisionModel), + [clipVisionModel] + ); + + return ( + + + + + + + {selectedModel?.format === 'checkpoint' && ( + + + + )} + + ); + } +); + +IPAdapterModelCombobox.displayName = 'IPALayerModelCombobox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx new file mode 100644 index 0000000000..bff6d29502 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx @@ -0,0 +1,119 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIDndImage from 'common/components/IAIDndImage'; +import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import type { ControlLayerDropData, ImageDraggableData } from 'features/dnd/types'; +import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { memo, useCallback, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import type { ControlLayerAction, ImageDTO } from 'services/api/types'; + +type Props = { + image: ImageWithDims | null; + onChangeImage: (imageDTO: ImageDTO | null) => void; + layerId: string; // required for the dnd/upload interactions +}; + +export const IPAdapterImagePreview = memo(({ image, onChangeImage, layerId }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isConnected = useAppSelector((s) => s.system.isConnected); + const activeTabName = useAppSelector(activeTabNameSelector); + const optimalDimension = useAppSelector(selectOptimalDimension); + const shift = useShiftModifier(); + + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( + image?.imageName ?? skipToken + ); + const handleResetControlImage = useCallback(() => { + onChangeImage(null); + }, [onChangeImage]); + + const handleSetControlImageToDimensions = useCallback(() => { + if (!controlImage) { + return; + } + + if (activeTabName === 'unifiedCanvas') { + dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)); + } else { + if (shift) { + const { width, height } = controlImage; + dispatch(widthChanged({ width, updateAspectRatio: true })); + dispatch(heightChanged({ height, updateAspectRatio: true })); + } else { + const { width, height } = calculateNewSize( + controlImage.width / controlImage.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, updateAspectRatio: true })); + dispatch(heightChanged({ height, updateAspectRatio: true })); + } + } + }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); + + const draggableData = useMemo(() => { + if (controlImage) { + return { + id: layerId, + payloadType: 'IMAGE_DTO', + payload: { imageDTO: controlImage }, + }; + } + }, [controlImage, layerId]); + + const droppableData = useMemo( + () => ({ + id: layerId, + actionType: 'SET_CONTROL_LAYER_IMAGE', + context: { layerId }, + }), + [layerId] + ); + + const postUploadAction = useMemo(() => ({ type: 'SET_CONTROL_LAYER_IMAGE', layerId }), [layerId]); + + useEffect(() => { + if (isConnected && isErrorControlImage) { + handleResetControlImage(); + } + }, [handleResetControlImage, isConnected, isErrorControlImage]); + + return ( + + + + <> + : undefined} + tooltip={t('controlnet.resetControlImage')} + /> + : undefined} + tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} + styleOverrides={setControlImageDimensionsStyleOverrides} + /> + + + ); +}); + +IPAdapterImagePreview.displayName = 'IPAdapterImagePreview'; + +const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 6 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx new file mode 100644 index 0000000000..70fd63f9c0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx @@ -0,0 +1,44 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import type { IPMethod } from 'features/controlLayers/util/controlAdapters'; +import { isIPMethod } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { assert } from 'tsafe'; + +type Props = { + method: IPMethod; + onChange: (method: IPMethod) => void; +}; + +export const IPAdapterMethod = memo(({ method, onChange }: Props) => { + const { t } = useTranslation(); + const options: { label: string; value: IPMethod }[] = useMemo( + () => [ + { label: t('controlnet.full'), value: 'full' }, + { label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' }, + { label: `${t('controlnet.composition')} (${t('common.beta')})`, value: 'composition' }, + ], + [t] + ); + const _onChange = useCallback( + (v) => { + assert(isIPMethod(v?.value)); + onChange(v.value); + }, + [onChange] + ); + const value = useMemo(() => options.find((o) => o.value === method), [options, method]); + + return ( + + + {t('controlnet.ipAdapterMethod')} + + + + ); +}); + +IPAdapterMethod.displayName = 'IPAdapterMethod'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ParamControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ParamControlAdapterModel.tsx deleted file mode 100644 index 73a7d695b3..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/ParamControlAdapterModel.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; -import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { useControlAdapterCLIPVisionModel } from 'features/controlAdapters/hooks/useControlAdapterCLIPVisionModel'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterModel } from 'features/controlAdapters/hooks/useControlAdapterModel'; -import { useControlAdapterModels } from 'features/controlAdapters/hooks/useControlAdapterModels'; -import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType'; -import { - controlAdapterCLIPVisionModelChanged, - controlAdapterModelChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { CLIPVisionModel } from 'features/controlAdapters/store/types'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import type { - AnyModelConfig, - ControlNetModelConfig, - IPAdapterModelConfig, - T2IAdapterModelConfig, -} from 'services/api/types'; - -type ParamControlAdapterModelProps = { - id: string; -}; - -const selectMainModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model); - -const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { - const isEnabled = useControlAdapterIsEnabled(id); - const controlAdapterType = useControlAdapterType(id); - const { modelConfig } = useControlAdapterModel(id); - const dispatch = useAppDispatch(); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base); - const currentCLIPVisionModel = useControlAdapterCLIPVisionModel(id); - const mainModel = useAppSelector(selectMainModel); - const { t } = useTranslation(); - - const [modelConfigs, { isLoading }] = useControlAdapterModels(controlAdapterType); - - const _onChange = useCallback( - (modelConfig: ControlNetModelConfig | IPAdapterModelConfig | T2IAdapterModelConfig | null) => { - if (!modelConfig) { - return; - } - dispatch( - controlAdapterModelChanged({ - id, - modelConfig, - }) - ); - }, - [dispatch, id] - ); - - const onCLIPVisionModelChange = useCallback( - (v) => { - if (!v?.value) { - return; - } - dispatch(controlAdapterCLIPVisionModelChanged({ id, clipVisionModel: v.value as CLIPVisionModel })); - }, - [dispatch, id] - ); - - const selectedModel = useMemo( - () => (modelConfig && controlAdapterType ? { ...modelConfig, model_type: controlAdapterType } : null), - [controlAdapterType, modelConfig] - ); - - const getIsDisabled = useCallback( - (model: AnyModelConfig): boolean => { - const isCompatible = currentBaseModel === model.base; - const hasMainModel = Boolean(currentBaseModel); - return !hasMainModel || !isCompatible; - }, - [currentBaseModel] - ); - - const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ - modelConfigs, - onChange: _onChange, - selectedModel, - getIsDisabled, - isLoading, - }); - - const clipVisionOptions = useMemo( - () => [ - { label: 'ViT-H', value: 'ViT-H' }, - { label: 'ViT-G', value: 'ViT-G' }, - ], - [] - ); - - const clipVisionModel = useMemo( - () => clipVisionOptions.find((o) => o.value === currentCLIPVisionModel), - [clipVisionOptions, currentCLIPVisionModel] - ); - - return ( - - - - - - - {modelConfig?.type === 'ip_adapter' && modelConfig.format === 'checkpoint' && ( - - - - )} - - ); -}; - -export default memo(ParamControlAdapterModel); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx index 0c74b2a9ea..0cd7d83dfe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { guidanceLayerDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; +import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; @@ -12,7 +12,7 @@ export const LayerDeleteButton = memo(({ layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const deleteLayer = useCallback(() => { - dispatch(guidanceLayerDeleted(layerId)); + dispatch(layerDeleted(layerId)); }, [dispatch, layerId]); return ( { const dispatch = useAppDispatch(); const { t } = useTranslation(); + const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const selectValidActions = useMemo( () => createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { @@ -37,9 +38,6 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { const addNegativePrompt = useCallback(() => { dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); - const addIPAdapter = useCallback(() => { - dispatch(guidanceLayerIPAdapterAdded(layerId)); - }, [dispatch, layerId]); return ( <> }> @@ -48,7 +46,7 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { }> {t('controlLayers.addNegativePrompt')} - }> + } isDisabled={isAddIPAdapterDisabled}> {t('controlLayers.addIPAdapter')} 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 a3dbfb00e7..baed22f6ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -38,7 +38,7 @@ export const RGLayer = memo(({ layerId }: Props) => { color: rgbColorToString(layer.previewColor), hasPositivePrompt: layer.positivePrompt !== null, hasNegativePrompt: layer.negativePrompt !== null, - hasIPAdapters: layer.ipAdapterIds.length > 0, + hasIPAdapters: layer.ipAdapters.length > 0, isSelected: layerId === controlLayers.present.selectedLayerId, autoNegative: layer.autoNegative, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx index 3a1f6d79cd..cb3c371c67 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx @@ -1,9 +1,11 @@ import { Divider, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { guidanceLayerIPAdapterDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ControlAdapterLayerConfig from 'features/controlLayers/components/CALayer/ControlAdapterLayerConfig'; -import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { + isRegionalGuidanceLayer, + rgLayerIPAdapterDeleted, + selectControlLayersSlice, +} from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback, useMemo } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; import { assert } from 'tsafe'; @@ -18,19 +20,19 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); - return layer.ipAdapterIds; + return layer.ipAdapters; }), [layerId] ); - const ipAdapterIds = useAppSelector(selectIPAdapterIds); + const ipAdapters = useAppSelector(selectIPAdapterIds); - if (ipAdapterIds.length === 0) { + if (ipAdapters.length === 0) { return null; } return ( <> - {ipAdapterIds.map((id, index) => ( + {ipAdapters.map(({ id }, index) => ( {index > 0 && ( @@ -55,7 +57,7 @@ type IPAdapterListItemProps = { const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber }: IPAdapterListItemProps) => { const dispatch = useAppDispatch(); const onDeleteIPAdapter = useCallback(() => { - dispatch(guidanceLayerIPAdapterDeleted({ layerId, ipAdapterId })); + dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); }, [dispatch, ipAdapterId, layerId]); return ( @@ -72,7 +74,7 @@ const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber } colorScheme="error" /> - + {/* */} ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts new file mode 100644 index 0000000000..17f0d4bf2d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -0,0 +1,95 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { caLayerAdded, ipaLayerAdded, rgLayerIPAdapterAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { + buildControlNet, + buildIPAdapter, + buildT2IAdapter, + CONTROLNET_PROCESSORS, + isProcessorType, +} from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import { useCallback, useMemo } from 'react'; +import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType'; +import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; + +export const useAddCALayer = () => { + const dispatch = useAppDispatch(); + const baseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs] = useControlNetAndT2IAdapterModels(); + const model: ControlNetModelConfig | T2IAdapterModelConfig | null = useMemo(() => { + // prefer to use a model that matches the base model + const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); + return compatibleModels[0] ?? modelConfigs[0] ?? null; + }, [baseModel, modelConfigs]); + const isDisabled = useMemo(() => !model, [model]); + const addCALayer = useCallback(() => { + if (!model) { + return; + } + + const id = uuidv4(); + const defaultPreprocessor = model.default_settings?.preprocessor; + const processorConfig = isProcessorType(defaultPreprocessor) + ? CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(baseModel) + : null; + + const builder = model.type === 'controlnet' ? buildControlNet : buildT2IAdapter; + const controlAdapter = builder(id, { + model: zModelIdentifierField.parse(model), + processorConfig, + }); + + dispatch(caLayerAdded(controlAdapter)); + }, [dispatch, model, baseModel]); + + return [addCALayer, isDisabled] as const; +}; + +export const useAddIPALayer = () => { + const dispatch = useAppDispatch(); + const baseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs] = useIPAdapterModels(); + const model: IPAdapterModelConfig | null = useMemo(() => { + // prefer to use a model that matches the base model + const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); + return compatibleModels[0] ?? modelConfigs[0] ?? null; + }, [baseModel, modelConfigs]); + const isDisabled = useMemo(() => !model, [model]); + const addIPALayer = useCallback(() => { + if (!model) { + return; + } + const id = uuidv4(); + const ipAdapter = buildIPAdapter(id, { + model: zModelIdentifierField.parse(model), + }); + dispatch(ipaLayerAdded(ipAdapter)); + }, [dispatch, model]); + + return [addIPALayer, isDisabled] as const; +}; + +export const useAddIPAdapterToIPALayer = (layerId: string) => { + const dispatch = useAppDispatch(); + const baseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs] = useIPAdapterModels(); + const model: IPAdapterModelConfig | null = useMemo(() => { + // prefer to use a model that matches the base model + const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true)); + return compatibleModels[0] ?? modelConfigs[0] ?? null; + }, [baseModel, modelConfigs]); + const isDisabled = useMemo(() => !model, [model]); + const addIPAdapter = useCallback(() => { + if (!model) { + return; + } + const id = uuidv4(); + const ipAdapter = buildIPAdapter(id, { + model: zModelIdentifierField.parse(model), + }); + dispatch(rgLayerIPAdapterAdded({ layerId, ipAdapter })); + }, [dispatch, model, layerId]); + + return [addIPAdapter, isDisabled] as const; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts index bb744b0535..e3e87d0c42 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts @@ -9,7 +9,7 @@ import { $lastMouseDownPos, $tool, brushSizeChanged, - rfLayerLineAdded, + rgLayerLineAdded, rgLayerPointsAdded, rgLayerRectAdded, } from 'features/controlLayers/store/controlLayersSlice'; @@ -71,7 +71,7 @@ export const useMouseEvents = () => { } if (tool === 'brush' || tool === 'eraser') { dispatch( - rfLayerLineAdded({ + rgLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool, @@ -181,7 +181,7 @@ export const useMouseEvents = () => { } if (tool === 'brush' || tool === 'eraser') { dispatch( - rfLayerLineAdded({ + rgLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool, diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts index 93c8bec8a6..56d380b1d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts @@ -13,7 +13,7 @@ const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlL .filter((l) => l.isEnabled) .filter((l) => { const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); - const hasAtLeastOneImagePrompt = l.ipAdapterIds.length > 0; + const hasAtLeastOneImagePrompt = l.ipAdapters.length > 0; return hasTextPrompt || hasAtLeastOneImagePrompt; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 8ea3bb5bee..4d7feaa6ee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -25,7 +25,7 @@ import { isEqual, partition } from 'lodash-es'; import { atom } from 'nanostores'; import type { RgbColor } from 'react-colorful'; import type { UndoableOptions } from 'redux-undo'; -import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; +import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -88,11 +88,19 @@ export const selectCALayer = (state: ControlLayersState, layerId: string): Contr assert(isControlAdapterLayer(layer)); return layer; }; -const selectIPALayer = (state: ControlLayersState, layerId: string): IPAdapterLayer => { +export const selectIPALayer = (state: ControlLayersState, layerId: string): IPAdapterLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isIPAdapterLayer(layer)); return layer; }; +export const selectCAOrIPALayer = ( + state: ControlLayersState, + layerId: string +): ControlAdapterLayer | IPAdapterLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isControlAdapterLayer(layer) || isIPAdapterLayer(layer)); + return layer; +}; const selectRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer)); @@ -199,6 +207,10 @@ export const controlLayersSlice = createSlice({ state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId); state.selectedLayerId = state.layers[0]?.id ?? null; }, + allLayersDeleted: (state) => { + state.layers = []; + state.selectedLayerId = null; + }, //#endregion //#region CA Layers @@ -272,19 +284,6 @@ export const controlLayersSlice = createSlice({ layer.controlAdapter.processorConfig = candidateProcessorConfig; } }, - caLayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { - const { layerId, weight } = action.payload; - const layer = selectCALayer(state, layerId); - layer.controlAdapter.weight = weight; - }, - caLayerBeginEndStepPctChanged: ( - state, - action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> - ) => { - const { layerId, beginEndStepPct } = action.payload; - const layer = selectCALayer(state, layerId); - layer.controlAdapter.beginEndStepPct = beginEndStepPct; - }, caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlMode }>) => { const { layerId, controlMode } = action.payload; const layer = selectCALayer(state, layerId); @@ -348,6 +347,21 @@ export const controlLayersSlice = createSlice({ const layer = selectIPALayer(state, layerId); layer.ipAdapter.method = method; }, + ipaLayerModelChanged: ( + state, + action: PayloadAction<{ + layerId: string; + modelConfig: IPAdapterModelConfig | null; + }> + ) => { + const { layerId, modelConfig } = action.payload; + const layer = selectIPALayer(state, layerId); + if (!modelConfig) { + layer.ipAdapter.model = null; + return; + } + layer.ipAdapter.model = zModelIdentifierField.parse(modelConfig); + }, ipaLayerCLIPVisionModelChanged: ( state, action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }> @@ -358,34 +372,61 @@ export const controlLayersSlice = createSlice({ }, //#endregion - //#region RG Layers - rgLayerAdded: (state, action: PayloadAction<{ layerId: string }>) => { - const { layerId } = action.payload; - const layer: RegionalGuidanceLayer = { - id: getRGLayerId(layerId), - type: 'regional_guidance_layer', - isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, - maskObjects: [], - previewColor: getVectorMaskPreviewColor(state), - x: 0, - y: 0, - autoNegative: 'invert', - needsPixelBbox: false, - positivePrompt: '', - negativePrompt: null, - ipAdapters: [], - isSelected: true, - }; - state.layers.push(layer); - state.selectedLayerId = layer.id; - for (const layer of state.layers.filter(isRenderableLayer)) { - if (layer.id !== layerId) { - layer.isSelected = false; - } + //#region CA or IPA Layers + caOrIPALayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { + const { layerId, weight } = action.payload; + const layer = selectCAOrIPALayer(state, layerId); + if (layer.type === 'control_adapter_layer') { + layer.controlAdapter.weight = weight; + } else { + layer.ipAdapter.weight = weight; } }, + caOrIPALayerBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> + ) => { + const { layerId, beginEndStepPct } = action.payload; + const layer = selectCAOrIPALayer(state, layerId); + if (layer.type === 'control_adapter_layer') { + layer.controlAdapter.beginEndStepPct = beginEndStepPct; + } else { + layer.ipAdapter.beginEndStepPct = beginEndStepPct; + } + }, + //#endregion + + //#region RG Layers + rgLayerAdded: { + reducer: (state, action: PayloadAction<{ layerId: string }>) => { + const { layerId } = action.payload; + const layer: RegionalGuidanceLayer = { + id: getRGLayerId(layerId), + type: 'regional_guidance_layer', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + maskObjects: [], + previewColor: getVectorMaskPreviewColor(state), + x: 0, + y: 0, + autoNegative: 'invert', + needsPixelBbox: false, + positivePrompt: '', + negativePrompt: null, + ipAdapters: [], + isSelected: true, + }; + state.layers.push(layer); + state.selectedLayerId = layer.id; + for (const layer of state.layers.filter(isRenderableLayer)) { + if (layer.id !== layerId) { + layer.isSelected = false; + } + } + }, + prepare: () => ({ payload: { layerId: uuidv4() } }), + }, rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; const layer = selectRGLayer(state, layerId); @@ -396,16 +437,6 @@ export const controlLayersSlice = createSlice({ const layer = selectRGLayer(state, layerId); layer.negativePrompt = prompt; }, - rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { - const { layerId, ipAdapter } = action.payload; - const layer = selectRGLayer(state, layerId); - layer.ipAdapters.push(ipAdapter); - }, - rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { - const { layerId, ipAdapterId } = action.payload; - const layer = selectRGLayer(state, layerId); - layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); - }, rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { const { layerId, color } = action.payload; const layer = selectRGLayer(state, layerId); @@ -483,6 +514,16 @@ export const controlLayersSlice = createSlice({ const layer = selectRGLayer(state, layerId); layer.autoNegative = autoNegative; }, + rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { + const { layerId, ipAdapter } = action.payload; + const layer = selectRGLayer(state, layerId); + layer.ipAdapters.push(ipAdapter); + }, + rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { + const { layerId, ipAdapterId } = action.payload; + const layer = selectRGLayer(state, layerId); + layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); + }, rgLayerIPAdapterImageChanged: ( state, action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }> @@ -657,13 +698,12 @@ export const { layerMovedToBack, selectedLayerReset, selectedLayerDeleted, + allLayersDeleted, // CA Layers caLayerAdded, caLayerImageChanged, caLayerProcessedImageChanged, caLayerModelChanged, - caLayerWeightChanged, - caLayerBeginEndStepPctChanged, caLayerControlModeChanged, caLayerProcessorConfigChanged, caLayerIsFilterEnabledChanged, @@ -674,18 +714,22 @@ export const { ipaLayerWeightChanged, ipaLayerBeginEndStepPctChanged, ipaLayerMethodChanged, + ipaLayerModelChanged, ipaLayerCLIPVisionModelChanged, + // CA or IPA Layers + caOrIPALayerWeightChanged, + caOrIPALayerBeginEndStepPctChanged, // RG Layers rgLayerAdded, rgLayerPositivePromptChanged, rgLayerNegativePromptChanged, - rgLayerIPAdapterAdded, - rgLayerIPAdapterDeleted, rgLayerPreviewColorChanged, rgLayerLineAdded, rgLayerPointsAdded, rgLayerRectAdded, rgLayerAutoNegativeChanged, + rgLayerIPAdapterAdded, + rgLayerIPAdapterDeleted, rgLayerIPAdapterImageChanged, rgLayerIPAdapterWeightChanged, rgLayerIPAdapterBeginEndStepPctChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index a388d65e94..3debe10791 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -72,7 +72,7 @@ export type ProcessorConfig = | PidiProcessorConfig | ZoeDepthProcessorConfig; -type ImageWithDims = { +export type ImageWithDims = { imageName: string; width: number; height: number; @@ -273,7 +273,7 @@ export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { type: 'zoe_depth_image_processor', }), }, -} +}; export const zProcessorType = z.enum([ 'canny_image_processor', 'color_map_image_processor', @@ -328,15 +328,15 @@ export const initialIPAdapter: Omit = { }; export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfig => { - return merge(deepClone(initialControlNet), { id, overrides }); + return merge(deepClone(initialControlNet), { id, ...overrides }); }; export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfig => { - return merge(deepClone(initialT2IAdapter), { id, overrides }); + return merge(deepClone(initialT2IAdapter), { id, ...overrides }); }; export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfig => { - return merge(deepClone(initialIPAdapter), { id, overrides }); + return merge(deepClone(initialIPAdapter), { id, ...overrides }); }; export const buildControlAdapterProcessor = ( diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index cbe1410a95..c79278b03d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -52,8 +52,7 @@ const STAGE_BG_DATAURL = const mapId = (object: { id: string }) => object.id; -const selectRenderableLayers = (n: Konva.Node) => - n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME; +const selectRenderableLayers = (n: Konva.Node) => n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME; const selectVectorMaskObjects = (node: Konva.Node) => { return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; @@ -432,9 +431,9 @@ const updateControlNetLayerImageSource = async ( konvaLayer: Konva.Layer, reduxLayer: ControlAdapterLayer ) => { - if (reduxLayer.imageName) { - const imageName = reduxLayer.imageName; - const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(reduxLayer.imageName)); + if (reduxLayer.controlAdapter.image) { + const { imageName } = reduxLayer.controlAdapter.image; + const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); const imageDTO = await req.unwrap(); req.unsubscribe(); const image = new Image(); @@ -442,8 +441,7 @@ const updateControlNetLayerImageSource = async ( image.onload = () => { // Find the existing image or create a new one - must find using the name, bc the id may have just changed const konvaImage = - konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`) ?? - createControlNetLayerImage(konvaLayer, image); + konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`) ?? createControlNetLayerImage(konvaLayer, image); // Update the image's attributes konvaImage.setAttrs({ @@ -502,11 +500,11 @@ const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay let imageSourceNeedsUpdate = false; if (canvasImageSource instanceof HTMLImageElement) { if ( - reduxLayer.imageName && - canvasImageSource.id !== getCALayerImageId(reduxLayer.id, reduxLayer.imageName) + reduxLayer.controlAdapter.image && + canvasImageSource.id !== getCALayerImageId(reduxLayer.id, reduxLayer.controlAdapter.image.imageName) ) { imageSourceNeedsUpdate = true; - } else if (!reduxLayer.imageName) { + } else if (!reduxLayer.controlAdapter.image) { imageSourceNeedsUpdate = true; } } else if (!canvasImageSource) { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx index d072cfde0f..eef997a11b 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ControlSettingsAccordion/ControlSettingsAccordion.tsx @@ -13,66 +13,52 @@ import { selectValidIPAdapters, selectValidT2IAdapters, } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { selectAllControlAdapterIds, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { Fragment, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; -const selector = createMemoizedSelector( - [selectControlAdaptersSlice, selectControlLayersSlice], - (controlAdapters, controlLayers) => { - const badges: string[] = []; - let isError = false; +const selector = createMemoizedSelector([selectControlAdaptersSlice], (controlAdapters) => { + const badges: string[] = []; + let isError = false; - const controlLayersAdapterIds = selectAllControlAdapterIds(controlLayers.present); + const enabledNonRegionalIPAdapterCount = selectAllIPAdapters(controlAdapters).filter((ca) => ca.isEnabled).length; - const enabledNonRegionalIPAdapterCount = selectAllIPAdapters(controlAdapters) - .filter((ca) => !controlLayersAdapterIds.includes(ca.id)) - .filter((ca) => ca.isEnabled).length; - - const validIPAdapterCount = selectValidIPAdapters(controlAdapters).length; - if (enabledNonRegionalIPAdapterCount > 0) { - badges.push(`${enabledNonRegionalIPAdapterCount} IP`); - } - if (enabledNonRegionalIPAdapterCount > validIPAdapterCount) { - isError = true; - } - - const enabledControlNetCount = selectAllControlNets(controlAdapters) - .filter((ca) => !controlLayersAdapterIds.includes(ca.id)) - .filter((ca) => ca.isEnabled).length; - const validControlNetCount = selectValidControlNets(controlAdapters).length; - if (enabledControlNetCount > 0) { - badges.push(`${enabledControlNetCount} ControlNet`); - } - if (enabledControlNetCount > validControlNetCount) { - isError = true; - } - - const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters) - .filter((ca) => !controlLayersAdapterIds.includes(ca.id)) - .filter((ca) => ca.isEnabled).length; - const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length; - if (enabledT2IAdapterCount > 0) { - badges.push(`${enabledT2IAdapterCount} T2I`); - } - if (enabledT2IAdapterCount > validT2IAdapterCount) { - isError = true; - } - - const controlAdapterIds = selectControlAdapterIds(controlAdapters).filter( - (id) => !controlLayersAdapterIds.includes(id) - ); - - return { - controlAdapterIds, - badges, - isError, // TODO: Add some visual indicator that the control adapters are in an error state - }; + const validIPAdapterCount = selectValidIPAdapters(controlAdapters).length; + if (enabledNonRegionalIPAdapterCount > 0) { + badges.push(`${enabledNonRegionalIPAdapterCount} IP`); } -); + if (enabledNonRegionalIPAdapterCount > validIPAdapterCount) { + isError = true; + } + + const enabledControlNetCount = selectAllControlNets(controlAdapters).filter((ca) => ca.isEnabled).length; + const validControlNetCount = selectValidControlNets(controlAdapters).length; + if (enabledControlNetCount > 0) { + badges.push(`${enabledControlNetCount} ControlNet`); + } + if (enabledControlNetCount > validControlNetCount) { + isError = true; + } + + const enabledT2IAdapterCount = selectAllT2IAdapters(controlAdapters).filter((ca) => ca.isEnabled).length; + const validT2IAdapterCount = selectValidT2IAdapters(controlAdapters).length; + if (enabledT2IAdapterCount > 0) { + badges.push(`${enabledT2IAdapterCount} T2I`); + } + if (enabledT2IAdapterCount > validT2IAdapterCount) { + isError = true; + } + + const controlAdapterIds = selectControlAdapterIds(controlAdapters); + + return { + controlAdapterIds, + badges, + isError, // TODO: Add some visual indicator that the control adapters are in an error state + }; +}); export const ControlSettingsAccordion: React.FC = memo(() => { const { t } = useTranslation(); From 905baf278720201faa2188d23e79c350c90f1db3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 1 May 2024 21:36:51 +1000 Subject: [PATCH 19/74] refactor(ui): continue wiring up CA logic across (wip) It works! --- invokeai/frontend/web/public/locales/en.json | 1 + .../listeners/imageDropped.ts | 60 +++ .../listeners/imageUploaded.ts | 38 ++ .../src/common/hooks/useIsReadyToEnqueue.ts | 147 +++--- .../components/CALayer/CALayer.tsx | 4 +- .../components/CALayer/CALayerConfig.tsx | 149 ------ .../CALayer/CALayerControlAdapterWrapper.tsx | 121 +++++ .../CALayer/CALayerImagePreview.tsx | 231 --------- .../ControlAndIPAdapter/ControlAdapter.tsx | 111 +++++ .../ControlAdapterBeginEndStepPct.tsx | 0 .../ControlAdapterControlModeSelect.tsx | 0 .../ControlAdapterImagePreview.tsx | 234 +++++++++ .../ControlAdapterModelCombobox.tsx | 0 .../ControlAdapterProcessorConfig.tsx} | 4 +- .../ControlAdapterProcessorTypeSelect.tsx} | 6 +- .../ControlAdapterWeight.tsx | 0 .../ControlAndIPAdapter/IPAdapter.tsx | 72 +++ .../IPAdapterImagePreview.tsx | 114 +++++ .../IPAdapterMethod.tsx | 0 .../IPAdapterModelSelect.tsx} | 4 +- .../processors/CannyProcessor.tsx | 2 +- .../processors/ColorMapProcessor.tsx | 2 +- .../processors/ContentShuffleProcessor.tsx | 2 +- .../processors/DWOpenposeProcessor.tsx | 2 +- .../processors/DepthAnythingProcessor.tsx | 2 +- .../processors/HedProcessor.tsx | 2 +- .../processors/LineartProcessor.tsx | 2 +- .../processors/MediapipeFaceProcessor.tsx | 2 +- .../processors/MidasDepthProcessor.tsx | 2 +- .../processors/MlsdImageProcessor.tsx | 2 +- .../processors/PidiProcessor.tsx | 2 +- .../processors/ProcessorWrapper.tsx | 0 .../processors/types.ts | 0 .../components/IPALayer/IPALayer.tsx | 4 +- .../components/IPALayer/IPALayerConfig.tsx | 105 ---- .../IPALayer/IPALayerIPAdapterWrapper.tsx | 106 ++++ .../IPALayer/IPAdapterImagePreview.tsx | 119 ----- .../RGLayer/RGLayerIPAdapterList.tsx | 49 +- .../RGLayer/RGLayerIPAdapterWrapper.tsx | 131 +++++ .../hooks/useControlLayersTitle.ts | 3 - .../controlLayers/store/controlLayersSlice.ts | 34 +- .../src/features/controlLayers/store/types.ts | 1 - .../controlLayers/util/controlAdapters.ts | 15 +- .../web/src/features/dnd/types/index.ts | 23 +- .../web/src/features/dnd/util/isValidDrop.ts | 6 + .../util/graph/addControlLayersToGraph.ts | 458 +++++++++++++++--- .../util/graph/addControlNetToLinearGraph.ts | 42 +- .../util/graph/addIPAdapterToLinearGraph.ts | 49 +- .../util/graph/addT2IAdapterToLinearGraph.ts | 43 +- .../graph/buildLinearSDXLTextToImageGraph.ts | 11 - .../util/graph/buildLinearTextToImageGraph.ts | 11 - .../frontend/web/src/services/api/types.ts | 19 +- 52 files changed, 1596 insertions(+), 951 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/ControlAdapterBeginEndStepPct.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/ControlAdapterControlModeSelect.tsx (100%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/ControlAdapterModelCombobox.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer/CALayerProcessor.tsx => ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx} (94%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer/CALayerProcessorCombobox.tsx => ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx} (91%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/ControlAdapterWeight.tsx (100%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx rename invokeai/frontend/web/src/features/controlLayers/components/{IPALayer => ControlAndIPAdapter}/IPAdapterMethod.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{IPALayer/IPALayerModelCombobox.tsx => ControlAndIPAdapter/IPAdapterModelSelect.tsx} (97%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/CannyProcessor.tsx (97%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/ColorMapProcessor.tsx (96%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/ContentShuffleProcessor.tsx (97%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/DWOpenposeProcessor.tsx (97%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/DepthAnythingProcessor.tsx (97%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/HedProcessor.tsx (95%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/LineartProcessor.tsx (95%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/MediapipeFaceProcessor.tsx (97%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/MidasDepthProcessor.tsx (97%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/MlsdImageProcessor.tsx (97%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/PidiProcessor.tsx (96%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/ProcessorWrapper.tsx (100%) rename invokeai/frontend/web/src/features/controlLayers/components/{CALayer => ControlAndIPAdapter}/processors/types.ts (100%) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 885a937de3..fd6eef527b 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -917,6 +917,7 @@ "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} missing input", "missingNodeTemplate": "Missing node template", "noControlImageForControlAdapter": "Control Adapter #{{number}} has no control image", + "imageNotProcessedForControlAdapter": "Control Adapter #{{number}}'s image is not processed", "noInitialImageSelected": "No initial image selected", "noModelForControlAdapter": "Control Adapter #{{number}} has no model selected.", "incompatibleBaseModelForControlAdapter": "Control Adapter #{{number}} model is incompatible with main model.", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 307e3487dd..de2ac3a39a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -7,6 +7,11 @@ import { controlAdapterImageChanged, controlAdapterIsEnabledChanged, } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { + caLayerImageChanged, + ipaLayerImageChanged, + rgLayerIPAdapterImageChanged, +} from 'features/controlLayers/store/controlLayersSlice'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { imageSelected } from 'features/gallery/store/gallerySlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; @@ -83,6 +88,61 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } + /** + * Image dropped on Control Adapter Layer + */ + if ( + overData.actionType === 'SET_CA_LAYER_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + const { layerId } = overData.context; + dispatch( + caLayerImageChanged({ + layerId, + imageDTO: activeData.payload.imageDTO, + }) + ); + return; + } + + /** + * Image dropped on IP Adapter Layer + */ + if ( + overData.actionType === 'SET_IPA_LAYER_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + const { layerId } = overData.context; + dispatch( + ipaLayerImageChanged({ + layerId, + imageDTO: activeData.payload.imageDTO, + }) + ); + return; + } + + /** + * Image dropped on RG Layer IP Adapter + */ + if ( + overData.actionType === 'SET_RG_LAYER_IP_ADAPTER_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + const { layerId, ipAdapterId } = overData.context; + dispatch( + rgLayerIPAdapterImageChanged({ + layerId, + ipAdapterId, + imageDTO: activeData.payload.imageDTO, + }) + ); + return; + } + /** * Image dropped on Canvas */ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index a2ca4baeb1..fd568ef1bd 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -6,6 +6,11 @@ import { controlAdapterImageChanged, controlAdapterIsEnabledChanged, } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { + caLayerImageChanged, + ipaLayerImageChanged, + rgLayerIPAdapterImageChanged, +} from 'features/controlLayers/store/controlLayersSlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { addToast } from 'features/system/store/systemSlice'; @@ -108,6 +113,39 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis return; } + if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') { + const { layerId } = postUploadAction; + dispatch(caLayerImageChanged({ layerId, imageDTO })); + dispatch( + addToast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), + }) + ); + } + + if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') { + const { layerId } = postUploadAction; + dispatch(ipaLayerImageChanged({ layerId, imageDTO })); + dispatch( + addToast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), + }) + ); + } + + if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') { + const { layerId, ipAdapterId } = postUploadAction; + dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); + dispatch( + addToast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), + }) + ); + } + if (postUploadAction?.type === 'SET_INITIAL_IMAGE') { dispatch(initialImageChanged(imageDTO)); dispatch( diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index b5650209a4..6073564305 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -16,6 +16,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import i18n from 'i18next'; import { forEach } from 'lodash-es'; import { getConnectedEdges } from 'reactflow'; +import { assert } from 'tsafe'; const selector = createMemoizedSelector( [ @@ -97,73 +98,93 @@ const selector = createMemoizedSelector( reasons.push(i18n.t('parameters.invoke.noModelSelected')); } - let enabledControlAdapters = selectControlAdapterAll(controlAdapters).filter((ca) => ca.isEnabled); - if (activeTabName === 'txt2img') { - // Special handling for control layers on txt2img - const enabledControlLayersAdapterIds = [] - // const enabledControlLayersAdapterIds = controlLayers.present.layers - // .filter((l) => l.isEnabled) - // .flatMap((layer) => { - // if (layer.type === 'regional_guidance_layer') { - // return layer.ipAdapterIds; - // } - // if (layer.type === 'control_adapter_layer') { - // return [layer.controlNetId]; - // } - // if (layer.type === 'ip_adapter_layer') { - // return [layer.ipAdapterId]; - // } - // }); + // Handling for Control Layers - only exists on txt2img tab now + controlLayers.present.layers + .filter((l) => l.isEnabled) + .flatMap((l) => { + if (l.type === 'control_adapter_layer') { + return l.controlAdapter; + } else if (l.type === 'ip_adapter_layer') { + return l.ipAdapter; + } else if (l.type === 'regional_guidance_layer') { + return l.ipAdapters; + } + assert(false); + }) + .forEach((ca, i) => { + const hasNoModel = !ca.model; + const mismatchedModelBase = ca.model?.base !== model?.base; + const hasNoImage = !ca.image; + const imageNotProcessed = + (ca.type === 'controlnet' || ca.type === 't2i_adapter') && !ca.processedImage && ca.processorConfig; - enabledControlAdapters = enabledControlAdapters.filter((ca) => enabledControlLayersAdapterIds.includes(ca.id)); + if (hasNoModel) { + reasons.push( + i18n.t('parameters.invoke.noModelForControlAdapter', { + number: i + 1, + }) + ); + } + if (mismatchedModelBase) { + // This should never happen, just a sanity check + reasons.push( + i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { + number: i + 1, + }) + ); + } + if (hasNoImage) { + reasons.push( + i18n.t('parameters.invoke.noControlImageForControlAdapter', { + number: i + 1, + }) + ); + } + if (imageNotProcessed) { + reasons.push( + i18n.t('parameters.invoke.imageNotProcessedForControlAdapter', { + number: i + 1, + }) + ); + } + }); } else { - const allControlLayerAdapterIds = [] - // const allControlLayerAdapterIds = controlLayers.present.layers.flatMap((layer) => { - // if (layer.type === 'regional_guidance_layer') { - // return layer.ipAdapterIds; - // } - // if (layer.type === 'control_adapter_layer') { - // return [layer.controlNetId]; - // } - // if (layer.type === 'ip_adapter_layer') { - // return [layer.ipAdapterId]; - // } - // }); - enabledControlAdapters = enabledControlAdapters.filter((ca) => !allControlLayerAdapterIds.includes(ca.id)); + // Handling for all other tabs + selectControlAdapterAll(controlAdapters) + .filter((ca) => ca.isEnabled) + .forEach((ca, i) => { + if (!ca.isEnabled) { + return; + } + + if (!ca.model) { + reasons.push( + 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( + i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { + number: i + 1, + }) + ); + } + + if ( + !ca.controlImage || + (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none') + ) { + reasons.push( + i18n.t('parameters.invoke.noControlImageForControlAdapter', { + number: i + 1, + }) + ); + } + }); } - - enabledControlAdapters.forEach((ca, i) => { - if (!ca.isEnabled) { - return; - } - - if (!ca.model) { - reasons.push( - 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( - i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { - number: i + 1, - }) - ); - } - - if ( - !ca.controlImage || - (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none') - ) { - reasons.push( - i18n.t('parameters.invoke.noControlImageForControlAdapter', { - number: i + 1, - }) - ); - } - }); } return { isReady: !reasons.length, reasons }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index 864e48c1d2..f9edf42c2f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -1,6 +1,6 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CALayerConfig } from 'features/controlLayers/components/CALayer/CALayerConfig'; +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'; @@ -43,7 +43,7 @@ export const CALayer = memo(({ layerId }: Props) => { {isOpen && ( - + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx deleted file mode 100644 index c998c30f14..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerConfig.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { ControlAdapterModelCombobox } from 'features/controlLayers/components/CALayer/ControlAdapterModelCombobox'; -import { - caLayerControlModeChanged, - caLayerImageChanged, - caLayerModelChanged, - caLayerProcessorConfigChanged, - caOrIPALayerBeginEndStepPctChanged, - caOrIPALayerWeightChanged, - selectCALayer, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { ControlMode, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiCaretUpBold } from 'react-icons/pi'; -import { useToggle } from 'react-use'; -import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; - -import { CALayerImagePreview } from './CALayerImagePreview'; -import { CALayerProcessor } from './CALayerProcessor'; -import { CALayerProcessorCombobox } from './CALayerProcessorCombobox'; -import { ControlAdapterBeginEndStepPct } from './ControlAdapterBeginEndStepPct'; -import { ControlAdapterControlModeSelect } from './ControlAdapterControlModeSelect'; -import { ControlAdapterWeight } from './ControlAdapterWeight'; - -type Props = { - layerId: string; -}; - -export const CALayerConfig = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const controlAdapter = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter); - const { t } = useTranslation(); - const [isExpanded, toggleIsExpanded] = useToggle(false); - - const onChangeBeginEndStepPct = useCallback( - (beginEndStepPct: [number, number]) => { - dispatch( - caOrIPALayerBeginEndStepPctChanged({ - layerId, - beginEndStepPct, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeControlMode = useCallback( - (controlMode: ControlMode) => { - dispatch( - caLayerControlModeChanged({ - layerId, - controlMode, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeWeight = useCallback( - (weight: number) => { - dispatch(caOrIPALayerWeightChanged({ layerId, weight })); - }, - [dispatch, layerId] - ); - - const onChangeProcessorConfig = useCallback( - (processorConfig: ProcessorConfig | null) => { - dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); - }, - [dispatch, layerId] - ); - - const onChangeModel = useCallback( - (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { - dispatch( - caLayerModelChanged({ - layerId, - modelConfig, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeImage = useCallback( - (imageDTO: ImageDTO | null) => { - dispatch(caLayerImageChanged({ layerId, imageDTO })); - }, - [dispatch, layerId] - ); - - return ( - - - - - - - - } - /> - - - - {controlAdapter.type === 'controlnet' && ( - - )} - - - - - - - - {isExpanded && ( - <> - - - - )} - - ); -}); - -CALayerConfig.displayName = 'CALayerConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx new file mode 100644 index 0000000000..2a2edeb8d8 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx @@ -0,0 +1,121 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { ControlAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapter'; +import { + caLayerControlModeChanged, + caLayerImageChanged, + caLayerModelChanged, + caLayerProcessorConfigChanged, + caOrIPALayerBeginEndStepPctChanged, + caOrIPALayerWeightChanged, + selectCALayer, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlMode, 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) => selectCALayer(s.controlLayers.present, layerId).controlAdapter); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch( + caOrIPALayerBeginEndStepPctChanged({ + layerId, + beginEndStepPct, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeControlMode = useCallback( + (controlMode: ControlMode) => { + dispatch( + caLayerControlModeChanged({ + layerId, + controlMode, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(caOrIPALayerWeightChanged({ layerId, weight })); + }, + [dispatch, layerId] + ); + + const onChangeProcessorConfig = useCallback( + (processorConfig: ProcessorConfig | null) => { + dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); + }, + [dispatch, layerId] + ); + + const onChangeModel = useCallback( + (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { + dispatch( + caLayerModelChanged({ + layerId, + modelConfig, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(caLayerImageChanged({ layerId, imageDTO })); + }, + [dispatch, layerId] + ); + + 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/CALayer/CALayerImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx deleted file mode 100644 index c20b408730..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerImagePreview.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { Box, Flex, Spinner, useShiftModifier } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -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 { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; -import type { ControlLayerDropData, ImageDraggableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi'; -import { - useAddImageToBoardMutation, - useChangeImageIsIntermediateMutation, - useGetImageDTOQuery, - useRemoveImageFromBoardMutation, -} from 'services/api/endpoints/images'; -import type { ControlLayerAction, ImageDTO } from 'services/api/types'; - -type Props = { - image: ImageWithDims | null; - processedImage: ImageWithDims | null; - onChangeImage: (imageDTO: ImageDTO | null) => void; - hasProcessor: boolean; - layerId: string; // required for the dnd/upload interactions -}; - -const selectPendingControlImages = createMemoizedSelector( - selectControlAdaptersSlice, - (controlAdapters) => controlAdapters.pendingControlImages -); - -export const CALayerImagePreview = memo(({ image, processedImage, onChangeImage, hasProcessor, layerId }: Props) => { - const { t } = useTranslation(); - 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 pendingControlImages = useAppSelector(selectPendingControlImages); - const shift = useShiftModifier(); - - const [isMouseOverImage, setIsMouseOverImage] = useState(false); - - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - image?.imageName ?? skipToken - ); - const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - processedImage?.imageName ?? skipToken - ); - - const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); - const [addToBoard] = useAddImageToBoardMutation(); - const [removeFromBoard] = useRemoveImageFromBoardMutation(); - const handleResetControlImage = useCallback(() => { - onChangeImage(null); - }, [onChangeImage]); - - const handleSaveControlImage = useCallback(async () => { - if (!processedControlImage) { - return; - } - - await changeIsIntermediate({ - imageDTO: processedControlImage, - is_intermediate: false, - }).unwrap(); - - if (autoAddBoardId !== 'none') { - addToBoard({ - imageDTO: processedControlImage, - board_id: autoAddBoardId, - }); - } else { - removeFromBoard({ imageDTO: processedControlImage }); - } - }, [processedControlImage, changeIsIntermediate, autoAddBoardId, addToBoard, removeFromBoard]); - - const handleSetControlImageToDimensions = useCallback(() => { - if (!controlImage) { - return; - } - - if (activeTabName === 'unifiedCanvas') { - dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)); - } else { - if (shift) { - const { width, height } = controlImage; - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); - } else { - const { width, height } = calculateNewSize( - controlImage.width / controlImage.height, - optimalDimension * optimalDimension - ); - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); - } - } - }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); - - const handleMouseEnter = useCallback(() => { - setIsMouseOverImage(true); - }, []); - - const handleMouseLeave = useCallback(() => { - setIsMouseOverImage(false); - }, []); - - const draggableData = useMemo(() => { - if (controlImage) { - return { - id: layerId, - payloadType: 'IMAGE_DTO', - payload: { imageDTO: controlImage }, - }; - } - }, [controlImage, layerId]); - - const droppableData = useMemo( - () => ({ - id: layerId, - actionType: 'SET_CONTROL_LAYER_IMAGE', - context: { layerId }, - }), - [layerId] - ); - - const postUploadAction = useMemo(() => ({ type: 'SET_CONTROL_LAYER_IMAGE', layerId }), [layerId]); - - const shouldShowProcessedImage = - controlImage && - processedControlImage && - !isMouseOverImage && - !pendingControlImages.includes(layerId) && - hasProcessor; - - useEffect(() => { - if (isConnected && (isErrorControlImage || isErrorProcessedControlImage)) { - handleResetControlImage(); - } - }, [handleResetControlImage, isConnected, isErrorControlImage, isErrorProcessedControlImage]); - - return ( - - - - - - - - <> - : undefined} - tooltip={t('controlnet.resetControlImage')} - /> - : undefined} - tooltip={t('controlnet.saveControlImage')} - styleOverrides={saveControlImageStyleOverrides} - /> - : undefined} - tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} - styleOverrides={setControlImageDimensionsStyleOverrides} - /> - - - {pendingControlImages.includes(layerId) && ( - - - - )} - - ); -}); - -CALayerImagePreview.displayName = 'CALayerImagePreview'; - -const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 }; -const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx new file mode 100644 index 0000000000..972198cc7e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx @@ -0,0 +1,111 @@ +import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; +import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox'; +import type { + ControlMode, + ControlNetConfig, + ProcessorConfig, + T2IAdapterConfig, +} 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: ControlNetConfig | T2IAdapterConfig; + onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; + onChangeControlMode: (controlMode: ControlMode) => void; + onChangeWeight: (weight: number) => void; + onChangeProcessorConfig: (processorConfig: ProcessorConfig | null) => void; + onChangeModel: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; + onChangeImage: (imageDTO: ImageDTO | null) => void; + droppableData: TypesafeDroppableData; + postUploadAction: PostUploadAction; +}; + +export const ControlAdapter = memo( + ({ + controlAdapter, + onChangeBeginEndStepPct, + onChangeControlMode, + onChangeWeight, + onChangeProcessorConfig, + onChangeModel, + onChangeImage, + 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/CALayer/ControlAdapterBeginEndStepPct.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterBeginEndStepPct.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterControlModeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterControlModeSelect.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx new file mode 100644 index 0000000000..e4f53c1c70 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx @@ -0,0 +1,234 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Box, Flex, Spinner, useShiftModifier } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +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 { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; +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'; +import { + useAddImageToBoardMutation, + useChangeImageIsIntermediateMutation, + useGetImageDTOQuery, + useRemoveImageFromBoardMutation, +} from 'services/api/endpoints/images'; +import type { ImageDTO, PostUploadAction } from 'services/api/types'; + +type Props = { + controlAdapterId: string; + image: ImageWithDims | null; + processedImage: ImageWithDims | null; + onChangeImage: (imageDTO: ImageDTO | null) => void; + hasProcessor: boolean; + droppableData: TypesafeDroppableData; + postUploadAction: PostUploadAction; +}; + +const selectPendingControlImages = createMemoizedSelector( + selectControlAdaptersSlice, + (controlAdapters) => controlAdapters.pendingControlImages +); + +export const ControlAdapterImagePreview = memo( + ({ + image, + processedImage, + onChangeImage, + hasProcessor, + controlAdapterId, + droppableData, + postUploadAction, + }: Props) => { + const { t } = useTranslation(); + 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 pendingControlImages = useAppSelector(selectPendingControlImages); + const shift = useShiftModifier(); + + const [isMouseOverImage, setIsMouseOverImage] = useState(false); + + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( + image?.imageName ?? skipToken + ); + const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( + processedImage?.imageName ?? skipToken + ); + + const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); + const [addToBoard] = useAddImageToBoardMutation(); + const [removeFromBoard] = useRemoveImageFromBoardMutation(); + const handleResetControlImage = useCallback(() => { + onChangeImage(null); + }, [onChangeImage]); + + const handleSaveControlImage = useCallback(async () => { + if (!processedControlImage) { + return; + } + + await changeIsIntermediate({ + imageDTO: processedControlImage, + is_intermediate: false, + }).unwrap(); + + if (autoAddBoardId !== 'none') { + addToBoard({ + imageDTO: processedControlImage, + board_id: autoAddBoardId, + }); + } else { + removeFromBoard({ imageDTO: processedControlImage }); + } + }, [processedControlImage, changeIsIntermediate, autoAddBoardId, addToBoard, removeFromBoard]); + + const handleSetControlImageToDimensions = useCallback(() => { + if (!controlImage) { + return; + } + + if (activeTabName === 'unifiedCanvas') { + dispatch( + setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) + ); + } else { + if (shift) { + const { width, height } = controlImage; + dispatch(widthChanged({ width, updateAspectRatio: true })); + dispatch(heightChanged({ height, updateAspectRatio: true })); + } else { + const { width, height } = calculateNewSize( + controlImage.width / controlImage.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, updateAspectRatio: true })); + dispatch(heightChanged({ height, updateAspectRatio: true })); + } + } + }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); + + const handleMouseEnter = useCallback(() => { + setIsMouseOverImage(true); + }, []); + + const handleMouseLeave = useCallback(() => { + setIsMouseOverImage(false); + }, []); + + const draggableData = useMemo(() => { + if (controlImage) { + return { + id: controlAdapterId, + payloadType: 'IMAGE_DTO', + payload: { imageDTO: controlImage }, + }; + } + }, [controlImage, controlAdapterId]); + + const shouldShowProcessedImage = + controlImage && + processedControlImage && + !isMouseOverImage && + !pendingControlImages.includes(controlAdapterId) && + hasProcessor; + + useEffect(() => { + if (isConnected && (isErrorControlImage || isErrorProcessedControlImage)) { + handleResetControlImage(); + } + }, [handleResetControlImage, isConnected, isErrorControlImage, isErrorProcessedControlImage]); + + return ( + + + + + + + + <> + : undefined} + tooltip={t('controlnet.resetControlImage')} + /> + : undefined} + tooltip={t('controlnet.saveControlImage')} + styleOverrides={saveControlImageStyleOverrides} + /> + : undefined} + tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} + styleOverrides={setControlImageDimensionsStyleOverrides} + /> + + + {pendingControlImages.includes(controlAdapterId) && ( + + + + )} + + ); + } +); + +ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview'; + +const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 }; +const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterModelCombobox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx similarity index 94% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx index b5ae89f53a..034dc5454e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx @@ -18,7 +18,7 @@ type Props = { onChange: (config: ProcessorConfig | null) => void; }; -export const CALayerProcessor = memo(({ config, onChange }: Props) => { +export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) => { if (!config) { return null; } @@ -82,4 +82,4 @@ export const CALayerProcessor = memo(({ config, onChange }: Props) => { } }); -CALayerProcessor.displayName = 'CALayerProcessor'; +ControlAdapterProcessorConfig.displayName = 'ControlAdapterProcessorConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx similarity index 91% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx index a01487af44..5f34946af5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerProcessorCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx @@ -22,7 +22,7 @@ const selectDisabledProcessors = createMemoizedSelector( (config) => config.sd.disabledControlNetProcessors ); -export const CALayerProcessorCombobox = memo(({ config, onChange }: Props) => { +export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Props) => { const { t } = useTranslation(); const disabledProcessors = useAppSelector(selectDisabledProcessors); const options = useMemo(() => { @@ -53,7 +53,7 @@ export const CALayerProcessorCombobox = memo(({ config, onChange }: Props) => { {t('controlnet.processor')} - + { ); }); -CALayerProcessorCombobox.displayName = 'CALayerProcessorCombobox'; +ControlAdapterProcessorTypeSelect.displayName = 'ControlAdapterProcessorTypeSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterWeight.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/ControlAdapterWeight.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx new file mode 100644 index 0000000000..a0aa7d79a1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx @@ -0,0 +1,72 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import { ControlAdapterBeginEndStepPct } 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'; +import { IPAdapterModelSelect } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect'; +import type { CLIPVisionModel, IPAdapterConfig, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { TypesafeDroppableData } from 'features/dnd/types'; +import { memo } from 'react'; +import type { ImageDTO, IPAdapterModelConfig, PostUploadAction } from 'services/api/types'; + +type Props = { + ipAdapter: IPAdapterConfig; + onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; + onChangeWeight: (weight: number) => void; + onChangeIPMethod: (method: IPMethod) => void; + onChangeModel: (modelConfig: IPAdapterModelConfig) => void; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void; + onChangeImage: (imageDTO: ImageDTO | null) => void; + droppableData: TypesafeDroppableData; + postUploadAction: PostUploadAction; +}; + +export const IPAdapter = memo( + ({ + ipAdapter, + onChangeBeginEndStepPct, + onChangeWeight, + onChangeIPMethod, + onChangeModel, + onChangeCLIPVisionModel, + onChangeImage, + droppableData, + postUploadAction, + }: Props) => { + return ( + + + + + + + + + + + + + + + + + + ); + } +); + +IPAdapter.displayName = 'IPAdapter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx new file mode 100644 index 0000000000..7de726cda5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx @@ -0,0 +1,114 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIDndImage from 'common/components/IAIDndImage'; +import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import type { 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 } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import type { ImageDTO, PostUploadAction } from 'services/api/types'; + +type Props = { + image: ImageWithDims | null; + onChangeImage: (imageDTO: ImageDTO | null) => void; + ipAdapterId: string; // required for the dnd/upload interactions + droppableData: TypesafeDroppableData; + postUploadAction: PostUploadAction; +}; + +export const IPAdapterImagePreview = memo( + ({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isConnected = useAppSelector((s) => s.system.isConnected); + const activeTabName = useAppSelector(activeTabNameSelector); + const optimalDimension = useAppSelector(selectOptimalDimension); + const shift = useShiftModifier(); + + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( + image?.imageName ?? skipToken + ); + const handleResetControlImage = useCallback(() => { + onChangeImage(null); + }, [onChangeImage]); + + const handleSetControlImageToDimensions = useCallback(() => { + if (!controlImage) { + return; + } + + if (activeTabName === 'unifiedCanvas') { + dispatch( + setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) + ); + } else { + if (shift) { + const { width, height } = controlImage; + dispatch(widthChanged({ width, updateAspectRatio: true })); + dispatch(heightChanged({ height, updateAspectRatio: true })); + } else { + const { width, height } = calculateNewSize( + controlImage.width / controlImage.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, updateAspectRatio: true })); + dispatch(heightChanged({ height, updateAspectRatio: true })); + } + } + }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); + + const draggableData = useMemo(() => { + if (controlImage) { + return { + id: ipAdapterId, + payloadType: 'IMAGE_DTO', + payload: { imageDTO: controlImage }, + }; + } + }, [controlImage, ipAdapterId]); + + useEffect(() => { + if (isConnected && isErrorControlImage) { + handleResetControlImage(); + } + }, [handleResetControlImage, isConnected, isErrorControlImage]); + + return ( + + + + <> + : undefined} + tooltip={t('controlnet.resetControlImage')} + /> + : undefined} + tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} + styleOverrides={setControlImageDimensionsStyleOverrides} + /> + + + ); + } +); + +IPAdapterImagePreview.displayName = 'IPAdapterImagePreview'; + +const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 6 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterMethod.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx index facd46aed1..e47bcd5182 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx @@ -22,7 +22,7 @@ type Props = { onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void; }; -export const IPAdapterModelCombobox = memo( +export const IPAdapterModelSelect = memo( ({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => { const { t } = useTranslation(); const currentBaseModel = useAppSelector((s) => s.generation.model?.base); @@ -97,4 +97,4 @@ export const IPAdapterModelCombobox = memo( } ); -IPAdapterModelCombobox.displayName = 'IPALayerModelCombobox'; +IPAdapterModelSelect.displayName = 'IPAdapterModelSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/CannyProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx index 5ae1e2cc0e..c4d6031912 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx @@ -1,5 +1,5 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import { type CannyProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ColorMapProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx index e867ecfe12..90c88a071b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ColorMapProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx @@ -1,5 +1,5 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import { type ColorMapProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ContentShuffleProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx index 19c75045b4..9e27d7052a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ContentShuffleProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx @@ -1,5 +1,5 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { ContentShuffleProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DWOpenposeProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx index 4d6776a913..5f21b4b8f6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DWOpenposeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx @@ -1,5 +1,5 @@ import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { DWOpenposeProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; import type { ChangeEvent } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DepthAnythingProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx index 90c8b32e69..b56c331741 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/DepthAnythingProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx @@ -1,6 +1,6 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { CONTROLNET_PROCESSORS, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/HedProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx index 3708287450..83cd015fe4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/HedProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx @@ -1,5 +1,5 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { HedProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/LineartProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx index ef18e9d61f..d882543af4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/LineartProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx @@ -1,5 +1,5 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { LineartProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MediapipeFaceProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx index e3d67f91bb..a3c2936916 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MediapipeFaceProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx @@ -1,5 +1,5 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import { CONTROLNET_PROCESSORS, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MidasDepthProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx index 36f008d6be..f12619caac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MidasDepthProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx @@ -1,5 +1,5 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { MidasDepthProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MlsdImageProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx index 69dc1ce4d9..a0e02ef17a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/MlsdImageProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx @@ -1,5 +1,5 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { MlsdProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx similarity index 96% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/PidiProcessor.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx index e4c894ef45..4885d16e6f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/PidiProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx @@ -1,5 +1,5 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { ProcessorComponentProps } from 'features/controlLayers/components/CALayer/processors/types'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { PidiProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import type { ChangeEvent } from 'react'; import { useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/ProcessorWrapper.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/types.ts similarity index 100% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayer/processors/types.ts rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/types.ts 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 71b06e6830..715e538679 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -1,5 +1,5 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { IPALayerConfig } from 'features/controlLayers/components/IPALayer/IPALayerConfig'; +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 { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; @@ -22,7 +22,7 @@ export const IPALayer = memo(({ layerId }: Props) => { {isOpen && ( - + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx deleted file mode 100644 index f1b035da1c..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerConfig.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { ControlAdapterBeginEndStepPct } from 'features/controlLayers/components/CALayer/ControlAdapterBeginEndStepPct'; -import { ControlAdapterWeight } from 'features/controlLayers/components/CALayer/ControlAdapterWeight'; -import { IPAdapterImagePreview } from 'features/controlLayers/components/IPALayer/IPAdapterImagePreview'; -import { IPAdapterMethod } from 'features/controlLayers/components/IPALayer/IPAdapterMethod'; -import { IPAdapterModelCombobox } from 'features/controlLayers/components/IPALayer/IPALayerModelCombobox'; -import { - caOrIPALayerBeginEndStepPctChanged, - caOrIPALayerWeightChanged, - ipaLayerCLIPVisionModelChanged, - ipaLayerImageChanged, - ipaLayerMethodChanged, - ipaLayerModelChanged, - selectIPALayer, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters'; -import { memo, useCallback } from 'react'; -import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; - -type Props = { - layerId: string; -}; - -export const IPALayerConfig = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const ipAdapter = useAppSelector((s) => selectIPALayer(s.controlLayers.present, layerId).ipAdapter); - - const onChangeBeginEndStepPct = useCallback( - (beginEndStepPct: [number, number]) => { - dispatch( - caOrIPALayerBeginEndStepPctChanged({ - layerId, - beginEndStepPct, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeWeight = useCallback( - (weight: number) => { - dispatch(caOrIPALayerWeightChanged({ layerId, weight })); - }, - [dispatch, layerId] - ); - - const onChangeIPMethod = useCallback( - (method: IPMethod) => { - dispatch(ipaLayerMethodChanged({ layerId, method })); - }, - [dispatch, layerId] - ); - - const onChangeModel = useCallback( - (modelConfig: IPAdapterModelConfig) => { - dispatch(ipaLayerModelChanged({ layerId, modelConfig })); - }, - [dispatch, layerId] - ); - - const onChangeCLIPVisionModel = useCallback( - (clipVisionModel: CLIPVisionModel) => { - dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel })); - }, - [dispatch, layerId] - ); - - const onChangeImage = useCallback( - (imageDTO: ImageDTO | null) => { - dispatch(ipaLayerImageChanged({ layerId, imageDTO })); - }, - [dispatch, layerId] - ); - - return ( - - - - - - - - - - - - - - - - - - ); -}); - -IPALayerConfig.displayName = 'IPALayerConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx new file mode 100644 index 0000000000..dfcfdc7c99 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx @@ -0,0 +1,106 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter'; +import { + caOrIPALayerBeginEndStepPctChanged, + caOrIPALayerWeightChanged, + ipaLayerCLIPVisionModelChanged, + ipaLayerImageChanged, + ipaLayerMethodChanged, + ipaLayerModelChanged, + selectIPALayer, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { IPALayerImageDropData } from 'features/dnd/types'; +import { memo, useCallback, useMemo } from 'react'; +import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; + +type Props = { + layerId: string; +}; + +export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const ipAdapter = useAppSelector((s) => selectIPALayer(s.controlLayers.present, layerId).ipAdapter); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch( + caOrIPALayerBeginEndStepPctChanged({ + layerId, + beginEndStepPct, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(caOrIPALayerWeightChanged({ layerId, weight })); + }, + [dispatch, layerId] + ); + + const onChangeIPMethod = useCallback( + (method: IPMethod) => { + dispatch(ipaLayerMethodChanged({ layerId, method })); + }, + [dispatch, layerId] + ); + + const onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig) => { + dispatch(ipaLayerModelChanged({ layerId, modelConfig })); + }, + [dispatch, layerId] + ); + + const onChangeCLIPVisionModel = useCallback( + (clipVisionModel: CLIPVisionModel) => { + dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel })); + }, + [dispatch, layerId] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(ipaLayerImageChanged({ layerId, imageDTO })); + }, + [dispatch, layerId] + ); + + const droppableData = useMemo( + () => ({ + actionType: 'SET_IPA_LAYER_IMAGE', + context: { + layerId, + }, + id: layerId, + }), + [layerId] + ); + + const postUploadAction = useMemo( + () => ({ + type: 'SET_IPA_LAYER_IMAGE', + layerId, + }), + [layerId] + ); + + return ( + + ); +}); + +IPALayerIPAdapterWrapper.displayName = 'IPALayerIPAdapterWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx deleted file mode 100644 index bff6d29502..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPAdapterImagePreview.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIDndImage from 'common/components/IAIDndImage'; -import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; -import type { ControlLayerDropData, ImageDraggableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import type { ControlLayerAction, ImageDTO } from 'services/api/types'; - -type Props = { - image: ImageWithDims | null; - onChangeImage: (imageDTO: ImageDTO | null) => void; - layerId: string; // required for the dnd/upload interactions -}; - -export const IPAdapterImagePreview = memo(({ image, onChangeImage, layerId }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isConnected = useAppSelector((s) => s.system.isConnected); - const activeTabName = useAppSelector(activeTabNameSelector); - const optimalDimension = useAppSelector(selectOptimalDimension); - const shift = useShiftModifier(); - - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - image?.imageName ?? skipToken - ); - const handleResetControlImage = useCallback(() => { - onChangeImage(null); - }, [onChangeImage]); - - const handleSetControlImageToDimensions = useCallback(() => { - if (!controlImage) { - return; - } - - if (activeTabName === 'unifiedCanvas') { - dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)); - } else { - if (shift) { - const { width, height } = controlImage; - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); - } else { - const { width, height } = calculateNewSize( - controlImage.width / controlImage.height, - optimalDimension * optimalDimension - ); - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); - } - } - }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); - - const draggableData = useMemo(() => { - if (controlImage) { - return { - id: layerId, - payloadType: 'IMAGE_DTO', - payload: { imageDTO: controlImage }, - }; - } - }, [controlImage, layerId]); - - const droppableData = useMemo( - () => ({ - id: layerId, - actionType: 'SET_CONTROL_LAYER_IMAGE', - context: { layerId }, - }), - [layerId] - ); - - const postUploadAction = useMemo(() => ({ type: 'SET_CONTROL_LAYER_IMAGE', layerId }), [layerId]); - - useEffect(() => { - if (isConnected && isErrorControlImage) { - handleResetControlImage(); - } - }, [handleResetControlImage, isConnected, isErrorControlImage]); - - return ( - - - - <> - : undefined} - tooltip={t('controlnet.resetControlImage')} - /> - : undefined} - tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} - styleOverrides={setControlImageDimensionsStyleOverrides} - /> - - - ); -}); - -IPAdapterImagePreview.displayName = 'IPAdapterImagePreview'; - -const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 6 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx index cb3c371c67..578d3789bf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx @@ -1,13 +1,9 @@ -import { Divider, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; +import { Divider, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - isRegionalGuidanceLayer, - rgLayerIPAdapterDeleted, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { PiTrashSimpleBold } from 'react-icons/pi'; +import { useAppSelector } from 'app/store/storeHooks'; +import { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper'; +import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useMemo } from 'react'; import { assert } from 'tsafe'; type Props = { @@ -39,7 +35,7 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { )} - + ))} @@ -47,36 +43,3 @@ export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { }); RGLayerIPAdapterList.displayName = 'RGLayerIPAdapterList'; - -type IPAdapterListItemProps = { - layerId: string; - ipAdapterId: string; - ipAdapterNumber: number; -}; - -const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber }: IPAdapterListItemProps) => { - const dispatch = useAppDispatch(); - const onDeleteIPAdapter = useCallback(() => { - dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); - }, [dispatch, ipAdapterId, layerId]); - - return ( - - - {`IP Adapter ${ipAdapterNumber}`} - - } - aria-label="Delete IP Adapter" - onClick={onDeleteIPAdapter} - variant="ghost" - colorScheme="error" - /> - - {/* */} - - ); -}); - -RGLayerIPAdapterListItem.displayName = 'RGLayerIPAdapterListItem'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx new file mode 100644 index 0000000000..cc8b0698a5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx @@ -0,0 +1,131 @@ +import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter'; +import { + rgLayerIPAdapterBeginEndStepPctChanged, + rgLayerIPAdapterCLIPVisionModelChanged, + rgLayerIPAdapterDeleted, + rgLayerIPAdapterImageChanged, + rgLayerIPAdapterMethodChanged, + rgLayerIPAdapterModelChanged, + rgLayerIPAdapterWeightChanged, + selectRGLayerIPAdapter, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { RGLayerIPAdapterImageDropData } 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'; + +type Props = { + layerId: string; + ipAdapterId: string; + ipAdapterNumber: number; +}; + +export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNumber }: Props) => { + const dispatch = useAppDispatch(); + const onDeleteIPAdapter = useCallback(() => { + dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); + }, [dispatch, ipAdapterId, layerId]); + const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapter(s.controlLayers.present, layerId, ipAdapterId)); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch( + rgLayerIPAdapterBeginEndStepPctChanged({ + layerId, + ipAdapterId, + beginEndStepPct, + }) + ); + }, + [dispatch, ipAdapterId, layerId] + ); + + const onChangeWeight = useCallback( + (weight: number) => { + dispatch(rgLayerIPAdapterWeightChanged({ layerId, ipAdapterId, weight })); + }, + [dispatch, ipAdapterId, layerId] + ); + + const onChangeIPMethod = useCallback( + (method: IPMethod) => { + dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method })); + }, + [dispatch, ipAdapterId, layerId] + ); + + const onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig) => { + dispatch(rgLayerIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig })); + }, + [dispatch, ipAdapterId, layerId] + ); + + const onChangeCLIPVisionModel = useCallback( + (clipVisionModel: CLIPVisionModel) => { + dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel })); + }, + [dispatch, ipAdapterId, layerId] + ); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); + }, + [dispatch, ipAdapterId, layerId] + ); + + const droppableData = useMemo( + () => ({ + actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', + context: { + layerId, + ipAdapterId, + }, + id: layerId, + }), + [ipAdapterId, layerId] + ); + + const postUploadAction = useMemo( + () => ({ + type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', + layerId, + ipAdapterId, + }), + [ipAdapterId, layerId] + ); + + return ( + + + {`IP Adapter ${ipAdapterNumber}`} + + } + aria-label="Delete IP Adapter" + onClick={onDeleteIPAdapter} + variant="ghost" + colorScheme="error" + /> + + + + ); +}); + +RGLayerIPAdapterWrapper.displayName = 'RGLayerIPAdapterWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts index 56d380b1d3..c42a27f28f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts @@ -5,9 +5,6 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => { - if (!controlLayers.present.isEnabled) { - return 0; - } const validLayers = controlLayers.present.layers .filter(isRegionalGuidanceLayer) .filter((l) => l.isEnabled) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 4d7feaa6ee..92fe9d0119 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -47,7 +47,6 @@ export const initialControlLayersState: ControlLayersState = { brushSize: 100, layers: [], globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity - isEnabled: true, positivePrompt: '', negativePrompt: '', positivePrompt2: '', @@ -77,10 +76,6 @@ const resetLayer = (layer: Layer) => { layer.bboxNeedsUpdate = false; return; } - - if (layer.type === 'control_adapter_layer') { - // TODO - } }; export const selectCALayer = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { @@ -101,12 +96,16 @@ export const selectCAOrIPALayer = ( assert(isControlAdapterLayer(layer) || isIPAdapterLayer(layer)); return layer; }; -const selectRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { +export const selectRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer)); return layer; }; -const selectRGLayerIPAdapter = (state: ControlLayersState, layerId: string, ipAdapterId: string): IPAdapterConfig => { +export const selectRGLayerIPAdapter = ( + state: ControlLayersState, + layerId: string, + ipAdapterId: string +): IPAdapterConfig => { const layer = state.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer)); const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); @@ -556,6 +555,22 @@ export const controlLayersSlice = createSlice({ const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); ipAdapter.method = method; }, + rgLayerIPAdapterModelChanged: ( + state, + action: PayloadAction<{ + layerId: string; + ipAdapterId: string; + modelConfig: IPAdapterModelConfig | null; + }> + ) => { + const { layerId, ipAdapterId, modelConfig } = action.payload; + const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); + if (!modelConfig) { + ipAdapter.model = null; + return; + } + ipAdapter.model = zModelIdentifierField.parse(modelConfig); + }, rgLayerIPAdapterCLIPVisionModelChanged: ( state, action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModel }> @@ -609,9 +624,6 @@ export const controlLayersSlice = createSlice({ globalMaskLayerOpacityChanged: (state, action: PayloadAction) => { state.globalMaskLayerOpacity = action.payload; }, - isEnabledChanged: (state, action: PayloadAction) => { - state.isEnabled = action.payload; - }, undo: (state) => { // Invalidate the bbox for all layers to prevent stale bboxes for (const layer of state.layers.filter(isRenderableLayer)) { @@ -734,6 +746,7 @@ export const { rgLayerIPAdapterWeightChanged, rgLayerIPAdapterBeginEndStepPctChanged, rgLayerIPAdapterMethodChanged, + rgLayerIPAdapterModelChanged, rgLayerIPAdapterCLIPVisionModelChanged, // Globals positivePromptChanged, @@ -746,7 +759,6 @@ export const { aspectRatioChanged, brushSizeChanged, globalMaskLayerOpacityChanged, - isEnabledChanged, undo, redo, } = controlLayersSlice.actions; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 3d5ba672ec..241c8f2f84 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -77,7 +77,6 @@ export type ControlLayersState = { layers: Layer[]; brushSize: number; globalMaskLayerOpacity: number; - isEnabled: boolean; positivePrompt: ParameterPositivePrompt; negativePrompt: ParameterNegativePrompt; positivePrompt2: ParameterPositiveStylePromptSDXL; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 3debe10791..0417c707e4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -80,7 +80,6 @@ export type ImageWithDims = { type ControlAdapterBase = { id: string; - isEnabled: boolean; weight: number; image: ImageWithDims | null; processedImage: ImageWithDims | null; @@ -97,11 +96,15 @@ export type ControlNetConfig = ControlAdapterBase & { model: ParameterControlNetModel | null; controlMode: ControlMode; }; +export const isControlNetConfig = (ca: ControlNetConfig | T2IAdapterConfig): ca is ControlNetConfig => + ca.type === 'controlnet'; export type T2IAdapterConfig = ControlAdapterBase & { type: 't2i_adapter'; model: ParameterT2IAdapterModel | null; }; +export const isT2IAdapterConfig = (ca: ControlNetConfig | T2IAdapterConfig): ca is T2IAdapterConfig => + ca.type === 't2i_adapter'; const zCLIPVisionModel = z.enum(['ViT-H', 'ViT-G']); export type CLIPVisionModel = z.infer; @@ -114,7 +117,6 @@ export const isIPMethod = (v: unknown): v is IPMethod => zIPMethod.safeParse(v). export type IPAdapterConfig = { id: string; type: 'ip_adapter'; - isEnabled: boolean; weight: number; method: IPMethod; image: ImageWithDims | null; @@ -295,10 +297,9 @@ export const isProcessorType = (v: unknown): v is ProcessorType => zProcessorTyp export const initialControlNet: Omit = { type: 'controlnet', - isEnabled: true, model: null, weight: 1, - beginEndStepPct: [0, 0], + beginEndStepPct: [0, 1], controlMode: 'balanced', image: null, processedImage: null, @@ -307,10 +308,9 @@ export const initialControlNet: Omit = { export const initialT2IAdapter: Omit = { type: 't2i_adapter', - isEnabled: true, model: null, weight: 1, - beginEndStepPct: [0, 0], + beginEndStepPct: [0, 1], image: null, processedImage: null, processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), @@ -318,10 +318,9 @@ export const initialT2IAdapter: Omit = { export const initialIPAdapter: Omit = { type: 'ip_adapter', - isEnabled: true, image: null, model: null, - beginEndStepPct: [0, 0], + beginEndStepPct: [0, 1], method: 'full', clipVisionModel: 'ViT-H', weight: 1, diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 739f15c882..7d109473ed 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -33,13 +33,28 @@ type ControlAdapterDropData = BaseDropData & { }; }; -export type ControlLayerDropData = BaseDropData & { - actionType: 'SET_CONTROL_LAYER_IMAGE'; +export type CALayerImageDropData = BaseDropData & { + actionType: 'SET_CA_LAYER_IMAGE'; context: { layerId: string; }; }; +export type IPALayerImageDropData = BaseDropData & { + actionType: 'SET_IPA_LAYER_IMAGE'; + context: { + layerId: string; + }; +}; + +export type RGLayerIPAdapterImageDropData = BaseDropData & { + actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE'; + context: { + layerId: string; + ipAdapterId: string; + }; +}; + export type CanvasInitialImageDropData = BaseDropData & { actionType: 'SET_CANVAS_INITIAL_IMAGE'; }; @@ -69,7 +84,9 @@ export type TypesafeDroppableData = | NodesImageDropData | AddToBoardDropData | RemoveFromBoardDropData - | ControlLayerDropData; + | CALayerImageDropData + | IPALayerImageDropData + | RGLayerIPAdapterImageDropData; type BaseDragData = { id: string; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index c2c9de3f0c..c1da111087 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -19,6 +19,12 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: return payloadType === 'IMAGE_DTO'; case 'SET_CONTROL_ADAPTER_IMAGE': return payloadType === 'IMAGE_DTO'; + case 'SET_CA_LAYER_IMAGE': + return payloadType === 'IMAGE_DTO'; + case 'SET_IPA_LAYER_IMAGE': + return payloadType === 'IMAGE_DTO'; + case 'SET_RG_LAYER_IP_ADAPTER_IMAGE': + return payloadType === 'IMAGE_DTO'; case 'SET_CANVAS_INITIAL_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SET_NODES_IMAGE': diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts index a7236af3cc..4581b51ee1 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts @@ -1,9 +1,23 @@ import { getStore } from 'app/store/nanostores/store'; import type { RootState } from 'app/store/store'; -import { selectAllIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice'; -import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs'; import { + isControlAdapterLayer, + isIPAdapterLayer, + isRegionalGuidanceLayer, +} from 'features/controlLayers/store/controlLayersSlice'; +import { + type ControlNetConfig, + type ImageWithDims, + type IPAdapterConfig, + isControlNetConfig, + isT2IAdapterConfig, + type ProcessorConfig, + type T2IAdapterConfig, +} from 'features/controlLayers/util/controlAdapters'; +import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs'; +import type { ImageField } from 'features/nodes/types/common'; +import { + CONTROL_NET_COLLECT, IP_ADAPTER_COLLECT, NEGATIVE_CONDITIONING, NEGATIVE_CONDITIONING_COLLECT, @@ -14,45 +28,383 @@ import { PROMPT_REGION_NEGATIVE_COND_PREFIX, PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX, PROMPT_REGION_POSITIVE_COND_PREFIX, + T2I_ADAPTER_COLLECT, } from 'features/nodes/util/graph/constants'; +import { upsertMetadata } from 'features/nodes/util/graph/metadata'; import { size } from 'lodash-es'; import { imagesApi } from 'services/api/endpoints/images'; -import type { CollectInvocation, Edge, IPAdapterInvocation, NonNullableGraph, S } from 'services/api/types'; +import type { + CollectInvocation, + ControlNetInvocation, + CoreMetadataInvocation, + Edge, + IPAdapterInvocation, + NonNullableGraph, + S, + T2IAdapterInvocation, +} from 'services/api/types'; import { assert } from 'tsafe'; -export const addControlLayersToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => { - if (!state.controlLayers.present.isEnabled) { +const buildControlImage = ( + image: ImageWithDims | null, + processedImage: ImageWithDims | null, + processorConfig: ProcessorConfig | null +): ImageField => { + if (processedImage && processorConfig) { + // We've processed the image in the app - use it for the control image. + return { + image_name: processedImage.imageName, + }; + } else if (image) { + // No processor selected, and we have an image - the user provided a processed image, use it for the control image. + return { + image_name: image.imageName, + }; + } + assert(false, 'Attempted to add unprocessed control image'); +}; + +const buildControlNetMetadata = (controlNet: ControlNetConfig): S['ControlNetMetadataField'] => { + const { beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet; + + assert(model, 'ControlNet model is required'); + assert(image, 'ControlNet image is required'); + + const processed_image = + processedImage && processorConfig + ? { + image_name: processedImage.imageName, + } + : null; + + return { + control_model: model, + control_weight: weight, + control_mode: controlMode, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + resize_mode: 'just_resize', + image: { + image_name: image.imageName, + }, + processed_image, + }; +}; + +const addControlNetCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { + if (graph.nodes[CONTROL_NET_COLLECT]) { + // You see, we've already got one! return; } + // Add the ControlNet collector + const controlNetIterateNode: CollectInvocation = { + id: CONTROL_NET_COLLECT, + type: 'collect', + is_intermediate: true, + }; + graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode; + graph.edges.push({ + source: { node_id: CONTROL_NET_COLLECT, field: 'collection' }, + destination: { + node_id: denoiseNodeId, + field: 'control', + }, + }); +}; + +const addGlobalControlNetsToGraph = async ( + controlNets: ControlNetConfig[], + graph: NonNullableGraph, + denoiseNodeId: string +) => { + if (controlNets.length === 0) { + return; + } + const controlNetMetadata: CoreMetadataInvocation['controlnets'] = []; + addControlNetCollectorSafe(graph, denoiseNodeId); + + for (const controlNet of controlNets) { + if (!controlNet.model) { + return; + } + const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet; + + const controlNetNode: ControlNetInvocation = { + id: `control_net_${id}`, + type: 'controlnet', + is_intermediate: true, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + control_mode: controlMode, + resize_mode: 'just_resize', + control_model: model, + control_weight: weight, + image: buildControlImage(image, processedImage, processorConfig), + }; + + graph.nodes[controlNetNode.id] = controlNetNode; + + controlNetMetadata.push(buildControlNetMetadata(controlNet)); + + graph.edges.push({ + source: { node_id: controlNetNode.id, field: 'control' }, + destination: { + node_id: CONTROL_NET_COLLECT, + field: 'item', + }, + }); + } + upsertMetadata(graph, { controlnets: controlNetMetadata }); +}; + +const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfig): S['T2IAdapterMetadataField'] => { + const { beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter; + + assert(model, 'T2I Adapter model is required'); + assert(image, 'T2I Adapter image is required'); + + const processed_image = + processedImage && processorConfig + ? { + image_name: processedImage.imageName, + } + : null; + + return { + t2i_adapter_model: model, + weight, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + resize_mode: 'just_resize', + image: { + image_name: image.imageName, + }, + processed_image, + }; +}; + +const addT2IAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { + if (graph.nodes[T2I_ADAPTER_COLLECT]) { + // You see, we've already got one! + return; + } + // Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect + const t2iAdapterCollectNode: CollectInvocation = { + id: T2I_ADAPTER_COLLECT, + type: 'collect', + is_intermediate: true, + }; + graph.nodes[T2I_ADAPTER_COLLECT] = t2iAdapterCollectNode; + graph.edges.push({ + source: { node_id: T2I_ADAPTER_COLLECT, field: 'collection' }, + destination: { + node_id: denoiseNodeId, + field: 't2i_adapter', + }, + }); +}; + +const addGlobalT2IAdaptersToGraph = async ( + t2iAdapters: T2IAdapterConfig[], + graph: NonNullableGraph, + denoiseNodeId: string +) => { + if (t2iAdapters.length === 0) { + return; + } + const t2iAdapterMetadata: CoreMetadataInvocation['t2iAdapters'] = []; + addT2IAdapterCollectorSafe(graph, denoiseNodeId); + + for (const t2iAdapter of t2iAdapters) { + if (!t2iAdapter.model) { + return; + } + const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter; + + const t2iAdapterNode: T2IAdapterInvocation = { + id: `t2i_adapter_${id}`, + type: 't2i_adapter', + is_intermediate: true, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + resize_mode: 'just_resize', + t2i_adapter_model: model, + weight: weight, + image: buildControlImage(image, processedImage, processorConfig), + }; + + graph.nodes[t2iAdapterNode.id] = t2iAdapterNode; + + t2iAdapterMetadata.push(buildT2IAdapterMetadata(t2iAdapter)); + + graph.edges.push({ + source: { node_id: t2iAdapterNode.id, field: 't2i_adapter' }, + destination: { + node_id: T2I_ADAPTER_COLLECT, + field: 'item', + }, + }); + } + + upsertMetadata(graph, { t2iAdapters: t2iAdapterMetadata }); +}; + +const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfig): S['IPAdapterMetadataField'] => { + const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; + + assert(model, 'IP Adapter model is required'); + assert(image, 'IP Adapter image is required'); + + return { + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + weight, + method, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.imageName, + }, + }; +}; + +const addIPAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => { + if (graph.nodes[IP_ADAPTER_COLLECT]) { + // You see, we've already got one! + return; + } + + const ipAdapterCollectNode: CollectInvocation = { + id: IP_ADAPTER_COLLECT, + type: 'collect', + is_intermediate: true, + }; + graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode; + graph.edges.push({ + source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' }, + destination: { + node_id: denoiseNodeId, + field: 'ip_adapter', + }, + }); +}; + +const addGlobalIPAdaptersToGraph = async ( + ipAdapters: IPAdapterConfig[], + graph: NonNullableGraph, + denoiseNodeId: string +) => { + if (ipAdapters.length === 0) { + return; + } + const ipAdapterMetdata: CoreMetadataInvocation['ipAdapters'] = []; + addIPAdapterCollectorSafe(graph, denoiseNodeId); + + for (const ipAdapter of ipAdapters) { + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; + assert(image, 'IP Adapter image is required'); + assert(model, 'IP Adapter model is required'); + + const ipAdapterNode: IPAdapterInvocation = { + id: `ip_adapter_${id}`, + type: 'ip_adapter', + is_intermediate: true, + weight, + method, + ip_adapter_model: model, + clip_vision_model: clipVisionModel, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], + image: { + image_name: image.imageName, + }, + }; + + graph.nodes[ipAdapterNode.id] = ipAdapterNode; + + ipAdapterMetdata.push(buildIPAdapterMetadata(ipAdapter)); + + graph.edges.push({ + source: { node_id: ipAdapterNode.id, field: 'ip_adapter' }, + destination: { + node_id: IP_ADAPTER_COLLECT, + field: 'item', + }, + }); + } + + upsertMetadata(graph, { ipAdapters: ipAdapterMetdata }); +}; + +export const addControlLayersToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => { const { dispatch } = getStore(); - const isSDXL = state.generation.model?.base === 'sdxl'; - const layers = state.controlLayers.present.layers - // Only support vector mask layers now - // TODO: Image masks + const mainModel = state.generation.model; + assert(mainModel, 'Missing main model when building graph'); + const isSDXL = mainModel.base === 'sdxl'; + + // Add global control adapters + const globalControlNets = state.controlLayers.present.layers + // Must be a CA layer + .filter(isControlAdapterLayer) + // Must be enabled + .filter((l) => l.isEnabled) + // We want the CAs themselves + .map((l) => l.controlAdapter) + // Must be a ControlNet + .filter(isControlNetConfig) + .filter((ca) => { + const hasModel = Boolean(ca.model); + const modelMatchesBase = ca.model?.base === mainModel.base; + const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig); + return hasModel && modelMatchesBase && hasControlImage; + }); + addGlobalControlNetsToGraph(globalControlNets, graph, denoiseNodeId); + + const globalT2IAdapters = state.controlLayers.present.layers + // Must be a CA layer + .filter(isControlAdapterLayer) + // Must be enabled + .filter((l) => l.isEnabled) + // We want the CAs themselves + .map((l) => l.controlAdapter) + // Must have a ControlNet CA + .filter(isT2IAdapterConfig) + .filter((ca) => { + const hasModel = Boolean(ca.model); + const modelMatchesBase = ca.model?.base === mainModel.base; + const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig); + return hasModel && modelMatchesBase && hasControlImage; + }); + addGlobalT2IAdaptersToGraph(globalT2IAdapters, graph, denoiseNodeId); + + const globalIPAdapters = state.controlLayers.present.layers + // Must be an IP Adapter layer + .filter(isIPAdapterLayer) + // Must be enabled + .filter((l) => l.isEnabled) + // We want the IP Adapters themselves + .map((l) => l.ipAdapter) + .filter((ca) => { + const hasModel = Boolean(ca.model); + const modelMatchesBase = ca.model?.base === mainModel.base; + const hasControlImage = Boolean(ca.image); + return hasModel && modelMatchesBase && hasControlImage; + }); + addGlobalIPAdaptersToGraph(globalIPAdapters, graph, denoiseNodeId); + + const rgLayers = state.controlLayers.present.layers + // Only RG layers are get masks .filter(isRegionalGuidanceLayer) // Only visible layers are rendered on the canvas .filter((l) => l.isEnabled) // Only layers with prompts get added to the graph .filter((l) => { const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); - const hasIPAdapter = l.ipAdapterIds.length !== 0; + const hasIPAdapter = l.ipAdapters.length !== 0; return hasTextPrompt || hasIPAdapter; }); - // Collect all IP Adapter ids for IP adapter layers - const layerIPAdapterIds = layers.flatMap((l) => l.ipAdapterIds); - - const regionalIPAdapters = selectAllIPAdapters(state.controlAdapters).filter( - ({ id, model, controlImage, isEnabled }) => { - const hasModel = Boolean(model); - const doesBaseMatch = model?.base === state.generation.model?.base; - const hasControlImage = controlImage; - const isRegional = layerIPAdapterIds.includes(id); - return isEnabled && hasModel && doesBaseMatch && hasControlImage && isRegional; - } - ); - - const layerIds = layers.map((l) => l.id); + const layerIds = rgLayers.map((l) => l.id); const blobs = await getRegionalPromptLayerBlobs(layerIds); assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); @@ -118,27 +470,11 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab }, }); - if (!graph.nodes[IP_ADAPTER_COLLECT] && regionalIPAdapters.length > 0) { - const ipAdapterCollectNode: CollectInvocation = { - id: IP_ADAPTER_COLLECT, - type: 'collect', - is_intermediate: true, - }; - graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode; - graph.edges.push({ - source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' }, - destination: { - node_id: denoiseNodeId, - field: 'ip_adapter', - }, - }); - } - // Upload the blobs to the backend, add each to graph // TODO: Store the uploaded image names in redux to reuse them, so long as the layer hasn't otherwise changed. This // would be a great perf win - not only would we skip re-uploading the same image, but we'd be able to use the node // cache (currently, when we re-use the same mask data, since it is a different image, the node cache is not used). - for (const layer of layers) { + for (const layer of rgLayers) { const blob = blobs[layer.id]; assert(blob, `Blob for layer ${layer.id} not found`); @@ -296,36 +632,32 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab } } - for (const ipAdapterId of layer.ipAdapterIds) { - const ipAdapter = selectAllIPAdapters(state.controlAdapters) - .filter(({ id, model, controlImage, isEnabled }) => { - const hasModel = Boolean(model); - const doesBaseMatch = model?.base === state.generation.model?.base; - const hasControlImage = controlImage; - const isRegional = layers.some((l) => l.ipAdapterIds.includes(id)); - return isEnabled && hasModel && doesBaseMatch && hasControlImage && isRegional; - }) - .find((ca) => ca.id === ipAdapterId); + // TODO(psyche): For some reason, I have to explicitly annotate regionalIPAdapters here. Not sure why. + const regionalIPAdapters: IPAdapterConfig[] = layer.ipAdapters.filter((ipAdapter) => { + const hasModel = Boolean(ipAdapter.model); + const modelMatchesBase = ipAdapter.model?.base === mainModel.base; + const hasControlImage = Boolean(ipAdapter.image); + return hasModel && modelMatchesBase && hasControlImage; + }); - if (!ipAdapter?.model) { - return; - } - const { id, weight, model, clipVisionModel, method, beginStepPct, endStepPct, controlImage } = ipAdapter; - - assert(controlImage, 'IP Adapter image is required'); + for (const ipAdapter of regionalIPAdapters) { + addIPAdapterCollectorSafe(graph, denoiseNodeId); + const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; + assert(model, 'IP Adapter model is required'); + assert(image, 'IP Adapter image is required'); const ipAdapterNode: IPAdapterInvocation = { id: `ip_adapter_${id}`, type: 'ip_adapter', is_intermediate: true, - weight: weight, - method: method, + weight, + method, ip_adapter_model: model, clip_vision_model: clipVisionModel, - begin_step_percent: beginStepPct, - end_step_percent: endStepPct, + begin_step_percent: beginEndStepPct[0], + end_step_percent: beginEndStepPct[1], image: { - image_name: controlImage, + image_name: image.imageName, }, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts index fb912d0be2..363d97badf 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts @@ -1,10 +1,8 @@ import type { RootState } from 'app/store/store'; import { selectValidControlNets } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { ControlAdapterProcessorType, ControlNetConfig } from 'features/controlAdapters/store/types'; -import { isControlAdapterLayer } from 'features/controlLayers/store/controlLayersSlice'; import type { ImageField } from 'features/nodes/types/common'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { differenceWith, intersectionWith } from 'lodash-es'; import type { CollectInvocation, ControlNetInvocation, @@ -17,9 +15,13 @@ import { assert } from 'tsafe'; import { CONTROL_NET_COLLECT } from './constants'; import { upsertMetadata } from './metadata'; -const getControlNets = (state: RootState) => { - // Start with the valid controlnets - const validControlNets = selectValidControlNets(state.controlAdapters).filter( +export const addControlNetToLinearGraph = async ( + state: RootState, + graph: NonNullableGraph, + baseNodeId: string +): Promise => { + const controlNetMetadata: CoreMetadataInvocation['controlnets'] = []; + const controlNets = selectValidControlNets(state.controlAdapters).filter( ({ model, processedControlImage, processorType, controlImage, isEnabled }) => { const hasModel = Boolean(model); const doesBaseMatch = model?.base === state.generation.model?.base; @@ -29,35 +31,9 @@ const getControlNets = (state: RootState) => { } ); - // txt2img tab has special handling - it uses layers exclusively, while the other tabs use the older control adapters - // accordion. We need to filter the list of valid T2I adapters according to the tab. + // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper. const activeTabName = activeTabNameSelector(state); - - if (activeTabName === 'txt2img') { - // Add only the cnets that are used in control layers - // Collect all ControlNet ids for enabled ControlNet layers - const layerControlNetIds = state.controlLayers.present.layers - .filter(isControlAdapterLayer) - .filter((l) => l.isEnabled) - .map((l) => l.controlNetId); - return intersectionWith(validControlNets, layerControlNetIds, (a, b) => a.id === b); - } else { - // Else, we want to exclude the cnets that are used in control layers - // Collect all ControlNet ids for all ControlNet layers - const layerControlNetIds = state.controlLayers.present.layers - .filter(isControlAdapterLayer) - .map((l) => l.controlNetId); - return differenceWith(validControlNets, layerControlNetIds, (a, b) => a.id === b); - } -}; - -export const addControlNetToLinearGraph = async ( - state: RootState, - graph: NonNullableGraph, - baseNodeId: string -): Promise => { - const controlNets = getControlNets(state); - const controlNetMetadata: CoreMetadataInvocation['controlnets'] = []; + assert(activeTabName !== 'txt2img', 'Tried to use addControlNetToLinearGraph on txt2img tab'); if (controlNets.length) { // Even though denoise_latents' control input is collection or scalar, keep it simple and always use a collect diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts index 2c53fb3827..12ba4e12a8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts @@ -1,10 +1,8 @@ import type { RootState } from 'app/store/store'; import { selectValidIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { IPAdapterConfig } from 'features/controlAdapters/store/types'; -import { isIPAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice'; import type { ImageField } from 'features/nodes/types/common'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { differenceWith, intersectionWith } from 'lodash-es'; import type { CollectInvocation, CoreMetadataInvocation, @@ -17,48 +15,21 @@ import { assert } from 'tsafe'; import { IP_ADAPTER_COLLECT } from './constants'; import { upsertMetadata } from './metadata'; -const getIPAdapters = (state: RootState) => { - // Start with the valid IP adapters - const validIPAdapters = selectValidIPAdapters(state.controlAdapters).filter(({ model, controlImage, isEnabled }) => { - const hasModel = Boolean(model); - const doesBaseMatch = model?.base === state.generation.model?.base; - const hasControlImage = controlImage; - return isEnabled && hasModel && doesBaseMatch && hasControlImage; - }); - - // Masked IP adapters are handled in the graph helper for regional control - skip them here - const maskedIPAdapterIds = state.controlLayers.present.layers - .filter(isRegionalGuidanceLayer) - .map((l) => l.ipAdapterIds) - .flat(); - const nonMaskedIPAdapters = differenceWith(validIPAdapters, maskedIPAdapterIds, (a, b) => a.id === b); - - // txt2img tab has special handling - it uses layers exclusively, while the other tabs use the older control adapters - // accordion. We need to filter the list of valid IP adapters according to the tab. - const activeTabName = activeTabNameSelector(state); - - if (activeTabName === 'txt2img') { - // If we are on the t2i tab, we only want to add the IP adapters that are used in unmasked IP Adapter layers - // Collect all IP Adapter ids for enabled IP adapter layers - const layerIPAdapterIds = state.controlLayers.present.layers - .filter(isIPAdapterLayer) - .filter((l) => l.isEnabled) - .map((l) => l.ipAdapterId); - return intersectionWith(nonMaskedIPAdapters, layerIPAdapterIds, (a, b) => a.id === b); - } else { - // Else, we want to exclude the IP adapters that are used in IP Adapter layers - // Collect all IP Adapter ids for enabled IP adapter layers - const layerIPAdapterIds = state.controlLayers.present.layers.filter(isIPAdapterLayer).map((l) => l.ipAdapterId); - return differenceWith(nonMaskedIPAdapters, layerIPAdapterIds, (a, b) => a.id === b); - } -}; - export const addIPAdapterToLinearGraph = async ( state: RootState, graph: NonNullableGraph, baseNodeId: string ): Promise => { - const ipAdapters = getIPAdapters(state); + // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper. + const activeTabName = activeTabNameSelector(state); + assert(activeTabName !== 'txt2img', 'Tried to use addT2IAdaptersToLinearGraph on txt2img tab'); + + const ipAdapters = selectValidIPAdapters(state.controlAdapters).filter(({ model, controlImage, isEnabled }) => { + const hasModel = Boolean(model); + const doesBaseMatch = model?.base === state.generation.model?.base; + const hasControlImage = controlImage; + return isEnabled && hasModel && doesBaseMatch && hasControlImage; + }); if (ipAdapters.length) { // Even though denoise_latents' ip adapter input is collection or scalar, keep it simple and always use a collect diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts index 1632449724..ddd87256f4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts @@ -1,10 +1,8 @@ import type { RootState } from 'app/store/store'; import { selectValidT2IAdapters } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { ControlAdapterProcessorType, T2IAdapterConfig } from 'features/controlAdapters/store/types'; -import { isControlAdapterLayer } from 'features/controlLayers/store/controlLayersSlice'; import type { ImageField } from 'features/nodes/types/common'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { differenceWith, intersectionWith } from 'lodash-es'; import type { CollectInvocation, CoreMetadataInvocation, @@ -17,9 +15,16 @@ import { assert } from 'tsafe'; import { T2I_ADAPTER_COLLECT } from './constants'; import { upsertMetadata } from './metadata'; -const getT2IAdapters = (state: RootState) => { - // Start with the valid controlnets - const validT2IAdapters = selectValidT2IAdapters(state.controlAdapters).filter( +export const addT2IAdaptersToLinearGraph = async ( + state: RootState, + graph: NonNullableGraph, + baseNodeId: string +): Promise => { + // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper. + const activeTabName = activeTabNameSelector(state); + assert(activeTabName !== 'txt2img', 'Tried to use addT2IAdaptersToLinearGraph on txt2img tab'); + + const t2iAdapters = selectValidT2IAdapters(state.controlAdapters).filter( ({ model, processedControlImage, processorType, controlImage, isEnabled }) => { const hasModel = Boolean(model); const doesBaseMatch = model?.base === state.generation.model?.base; @@ -29,34 +34,6 @@ const getT2IAdapters = (state: RootState) => { } ); - // txt2img tab has special handling - it uses layers exclusively, while the other tabs use the older control adapters - // accordion. We need to filter the list of valid T2I adapters according to the tab. - const activeTabName = activeTabNameSelector(state); - - if (activeTabName === 'txt2img') { - // Add only the T2Is that are used in control layers - // Collect all ids for enabled control adapter layers - const layerControlAdapterIds = state.controlLayers.present.layers - .filter(isControlAdapterLayer) - .filter((l) => l.isEnabled) - .map((l) => l.controlNetId); - return intersectionWith(validT2IAdapters, layerControlAdapterIds, (a, b) => a.id === b); - } else { - // Else, we want to exclude the T2Is that are used in control layers - const layerControlAdapterIds = state.controlLayers.present.layers - .filter(isControlAdapterLayer) - .map((l) => l.controlNetId); - return differenceWith(validT2IAdapters, layerControlAdapterIds, (a, b) => a.id === b); - } -}; - -export const addT2IAdaptersToLinearGraph = async ( - state: RootState, - graph: NonNullableGraph, - baseNodeId: string -): Promise => { - const t2iAdapters = getT2IAdapters(state); - if (t2iAdapters.length) { // Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect const t2iAdapterCollectNode: CollectInvocation = { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts index 010fb9c5e4..9134ef9de7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts @@ -4,13 +4,10 @@ import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetch import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph'; import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; import { addVAEToGraph } from './addVAEToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { @@ -264,14 +261,6 @@ export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise // add LoRA support await addSDXLLoRAsToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // add IP Adapter - await addIPAdapterToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - await addT2IAdaptersToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - await addControlLayersToGraph(state, graph, SDXL_DENOISE_LATENTS); // NSFW & watermark - must be last thing added to graph diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts index ea59d7e41d..340a24bca4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts @@ -5,13 +5,10 @@ import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLay import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addHrfToGraph } from './addHrfToGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; import { addVAEToGraph } from './addVAEToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { @@ -246,14 +243,6 @@ export const buildLinearTextToImageGraph = async (state: RootState): Promise Date: Thu, 2 May 2024 08:18:34 +1000 Subject: [PATCH 20/74] feat(ui): auto-process for control layer CAs --- .../middleware/listenerMiddleware/index.ts | 2 + .../listeners/controlAdapterPreprocessor.ts | 147 ++++++ .../components/CALayer/CALayer.tsx | 4 +- .../CALayer/CALayerControlAdapterWrapper.tsx | 4 +- .../ControlAndIPAdapter/ControlAdapter.tsx | 10 +- .../ControlAdapterImagePreview.tsx | 39 +- .../IPALayer/IPALayerIPAdapterWrapper.tsx | 4 +- .../RGLayer/RGLayerIPAdapterWrapper.tsx | 4 +- .../controlLayers/store/controlLayersSlice.ts | 79 ++-- .../controlLayers/util/controlAdapters.ts | 444 +++++++++++------- 10 files changed, 495 insertions(+), 242 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index cd0c1290e9..36040b5e41 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -16,6 +16,7 @@ import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listen import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet'; import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged'; import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery'; +import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor'; import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess'; import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed'; import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas'; @@ -157,3 +158,4 @@ addUpscaleRequestedListener(startAppListening); addDynamicPromptsListener(startAppListening); addSetDefaultSettingsListener(startAppListening); +addControlAdapterPreprocessor(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts new file mode 100644 index 0000000000..9cb7efe572 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -0,0 +1,147 @@ +import { isAnyOf } from '@reduxjs/toolkit'; +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { parseify } from 'common/util/serialize'; +import { + caLayerImageChanged, + caLayerIsProcessingImageChanged, + caLayerModelChanged, + caLayerProcessedImageChanged, + caLayerProcessorConfigChanged, + isControlAdapterLayer, +} from 'features/controlLayers/store/controlLayersSlice'; +import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { isImageOutput } from 'features/nodes/types/common'; +import { addToast } from 'features/system/store/systemSlice'; +import { t } from 'i18next'; +import { imagesApi } from 'services/api/endpoints/images'; +import { queueApi } from 'services/api/endpoints/queue'; +import type { BatchConfig, ImageDTO } from 'services/api/types'; +import { socketInvocationComplete } from 'services/events/actions'; + +const matcher = isAnyOf(caLayerImageChanged, caLayerProcessorConfigChanged, caLayerModelChanged); + +const DEBOUNCE_MS = 300; +const log = logger('session'); + +export const addControlAdapterPreprocessor = (startAppListening: AppStartListening) => { + startAppListening({ + matcher, + effect: async (action, { dispatch, getState, cancelActiveListeners, delay, take }) => { + const { layerId } = action.payload; + const precheckLayer = getState() + .controlLayers.present.layers.filter(isControlAdapterLayer) + .find((l) => l.id === layerId); + + // Conditions to bail + if ( + // Layer doesn't exist + !precheckLayer || + // Layer doesn't have an image + !precheckLayer.controlAdapter.image || + // Layer doesn't have a processor config + !precheckLayer.controlAdapter.processorConfig || + // Layer is already processing an image + precheckLayer.controlAdapter.isProcessingImage + ) { + return; + } + + // Cancel any in-progress instances of this listener + cancelActiveListeners(); + log.trace('Control Layer CA auto-process triggered'); + + // Delay before starting actual work + await delay(DEBOUNCE_MS); + dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: true })); + + // Double-check that we are still eligible for processing + const state = getState(); + const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); + const image = layer?.controlAdapter.image; + const config = layer?.controlAdapter.processorConfig; + + // If we have no image or there is no processor config, bail + if (!layer || !image || !config) { + return; + } + + // @ts-expect-error: TS isn't able to narrow the typing of buildNode and `config` will error... + const processorNode = CONTROLNET_PROCESSORS[config.type].buildNode(image, config); + const enqueueBatchArg: BatchConfig = { + prepend: true, + batch: { + graph: { + nodes: { + [processorNode.id]: { ...processorNode, is_intermediate: true }, + }, + edges: [], + }, + runs: 1, + }, + }; + + try { + const req = dispatch( + queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, { + fixedCacheKey: 'enqueueBatch', + }) + ); + const enqueueResult = await req.unwrap(); + req.reset(); + log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued')); + + const [invocationCompleteAction] = await take( + (action): action is ReturnType => + socketInvocationComplete.match(action) && + action.payload.data.queue_batch_id === enqueueResult.batch.batch_id && + action.payload.data.source_node_id === processorNode.id + ); + + // We still have to check the output type + if (isImageOutput(invocationCompleteAction.payload.data.result)) { + const { image_name } = invocationCompleteAction.payload.data.result.image; + + // Wait for the ImageDTO to be received + const [{ payload }] = await take( + (action) => + imagesApi.endpoints.getImageDTO.matchFulfilled(action) && action.payload.image_name === image_name + ); + + const imageDTO = payload as ImageDTO; + + log.debug({ layerId, imageDTO }, 'ControlNet image processed'); + + // Update the processed image in the store + dispatch( + caLayerProcessedImageChanged({ + layerId, + imageDTO, + }) + ); + dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false })); + } + } catch (error) { + console.log(error); + log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue')); + dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false })); + + if (error instanceof Object) { + if ('data' in error && 'status' in error) { + if (error.status === 403) { + dispatch(caLayerImageChanged({ layerId, imageDTO: null })); + return; + } + } + } + + dispatch( + addToast({ + title: t('queue.graphFailedToQueue'), + status: 'error', + }) + ); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index f9edf42c2f..24de817df2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -5,7 +5,7 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; -import { layerSelected, selectCALayer } from 'features/controlLayers/store/controlLayersSlice'; +import { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import CALayerOpacity from './CALayerOpacity'; @@ -16,7 +16,7 @@ type Props = { export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).isSelected); + const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected); const onClickCapture = useCallback(() => { // Must be capture so that the layer is selected before deleting/resetting/etc dispatch(layerSelected(layerId)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx index 2a2edeb8d8..6793a33f69 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx @@ -7,7 +7,7 @@ import { caLayerProcessorConfigChanged, caOrIPALayerBeginEndStepPctChanged, caOrIPALayerWeightChanged, - selectCALayer, + selectCALayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import type { ControlMode, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import type { CALayerImageDropData } from 'features/dnd/types'; @@ -25,7 +25,7 @@ type Props = { export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); - const controlAdapter = useAppSelector((s) => selectCALayer(s.controlLayers.present, layerId).controlAdapter); + const controlAdapter = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).controlAdapter); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx index 972198cc7e..ecdfa46ef6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx @@ -87,11 +87,8 @@ export const ControlAdapter = memo( @@ -99,7 +96,10 @@ export const ControlAdapter = memo( {isExpanded && ( <> - + )} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx index e4f53c1c70..7def6b2b56 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx @@ -1,14 +1,12 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Box, Flex, Spinner, useShiftModifier } from '@invoke-ai/ui-library'; import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; 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 { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import type { ControlNetConfig, T2IAdapterConfig } from 'features/controlLayers/util/controlAdapters'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; @@ -25,46 +23,29 @@ import { import type { ImageDTO, PostUploadAction } from 'services/api/types'; type Props = { - controlAdapterId: string; - image: ImageWithDims | null; - processedImage: ImageWithDims | null; + controlAdapter: ControlNetConfig | T2IAdapterConfig; onChangeImage: (imageDTO: ImageDTO | null) => void; - hasProcessor: boolean; droppableData: TypesafeDroppableData; postUploadAction: PostUploadAction; }; -const selectPendingControlImages = createMemoizedSelector( - selectControlAdaptersSlice, - (controlAdapters) => controlAdapters.pendingControlImages -); - export const ControlAdapterImagePreview = memo( - ({ - image, - processedImage, - onChangeImage, - hasProcessor, - controlAdapterId, - droppableData, - postUploadAction, - }: Props) => { + ({ controlAdapter, onChangeImage, droppableData, postUploadAction }: Props) => { const { t } = useTranslation(); 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 pendingControlImages = useAppSelector(selectPendingControlImages); const shift = useShiftModifier(); const [isMouseOverImage, setIsMouseOverImage] = useState(false); const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - image?.imageName ?? skipToken + controlAdapter.image?.imageName ?? skipToken ); const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - processedImage?.imageName ?? skipToken + controlAdapter.processedImage?.imageName ?? skipToken ); const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); @@ -130,19 +111,19 @@ export const ControlAdapterImagePreview = memo( const draggableData = useMemo(() => { if (controlImage) { return { - id: controlAdapterId, + id: controlAdapter.id, payloadType: 'IMAGE_DTO', payload: { imageDTO: controlImage }, }; } - }, [controlImage, controlAdapterId]); + }, [controlImage, controlAdapter.id]); const shouldShowProcessedImage = controlImage && processedControlImage && !isMouseOverImage && - !pendingControlImages.includes(controlAdapterId) && - hasProcessor; + !controlAdapter.isProcessingImage && + controlAdapter.processorConfig !== null; useEffect(() => { if (isConnected && (isErrorControlImage || isErrorProcessedControlImage)) { @@ -207,7 +188,7 @@ export const ControlAdapterImagePreview = memo( /> - {pendingControlImages.includes(controlAdapterId) && ( + {controlAdapter.isProcessingImage && ( { const dispatch = useAppDispatch(); - const ipAdapter = useAppSelector((s) => selectIPALayer(s.controlLayers.present, layerId).ipAdapter); + const ipAdapter = useAppSelector((s) => selectIPALayerOrThrow(s.controlLayers.present, layerId).ipAdapter); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { 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 cc8b0698a5..015cf75e4d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx @@ -9,7 +9,7 @@ import { rgLayerIPAdapterMethodChanged, rgLayerIPAdapterModelChanged, rgLayerIPAdapterWeightChanged, - selectRGLayerIPAdapter, + selectRGLayerIPAdapterOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters'; import type { RGLayerIPAdapterImageDropData } from 'features/dnd/types'; @@ -28,7 +28,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu const onDeleteIPAdapter = useCallback(() => { dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); }, [dispatch, ipAdapterId, layerId]); - const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapter(s.controlLayers.present, layerId, ipAdapterId)); + const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.controlLayers.present, layerId, ipAdapterId)); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 92fe9d0119..fa179a3bbd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -78,17 +78,17 @@ const resetLayer = (layer: Layer) => { } }; -export const selectCALayer = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { +export const selectCALayerOrThrow = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isControlAdapterLayer(layer)); return layer; }; -export const selectIPALayer = (state: ControlLayersState, layerId: string): IPAdapterLayer => { +export const selectIPALayerOrThrow = (state: ControlLayersState, layerId: string): IPAdapterLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isIPAdapterLayer(layer)); return layer; }; -export const selectCAOrIPALayer = ( +export const selectCAOrIPALayerOrThrow = ( state: ControlLayersState, layerId: string ): ControlAdapterLayer | IPAdapterLayer => { @@ -96,12 +96,12 @@ export const selectCAOrIPALayer = ( assert(isControlAdapterLayer(layer) || isIPAdapterLayer(layer)); return layer; }; -export const selectRGLayer = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { +export const selectRGLayerOrThrow = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer)); return layer; }; -export const selectRGLayerIPAdapter = ( +export const selectRGLayerIPAdapterOrThrow = ( state: ControlLayersState, layerId: string, ipAdapterId: string @@ -246,7 +246,7 @@ export const controlLayersSlice = createSlice({ }, caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; - const layer = selectCALayer(state, layerId); + const layer = selectCALayerOrThrow(state, layerId); layer.bbox = null; layer.bboxNeedsUpdate = true; layer.isEnabled = true; @@ -255,7 +255,7 @@ export const controlLayersSlice = createSlice({ }, caLayerProcessedImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; - const layer = selectCALayer(state, layerId); + const layer = selectCALayerOrThrow(state, layerId); layer.bbox = null; layer.bboxNeedsUpdate = true; layer.isEnabled = true; @@ -269,7 +269,7 @@ export const controlLayersSlice = createSlice({ }> ) => { const { layerId, modelConfig } = action.payload; - const layer = selectCALayer(state, layerId); + const layer = selectCALayerOrThrow(state, layerId); if (!modelConfig) { layer.controlAdapter.model = null; return; @@ -285,7 +285,7 @@ export const controlLayersSlice = createSlice({ }, caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlMode }>) => { const { layerId, controlMode } = action.payload; - const layer = selectCALayer(state, layerId); + const layer = selectCALayerOrThrow(state, layerId); assert(layer.controlAdapter.type === 'controlnet'); layer.controlAdapter.controlMode = controlMode; }, @@ -294,19 +294,27 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; processorConfig: ProcessorConfig | null }> ) => { const { layerId, processorConfig } = action.payload; - const layer = selectCALayer(state, layerId); + const layer = selectCALayerOrThrow(state, layerId); layer.controlAdapter.processorConfig = processorConfig; }, caLayerIsFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => { const { layerId, isFilterEnabled } = action.payload; - const layer = selectCALayer(state, layerId); + const layer = selectCALayerOrThrow(state, layerId); layer.isFilterEnabled = isFilterEnabled; }, caLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { const { layerId, opacity } = action.payload; - const layer = selectCALayer(state, layerId); + const layer = selectCALayerOrThrow(state, layerId); layer.opacity = opacity; }, + caLayerIsProcessingImageChanged: ( + state, + action: PayloadAction<{ layerId: string; isProcessingImage: boolean }> + ) => { + const { layerId, isProcessingImage } = action.payload; + const layer = selectCALayerOrThrow(state, layerId); + layer.controlAdapter.isProcessingImage = isProcessingImage; + }, //#endregion //#region IP Adapter Layers @@ -325,12 +333,12 @@ export const controlLayersSlice = createSlice({ }, ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; - const layer = selectIPALayer(state, layerId); + const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, ipaLayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { const { layerId, weight } = action.payload; - const layer = selectIPALayer(state, layerId); + const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.weight = weight; }, ipaLayerBeginEndStepPctChanged: ( @@ -338,12 +346,12 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> ) => { const { layerId, beginEndStepPct } = action.payload; - const layer = selectIPALayer(state, layerId); + const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.beginEndStepPct = beginEndStepPct; }, ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethod }>) => { const { layerId, method } = action.payload; - const layer = selectIPALayer(state, layerId); + const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.method = method; }, ipaLayerModelChanged: ( @@ -354,7 +362,7 @@ export const controlLayersSlice = createSlice({ }> ) => { const { layerId, modelConfig } = action.payload; - const layer = selectIPALayer(state, layerId); + const layer = selectIPALayerOrThrow(state, layerId); if (!modelConfig) { layer.ipAdapter.model = null; return; @@ -366,7 +374,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }> ) => { const { layerId, clipVisionModel } = action.payload; - const layer = selectIPALayer(state, layerId); + const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.clipVisionModel = clipVisionModel; }, //#endregion @@ -374,7 +382,7 @@ export const controlLayersSlice = createSlice({ //#region CA or IPA Layers caOrIPALayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { const { layerId, weight } = action.payload; - const layer = selectCAOrIPALayer(state, layerId); + const layer = selectCAOrIPALayerOrThrow(state, layerId); if (layer.type === 'control_adapter_layer') { layer.controlAdapter.weight = weight; } else { @@ -386,7 +394,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> ) => { const { layerId, beginEndStepPct } = action.payload; - const layer = selectCAOrIPALayer(state, layerId); + const layer = selectCAOrIPALayerOrThrow(state, layerId); if (layer.type === 'control_adapter_layer') { layer.controlAdapter.beginEndStepPct = beginEndStepPct; } else { @@ -428,17 +436,17 @@ export const controlLayersSlice = createSlice({ }, rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; - const layer = selectRGLayer(state, layerId); + const layer = selectRGLayerOrThrow(state, layerId); layer.positivePrompt = prompt; }, rgLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; - const layer = selectRGLayer(state, layerId); + const layer = selectRGLayerOrThrow(state, layerId); layer.negativePrompt = prompt; }, rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { const { layerId, color } = action.payload; - const layer = selectRGLayer(state, layerId); + const layer = selectRGLayerOrThrow(state, layerId); layer.previewColor = color; }, rgLayerLineAdded: { @@ -452,7 +460,7 @@ export const controlLayersSlice = createSlice({ }> ) => { const { layerId, points, tool, lineUuid } = action.payload; - const layer = selectRGLayer(state, layerId); + const layer = selectRGLayerOrThrow(state, layerId); const lineId = getRGLayerLineId(layer.id, lineUuid); layer.maskObjects.push({ type: 'vector_mask_line', @@ -474,7 +482,7 @@ export const controlLayersSlice = createSlice({ }, rgLayerPointsAdded: (state, action: PayloadAction<{ layerId: string; point: [number, number] }>) => { const { layerId, point } = action.payload; - const layer = selectRGLayer(state, layerId); + const layer = selectRGLayerOrThrow(state, layerId); const lastLine = layer.maskObjects.findLast(isLine); if (!lastLine) { return; @@ -491,7 +499,7 @@ export const controlLayersSlice = createSlice({ // Ignore zero-area rectangles return; } - const layer = selectRGLayer(state, layerId); + const layer = selectRGLayerOrThrow(state, layerId); const id = getRGLayerRectId(layer.id, rectUuid); layer.maskObjects.push({ type: 'vector_mask_rect', @@ -510,17 +518,17 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }> ) => { const { layerId, autoNegative } = action.payload; - const layer = selectRGLayer(state, layerId); + const layer = selectRGLayerOrThrow(state, layerId); layer.autoNegative = autoNegative; }, rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { const { layerId, ipAdapter } = action.payload; - const layer = selectRGLayer(state, layerId); + const layer = selectRGLayerOrThrow(state, layerId); layer.ipAdapters.push(ipAdapter); }, rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { const { layerId, ipAdapterId } = action.payload; - const layer = selectRGLayer(state, layerId); + const layer = selectRGLayerOrThrow(state, layerId); layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); }, rgLayerIPAdapterImageChanged: ( @@ -528,7 +536,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }> ) => { const { layerId, ipAdapterId, imageDTO } = action.payload; - const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, rgLayerIPAdapterWeightChanged: ( @@ -536,7 +544,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; ipAdapterId: string; weight: number }> ) => { const { layerId, ipAdapterId, weight } = action.payload; - const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); ipAdapter.weight = weight; }, rgLayerIPAdapterBeginEndStepPctChanged: ( @@ -544,7 +552,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; ipAdapterId: string; beginEndStepPct: [number, number] }> ) => { const { layerId, ipAdapterId, beginEndStepPct } = action.payload; - const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); ipAdapter.beginEndStepPct = beginEndStepPct; }, rgLayerIPAdapterMethodChanged: ( @@ -552,7 +560,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethod }> ) => { const { layerId, ipAdapterId, method } = action.payload; - const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); ipAdapter.method = method; }, rgLayerIPAdapterModelChanged: ( @@ -564,7 +572,7 @@ export const controlLayersSlice = createSlice({ }> ) => { const { layerId, ipAdapterId, modelConfig } = action.payload; - const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); if (!modelConfig) { ipAdapter.model = null; return; @@ -576,7 +584,7 @@ export const controlLayersSlice = createSlice({ action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModel }> ) => { const { layerId, ipAdapterId, clipVisionModel } = action.payload; - const ipAdapter = selectRGLayerIPAdapter(state, layerId, ipAdapterId); + const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); ipAdapter.clipVisionModel = clipVisionModel; }, //#endregion @@ -720,6 +728,7 @@ export const { caLayerProcessorConfigChanged, caLayerIsFilterEnabledChanged, caLayerOpacityChanged, + caLayerIsProcessingImageChanged, // IPA Layers ipaLayerAdded, ipaLayerImageChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 0417c707e4..261cfd2f85 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -13,6 +13,7 @@ import type { ControlNetModelConfig, DepthAnythingImageProcessorInvocation, DWOpenposeImageProcessorInvocation, + Graph, HedImageProcessorInvocation, ImageDTO, LineartAnimeImageProcessorInvocation, @@ -34,27 +35,33 @@ export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSiz zDepthAnythingModelSize.safeParse(v).success; export type CannyProcessorConfig = Required< - Pick + Pick +>; +export type ColorMapProcessorConfig = Required< + Pick >; -export type ColorMapProcessorConfig = Required>; export type ContentShuffleProcessorConfig = Required< - Pick + Pick >; -export type DepthAnythingProcessorConfig = Required>; -export type HedProcessorConfig = Required>; -export type LineartAnimeProcessorConfig = Required>; -export type LineartProcessorConfig = Required>; +export type DepthAnythingProcessorConfig = Required< + Pick +>; +export type HedProcessorConfig = Required>; +export type LineartAnimeProcessorConfig = Required>; +export type LineartProcessorConfig = Required>; export type MediapipeFaceProcessorConfig = Required< - Pick + Pick >; -export type MidasDepthProcessorConfig = Required>; -export type MlsdProcessorConfig = Required>; -export type NormalbaeProcessorConfig = Required>; +export type MidasDepthProcessorConfig = Required< + Pick +>; +export type MlsdProcessorConfig = Required>; +export type NormalbaeProcessorConfig = Required>; export type DWOpenposeProcessorConfig = Required< - Pick + Pick >; -export type PidiProcessorConfig = Required>; -export type ZoeDepthProcessorConfig = Required>; +export type PidiProcessorConfig = Required>; +export type ZoeDepthProcessorConfig = Required>; export type ProcessorConfig = | CannyProcessorConfig @@ -83,6 +90,7 @@ type ControlAdapterBase = { weight: number; image: ImageWithDims | null; processedImage: ImageWithDims | null; + isProcessingImage: boolean; processorConfig: ProcessorConfig | null; beginEndStepPct: [number, number]; }; @@ -125,157 +133,6 @@ export type IPAdapterConfig = { beginEndStepPct: [number, number]; }; -type ProcessorData = { - labelTKey: string; - descriptionTKey: string; - buildDefaults(baseModel?: BaseModelType): Extract; -}; - -type ControlNetProcessorsDict = { - [key in ProcessorConfig['type']]: ProcessorData; -}; -/** - * A dict of ControlNet processors, including: - * - label translation key - * - description translation key - * - a builder to create default values for the config - * - * TODO: Generate from the OpenAPI schema - */ -export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { - canny_image_processor: { - labelTKey: 'controlnet.canny', - descriptionTKey: 'controlnet.cannyDescription', - buildDefaults: () => ({ - id: `canny_image_processor_${uuidv4()}`, - type: 'canny_image_processor', - low_threshold: 100, - high_threshold: 200, - }), - }, - color_map_image_processor: { - labelTKey: 'controlnet.colorMap', - descriptionTKey: 'controlnet.colorMapDescription', - buildDefaults: () => ({ - id: `color_map_image_processor_${uuidv4()}`, - type: 'color_map_image_processor', - color_map_tile_size: 64, - }), - }, - content_shuffle_image_processor: { - labelTKey: 'controlnet.contentShuffle', - descriptionTKey: 'controlnet.contentShuffleDescription', - buildDefaults: (baseModel) => ({ - id: `content_shuffle_image_processor_${uuidv4()}`, - type: 'content_shuffle_image_processor', - h: baseModel === 'sdxl' ? 1024 : 512, - w: baseModel === 'sdxl' ? 1024 : 512, - f: baseModel === 'sdxl' ? 512 : 256, - }), - }, - depth_anything_image_processor: { - labelTKey: 'controlnet.depthAnything', - descriptionTKey: 'controlnet.depthAnythingDescription', - buildDefaults: () => ({ - id: `depth_anything_image_processor_${uuidv4()}`, - type: 'depth_anything_image_processor', - model_size: 'small', - }), - }, - hed_image_processor: { - labelTKey: 'controlnet.hed', - descriptionTKey: 'controlnet.hedDescription', - buildDefaults: () => ({ - id: `hed_image_processor_${uuidv4()}`, - type: 'hed_image_processor', - scribble: false, - }), - }, - lineart_anime_image_processor: { - labelTKey: 'controlnet.lineartAnime', - descriptionTKey: 'controlnet.lineartAnimeDescription', - buildDefaults: () => ({ - id: `lineart_anime_image_processor_${uuidv4()}`, - type: 'lineart_anime_image_processor', - }), - }, - lineart_image_processor: { - labelTKey: 'controlnet.lineart', - descriptionTKey: 'controlnet.lineartDescription', - buildDefaults: () => ({ - id: `lineart_image_processor_${uuidv4()}`, - type: 'lineart_image_processor', - coarse: false, - }), - }, - mediapipe_face_processor: { - labelTKey: 'controlnet.mediapipeFace', - descriptionTKey: 'controlnet.mediapipeFaceDescription', - buildDefaults: () => ({ - id: `mediapipe_face_processor_${uuidv4()}`, - type: 'mediapipe_face_processor', - max_faces: 1, - min_confidence: 0.5, - }), - }, - midas_depth_image_processor: { - labelTKey: 'controlnet.depthMidas', - descriptionTKey: 'controlnet.depthMidasDescription', - buildDefaults: () => ({ - id: `midas_depth_image_processor_${uuidv4()}`, - type: 'midas_depth_image_processor', - a_mult: 2, - bg_th: 0.1, - }), - }, - mlsd_image_processor: { - labelTKey: 'controlnet.mlsd', - descriptionTKey: 'controlnet.mlsdDescription', - buildDefaults: () => ({ - id: `mlsd_image_processor_${uuidv4()}`, - type: 'mlsd_image_processor', - thr_d: 0.1, - thr_v: 0.1, - }), - }, - normalbae_image_processor: { - labelTKey: 'controlnet.normalBae', - descriptionTKey: 'controlnet.normalBaeDescription', - buildDefaults: () => ({ - id: `normalbae_image_processor_${uuidv4()}`, - type: 'normalbae_image_processor', - }), - }, - dw_openpose_image_processor: { - labelTKey: 'controlnet.dwOpenpose', - descriptionTKey: 'controlnet.dwOpenposeDescription', - buildDefaults: () => ({ - id: `dw_openpose_image_processor_${uuidv4()}`, - type: 'dw_openpose_image_processor', - draw_body: true, - draw_face: false, - draw_hands: false, - }), - }, - pidi_image_processor: { - labelTKey: 'controlnet.pidi', - descriptionTKey: 'controlnet.pidiDescription', - buildDefaults: () => ({ - id: `pidi_image_processor_${uuidv4()}`, - type: 'pidi_image_processor', - scribble: false, - safe: false, - }), - }, - zoe_depth_image_processor: { - labelTKey: 'controlnet.depthZoe', - descriptionTKey: 'controlnet.depthZoeDescription', - buildDefaults: () => ({ - id: `zoe_depth_image_processor_${uuidv4()}`, - type: 'zoe_depth_image_processor', - }), - }, -}; export const zProcessorType = z.enum([ 'canny_image_processor', 'color_map_image_processor', @@ -295,6 +152,261 @@ export const zProcessorType = z.enum([ export type ProcessorType = z.infer; export const isProcessorType = (v: unknown): v is ProcessorType => zProcessorType.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); +const getId = (type: ProcessorType): string => `${type}_${uuidv4()}`; + +type CAProcessorsData = { + [key in ProcessorType]: 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 CONTROLNET_PROCESSORS: CAProcessorsData = { + canny_image_processor: { + type: 'canny_image_processor', + labelTKey: 'controlnet.canny', + descriptionTKey: 'controlnet.cannyDescription', + buildDefaults: () => ({ + id: getId('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.imageName }, + 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: getId('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.imageName }, + }), + }, + content_shuffle_image_processor: { + type: 'content_shuffle_image_processor', + labelTKey: 'controlnet.contentShuffle', + descriptionTKey: 'controlnet.contentShuffleDescription', + buildDefaults: (baseModel) => ({ + id: getId('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.imageName }, + 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: getId('depth_anything_image_processor'), + type: 'depth_anything_image_processor', + model_size: 'small', + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + resolution: minDim(image), + }), + }, + hed_image_processor: { + type: 'hed_image_processor', + labelTKey: 'controlnet.hed', + descriptionTKey: 'controlnet.hedDescription', + buildDefaults: () => ({ + id: getId('hed_image_processor'), + type: 'hed_image_processor', + scribble: false, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + 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: getId('lineart_anime_image_processor'), + type: 'lineart_anime_image_processor', + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + lineart_image_processor: { + type: 'lineart_image_processor', + labelTKey: 'controlnet.lineart', + descriptionTKey: 'controlnet.lineartDescription', + buildDefaults: () => ({ + id: getId('lineart_image_processor'), + type: 'lineart_image_processor', + coarse: false, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + mediapipe_face_processor: { + type: 'mediapipe_face_processor', + labelTKey: 'controlnet.mediapipeFace', + descriptionTKey: 'controlnet.mediapipeFaceDescription', + buildDefaults: () => ({ + id: getId('mediapipe_face_processor'), + type: 'mediapipe_face_processor', + max_faces: 1, + min_confidence: 0.5, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + 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: getId('midas_depth_image_processor'), + type: 'midas_depth_image_processor', + a_mult: 2, + bg_th: 0.1, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + mlsd_image_processor: { + type: 'mlsd_image_processor', + labelTKey: 'controlnet.mlsd', + descriptionTKey: 'controlnet.mlsdDescription', + buildDefaults: () => ({ + id: getId('mlsd_image_processor'), + type: 'mlsd_image_processor', + thr_d: 0.1, + thr_v: 0.1, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + detect_resolution: minDim(image), + image_resolution: minDim(image), + }), + }, + normalbae_image_processor: { + type: 'normalbae_image_processor', + labelTKey: 'controlnet.normalBae', + descriptionTKey: 'controlnet.normalBaeDescription', + buildDefaults: () => ({ + id: getId('normalbae_image_processor'), + type: 'normalbae_image_processor', + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + 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: getId('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.imageName }, + image_resolution: minDim(image), + }), + }, + pidi_image_processor: { + type: 'pidi_image_processor', + labelTKey: 'controlnet.pidi', + descriptionTKey: 'controlnet.pidiDescription', + buildDefaults: () => ({ + id: getId('pidi_image_processor'), + type: 'pidi_image_processor', + scribble: false, + safe: false, + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + 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: getId('zoe_depth_image_processor'), + type: 'zoe_depth_image_processor', + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + }), + }, +}; + export const initialControlNet: Omit = { type: 'controlnet', model: null, @@ -303,6 +415,7 @@ export const initialControlNet: Omit = { controlMode: 'balanced', image: null, processedImage: null, + isProcessingImage: false, processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), }; @@ -313,6 +426,7 @@ export const initialT2IAdapter: Omit = { beginEndStepPct: [0, 1], image: null, processedImage: null, + isProcessingImage: false, processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), }; From 47ee08db919ef2b618aef805e8ca5bbd6215080b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 08:21:03 +1000 Subject: [PATCH 21/74] fix(ui): processor select styling --- .../ControlAdapterProcessorTypeSelect.tsx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx index 5f34946af5..b6bba6301f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx @@ -48,20 +48,21 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro const value = useMemo(() => options.find((o) => o.value === config?.type) ?? null, [options, config?.type]); return ( - - - {t('controlnet.processor')} - - + + + + {t('controlnet.processor')} + - } - variant="ghost" - /> - - + + } + variant="ghost" + size="sm" + /> + ); }); From ca1c3c08733f35323a2655f174445b684c4ce69d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 08:27:16 +1000 Subject: [PATCH 22/74] fix(ui): do not re-process if processor config hasn't changed --- .../listeners/controlAdapterPreprocessor.ts | 10 +++++-- .../controlLayers/util/controlAdapters.ts | 30 +++++++++---------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index 9cb7efe572..db33a0541d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -14,6 +14,7 @@ import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapte import { isImageOutput } from 'features/nodes/types/common'; import { addToast } from 'features/system/store/systemSlice'; import { t } from 'i18next'; +import { isEqual } from 'lodash-es'; import { imagesApi } from 'services/api/endpoints/images'; import { queueApi } from 'services/api/endpoints/queue'; import type { BatchConfig, ImageDTO } from 'services/api/types'; @@ -27,8 +28,11 @@ const log = logger('session'); export const addControlAdapterPreprocessor = (startAppListening: AppStartListening) => { startAppListening({ matcher, - effect: async (action, { dispatch, getState, cancelActiveListeners, delay, take }) => { + effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take }) => { const { layerId } = action.payload; + const precheckLayerOriginal = getOriginalState() + .controlLayers.present.layers.filter(isControlAdapterLayer) + .find((l) => l.id === layerId); const precheckLayer = getState() .controlLayers.present.layers.filter(isControlAdapterLayer) .find((l) => l.id === layerId); @@ -42,7 +46,9 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // Layer doesn't have a processor config !precheckLayer.controlAdapter.processorConfig || // Layer is already processing an image - precheckLayer.controlAdapter.isProcessingImage + precheckLayer.controlAdapter.isProcessingImage || + // Processor config is the same + isEqual(precheckLayerOriginal?.controlAdapter.processorConfig, precheckLayer.controlAdapter.processorConfig) ) { return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 261cfd2f85..ba10510ba7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -26,7 +26,6 @@ import type { T2IAdapterModelConfig, ZoeDepthImageProcessorInvocation, } from 'services/api/types'; -import { v4 as uuidv4 } from 'uuid'; import { z } from 'zod'; const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']); @@ -164,7 +163,6 @@ type ProcessorData = { }; const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); -const getId = (type: ProcessorType): string => `${type}_${uuidv4()}`; type CAProcessorsData = { [key in ProcessorType]: ProcessorData; @@ -184,7 +182,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.canny', descriptionTKey: 'controlnet.cannyDescription', buildDefaults: () => ({ - id: getId('canny_image_processor'), + id: 'canny_image_processor', type: 'canny_image_processor', low_threshold: 100, high_threshold: 200, @@ -202,7 +200,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.colorMap', descriptionTKey: 'controlnet.colorMapDescription', buildDefaults: () => ({ - id: getId('color_map_image_processor'), + id: 'color_map_image_processor', type: 'color_map_image_processor', color_map_tile_size: 64, }), @@ -217,7 +215,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.contentShuffle', descriptionTKey: 'controlnet.contentShuffleDescription', buildDefaults: (baseModel) => ({ - id: getId('content_shuffle_image_processor'), + id: 'content_shuffle_image_processor', type: 'content_shuffle_image_processor', h: baseModel === 'sdxl' ? 1024 : 512, w: baseModel === 'sdxl' ? 1024 : 512, @@ -235,7 +233,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.depthAnything', descriptionTKey: 'controlnet.depthAnythingDescription', buildDefaults: () => ({ - id: getId('depth_anything_image_processor'), + id: 'depth_anything_image_processor', type: 'depth_anything_image_processor', model_size: 'small', }), @@ -250,7 +248,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.hed', descriptionTKey: 'controlnet.hedDescription', buildDefaults: () => ({ - id: getId('hed_image_processor'), + id: 'hed_image_processor', type: 'hed_image_processor', scribble: false, }), @@ -266,7 +264,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.lineartAnime', descriptionTKey: 'controlnet.lineartAnimeDescription', buildDefaults: () => ({ - id: getId('lineart_anime_image_processor'), + id: 'lineart_anime_image_processor', type: 'lineart_anime_image_processor', }), buildNode: (image, config) => ({ @@ -281,7 +279,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.lineart', descriptionTKey: 'controlnet.lineartDescription', buildDefaults: () => ({ - id: getId('lineart_image_processor'), + id: 'lineart_image_processor', type: 'lineart_image_processor', coarse: false, }), @@ -297,7 +295,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.mediapipeFace', descriptionTKey: 'controlnet.mediapipeFaceDescription', buildDefaults: () => ({ - id: getId('mediapipe_face_processor'), + id: 'mediapipe_face_processor', type: 'mediapipe_face_processor', max_faces: 1, min_confidence: 0.5, @@ -314,7 +312,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.depthMidas', descriptionTKey: 'controlnet.depthMidasDescription', buildDefaults: () => ({ - id: getId('midas_depth_image_processor'), + id: 'midas_depth_image_processor', type: 'midas_depth_image_processor', a_mult: 2, bg_th: 0.1, @@ -331,7 +329,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.mlsd', descriptionTKey: 'controlnet.mlsdDescription', buildDefaults: () => ({ - id: getId('mlsd_image_processor'), + id: 'mlsd_image_processor', type: 'mlsd_image_processor', thr_d: 0.1, thr_v: 0.1, @@ -348,7 +346,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.normalBae', descriptionTKey: 'controlnet.normalBaeDescription', buildDefaults: () => ({ - id: getId('normalbae_image_processor'), + id: 'normalbae_image_processor', type: 'normalbae_image_processor', }), buildNode: (image, config) => ({ @@ -363,7 +361,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.dwOpenpose', descriptionTKey: 'controlnet.dwOpenposeDescription', buildDefaults: () => ({ - id: getId('dw_openpose_image_processor'), + id: 'dw_openpose_image_processor', type: 'dw_openpose_image_processor', draw_body: true, draw_face: false, @@ -380,7 +378,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.pidi', descriptionTKey: 'controlnet.pidiDescription', buildDefaults: () => ({ - id: getId('pidi_image_processor'), + id: 'pidi_image_processor', type: 'pidi_image_processor', scribble: false, safe: false, @@ -397,7 +395,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { labelTKey: 'controlnet.depthZoe', descriptionTKey: 'controlnet.depthZoeDescription', buildDefaults: () => ({ - id: getId('zoe_depth_image_processor'), + id: 'zoe_depth_image_processor', type: 'zoe_depth_image_processor', }), buildNode: (image, config) => ({ From 45c2ac41d5958d407baa02d5abb71f6faf17c8a3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 08:32:54 +1000 Subject: [PATCH 23/74] feat(ui): processor layout/styling --- .../components/ControlAndIPAdapter/ControlAdapter.tsx | 4 ++-- .../ControlAdapterProcessorTypeSelect.tsx | 2 +- .../ControlAndIPAdapter/processors/CannyProcessor.tsx | 4 ++-- .../ControlAndIPAdapter/processors/ColorMapProcessor.tsx | 2 +- .../processors/ContentShuffleProcessor.tsx | 6 +++--- .../ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx | 6 +++--- .../processors/DepthAnythingProcessor.tsx | 2 +- .../ControlAndIPAdapter/processors/HedProcessor.tsx | 2 +- .../ControlAndIPAdapter/processors/LineartProcessor.tsx | 2 +- .../processors/MediapipeFaceProcessor.tsx | 4 ++-- .../ControlAndIPAdapter/processors/MidasDepthProcessor.tsx | 4 ++-- .../ControlAndIPAdapter/processors/MlsdImageProcessor.tsx | 4 ++-- .../ControlAndIPAdapter/processors/PidiProcessor.tsx | 4 ++-- .../ControlAndIPAdapter/processors/ProcessorWrapper.tsx | 2 +- 14 files changed, 24 insertions(+), 24 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx index ecdfa46ef6..986e9a6373 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx @@ -95,13 +95,13 @@ export const ControlAdapter = memo( {isExpanded && ( - <> + - + )} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx index b6bba6301f..8041f7f6ba 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx @@ -51,7 +51,7 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro - {t('controlnet.processor')} + {t('controlnet.processor')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx index c4d6031912..999c8c7764 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx @@ -27,7 +27,7 @@ export const CannyProcessor = ({ onChange, config }: Props) => { return ( - {t('controlnet.lowThreshold')} + {t('controlnet.lowThreshold')} { /> - {t('controlnet.highThreshold')} + {t('controlnet.highThreshold')} { return ( - {t('controlnet.colorMapTileSize')} + {t('controlnet.colorMapTileSize')} { return ( - {t('controlnet.w')} + {t('controlnet.w')} { - {t('controlnet.h')} + {t('controlnet.h')} { - {t('controlnet.f')} + {t('controlnet.f')} { - {t('controlnet.body')} + {t('controlnet.body')} - {t('controlnet.face')} + {t('controlnet.face')} - {t('controlnet.hands')} + {t('controlnet.hands')} { return ( - {t('controlnet.modelSize')} + {t('controlnet.modelSize')} { return ( - {t('controlnet.scribble')} + {t('controlnet.scribble')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx index d882543af4..aeb4121a36 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx @@ -22,7 +22,7 @@ export const LineartProcessor = memo(({ onChange, config }: Props) => { return ( - {t('controlnet.coarse')} + {t('controlnet.coarse')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx index a3c2936916..72f0d52dc5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx @@ -29,7 +29,7 @@ export const MediapipeFaceProcessor = memo(({ onChange, config }: Props) => { return ( - {t('controlnet.maxFaces')} + {t('controlnet.maxFaces')} { /> - {t('controlnet.minConfidence')} + {t('controlnet.minConfidence')} { return ( - {t('controlnet.amult')} + {t('controlnet.amult')} { /> - {t('controlnet.bgth')} + {t('controlnet.bgth')} { return ( - {t('controlnet.w')} + {t('controlnet.w')} { /> - {t('controlnet.h')} + {t('controlnet.h')} { return ( - {t('controlnet.scribble')} + {t('controlnet.scribble')} - {t('controlnet.safe')} + {t('controlnet.safe')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx index 0b99887b53..2b2468703b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx @@ -6,7 +6,7 @@ type Props = PropsWithChildren; const ProcessorWrapper = (props: Props) => { return ( - + {props.children} ); From 6effa1962694297ac6e19202696095e4a471a8b6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 08:39:08 +1000 Subject: [PATCH 24/74] fix(ui): edge cases in auto-process --- .../listeners/controlAdapterPreprocessor.ts | 23 +++++++++++-------- .../controlLayers/store/controlLayersSlice.ts | 13 +++++++++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index db33a0541d..50395dc9dc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -38,17 +38,20 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni .find((l) => l.id === layerId); // Conditions to bail + const layerDoesNotExist = !precheckLayer; + const layerHasNoImage = !precheckLayer?.controlAdapter.image; + const layerHasNoProcessorConfig = !precheckLayer?.controlAdapter.processorConfig; + const layerIsAlreadyProcessingImage = precheckLayer?.controlAdapter.isProcessingImage; + const areImageAndProcessorUnchanged = + isEqual(precheckLayer?.controlAdapter.image, precheckLayerOriginal?.controlAdapter.image) && + isEqual(precheckLayer?.controlAdapter.processorConfig, precheckLayerOriginal?.controlAdapter.processorConfig); + if ( - // Layer doesn't exist - !precheckLayer || - // Layer doesn't have an image - !precheckLayer.controlAdapter.image || - // Layer doesn't have a processor config - !precheckLayer.controlAdapter.processorConfig || - // Layer is already processing an image - precheckLayer.controlAdapter.isProcessingImage || - // Processor config is the same - isEqual(precheckLayerOriginal?.controlAdapter.processorConfig, precheckLayer.controlAdapter.processorConfig) + layerDoesNotExist || + layerHasNoImage || + layerHasNoProcessorConfig || + areImageAndProcessorUnchanged || + layerIsAlreadyProcessingImage ) { return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index fa179a3bbd..d4c43fdc89 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -250,8 +250,17 @@ export const controlLayersSlice = createSlice({ layer.bbox = null; layer.bboxNeedsUpdate = true; layer.isEnabled = true; - layer.controlAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - layer.controlAdapter.processedImage = null; + if (imageDTO) { + const newImage = imageDTOToImageWithDims(imageDTO); + if (isEqual(newImage, layer.controlAdapter.image)) { + return; + } + layer.controlAdapter.image = newImage; + layer.controlAdapter.processedImage = null; + } else { + layer.controlAdapter.image = null; + layer.controlAdapter.processedImage = null; + } }, caLayerProcessedImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; From 9437d701b206ac7a40f78763ae11fc88f1477a0b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 08:46:09 +1000 Subject: [PATCH 25/74] fix(ui): disable clear processor when no processor selected --- .../ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx index 8041f7f6ba..46e6131353 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx @@ -56,8 +56,9 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro } variant="ghost" size="sm" From ffba4871d050bd6994ace31e56f9cc427c13d8f1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 08:46:21 +1000 Subject: [PATCH 26/74] tidy(ui): "scribble" -> "Scribble" --- invokeai/frontend/web/public/locales/en.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index fd6eef527b..c80283b664 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -225,7 +225,7 @@ "composition": "Composition Only", "safe": "Safe", "saveControlImage": "Save Control Image", - "scribble": "scribble", + "scribble": "Scribble", "selectModel": "Select a model", "selectCLIPVisionModel": "Select a CLIP Vision model", "setControlImageDimensions": "Copy size to W/H (optimize for model)", @@ -1543,6 +1543,8 @@ "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalIPAdapter": "Global $t(common.ipAdapter)", "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)", - "opacityFilter": "Opacity Filter" + "opacityFilter": "Opacity Filter", + "clearProcessor": "Clear Processor", + "resetProcessor": "Reset Processor to Defaults" } } From efb571401ce3b4fef6bf546e461d33eb43ff32fb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 08:50:19 +1000 Subject: [PATCH 27/74] feat(ui): tweak control adapter layout --- .../ControlAndIPAdapter/ControlAdapter.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx index 986e9a6373..3a160f91d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx @@ -1,4 +1,4 @@ -import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; +import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox'; import type { ControlMode, @@ -48,7 +48,7 @@ export const ControlAdapter = memo( const [isExpanded, toggleIsExpanded] = useToggle(false); return ( - + @@ -71,7 +71,7 @@ export const ControlAdapter = memo( } /> - + {controlAdapter.type === 'controlnet' && ( {isExpanded && ( - - - - + <> + + + + + + )} ); From 8ceb94497e335d1263d85110b7d2e4287fae7b38 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 08:56:13 +1000 Subject: [PATCH 28/74] fix(ui): fix canvas rendering of control images --- .../controlLayers/store/controlLayersSlice.ts | 3 +++ .../features/controlLayers/util/renderers.ts | 25 +++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index d4c43fdc89..13b6188692 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -305,6 +305,9 @@ export const controlLayersSlice = createSlice({ const { layerId, processorConfig } = action.payload; const layer = selectCALayerOrThrow(state, layerId); layer.controlAdapter.processorConfig = processorConfig; + if (!processorConfig) { + layer.controlAdapter.processedImage = null; + } }, caLayerIsFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => { const { layerId, isFilterEnabled } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index c79278b03d..85f48baa6e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -431,29 +431,30 @@ const updateControlNetLayerImageSource = async ( konvaLayer: Konva.Layer, reduxLayer: ControlAdapterLayer ) => { - if (reduxLayer.controlAdapter.image) { - const { imageName } = reduxLayer.controlAdapter.image; + const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image; + if (image) { + const { imageName } = image; const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); const imageDTO = await req.unwrap(); req.unsubscribe(); - const image = new Image(); + const imageEl = new Image(); const imageId = getCALayerImageId(reduxLayer.id, imageName); - image.onload = () => { + imageEl.onload = () => { // Find the existing image or create a new one - must find using the name, bc the id may have just changed const konvaImage = - konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`) ?? createControlNetLayerImage(konvaLayer, image); + konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`) ?? createControlNetLayerImage(konvaLayer, imageEl); // Update the image's attributes konvaImage.setAttrs({ id: imageId, - image, + image: imageEl, }); updateControlNetLayerImageAttrs(stage, konvaImage, reduxLayer); // Must cache after this to apply the filters konvaImage.cache(); - image.id = imageId; + imageEl.id = imageId; }; - image.src = imageDTO.image_url; + imageEl.src = imageDTO.image_url; } else { konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`)?.destroy(); } @@ -499,12 +500,10 @@ const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay const canvasImageSource = konvaImage?.image(); let imageSourceNeedsUpdate = false; if (canvasImageSource instanceof HTMLImageElement) { - if ( - reduxLayer.controlAdapter.image && - canvasImageSource.id !== getCALayerImageId(reduxLayer.id, reduxLayer.controlAdapter.image.imageName) - ) { + const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image; + if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) { imageSourceNeedsUpdate = true; - } else if (!reduxLayer.controlAdapter.image) { + } else if (!image) { imageSourceNeedsUpdate = true; } } else if (!canvasImageSource) { From 1b617768cf06545417e2d701ed766938a1714de2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 09:12:14 +1000 Subject: [PATCH 29/74] fix(ui): canvas infinite loop when setting bbox dims When typing in a number into the w/h number inputs, if the number is less than the step, it appears the value of 0 is used. This is unexpected; it means Chakra isn't clamping the value correctly (or maybe our wrapper isn't clamping it). Add checks to never bail if the width or height value from the number input component is 0. --- .../components/ImageSettingsAccordion/ImageSizeCanvas.tsx | 6 ++++++ .../components/ImageSettingsAccordion/ImageSizeLinear.tsx | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx index 878174fe75..1dc8f49b78 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx @@ -16,6 +16,9 @@ export const ImageSizeCanvas = memo(() => { const onChangeWidth = useCallback( (width: number) => { + if (width === 0) { + return; + } dispatch(setBoundingBoxDimensions({ width }, optimalDimension)); }, [dispatch, optimalDimension] @@ -23,6 +26,9 @@ export const ImageSizeCanvas = memo(() => { const onChangeHeight = useCallback( (height: number) => { + if (height === 0) { + return; + } dispatch(setBoundingBoxDimensions({ height }, optimalDimension)); }, [dispatch, optimalDimension] diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx index 7e436556da..0b28200ca2 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx @@ -18,6 +18,9 @@ export const ImageSizeLinear = memo(() => { const onChangeWidth = useCallback( (width: number) => { + if (width === 0) { + return; + } dispatch(widthChanged({ width })); }, [dispatch] @@ -25,6 +28,9 @@ export const ImageSizeLinear = memo(() => { const onChangeHeight = useCallback( (height: number) => { + if (height === 0) { + return; + } dispatch(heightChanged({ height })); }, [dispatch] From 311ba8c04bee5476c9be89740b216bb130ef63c1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 09:36:26 +1000 Subject: [PATCH 30/74] fix(ui): ensure canvas size is correctly updated when model changed Closes #6293 --- .../web/src/features/canvas/store/canvasSlice.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index c316fee2b1..a22f23d9d3 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -8,6 +8,7 @@ import calculateScale from 'features/canvas/util/calculateScale'; import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants'; import floorCoordinates from 'features/canvas/util/floorCoordinates'; import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions'; +import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import { modelChanged } from 'features/parameters/store/generationSlice'; @@ -588,8 +589,9 @@ export const canvasSlice = createSlice({ }, extraReducers: (builder) => { builder.addCase(modelChanged, (state, action) => { - if (action.meta.previousModel?.base === action.payload?.base) { - // The base model hasn't changed, we don't need to optimize the size + const newModel = action.payload; + if (!newModel || action.meta.previousModel?.base === newModel.base) { + // Model was cleared or the base didn't change return; } const optimalDimension = getOptimalDimension(action.payload); @@ -597,14 +599,8 @@ export const canvasSlice = createSlice({ if (getIsSizeOptimal(width, height, optimalDimension)) { return; } - setBoundingBoxDimensionsReducer( - state, - { - width, - height, - }, - optimalDimension - ); + const newSize = calculateNewSize(state.aspectRatio.value, optimalDimension * optimalDimension); + setBoundingBoxDimensionsReducer(state, newSize, optimalDimension); }); builder.addCase(socketQueueItemStatusChanged, (state, action) => { From 6e966909ab055c56b07f60c89c4904b7ff7289e1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 09:49:24 +1000 Subject: [PATCH 31/74] chore(ui): lint --- .../store/controlAdaptersSlice.ts | 4 +--- .../controlLayers/store/controlLayersSlice.ts | 19 ++----------------- .../src/features/controlLayers/store/types.ts | 2 +- .../controlLayers/util/controlAdapters.ts | 14 +++++++------- 4 files changed, 11 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts index 0c1ac20200..f8afde677c 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts @@ -1,5 +1,5 @@ import type { PayloadAction, Update } from '@reduxjs/toolkit'; -import { createEntityAdapter, createSlice, isAnyOf } from '@reduxjs/toolkit'; +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; import { getSelectorsOptions } from 'app/store/createMemoizedSelector'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; @@ -481,8 +481,6 @@ export const { t2iAdaptersReset, } = controlAdaptersSlice.actions; -export const isAnyControlAdapterAdded = isAnyOf(controlAdapterAdded, controlAdapterRecalled); - export const selectControlAdaptersSlice = (state: RootState) => state.controlAdapters; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 13b6188692..5ab7b87ba0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -88,7 +88,7 @@ export const selectIPALayerOrThrow = (state: ControlLayersState, layerId: string assert(isIPAdapterLayer(layer)); return layer; }; -export const selectCAOrIPALayerOrThrow = ( +const selectCAOrIPALayerOrThrow = ( state: ControlLayersState, layerId: string ): ControlAdapterLayer | IPAdapterLayer => { @@ -96,7 +96,7 @@ export const selectCAOrIPALayerOrThrow = ( assert(isControlAdapterLayer(layer) || isIPAdapterLayer(layer)); return layer; }; -export const selectRGLayerOrThrow = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { +const selectRGLayerOrThrow = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { const layer = state.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer)); return layer; @@ -348,19 +348,6 @@ export const controlLayersSlice = createSlice({ const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - ipaLayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { - const { layerId, weight } = action.payload; - const layer = selectIPALayerOrThrow(state, layerId); - layer.ipAdapter.weight = weight; - }, - ipaLayerBeginEndStepPctChanged: ( - state, - action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> - ) => { - const { layerId, beginEndStepPct } = action.payload; - const layer = selectIPALayerOrThrow(state, layerId); - layer.ipAdapter.beginEndStepPct = beginEndStepPct; - }, ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethod }>) => { const { layerId, method } = action.payload; const layer = selectIPALayerOrThrow(state, layerId); @@ -744,8 +731,6 @@ export const { // IPA Layers ipaLayerAdded, ipaLayerImageChanged, - ipaLayerWeightChanged, - ipaLayerBeginEndStepPctChanged, ipaLayerMethodChanged, ipaLayerModelChanged, ipaLayerCLIPVisionModelChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 241c8f2f84..a4d88f3a0a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,4 +1,4 @@ -import type { ControlNetConfig, IPAdapterConfig,T2IAdapterConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlLayers/util/controlAdapters'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { ParameterAutoNegative, diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index ba10510ba7..4b756b33e7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -46,7 +46,7 @@ export type DepthAnythingProcessorConfig = Required< Pick >; export type HedProcessorConfig = Required>; -export type LineartAnimeProcessorConfig = Required>; +type LineartAnimeProcessorConfig = Required>; export type LineartProcessorConfig = Required>; export type MediapipeFaceProcessorConfig = Required< Pick @@ -55,12 +55,12 @@ export type MidasDepthProcessorConfig = Required< Pick >; export type MlsdProcessorConfig = Required>; -export type NormalbaeProcessorConfig = Required>; +type NormalbaeProcessorConfig = Required>; export type DWOpenposeProcessorConfig = Required< Pick >; export type PidiProcessorConfig = Required>; -export type ZoeDepthProcessorConfig = Required>; +type ZoeDepthProcessorConfig = Required>; export type ProcessorConfig = | CannyProcessorConfig @@ -132,7 +132,7 @@ export type IPAdapterConfig = { beginEndStepPct: [number, number]; }; -export const zProcessorType = z.enum([ +const zProcessorType = z.enum([ 'canny_image_processor', 'color_map_image_processor', 'content_shuffle_image_processor', @@ -405,7 +405,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { }, }; -export const initialControlNet: Omit = { +const initialControlNet: Omit = { type: 'controlnet', model: null, weight: 1, @@ -417,7 +417,7 @@ export const initialControlNet: Omit = { processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), }; -export const initialT2IAdapter: Omit = { +const initialT2IAdapter: Omit = { type: 't2i_adapter', model: null, weight: 1, @@ -428,7 +428,7 @@ export const initialT2IAdapter: Omit = { processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), }; -export const initialIPAdapter: Omit = { +const initialIPAdapter: Omit = { type: 'ip_adapter', image: null, model: null, From 54acd3f2b1133e5cbd1bbeef571d656dbfa10fd3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 09:50:06 +1000 Subject: [PATCH 32/74] ci(ui): restore error status for circular deps --- invokeai/frontend/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 9e661e0737..2730367fe5 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -25,7 +25,7 @@ "typegen": "node scripts/typegen.js", "preview": "vite preview", "lint:knip": "knip", - "lint:dpdm": "dpdm --no-warning --no-tree --transform --exit-code circular:0 src/main.tsx", + "lint:dpdm": "dpdm --no-warning --no-tree --transform --exit-code circular:1 src/main.tsx", "lint:eslint": "eslint --max-warnings=0 .", "lint:prettier": "prettier --check .", "lint:tsc": "tsc --noEmit", From 6f572e1cce1c6ead5952f05d044fc0390afd0c36 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 10:01:43 +1000 Subject: [PATCH 33/74] fix(ui): convert t2i to cnet and vice-versa when model changes --- .../controlLayers/store/controlLayersSlice.ts | 15 ++++++++++++++- .../controlLayers/util/controlAdapters.ts | 17 ++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 5ab7b87ba0..c27a98c826 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -12,7 +12,12 @@ import type { ProcessorConfig, T2IAdapterConfig, } from 'features/controlLayers/util/controlAdapters'; -import { buildControlAdapterProcessor, imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import { + buildControlAdapterProcessor, + controlNetToT2IAdapter, + imageDTOToImageWithDims, + t2iAdapterToControlNet, +} from 'features/controlLayers/util/controlAdapters'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; @@ -284,6 +289,14 @@ export const controlLayersSlice = createSlice({ return; } layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig); + + // We may need to convert the CA to match the model + if (layer.controlAdapter.type === 't2i_adapter' && layer.controlAdapter.model.type === 'controlnet') { + layer.controlAdapter = t2iAdapterToControlNet(layer.controlAdapter); + } else if (layer.controlAdapter.type === 'controlnet' && layer.controlAdapter.model.type === 't2i_adapter') { + layer.controlAdapter = controlNetToT2IAdapter(layer.controlAdapter); + } + const candidateProcessorConfig = buildControlAdapterProcessor(modelConfig); if (candidateProcessorConfig?.type !== layer.controlAdapter.processorConfig?.type) { // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 4b756b33e7..6cedc81a0b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -4,7 +4,7 @@ import type { ParameterIPAdapterModel, ParameterT2IAdapterModel, } from 'features/parameters/types/parameterSchemas'; -import { merge } from 'lodash-es'; +import { merge, omit } from 'lodash-es'; import type { BaseModelType, CannyImageProcessorInvocation, @@ -466,3 +466,18 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO) width, height, }); + +export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfig): ControlNetConfig => { + return { + ...deepClone(t2iAdapter), + type: 'controlnet', + controlMode: initialControlNet.controlMode, + }; +}; + +export const controlNetToT2IAdapter = (controlNet: ControlNetConfig): T2IAdapterConfig => { + return { + ...omit(deepClone(controlNet), 'controlMode'), + type: 't2i_adapter', + }; +}; From c35625eb443aadb28c4eb1575f7e38b342a3d972 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 10:09:29 +1000 Subject: [PATCH 34/74] feat(ui): processor layout changes --- .../components/ControlAndIPAdapter/ControlAdapter.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx index 3a160f91d4..087f634d73 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx @@ -71,8 +71,8 @@ export const ControlAdapter = memo( } /> - - + + {controlAdapter.type === 'controlnet' && ( Date: Thu, 2 May 2024 11:28:31 +1000 Subject: [PATCH 35/74] fix(nodes): fix constraints in cnet processors There were some invalid constraints with the processors - minimum of 0 for resolution or multiple of 64 for resolution. Made minimum 1px and no multiple ofs. --- .../controlnet_image_processors.py | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 6510d2f74a..d2f01622b2 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -165,13 +165,13 @@ class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithBoard): title="Canny Processor", tags=["controlnet", "canny"], category="controlnet", - version="1.3.2", + version="1.3.3", ) class CannyImageProcessorInvocation(ImageProcessorInvocation): """Canny edge detection for ControlNet""" - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) low_threshold: int = InputField( default=100, ge=0, le=255, description="The low threshold of the Canny pixel gradient (0-255)" ) @@ -199,13 +199,13 @@ class CannyImageProcessorInvocation(ImageProcessorInvocation): title="HED (softedge) Processor", tags=["controlnet", "hed", "softedge"], category="controlnet", - version="1.2.2", + version="1.2.3", ) class HedImageProcessorInvocation(ImageProcessorInvocation): """Applies HED edge detection to image""" - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) # safe not supported in controlnet_aux v0.0.3 # safe: bool = InputField(default=False, description=FieldDescriptions.safe_mode) scribble: bool = InputField(default=False, description=FieldDescriptions.scribble_mode) @@ -228,13 +228,13 @@ class HedImageProcessorInvocation(ImageProcessorInvocation): title="Lineart Processor", tags=["controlnet", "lineart"], category="controlnet", - version="1.2.2", + version="1.2.3", ) class LineartImageProcessorInvocation(ImageProcessorInvocation): """Applies line art processing to image""" - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) coarse: bool = InputField(default=False, description="Whether to use coarse mode") def run_processor(self, image: Image.Image) -> Image.Image: @@ -250,13 +250,13 @@ class LineartImageProcessorInvocation(ImageProcessorInvocation): title="Lineart Anime Processor", tags=["controlnet", "lineart", "anime"], category="controlnet", - version="1.2.2", + version="1.2.3", ) class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation): """Applies line art anime processing to image""" - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) def run_processor(self, image: Image.Image) -> Image.Image: processor = LineartAnimeProcessor() @@ -273,15 +273,15 @@ class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation): title="Midas Depth Processor", tags=["controlnet", "midas"], category="controlnet", - version="1.2.3", + version="1.2.4", ) class MidasDepthImageProcessorInvocation(ImageProcessorInvocation): """Applies Midas depth processing to image""" a_mult: float = InputField(default=2.0, ge=0, description="Midas parameter `a_mult` (a = a_mult * PI)") bg_th: float = InputField(default=0.1, ge=0, description="Midas parameter `bg_th`") - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) # depth_and_normal not supported in controlnet_aux v0.0.3 # depth_and_normal: bool = InputField(default=False, description="whether to use depth and normal mode") @@ -304,13 +304,13 @@ class MidasDepthImageProcessorInvocation(ImageProcessorInvocation): title="Normal BAE Processor", tags=["controlnet"], category="controlnet", - version="1.2.2", + version="1.2.3", ) class NormalbaeImageProcessorInvocation(ImageProcessorInvocation): """Applies NormalBae processing to image""" - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) def run_processor(self, image): normalbae_processor = NormalBaeDetector.from_pretrained("lllyasviel/Annotators") @@ -321,13 +321,13 @@ class NormalbaeImageProcessorInvocation(ImageProcessorInvocation): @invocation( - "mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet", version="1.2.2" + "mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet", version="1.2.3" ) class MlsdImageProcessorInvocation(ImageProcessorInvocation): """Applies MLSD processing to image""" - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) thr_v: float = InputField(default=0.1, ge=0, description="MLSD parameter `thr_v`") thr_d: float = InputField(default=0.1, ge=0, description="MLSD parameter `thr_d`") @@ -344,13 +344,13 @@ class MlsdImageProcessorInvocation(ImageProcessorInvocation): @invocation( - "pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet", version="1.2.2" + "pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet", version="1.2.3" ) class PidiImageProcessorInvocation(ImageProcessorInvocation): """Applies PIDI processing to image""" - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) safe: bool = InputField(default=False, description=FieldDescriptions.safe_mode) scribble: bool = InputField(default=False, description=FieldDescriptions.scribble_mode) @@ -371,13 +371,13 @@ class PidiImageProcessorInvocation(ImageProcessorInvocation): title="Content Shuffle Processor", tags=["controlnet", "contentshuffle"], category="controlnet", - version="1.2.2", + version="1.2.3", ) class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation): """Applies content shuffle processing to image""" - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) h: int = InputField(default=512, ge=0, description="Content shuffle `h` parameter") w: int = InputField(default=512, ge=0, description="Content shuffle `w` parameter") f: int = InputField(default=256, ge=0, description="Content shuffle `f` parameter") @@ -401,7 +401,7 @@ class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation): title="Zoe (Depth) Processor", tags=["controlnet", "zoe", "depth"], category="controlnet", - version="1.2.2", + version="1.2.3", ) class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation): """Applies Zoe depth processing to image""" @@ -417,15 +417,15 @@ class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation): title="Mediapipe Face Processor", tags=["controlnet", "mediapipe", "face"], category="controlnet", - version="1.2.3", + version="1.2.4", ) class MediapipeFaceProcessorInvocation(ImageProcessorInvocation): """Applies mediapipe face processing to image""" max_faces: int = InputField(default=1, ge=1, description="Maximum number of faces to detect") min_confidence: float = InputField(default=0.5, ge=0, le=1, description="Minimum confidence for face detection") - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) def run_processor(self, image): mediapipe_face_processor = MediapipeFaceDetector() @@ -444,7 +444,7 @@ class MediapipeFaceProcessorInvocation(ImageProcessorInvocation): title="Leres (Depth) Processor", tags=["controlnet", "leres", "depth"], category="controlnet", - version="1.2.2", + version="1.2.3", ) class LeresImageProcessorInvocation(ImageProcessorInvocation): """Applies leres processing to image""" @@ -452,8 +452,8 @@ class LeresImageProcessorInvocation(ImageProcessorInvocation): thr_a: float = InputField(default=0, description="Leres parameter `thr_a`") thr_b: float = InputField(default=0, description="Leres parameter `thr_b`") boost: bool = InputField(default=False, description="Whether to use boost mode") - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) def run_processor(self, image): leres_processor = LeresDetector.from_pretrained("lllyasviel/Annotators") @@ -473,7 +473,7 @@ class LeresImageProcessorInvocation(ImageProcessorInvocation): title="Tile Resample Processor", tags=["controlnet", "tile"], category="controlnet", - version="1.2.2", + version="1.2.3", ) class TileResamplerProcessorInvocation(ImageProcessorInvocation): """Tile resampler processor""" @@ -513,13 +513,13 @@ class TileResamplerProcessorInvocation(ImageProcessorInvocation): title="Segment Anything Processor", tags=["controlnet", "segmentanything"], category="controlnet", - version="1.2.3", + version="1.2.4", ) class SegmentAnythingProcessorInvocation(ImageProcessorInvocation): """Applies segment anything processing to image""" - detect_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) def run_processor(self, image): # segment_anything_processor = SamDetector.from_pretrained("ybelkada/segment-anything", subfolder="checkpoints") @@ -560,12 +560,12 @@ class SamDetectorReproducibleColors(SamDetector): title="Color Map Processor", tags=["controlnet"], category="controlnet", - version="1.2.2", + version="1.2.3", ) class ColorMapImageProcessorInvocation(ImageProcessorInvocation): """Generates a color map from the provided image""" - color_map_tile_size: int = InputField(default=64, ge=0, description=FieldDescriptions.tile_size) + color_map_tile_size: int = InputField(default=64, ge=1, description=FieldDescriptions.tile_size) def run_processor(self, image: Image.Image): np_image = np.array(image, dtype=np.uint8) @@ -592,7 +592,7 @@ DEPTH_ANYTHING_MODEL_SIZES = Literal["large", "base", "small"] title="Depth Anything Processor", tags=["controlnet", "depth", "depth anything"], category="controlnet", - version="1.1.1", + version="1.1.2", ) class DepthAnythingImageProcessorInvocation(ImageProcessorInvocation): """Generates a depth map based on the Depth Anything algorithm""" @@ -600,7 +600,7 @@ class DepthAnythingImageProcessorInvocation(ImageProcessorInvocation): model_size: DEPTH_ANYTHING_MODEL_SIZES = InputField( default="small", description="The size of the depth model to use" ) - resolution: int = InputField(default=512, ge=64, multiple_of=64, description=FieldDescriptions.image_res) + resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) def run_processor(self, image: Image.Image): depth_anything_detector = DepthAnythingDetector() @@ -615,7 +615,7 @@ class DepthAnythingImageProcessorInvocation(ImageProcessorInvocation): title="DW Openpose Image Processor", tags=["controlnet", "dwpose", "openpose"], category="controlnet", - version="1.1.0", + version="1.1.1", ) class DWOpenposeImageProcessorInvocation(ImageProcessorInvocation): """Generates an openpose pose from an image using DWPose""" @@ -623,7 +623,7 @@ class DWOpenposeImageProcessorInvocation(ImageProcessorInvocation): draw_body: bool = InputField(default=True) draw_face: bool = InputField(default=False) draw_hands: bool = InputField(default=False) - image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) + image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) def run_processor(self, image: Image.Image): dw_openpose = DWOpenposeDetector() @@ -642,15 +642,15 @@ class DWOpenposeImageProcessorInvocation(ImageProcessorInvocation): title="Heuristic Resize", tags=["image, controlnet"], category="image", - version="1.0.0", + version="1.0.1", classification=Classification.Prototype, ) class HeuristicResizeInvocation(BaseInvocation): """Resize an image using a heuristic method. Preserves edge maps.""" image: ImageField = InputField(description="The image to resize") - width: int = InputField(default=512, gt=0, description="The width to resize to (px)") - height: int = InputField(default=512, gt=0, description="The height to resize to (px)") + width: int = InputField(default=512, ge=1, description="The width to resize to (px)") + height: int = InputField(default=512, ge=1, description="The height to resize to (px)") def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name, "RGB") From b1d8f3a3f9713eb36970a4e1dfa35fc3d7688dbf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 15:50:29 +1000 Subject: [PATCH 36/74] tidy(ui): revert changes to old CA implementation These changes were left over from the previous attempt to handle control adapters in control layers with the same logic. Control Layers are now handled totally separately, so these changes may be reverted. --- .../listeners/canvasImageToControlNet.ts | 4 +- .../listeners/canvasMaskToControlNet.ts | 4 +- .../listeners/controlNetAutoProcess.ts | 6 - .../listeners/controlNetImageProcessed.ts | 2 +- .../listeners/imageDropped.ts | 2 +- .../listeners/imageUploaded.ts | 2 +- .../listeners/modelSelected.ts | 4 +- .../components/ControlAdapterConfig.tsx | 2 +- .../ParamControlAdapterIPMethod.tsx | 6 +- .../parameters/ParamControlAdapterModel.tsx | 11 +- .../hooks/useControlAdapterIPMethod.ts | 8 +- .../store/controlAdaptersSlice.ts | 131 +++++------------- .../features/controlAdapters/store/types.ts | 4 - .../util/buildControlAdapter.ts | 4 - .../web/src/features/metadata/util/parsers.ts | 6 +- 15 files changed, 60 insertions(+), 136 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts index b1b19b35dc..55392ebff4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts @@ -48,10 +48,12 @@ export const addCanvasImageToControlNetListener = (startAppListening: AppStartLi }) ).unwrap(); + const { image_name } = imageDTO; + dispatch( controlAdapterImageChanged({ id, - controlImage: imageDTO, + controlImage: image_name, }) ); }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts index b3014277f1..569b4badc7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts @@ -58,10 +58,12 @@ export const addCanvasMaskToControlNetListener = (startAppListening: AppStartLis }) ).unwrap(); + const { image_name } = imageDTO; + dispatch( controlAdapterImageChanged({ id, - controlImage: imageDTO, + controlImage: image_name, }) ); }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts index 14af0246a2..e52df30681 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts @@ -12,7 +12,6 @@ import { selectControlAdapterById, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { isEqual } from 'lodash-es'; type AnyControlAdapterParamChangeAction = | ReturnType @@ -53,11 +52,6 @@ const predicate: AnyListenerPredicate = (action, state, prevState) => return false; } - if (prevCA.controlImage === ca.controlImage && isEqual(prevCA.processorNode, ca.processorNode)) { - // Don't re-process if the processor hasn't changed - return false; - } - const isProcessorSelected = processorType !== 'none'; const hasControlImage = Boolean(controlImage); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts index 08afc98836..0055866aa7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts @@ -91,7 +91,7 @@ export const addControlNetImageProcessedListener = (startAppListening: AppStartL dispatch( controlAdapterProcessedImageChanged({ id, - processedControlImage, + processedControlImage: processedControlImage.image_name, }) ); } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index de2ac3a39a..5db78ed75e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -76,7 +76,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => dispatch( controlAdapterImageChanged({ id, - controlImage: activeData.payload.imageDTO, + controlImage: activeData.payload.imageDTO.image_name, }) ); dispatch( diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index fd568ef1bd..d0edfafd57 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -101,7 +101,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis dispatch( controlAdapterImageChanged({ id, - controlImage: imageDTO, + controlImage: imageDTO.image_name, }) ); dispatch( diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index b69e56e84a..bc049cf498 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,7 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { - controlAdapterModelChanged, + controlAdapterIsEnabledChanged, selectControlAdapterAll, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { loraRemoved } from 'features/lora/store/loraSlice'; @@ -54,7 +54,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = // handle incompatible controlnets selectControlAdapterAll(state.controlAdapters).forEach((ca) => { if (ca.model?.base !== newBaseModel) { - dispatch(controlAdapterModelChanged({ id: ca.id, modelConfig: null })); + dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false })); modelsCleared += 1; } }); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx index 032e46f477..fcc816d75f 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx @@ -113,7 +113,7 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => { - {controlAdapterType === 'ip_adapter' && } + diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx index d7d91ab780..c7aaa9f26c 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx @@ -46,9 +46,13 @@ const ParamControlAdapterIPMethod = ({ id }: Props) => { const value = useMemo(() => options.find((o) => o.value === method), [options, method]); + if (!method) { + return null; + } + return ( - + {t('controlnet.ipAdapterMethod')} diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx index 73a7d695b3..00c7d5859d 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx @@ -102,9 +102,13 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { ); return ( - + - + { { const selector = useMemo( () => createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - assert(ca?.type === 'ip_adapter'); - return ca.method; + const cn = selectControlAdapterById(controlAdapters, id); + if (cn && cn?.type === 'ip_adapter') { + return cn.method; + } }), [id] ); diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts index f8afde677c..8ec397f99c 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts @@ -7,7 +7,7 @@ import { buildControlAdapter } from 'features/controlAdapters/util/buildControlA import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { merge, uniq } from 'lodash-es'; -import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; +import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; import { socketInvocationError } from 'services/events/actions'; import { v4 as uuidv4 } from 'uuid'; @@ -134,46 +134,23 @@ export const controlAdaptersSlice = createSlice({ const { id, isEnabled } = action.payload; caAdapter.updateOne(state, { id, changes: { isEnabled } }); }, - controlAdapterImageChanged: (state, action: PayloadAction<{ id: string; controlImage: ImageDTO | null }>) => { + controlAdapterImageChanged: ( + state, + action: PayloadAction<{ + id: string; + controlImage: string | null; + }> + ) => { const { id, controlImage } = action.payload; const ca = selectControlAdapterById(state, id); if (!ca) { return; } - if (isControlNetOrT2IAdapter(ca)) { - if (controlImage) { - const { image_name, width, height } = controlImage; - const processorNode = deepClone(ca.processorNode); - const minDim = Math.min(controlImage.width, controlImage.height); - if ('detect_resolution' in processorNode) { - processorNode.detect_resolution = minDim; - } - if ('image_resolution' in processorNode) { - processorNode.image_resolution = minDim; - } - if ('resolution' in processorNode) { - processorNode.resolution = minDim; - } - caAdapter.updateOne(state, { - id, - changes: { - processorNode, - controlImage: image_name, - controlImageDimensions: { width, height }, - processedControlImage: null, - }, - }); - } else { - caAdapter.updateOne(state, { - id, - changes: { controlImage: null, controlImageDimensions: null, processedControlImage: null }, - }); - } - } else { - // ip adapter - caAdapter.updateOne(state, { id, changes: { controlImage: controlImage?.image_name ?? null } }); - } + caAdapter.updateOne(state, { + id, + changes: { controlImage, processedControlImage: null }, + }); if (controlImage !== null && isControlNetOrT2IAdapter(ca) && ca.processorType !== 'none') { state.pendingControlImages.push(id); @@ -183,7 +160,7 @@ export const controlAdaptersSlice = createSlice({ state, action: PayloadAction<{ id: string; - processedControlImage: ImageDTO | null; + processedControlImage: string | null; }> ) => { const { id, processedControlImage } = action.payload; @@ -196,24 +173,12 @@ export const controlAdaptersSlice = createSlice({ return; } - if (processedControlImage) { - const { image_name, width, height } = processedControlImage; - caAdapter.updateOne(state, { - id, - changes: { - processedControlImage: image_name, - processedControlImageDimensions: { width, height }, - }, - }); - } else { - caAdapter.updateOne(state, { - id, - changes: { - processedControlImage: null, - processedControlImageDimensions: null, - }, - }); - } + caAdapter.updateOne(state, { + id, + changes: { + processedControlImage, + }, + }); state.pendingControlImages = state.pendingControlImages.filter((pendingId) => pendingId !== id); }, @@ -227,7 +192,7 @@ export const controlAdaptersSlice = createSlice({ state, action: PayloadAction<{ id: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig | null; + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig; }> ) => { const { id, modelConfig } = action.payload; @@ -236,11 +201,6 @@ export const controlAdaptersSlice = createSlice({ return; } - if (modelConfig === null) { - caAdapter.updateOne(state, { id, changes: { model: null } }); - return; - } - const model = zModelIdentifierField.parse(modelConfig); if (!isControlNetOrT2IAdapter(cn)) { @@ -248,36 +208,22 @@ export const controlAdaptersSlice = createSlice({ return; } + const update: Update = { + id, + changes: { model, shouldAutoConfig: true }, + }; + + update.changes.processedControlImage = null; + if (modelConfig.type === 'ip_adapter') { // should never happen... return; } - // We always update the model - const update: Update = { id, changes: { model } }; - - // Build the default processor for this model const processor = buildControlAdapterProcessor(modelConfig); - if (processor.processorType !== cn.processorNode.type) { - // If the processor type has changed, update the processor node - update.changes.shouldAutoConfig = true; - update.changes.processedControlImage = null; - update.changes.processorType = processor.processorType; - update.changes.processorNode = processor.processorNode; + update.changes.processorType = processor.processorType; + update.changes.processorNode = processor.processorNode; - if (cn.controlImageDimensions) { - const minDim = Math.min(cn.controlImageDimensions.width, cn.controlImageDimensions.height); - if ('detect_resolution' in update.changes.processorNode) { - update.changes.processorNode.detect_resolution = minDim; - } - if ('image_resolution' in update.changes.processorNode) { - update.changes.processorNode.image_resolution = minDim; - } - if ('resolution' in update.changes.processorNode) { - update.changes.processorNode.resolution = minDim; - } - } - } caAdapter.updateOne(state, update); }, controlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { @@ -394,23 +340,8 @@ export const controlAdaptersSlice = createSlice({ if (update.changes.shouldAutoConfig && modelConfig) { const processor = buildControlAdapterProcessor(modelConfig); - if (processor.processorType !== cn.processorNode.type) { - update.changes.processorType = processor.processorType; - update.changes.processorNode = processor.processorNode; - // Copy image resolution settings, urgh - if (cn.controlImageDimensions) { - const minDim = Math.min(cn.controlImageDimensions.width, cn.controlImageDimensions.height); - if ('detect_resolution' in update.changes.processorNode) { - update.changes.processorNode.detect_resolution = minDim; - } - if ('image_resolution' in update.changes.processorNode) { - update.changes.processorNode.image_resolution = minDim; - } - if ('resolution' in update.changes.processorNode) { - update.changes.processorNode.resolution = minDim; - } - } - } + update.changes.processorType = processor.processorType; + update.changes.processorNode = processor.processorNode; } caAdapter.updateOne(state, update); diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts b/invokeai/frontend/web/src/features/controlAdapters/store/types.ts index 80af59cd01..7e2f18af5c 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/store/types.ts @@ -225,9 +225,7 @@ export type ControlNetConfig = { controlMode: ControlMode; resizeMode: ResizeMode; controlImage: string | null; - controlImageDimensions: { width: number; height: number } | null; processedControlImage: string | null; - processedControlImageDimensions: { width: number; height: number } | null; processorType: ControlAdapterProcessorType; processorNode: RequiredControlAdapterProcessorNode; shouldAutoConfig: boolean; @@ -243,9 +241,7 @@ export type T2IAdapterConfig = { endStepPct: number; resizeMode: ResizeMode; controlImage: string | null; - controlImageDimensions: { width: number; height: number } | null; processedControlImage: string | null; - processedControlImageDimensions: { width: number; height: number } | null; processorType: ControlAdapterProcessorType; processorNode: RequiredControlAdapterProcessorNode; shouldAutoConfig: boolean; diff --git a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts b/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts index 7c9c28e2b3..ad7bdba363 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts @@ -20,9 +20,7 @@ export const initialControlNet: Omit = { controlMode: 'balanced', resizeMode: 'just_resize', controlImage: null, - controlImageDimensions: null, processedControlImage: null, - processedControlImageDimensions: null, processorType: 'canny_image_processor', processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation, shouldAutoConfig: true, @@ -37,9 +35,7 @@ export const initialT2IAdapter: Omit = { endStepPct: 1, resizeMode: 'just_resize', controlImage: null, - controlImageDimensions: null, processedControlImage: null, - processedControlImageDimensions: null, processorType: 'canny_image_processor', processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation, shouldAutoConfig: true, diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 5d2bd78784..3decea6737 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -286,9 +286,7 @@ const parseControlNet: MetadataParseFunc = async (meta controlMode: control_mode ?? initialControlNet.controlMode, resizeMode: resize_mode ?? initialControlNet.resizeMode, controlImage: image?.image_name ?? null, - controlImageDimensions: null, processedControlImage: processedImage?.image_name ?? null, - processedControlImageDimensions: null, processorType, processorNode, shouldAutoConfig: true, @@ -352,11 +350,9 @@ const parseT2IAdapter: MetadataParseFunc = async (meta endStepPct: end_step_percent ?? initialT2IAdapter.endStepPct, resizeMode: resize_mode ?? initialT2IAdapter.resizeMode, controlImage: image?.image_name ?? null, - controlImageDimensions: null, processedControlImage: processedImage?.image_name ?? null, - processedControlImageDimensions: null, - processorNode, processorType, + processorNode, shouldAutoConfig: true, id: uuidv4(), }; From f9555f03f5c31f90b0596d20a6be938d42d12d01 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 15:59:59 +1000 Subject: [PATCH 37/74] tidy(ui): "CONTROLNET_PROCESSORS" -> "CA_PROCESSOR_DATA" --- .../listeners/controlAdapterPreprocessor.ts | 4 ++-- .../ControlAdapterProcessorTypeSelect.tsx | 6 +++--- .../ControlAndIPAdapter/processors/CannyProcessor.tsx | 4 ++-- .../ControlAndIPAdapter/processors/ColorMapProcessor.tsx | 4 ++-- .../processors/ContentShuffleProcessor.tsx | 4 ++-- .../processors/DWOpenposeProcessor.tsx | 4 ++-- .../processors/DepthAnythingProcessor.tsx | 4 ++-- .../processors/MediapipeFaceProcessor.tsx | 4 ++-- .../processors/MidasDepthProcessor.tsx | 4 ++-- .../ControlAndIPAdapter/processors/MlsdImageProcessor.tsx | 4 ++-- .../web/src/features/controlLayers/hooks/addLayerHooks.ts | 4 ++-- .../src/features/controlLayers/util/controlAdapters.ts | 8 ++++---- 12 files changed, 27 insertions(+), 27 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index 50395dc9dc..7d5aa27f20 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -10,7 +10,7 @@ import { caLayerProcessorConfigChanged, isControlAdapterLayer, } from 'features/controlLayers/store/controlLayersSlice'; -import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { isImageOutput } from 'features/nodes/types/common'; import { addToast } from 'features/system/store/systemSlice'; import { t } from 'i18next'; @@ -76,7 +76,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni } // @ts-expect-error: TS isn't able to narrow the typing of buildNode and `config` will error... - const processorNode = CONTROLNET_PROCESSORS[config.type].buildNode(image, config); + const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config); const enqueueBatchArg: BatchConfig = { prepend: true, batch: { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx index 46e6131353..1d14d8606f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx @@ -4,7 +4,7 @@ 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 { CONTROLNET_PROCESSORS, isProcessorType } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA, isProcessorType } from 'features/controlLayers/util/controlAdapters'; import { configSelector } from 'features/system/store/configSelectors'; import { includes, map } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; @@ -26,7 +26,7 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro const { t } = useTranslation(); const disabledProcessors = useAppSelector(selectDisabledProcessors); const options = useMemo(() => { - return map(CONTROLNET_PROCESSORS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( + return map(CA_PROCESSOR_DATA, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( (o) => !includes(disabledProcessors, o.value) ); }, [disabledProcessors, t]); @@ -37,7 +37,7 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro onChange(null); } else { assert(isProcessorType(v.value)); - onChange(CONTROLNET_PROCESSORS[v.value].buildDefaults()); + onChange(CA_PROCESSOR_DATA[v.value].buildDefaults()); } }, [onChange] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx index 999c8c7764..cc3e9ba996 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { type CannyProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { type CannyProcessorConfig, CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['canny_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['canny_image_processor'].buildDefaults(); export const CannyProcessor = ({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx index bf2d0d5d6d..eda9af47a5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { type ColorMapProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { type ColorMapProcessorConfig, CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['color_map_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['color_map_image_processor'].buildDefaults(); export const ColorMapProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx index 041ab0ac9a..c03efd27c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { ContentShuffleProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['content_shuffle_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['content_shuffle_image_processor'].buildDefaults(); export const ContentShuffleProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx index 70d608f7a9..3bbe813dcc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx @@ -1,7 +1,7 @@ import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { DWOpenposeProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['dw_openpose_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['dw_openpose_image_processor'].buildDefaults(); export const DWOpenposeProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx index d2e14a17f9..00993789b1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx @@ -2,14 +2,14 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['depth_anything_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['depth_anything_image_processor'].buildDefaults(); export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx index 72f0d52dc5..0f45d83ef0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { CONTROLNET_PROCESSORS, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['mediapipe_face_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['mediapipe_face_processor'].buildDefaults(); export const MediapipeFaceProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx index 9078d14a53..1ce728984c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { MidasDepthProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['midas_depth_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['midas_depth_image_processor'].buildDefaults(); export const MidasDepthProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx index 5fc0c21ecd..b6eef311ef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { MlsdProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['mlsd_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['mlsd_image_processor'].buildDefaults(); export const MlsdImageProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 17f0d4bf2d..75ffd81202 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -4,7 +4,7 @@ import { buildControlNet, buildIPAdapter, buildT2IAdapter, - CONTROLNET_PROCESSORS, + CA_PROCESSOR_DATA, isProcessorType, } from 'features/controlLayers/util/controlAdapters'; import { zModelIdentifierField } from 'features/nodes/types/common'; @@ -31,7 +31,7 @@ export const useAddCALayer = () => { const id = uuidv4(); const defaultPreprocessor = model.default_settings?.preprocessor; const processorConfig = isProcessorType(defaultPreprocessor) - ? CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(baseModel) + ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(baseModel) : null; const builder = model.type === 'controlnet' ? buildControlNet : buildT2IAdapter; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 6cedc81a0b..afc168d749 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -176,7 +176,7 @@ type CAProcessorsData = { * * TODO: Generate from the OpenAPI schema */ -export const CONTROLNET_PROCESSORS: CAProcessorsData = { +export const CA_PROCESSOR_DATA: CAProcessorsData = { canny_image_processor: { type: 'canny_image_processor', labelTKey: 'controlnet.canny', @@ -414,7 +414,7 @@ const initialControlNet: Omit = { image: null, processedImage: null, isProcessingImage: false, - processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), + processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; const initialT2IAdapter: Omit = { @@ -425,7 +425,7 @@ const initialT2IAdapter: Omit = { image: null, processedImage: null, isProcessingImage: false, - processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), + processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; const initialIPAdapter: Omit = { @@ -457,7 +457,7 @@ export const buildControlAdapterProcessor = ( if (!isProcessorType(defaultPreprocessor)) { return null; } - const processorConfig = CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(modelConfig.base); + const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base); return processorConfig; }; From 2cde8a643e4b96455b4f656af12c82073c2866e3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 16:06:06 +1000 Subject: [PATCH 38/74] tidy(ui): suffix a control adapter types/objects with V2 Prevent mixing the old and new implementations up --- .../CALayer/CALayerControlAdapterWrapper.tsx | 4 +- .../ControlAndIPAdapter/ControlAdapter.tsx | 10 +-- .../ControlAdapterControlModeSelect.tsx | 10 +-- .../ControlAdapterImagePreview.tsx | 4 +- .../ControlAdapterProcessorTypeSelect.tsx | 4 +- .../ControlAndIPAdapter/IPAdapter.tsx | 8 +-- .../ControlAndIPAdapter/IPAdapterMethod.tsx | 12 ++-- .../IPAdapterModelSelect.tsx | 10 +-- .../processors/CannyProcessor.tsx | 2 +- .../processors/ColorMapProcessor.tsx | 2 +- .../IPALayer/IPALayerIPAdapterWrapper.tsx | 6 +- .../RGLayer/RGLayerIPAdapterWrapper.tsx | 6 +- .../controlLayers/hooks/addLayerHooks.ts | 4 +- .../controlLayers/store/controlLayersSlice.ts | 38 +++++----- .../src/features/controlLayers/store/types.ts | 12 ++-- .../util/controlAdapters.test.ts | 16 ++--- .../controlLayers/util/controlAdapters.ts | 72 +++++++++---------- .../util/graph/addControlLayersToGraph.ts | 28 ++++---- 18 files changed, 126 insertions(+), 122 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx index 6793a33f69..8ff1f9711f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx @@ -9,7 +9,7 @@ import { caOrIPALayerWeightChanged, selectCALayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; -import type { ControlMode, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import type { CALayerImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; import type { @@ -40,7 +40,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { ); const onChangeControlMode = useCallback( - (controlMode: ControlMode) => { + (controlMode: ControlModeV2) => { dispatch( caLayerControlModeChanged({ layerId, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx index 087f634d73..c28c40ecc1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx @@ -1,10 +1,10 @@ import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox'; import type { - ControlMode, - ControlNetConfig, + ControlModeV2, + ControlNetConfigV2, ProcessorConfig, - T2IAdapterConfig, + T2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import type { TypesafeDroppableData } from 'features/dnd/types'; import { memo } from 'react'; @@ -21,9 +21,9 @@ import { ControlAdapterProcessorTypeSelect } from './ControlAdapterProcessorType import { ControlAdapterWeight } from './ControlAdapterWeight'; type Props = { - controlAdapter: ControlNetConfig | T2IAdapterConfig; + controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; - onChangeControlMode: (controlMode: ControlMode) => void; + onChangeControlMode: (controlMode: ControlModeV2) => void; onChangeWeight: (weight: number) => void; onChangeProcessorConfig: (processorConfig: ProcessorConfig | null) => void; onChangeModel: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx index 34f4c85467..2c35ce51b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx @@ -1,15 +1,15 @@ 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 { ControlMode } from 'features/controlLayers/util/controlAdapters'; -import { isControlMode } from 'features/controlLayers/util/controlAdapters'; +import type { ControlModeV2 } from 'features/controlLayers/util/controlAdapters'; +import { isControlModeV2 } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; type Props = { - controlMode: ControlMode; - onChange: (controlMode: ControlMode) => void; + controlMode: ControlModeV2; + onChange: (controlMode: ControlModeV2) => void; }; export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => { @@ -26,7 +26,7 @@ export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: const handleControlModeChange = useCallback( (v) => { - assert(isControlMode(v?.value)); + assert(isControlModeV2(v?.value)); onChange(v.value); }, [onChange] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx index 7def6b2b56..675118c534 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx @@ -6,7 +6,7 @@ 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 { ControlNetConfig, T2IAdapterConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ControlNetConfigV2, T2IAdapterConfigV2 } from 'features/controlLayers/util/controlAdapters'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; @@ -23,7 +23,7 @@ import { import type { ImageDTO, PostUploadAction } from 'services/api/types'; type Props = { - controlAdapter: ControlNetConfig | T2IAdapterConfig; + controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; onChangeImage: (imageDTO: ImageDTO | null) => void; droppableData: TypesafeDroppableData; postUploadAction: PostUploadAction; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx index 1d14d8606f..5598b81787 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx @@ -4,7 +4,7 @@ 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, isProcessorType } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/util/controlAdapters'; import { configSelector } from 'features/system/store/configSelectors'; import { includes, map } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; @@ -36,7 +36,7 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro if (!v) { onChange(null); } else { - assert(isProcessorType(v.value)); + assert(isProcessorTypeV2(v.value)); onChange(CA_PROCESSOR_DATA[v.value].buildDefaults()); } }, 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 a0aa7d79a1..86ed77ce36 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx @@ -4,18 +4,18 @@ import { ControlAdapterWeight } from 'features/controlLayers/components/ControlA import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview'; import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod'; import { IPAdapterModelSelect } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect'; -import type { CLIPVisionModel, IPAdapterConfig, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { CLIPVisionModelV2, IPAdapterConfigV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { TypesafeDroppableData } from 'features/dnd/types'; import { memo } from 'react'; import type { ImageDTO, IPAdapterModelConfig, PostUploadAction } from 'services/api/types'; type Props = { - ipAdapter: IPAdapterConfig; + ipAdapter: IPAdapterConfigV2; onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; onChangeWeight: (weight: number) => void; - onChangeIPMethod: (method: IPMethod) => void; + onChangeIPMethod: (method: IPMethodV2) => void; onChangeModel: (modelConfig: IPAdapterModelConfig) => void; - onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; onChangeImage: (imageDTO: ImageDTO | null) => void; droppableData: TypesafeDroppableData; postUploadAction: PostUploadAction; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx index 70fd63f9c0..4f6a468fc3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx @@ -1,20 +1,20 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import type { IPMethod } from 'features/controlLayers/util/controlAdapters'; -import { isIPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; +import { isIPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; type Props = { - method: IPMethod; - onChange: (method: IPMethod) => void; + method: IPMethodV2; + onChange: (method: IPMethodV2) => void; }; export const IPAdapterMethod = memo(({ method, onChange }: Props) => { const { t } = useTranslation(); - const options: { label: string; value: IPMethod }[] = useMemo( + const options: { label: string; value: IPMethodV2 }[] = useMemo( () => [ { label: t('controlnet.full'), value: 'full' }, { label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' }, @@ -24,7 +24,7 @@ export const IPAdapterMethod = memo(({ method, onChange }: Props) => { ); const _onChange = useCallback( (v) => { - assert(isIPMethod(v?.value)); + assert(isIPMethodV2(v?.value)); onChange(v.value); }, [onChange] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx index e47bcd5182..b0541dca2c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx @@ -2,8 +2,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import type { CLIPVisionModel } from 'features/controlLayers/util/controlAdapters'; -import { isCLIPVisionModel } from 'features/controlLayers/util/controlAdapters'; +import type { CLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters'; +import { isCLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useIPAdapterModels } from 'services/api/hooks/modelsByType'; @@ -18,8 +18,8 @@ const CLIP_VISION_OPTIONS = [ type Props = { modelKey: string | null; onChangeModel: (modelConfig: IPAdapterModelConfig) => void; - clipVisionModel: CLIPVisionModel; - onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void; + clipVisionModel: CLIPVisionModelV2; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; }; export const IPAdapterModelSelect = memo( @@ -41,7 +41,7 @@ export const IPAdapterModelSelect = memo( const _onChangeCLIPVisionModel = useCallback( (v) => { - assert(isCLIPVisionModel(v?.value)); + assert(isCLIPVisionModelV2(v?.value)); onChangeCLIPVisionModel(v.value); }, [onChangeCLIPVisionModel] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx index cc3e9ba996..ef6e4160d6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx @@ -1,6 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { type CannyProcessorConfig, CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA, type CannyProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx index eda9af47a5..6faa00dd14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx @@ -1,6 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { type ColorMapProcessorConfig, CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA, type ColorMapProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; 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 b8dfae6c03..9f99710dac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx @@ -9,7 +9,7 @@ import { ipaLayerModelChanged, selectIPALayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; -import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { IPALayerImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; @@ -42,7 +42,7 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { ); const onChangeIPMethod = useCallback( - (method: IPMethod) => { + (method: IPMethodV2) => { dispatch(ipaLayerMethodChanged({ layerId, method })); }, [dispatch, layerId] @@ -56,7 +56,7 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { ); const onChangeCLIPVisionModel = useCallback( - (clipVisionModel: CLIPVisionModel) => { + (clipVisionModel: CLIPVisionModelV2) => { dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel })); }, [dispatch, layerId] 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 015cf75e4d..f7be62eb0a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx @@ -11,7 +11,7 @@ import { rgLayerIPAdapterWeightChanged, selectRGLayerIPAdapterOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; -import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { RGLayerIPAdapterImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; @@ -51,7 +51,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu ); const onChangeIPMethod = useCallback( - (method: IPMethod) => { + (method: IPMethodV2) => { dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method })); }, [dispatch, ipAdapterId, layerId] @@ -65,7 +65,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu ); const onChangeCLIPVisionModel = useCallback( - (clipVisionModel: CLIPVisionModel) => { + (clipVisionModel: CLIPVisionModelV2) => { dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel })); }, [dispatch, ipAdapterId, layerId] diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 75ffd81202..7a4e7ebc09 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -5,7 +5,7 @@ import { buildIPAdapter, buildT2IAdapter, CA_PROCESSOR_DATA, - isProcessorType, + isProcessorTypeV2, } from 'features/controlLayers/util/controlAdapters'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { useCallback, useMemo } from 'react'; @@ -30,7 +30,7 @@ export const useAddCALayer = () => { const id = uuidv4(); const defaultPreprocessor = model.default_settings?.preprocessor; - const processorConfig = isProcessorType(defaultPreprocessor) + const processorConfig = isProcessorTypeV2(defaultPreprocessor) ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(baseModel) : null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index c27a98c826..5f70cbf4a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -4,16 +4,16 @@ import type { PersistConfig, RootState } from 'app/store/store'; import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; import type { - CLIPVisionModel, - ControlMode, - ControlNetConfig, - IPAdapterConfig, - IPMethod, + CLIPVisionModelV2, + ControlModeV2, + ControlNetConfigV2, + IPAdapterConfigV2, + IPMethodV2, ProcessorConfig, - T2IAdapterConfig, + T2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import { - buildControlAdapterProcessor, + buildControlAdapterProcessorV2, controlNetToT2IAdapter, imageDTOToImageWithDims, t2iAdapterToControlNet, @@ -110,7 +110,7 @@ export const selectRGLayerIPAdapterOrThrow = ( state: ControlLayersState, layerId: string, ipAdapterId: string -): IPAdapterConfig => { +): IPAdapterConfigV2 => { const layer = state.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer)); const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); @@ -221,7 +221,7 @@ export const controlLayersSlice = createSlice({ caLayerAdded: { reducer: ( state, - action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }> + action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2 }> ) => { const { layerId, controlAdapter } = action.payload; const layer: ControlAdapterLayer = { @@ -245,7 +245,7 @@ export const controlLayersSlice = createSlice({ } } }, - prepare: (controlAdapter: ControlNetConfig | T2IAdapterConfig) => ({ + prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({ payload: { layerId: uuidv4(), controlAdapter }, }), }, @@ -297,7 +297,7 @@ export const controlLayersSlice = createSlice({ layer.controlAdapter = controlNetToT2IAdapter(layer.controlAdapter); } - const candidateProcessorConfig = buildControlAdapterProcessor(modelConfig); + const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig); if (candidateProcessorConfig?.type !== layer.controlAdapter.processorConfig?.type) { // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth // model. We need to use the new processor. @@ -305,7 +305,7 @@ export const controlLayersSlice = createSlice({ layer.controlAdapter.processorConfig = candidateProcessorConfig; } }, - caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlMode }>) => { + caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlModeV2 }>) => { const { layerId, controlMode } = action.payload; const layer = selectCALayerOrThrow(state, layerId); assert(layer.controlAdapter.type === 'controlnet'); @@ -344,7 +344,7 @@ export const controlLayersSlice = createSlice({ //#region IP Adapter Layers ipaLayerAdded: { - reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { + reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { const { layerId, ipAdapter } = action.payload; const layer: IPAdapterLayer = { id: getIPALayerId(layerId), @@ -354,14 +354,14 @@ export const controlLayersSlice = createSlice({ }; state.layers.push(layer); }, - prepare: (ipAdapter: IPAdapterConfig) => ({ payload: { layerId: uuidv4(), ipAdapter } }), + prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }), }, ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethod }>) => { + ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethodV2 }>) => { const { layerId, method } = action.payload; const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.method = method; @@ -383,7 +383,7 @@ export const controlLayersSlice = createSlice({ }, ipaLayerCLIPVisionModelChanged: ( state, - action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }> + action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModelV2 }> ) => { const { layerId, clipVisionModel } = action.payload; const layer = selectIPALayerOrThrow(state, layerId); @@ -533,7 +533,7 @@ export const controlLayersSlice = createSlice({ const layer = selectRGLayerOrThrow(state, layerId); layer.autoNegative = autoNegative; }, - rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { + rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { const { layerId, ipAdapter } = action.payload; const layer = selectRGLayerOrThrow(state, layerId); layer.ipAdapters.push(ipAdapter); @@ -569,7 +569,7 @@ export const controlLayersSlice = createSlice({ }, rgLayerIPAdapterMethodChanged: ( state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethod }> + action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethodV2 }> ) => { const { layerId, ipAdapterId, method } = action.payload; const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); @@ -593,7 +593,7 @@ export const controlLayersSlice = createSlice({ }, rgLayerIPAdapterCLIPVisionModelChanged: ( state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModel }> + action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> ) => { const { layerId, ipAdapterId, clipVisionModel } = action.payload; const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a4d88f3a0a..cbf47ff3ad 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,4 +1,8 @@ -import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlLayers/util/controlAdapters'; +import type { + ControlNetConfigV2, + IPAdapterConfigV2, + T2IAdapterConfigV2, +} from 'features/controlLayers/util/controlAdapters'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { ParameterAutoNegative, @@ -50,12 +54,12 @@ export type ControlAdapterLayer = RenderableLayerBase & { type: 'control_adapter_layer'; // technically, also t2i adapter layer opacity: number; isFilterEnabled: boolean; - controlAdapter: ControlNetConfig | T2IAdapterConfig; + controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; }; export type IPAdapterLayer = LayerBase & { type: 'ip_adapter_layer'; - ipAdapter: IPAdapterConfig; + ipAdapter: IPAdapterConfigV2; }; export type RegionalGuidanceLayer = RenderableLayerBase & { @@ -63,7 +67,7 @@ export type RegionalGuidanceLayer = RenderableLayerBase & { maskObjects: (VectorMaskLine | VectorMaskRect)[]; positivePrompt: ParameterPositivePrompt | null; negativePrompt: ParameterNegativePrompt | null; // Up to one text prompt per mask - ipAdapters: IPAdapterConfig[]; // Any number of image prompts + ipAdapters: IPAdapterConfigV2[]; // Any number of image prompts previewColor: RgbColor; autoNegative: ParameterAutoNegative; needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts index 656b759faa..880514bf7c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts @@ -4,20 +4,20 @@ import { assert } from 'tsafe'; import { describe, test } from 'vitest'; import type { - CLIPVisionModel, - ControlMode, + CLIPVisionModelV2, + ControlModeV2, DepthAnythingModelSize, - IPMethod, + IPMethodV2, ProcessorConfig, - ProcessorType, + ProcessorTypeV2, } from './controlAdapters'; describe('Control Adapter Types', () => { - test('ProcessorType', () => assert>()); - test('IP Adapter Method', () => assert, IPMethod>>()); + test('ProcessorType', () => assert>()); + test('IP Adapter Method', () => assert, IPMethodV2>>()); test('CLIP Vision Model', () => - assert, CLIPVisionModel>>()); - test('Control Mode', () => assert, ControlMode>>()); + assert, CLIPVisionModelV2>>()); + test('Control Mode', () => assert, ControlModeV2>>()); test('DepthAnything Model Size', () => assert, DepthAnythingModelSize>>()); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index afc168d749..360cfcabc6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -94,45 +94,45 @@ type ControlAdapterBase = { beginEndStepPct: [number, number]; }; -const zControlMode = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); -export type ControlMode = z.infer; -export const isControlMode = (v: unknown): v is ControlMode => zControlMode.safeParse(v).success; +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 type ControlNetConfig = ControlAdapterBase & { +export type ControlNetConfigV2 = ControlAdapterBase & { type: 'controlnet'; model: ParameterControlNetModel | null; - controlMode: ControlMode; + controlMode: ControlModeV2; }; -export const isControlNetConfig = (ca: ControlNetConfig | T2IAdapterConfig): ca is ControlNetConfig => +export const isControlNetConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is ControlNetConfigV2 => ca.type === 'controlnet'; -export type T2IAdapterConfig = ControlAdapterBase & { +export type T2IAdapterConfigV2 = ControlAdapterBase & { type: 't2i_adapter'; model: ParameterT2IAdapterModel | null; }; -export const isT2IAdapterConfig = (ca: ControlNetConfig | T2IAdapterConfig): ca is T2IAdapterConfig => +export const isT2IAdapterConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is T2IAdapterConfigV2 => ca.type === 't2i_adapter'; -const zCLIPVisionModel = z.enum(['ViT-H', 'ViT-G']); -export type CLIPVisionModel = z.infer; -export const isCLIPVisionModel = (v: unknown): v is CLIPVisionModel => zCLIPVisionModel.safeParse(v).success; +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; -const zIPMethod = z.enum(['full', 'style', 'composition']); -export type IPMethod = z.infer; -export const isIPMethod = (v: unknown): v is IPMethod => zIPMethod.safeParse(v).success; +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 type IPAdapterConfig = { +export type IPAdapterConfigV2 = { id: string; type: 'ip_adapter'; weight: number; - method: IPMethod; + method: IPMethodV2; image: ImageWithDims | null; model: ParameterIPAdapterModel | null; - clipVisionModel: CLIPVisionModel; + clipVisionModel: CLIPVisionModelV2; beginEndStepPct: [number, number]; }; -const zProcessorType = z.enum([ +const zProcessorTypeV2 = z.enum([ 'canny_image_processor', 'color_map_image_processor', 'content_shuffle_image_processor', @@ -148,10 +148,10 @@ const zProcessorType = z.enum([ 'pidi_image_processor', 'zoe_depth_image_processor', ]); -export type ProcessorType = z.infer; -export const isProcessorType = (v: unknown): v is ProcessorType => zProcessorType.safeParse(v).success; +export type ProcessorTypeV2 = z.infer; +export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success; -type ProcessorData = { +type ProcessorData = { type: T; labelTKey: string; descriptionTKey: string; @@ -165,7 +165,7 @@ type ProcessorData = { const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); type CAProcessorsData = { - [key in ProcessorType]: ProcessorData; + [key in ProcessorTypeV2]: ProcessorData; }; /** * A dict of ControlNet processors, including: @@ -405,7 +405,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }, }; -const initialControlNet: Omit = { +const initialControlNetV2: Omit = { type: 'controlnet', model: null, weight: 1, @@ -417,7 +417,7 @@ const initialControlNet: Omit = { processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -const initialT2IAdapter: Omit = { +const initialT2IAdapterV2: Omit = { type: 't2i_adapter', model: null, weight: 1, @@ -428,7 +428,7 @@ const initialT2IAdapter: Omit = { processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -const initialIPAdapter: Omit = { +const initialIPAdapterV2: Omit = { type: 'ip_adapter', image: null, model: null, @@ -438,23 +438,23 @@ const initialIPAdapter: Omit = { weight: 1, }; -export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfig => { - return merge(deepClone(initialControlNet), { id, ...overrides }); +export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfigV2 => { + return merge(deepClone(initialControlNetV2), { id, ...overrides }); }; -export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfig => { - return merge(deepClone(initialT2IAdapter), { id, ...overrides }); +export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfigV2 => { + return merge(deepClone(initialT2IAdapterV2), { id, ...overrides }); }; -export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfig => { - return merge(deepClone(initialIPAdapter), { id, ...overrides }); +export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfigV2 => { + return merge(deepClone(initialIPAdapterV2), { id, ...overrides }); }; -export const buildControlAdapterProcessor = ( +export const buildControlAdapterProcessorV2 = ( modelConfig: ControlNetModelConfig | T2IAdapterModelConfig ): ProcessorConfig | null => { const defaultPreprocessor = modelConfig.default_settings?.preprocessor; - if (!isProcessorType(defaultPreprocessor)) { + if (!isProcessorTypeV2(defaultPreprocessor)) { return null; } const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base); @@ -467,15 +467,15 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO) height, }); -export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfig): ControlNetConfig => { +export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfigV2): ControlNetConfigV2 => { return { ...deepClone(t2iAdapter), type: 'controlnet', - controlMode: initialControlNet.controlMode, + controlMode: initialControlNetV2.controlMode, }; }; -export const controlNetToT2IAdapter = (controlNet: ControlNetConfig): T2IAdapterConfig => { +export const controlNetToT2IAdapter = (controlNet: ControlNetConfigV2): T2IAdapterConfigV2 => { return { ...omit(deepClone(controlNet), 'controlMode'), type: 't2i_adapter', diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts index 4581b51ee1..da13fed9f5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts @@ -6,13 +6,13 @@ import { isRegionalGuidanceLayer, } from 'features/controlLayers/store/controlLayersSlice'; import { - type ControlNetConfig, + type ControlNetConfigV2, type ImageWithDims, - type IPAdapterConfig, - isControlNetConfig, - isT2IAdapterConfig, + type IPAdapterConfigV2, + isControlNetConfigV2, + isT2IAdapterConfigV2, type ProcessorConfig, - type T2IAdapterConfig, + type T2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs'; import type { ImageField } from 'features/nodes/types/common'; @@ -64,7 +64,7 @@ const buildControlImage = ( assert(false, 'Attempted to add unprocessed control image'); }; -const buildControlNetMetadata = (controlNet: ControlNetConfig): S['ControlNetMetadataField'] => { +const buildControlNetMetadata = (controlNet: ControlNetConfigV2): S['ControlNetMetadataField'] => { const { beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet; assert(model, 'ControlNet model is required'); @@ -113,7 +113,7 @@ const addControlNetCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: stri }; const addGlobalControlNetsToGraph = async ( - controlNets: ControlNetConfig[], + controlNets: ControlNetConfigV2[], graph: NonNullableGraph, denoiseNodeId: string ) => { @@ -157,7 +157,7 @@ const addGlobalControlNetsToGraph = async ( upsertMetadata(graph, { controlnets: controlNetMetadata }); }; -const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfig): S['T2IAdapterMetadataField'] => { +const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfigV2): S['T2IAdapterMetadataField'] => { const { beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter; assert(model, 'T2I Adapter model is required'); @@ -205,7 +205,7 @@ const addT2IAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: stri }; const addGlobalT2IAdaptersToGraph = async ( - t2iAdapters: T2IAdapterConfig[], + t2iAdapters: T2IAdapterConfigV2[], graph: NonNullableGraph, denoiseNodeId: string ) => { @@ -249,7 +249,7 @@ const addGlobalT2IAdaptersToGraph = async ( upsertMetadata(graph, { t2iAdapters: t2iAdapterMetadata }); }; -const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfig): S['IPAdapterMetadataField'] => { +const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfigV2): S['IPAdapterMetadataField'] => { const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; assert(model, 'IP Adapter model is required'); @@ -290,7 +290,7 @@ const addIPAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: strin }; const addGlobalIPAdaptersToGraph = async ( - ipAdapters: IPAdapterConfig[], + ipAdapters: IPAdapterConfigV2[], graph: NonNullableGraph, denoiseNodeId: string ) => { @@ -351,7 +351,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab // We want the CAs themselves .map((l) => l.controlAdapter) // Must be a ControlNet - .filter(isControlNetConfig) + .filter(isControlNetConfigV2) .filter((ca) => { const hasModel = Boolean(ca.model); const modelMatchesBase = ca.model?.base === mainModel.base; @@ -368,7 +368,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab // We want the CAs themselves .map((l) => l.controlAdapter) // Must have a ControlNet CA - .filter(isT2IAdapterConfig) + .filter(isT2IAdapterConfigV2) .filter((ca) => { const hasModel = Boolean(ca.model); const modelMatchesBase = ca.model?.base === mainModel.base; @@ -633,7 +633,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab } // TODO(psyche): For some reason, I have to explicitly annotate regionalIPAdapters here. Not sure why. - const regionalIPAdapters: IPAdapterConfig[] = layer.ipAdapters.filter((ipAdapter) => { + const regionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipAdapter) => { const hasModel = Boolean(ipAdapter.model); const modelMatchesBase = ipAdapter.model?.base === mainModel.base; const hasControlImage = Boolean(ipAdapter.image); From 4cd78b9478b877746359716f19659e21930eca6b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 16:27:58 +1000 Subject: [PATCH 39/74] feat(ui): add getImageDTO imperative RTKQ helper --- .../web/src/services/api/endpoints/images.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index ecb65f31b1..70358ebc8c 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -1,5 +1,6 @@ import type { EntityState, Update } from '@reduxjs/toolkit'; import type { PatchCollection } from '@reduxjs/toolkit/dist/query/core/buildThunks'; +import { getStore } from 'app/store/nanostores/store'; import type { JSONObject } from 'common/types'; import type { BoardId } from 'features/gallery/store/types'; import { ASSETS_CATEGORIES, IMAGE_CATEGORIES, IMAGE_LIMIT } from 'features/gallery/store/types'; @@ -1319,3 +1320,22 @@ export const { useUnstarImagesMutation, useBulkDownloadImagesMutation, } = imagesApi; + +/** + * Imperative RTKQ helper to fetch an ImageDTO. + * @param image_name The name of the image to fetch + * @param forceRefetch Whether to force a refetch of the image + * @returns + */ +export const getImageDTO = async (image_name: string, forceRefetch?: boolean): Promise => { + const options = { + subscribe: false, + forceRefetch, + }; + const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(image_name, options)); + try { + return await req.unwrap(); + } catch { + return null; + } +}; From 6363095b293d5296c337f5a13b65420b4bfeff21 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 17:41:02 +1000 Subject: [PATCH 40/74] feat(ui): control adapter recall for control layers - Add set of metadata handlers for the control layers CAs - Use these conditionally depending on the active tab - when recalling on txt2img, the CAs go to control layers, else they go to the old CA area. --- .../controlLayers/store/controlLayersSlice.ts | 12 + .../controlLayers/util/controlAdapters.ts | 6 +- .../ImageMetadataActions.tsx | 15 +- .../features/gallery/hooks/useImageActions.ts | 11 +- .../components/MetadataControlNetsV2.tsx | 72 ++++++ .../components/MetadataIPAdaptersV2.tsx | 72 ++++++ .../components/MetadataT2IAdaptersV2.tsx | 72 ++++++ .../web/src/features/metadata/types.ts | 9 + .../src/features/metadata/util/handlers.ts | 58 ++++- .../web/src/features/metadata/util/parsers.ts | 216 +++++++++++++++++- .../src/features/metadata/util/recallers.ts | 60 +++++ .../src/features/metadata/util/validators.ts | 63 +++++ .../parameters/hooks/usePreselectedImage.ts | 2 +- 13 files changed, 654 insertions(+), 14 deletions(-) create mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataControlNetsV2.tsx create mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataIPAdaptersV2.tsx create mode 100644 invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 5f70cbf4a9..7c8d6a895c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -340,6 +340,12 @@ export const controlLayersSlice = createSlice({ const layer = selectCALayerOrThrow(state, layerId); layer.controlAdapter.isProcessingImage = isProcessingImage; }, + caLayerControlNetsDeleted: (state) => { + state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 'controlnet'); + }, + caLayerT2IAdaptersDeleted: (state) => { + state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 't2i_adapter'); + }, //#endregion //#region IP Adapter Layers @@ -389,6 +395,9 @@ export const controlLayersSlice = createSlice({ const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.clipVisionModel = clipVisionModel; }, + ipaLayersDeleted: (state) => { + state.layers = state.layers.filter((l) => !isIPAdapterLayer(l)); + }, //#endregion //#region CA or IPA Layers @@ -741,12 +750,15 @@ export const { caLayerIsFilterEnabledChanged, caLayerOpacityChanged, caLayerIsProcessingImageChanged, + caLayerControlNetsDeleted, + caLayerT2IAdaptersDeleted, // IPA Layers ipaLayerAdded, ipaLayerImageChanged, ipaLayerMethodChanged, ipaLayerModelChanged, ipaLayerCLIPVisionModelChanged, + ipaLayersDeleted, // CA or IPA Layers caOrIPALayerWeightChanged, caOrIPALayerBeginEndStepPctChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 360cfcabc6..2964a2eb6c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -405,7 +405,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }, }; -const initialControlNetV2: Omit = { +export const initialControlNetV2: Omit = { type: 'controlnet', model: null, weight: 1, @@ -417,7 +417,7 @@ const initialControlNetV2: Omit = { processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -const initialT2IAdapterV2: Omit = { +export const initialT2IAdapterV2: Omit = { type: 't2i_adapter', model: null, weight: 1, @@ -428,7 +428,7 @@ const initialT2IAdapterV2: Omit = { processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -const initialIPAdapterV2: Omit = { +export const initialIPAdapterV2: Omit = { type: 'ip_adapter', image: null, model: null, diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index ce75ea62e0..007a89f1a1 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -1,9 +1,14 @@ +import { useAppSelector } from 'app/store/storeHooks'; import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets'; +import { MetadataControlNetsV2 } from 'features/metadata/components/MetadataControlNetsV2'; import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters'; +import { MetadataIPAdaptersV2 } from 'features/metadata/components/MetadataIPAdaptersV2'; import { MetadataItem } from 'features/metadata/components/MetadataItem'; import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs'; import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters'; +import { MetadataT2IAdaptersV2 } from 'features/metadata/components/MetadataT2IAdaptersV2'; import { handlers } from 'features/metadata/util/handlers'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; type Props = { @@ -11,6 +16,7 @@ type Props = { }; const ImageMetadataActions = (props: Props) => { + const activeTabName = useAppSelector(activeTabNameSelector); const { metadata } = props; if (!metadata || Object.keys(metadata).length === 0) { @@ -46,9 +52,12 @@ const ImageMetadataActions = (props: Props) => { - - - + {activeTabName !== 'txt2img' && } + {activeTabName !== 'txt2img' && } + {activeTabName !== 'txt2img' && } + {activeTabName === 'txt2img' && } + {activeTabName === 'txt2img' && } + {activeTabName === 'txt2img' && } ); }; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts index c3ae0cea5f..3f0f0bdf91 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts @@ -1,8 +1,11 @@ +import { useAppSelector } from 'app/store/storeHooks'; import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { useCallback, useEffect, useState } from 'react'; import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; export const useImageActions = (image_name?: string) => { + const activeTabName = useAppSelector(activeTabNameSelector); const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(image_name); const [hasMetadata, setHasMetadata] = useState(false); const [hasSeed, setHasSeed] = useState(false); @@ -40,13 +43,13 @@ export const useImageActions = (image_name?: string) => { }, [metadata]); const recallAll = useCallback(() => { - parseAndRecallAllMetadata(metadata); - }, [metadata]); + parseAndRecallAllMetadata(metadata, activeTabName === 'txt2img'); + }, [activeTabName, metadata]); const remix = useCallback(() => { // Recalls all metadata parameters except seed - parseAndRecallAllMetadata(metadata, ['seed']); - }, [metadata]); + parseAndRecallAllMetadata(metadata, activeTabName === 'txt2img', ['seed']); + }, [activeTabName, metadata]); const recallSeed = useCallback(() => { handlers.seed.parse(metadata).then((seed) => { diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataControlNetsV2.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataControlNetsV2.tsx new file mode 100644 index 0000000000..5f4df78afc --- /dev/null +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataControlNetsV2.tsx @@ -0,0 +1,72 @@ +import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; +import type { ControlNetConfigV2Metadata, MetadataHandlers } from 'features/metadata/types'; +import { handlers } from 'features/metadata/util/handlers'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +type Props = { + metadata: unknown; +}; + +export const MetadataControlNetsV2 = ({ metadata }: Props) => { + const [controlNets, setControlNets] = useState([]); + + useEffect(() => { + const parse = async () => { + try { + const parsed = await handlers.controlNetsV2.parse(metadata); + setControlNets(parsed); + } catch (e) { + setControlNets([]); + } + }; + parse(); + }, [metadata]); + + const label = useMemo(() => handlers.controlNetsV2.getLabel(), []); + + return ( + <> + {controlNets.map((controlNet) => ( + + ))} + + ); +}; + +const MetadataViewControlNet = ({ + label, + controlNet, + handlers, +}: { + label: string; + controlNet: ControlNetConfigV2Metadata; + handlers: MetadataHandlers; +}) => { + const onRecall = useCallback(() => { + if (!handlers.recallItem) { + return; + } + handlers.recallItem(controlNet, true); + }, [handlers, controlNet]); + + const [renderedValue, setRenderedValue] = useState(null); + useEffect(() => { + const _renderValue = async () => { + if (!handlers.renderItemValue) { + setRenderedValue(null); + return; + } + const rendered = await handlers.renderItemValue(controlNet); + setRenderedValue(rendered); + }; + + _renderValue(); + }, [handlers, controlNet]); + + return ; +}; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataIPAdaptersV2.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataIPAdaptersV2.tsx new file mode 100644 index 0000000000..201ebc4cb4 --- /dev/null +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataIPAdaptersV2.tsx @@ -0,0 +1,72 @@ +import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; +import type { IPAdapterConfigV2Metadata, MetadataHandlers } from 'features/metadata/types'; +import { handlers } from 'features/metadata/util/handlers'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +type Props = { + metadata: unknown; +}; + +export const MetadataIPAdaptersV2 = ({ metadata }: Props) => { + const [ipAdapters, setIPAdapters] = useState([]); + + useEffect(() => { + const parse = async () => { + try { + const parsed = await handlers.ipAdaptersV2.parse(metadata); + setIPAdapters(parsed); + } catch (e) { + setIPAdapters([]); + } + }; + parse(); + }, [metadata]); + + const label = useMemo(() => handlers.ipAdaptersV2.getLabel(), []); + + return ( + <> + {ipAdapters.map((ipAdapter) => ( + + ))} + + ); +}; + +const MetadataViewIPAdapter = ({ + label, + ipAdapter, + handlers, +}: { + label: string; + ipAdapter: IPAdapterConfigV2Metadata; + handlers: MetadataHandlers; +}) => { + const onRecall = useCallback(() => { + if (!handlers.recallItem) { + return; + } + handlers.recallItem(ipAdapter, true); + }, [handlers, ipAdapter]); + + const [renderedValue, setRenderedValue] = useState(null); + useEffect(() => { + const _renderValue = async () => { + if (!handlers.renderItemValue) { + setRenderedValue(null); + return; + } + const rendered = await handlers.renderItemValue(ipAdapter); + setRenderedValue(rendered); + }; + + _renderValue(); + }, [handlers, ipAdapter]); + + return ; +}; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx new file mode 100644 index 0000000000..42d3de2ec2 --- /dev/null +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx @@ -0,0 +1,72 @@ +import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; +import type { MetadataHandlers, T2IAdapterConfigV2Metadata } from 'features/metadata/types'; +import { handlers } from 'features/metadata/util/handlers'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +type Props = { + metadata: unknown; +}; + +export const MetadataT2IAdaptersV2 = ({ metadata }: Props) => { + const [t2iAdapters, setT2IAdapters] = useState([]); + + useEffect(() => { + const parse = async () => { + try { + const parsed = await handlers.t2iAdaptersV2.parse(metadata); + setT2IAdapters(parsed); + } catch (e) { + setT2IAdapters([]); + } + }; + parse(); + }, [metadata]); + + const label = useMemo(() => handlers.t2iAdaptersV2.getLabel(), []); + + return ( + <> + {t2iAdapters.map((t2iAdapter) => ( + + ))} + + ); +}; + +const MetadataViewT2IAdapter = ({ + label, + t2iAdapter, + handlers, +}: { + label: string; + t2iAdapter: T2IAdapterConfigV2Metadata; + handlers: MetadataHandlers; +}) => { + const onRecall = useCallback(() => { + if (!handlers.recallItem) { + return; + } + handlers.recallItem(t2iAdapter, true); + }, [handlers, t2iAdapter]); + + const [renderedValue, setRenderedValue] = useState(null); + useEffect(() => { + const _renderValue = async () => { + if (!handlers.renderItemValue) { + setRenderedValue(null); + return; + } + const rendered = await handlers.renderItemValue(t2iAdapter); + setRenderedValue(rendered); + }; + + _renderValue(); + }, [handlers, t2iAdapter]); + + return ; +}; diff --git a/invokeai/frontend/web/src/features/metadata/types.ts b/invokeai/frontend/web/src/features/metadata/types.ts index 0791cdf449..8621031896 100644 --- a/invokeai/frontend/web/src/features/metadata/types.ts +++ b/invokeai/frontend/web/src/features/metadata/types.ts @@ -1,4 +1,5 @@ import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlAdapters/store/types'; +import type { ControlNetConfigV2, IPAdapterConfigV2, T2IAdapterConfigV2 } from 'features/controlLayers/util/controlAdapters'; import type { O } from 'ts-toolbelt'; /** @@ -135,3 +136,11 @@ export type AnyControlAdapterConfigMetadata = | ControlNetConfigMetadata | T2IAdapterConfigMetadata | IPAdapterConfigMetadata; + +export type ControlNetConfigV2Metadata = O.NonNullable; +export type T2IAdapterConfigV2Metadata = O.NonNullable; +export type IPAdapterConfigV2Metadata = O.NonNullable; +export type AnyControlAdapterConfigV2Metadata = + | ControlNetConfigV2Metadata + | T2IAdapterConfigV2Metadata + | IPAdapterConfigV2Metadata; diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 2fb840afcb..467f702cea 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -3,6 +3,7 @@ import { toast } from 'common/util/toast'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { AnyControlAdapterConfigMetadata, + AnyControlAdapterConfigV2Metadata, BuildMetadataHandlers, MetadataGetLabelFunc, MetadataHandlers, @@ -43,6 +44,14 @@ const renderControlAdapterValue: MetadataRenderValueFunc = async (value) => { + try { + const modelConfig = await fetchModelConfig(value.model.key ?? 'none'); + return `${modelConfig.name} (${modelConfig.base.toUpperCase()}) - ${value.weight}`; + } catch { + return `${value.model.key} (${value.model.base.toUpperCase()}) - ${value.weight}`; + } +}; const parameterSetToast = (parameter: string, description?: string) => { toast({ @@ -341,6 +350,36 @@ export const handlers = { itemValidator: validators.t2iAdapter, renderItemValue: renderControlAdapterValue, }), + controlNetsV2: buildHandlers({ + getLabel: () => t('common.controlNet'), + parser: parsers.controlNetsV2, + itemParser: parsers.controlNetV2, + recaller: recallers.controlNetsV2, + itemRecaller: recallers.controlNetV2, + validator: validators.controlNetsV2, + itemValidator: validators.controlNetV2, + renderItemValue: renderControlAdapterValueV2, + }), + ipAdaptersV2: buildHandlers({ + getLabel: () => t('common.ipAdapter'), + parser: parsers.ipAdaptersV2, + itemParser: parsers.ipAdapterV2, + recaller: recallers.ipAdaptersV2, + itemRecaller: recallers.ipAdapterV2, + validator: validators.ipAdaptersV2, + itemValidator: validators.ipAdapterV2, + renderItemValue: renderControlAdapterValueV2, + }), + t2iAdaptersV2: buildHandlers({ + getLabel: () => t('common.t2iAdapter'), + parser: parsers.t2iAdaptersV2, + itemParser: parsers.t2iAdapterV2, + recaller: recallers.t2iAdaptersV2, + itemRecaller: recallers.t2iAdapterV2, + validator: validators.t2iAdaptersV2, + itemValidator: validators.t2iAdapterV2, + renderItemValue: renderControlAdapterValueV2, + }), } as const; export const parseAndRecallPrompts = async (metadata: unknown) => { @@ -395,10 +434,25 @@ export const parseAndRecallImageDimensions = async (metadata: unknown) => { } }; -export const parseAndRecallAllMetadata = async (metadata: unknown, skip: (keyof typeof handlers)[] = []) => { +// These handlers should be omitted when recalling to control layers +const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters']; +// These handlers should be omitted when recalling to the rest of the app +const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNetsV2', 'ipAdaptersV2', 't2iAdaptersV2']; + +export const parseAndRecallAllMetadata = async ( + metadata: unknown, + toControlLayers: boolean, + skip: (keyof typeof handlers)[] = [] +) => { + const skipKeys = skip ?? []; + if (toControlLayers) { + skipKeys.push(...TO_CONTROL_LAYERS_SKIP_KEYS); + } else { + skipKeys.push(...NOT_TO_CONTROL_LAYERS_SKIP_KEYS); + } const results = await Promise.allSettled( objectKeys(handlers) - .filter((key) => !skip.includes(key)) + .filter((key) => !skipKeys.includes(key)) .map((key) => { const { parse, recall } = handlers[key]; return parse(metadata).then((value) => { diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 3decea6737..8641977b1f 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -5,13 +5,24 @@ import { initialT2IAdapter, } from 'features/controlAdapters/util/buildControlAdapter'; import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; +import { + CA_PROCESSOR_DATA, + imageDTOToImageWithDims, + initialControlNetV2, + initialIPAdapterV2, + initialT2IAdapterV2, + isProcessorTypeV2, +} from 'features/controlLayers/util/controlAdapters'; import type { LoRA } from 'features/lora/store/loraSlice'; import { defaultLoRAConfig } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, + ControlNetConfigV2Metadata, IPAdapterConfigMetadata, + IPAdapterConfigV2Metadata, MetadataParseFunc, T2IAdapterConfigMetadata, + T2IAdapterConfigV2Metadata, } from 'features/metadata/types'; import { fetchModelConfigWithTypeGuard, getModelKey } from 'features/metadata/util/modelFetchingHelpers'; import { zControlField, zIPAdapterField, zModelIdentifierField, zT2IAdapterField } from 'features/nodes/types/common'; @@ -58,7 +69,7 @@ import { isParameterWidth, } from 'features/parameters/types/parameterSchemas'; import { get, isArray, isString } from 'lodash-es'; -import { imagesApi } from 'services/api/endpoints/images'; +import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import { isControlNetModelConfig, @@ -428,6 +439,203 @@ const parseAllIPAdapters: MetadataParseFunc = async ( } }; +//#region V2/Control Layers +const parseControlNetV2: MetadataParseFunc = async (metadataItem) => { + const control_model = await getProperty(metadataItem, 'control_model'); + const key = await getModelKey(control_model, 'controlnet'); + const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig); + const image = zControlField.shape.image + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'image')); + const processedImage = zControlField.shape.image + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'processed_image')); + const control_weight = zControlField.shape.control_weight + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'control_weight')); + const begin_step_percent = zControlField.shape.begin_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'begin_step_percent')); + const end_step_percent = zControlField.shape.end_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'end_step_percent')); + const control_mode = zControlField.shape.control_mode + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'control_mode')); + + const id = uuidv4(); + const defaultPreprocessor = controlNetModel.default_settings?.preprocessor; + const processorConfig = isProcessorTypeV2(defaultPreprocessor) + ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults() + : null; + const beginEndStepPct: [number, number] = [ + begin_step_percent ?? initialControlNetV2.beginEndStepPct[0], + end_step_percent ?? initialControlNetV2.beginEndStepPct[1], + ]; + const imageDTO = image ? await getImageDTO(image.image_name) : null; + const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null; + + const controlNet: ControlNetConfigV2Metadata = { + id, + type: 'controlnet', + model: zModelIdentifierField.parse(controlNetModel), + weight: typeof control_weight === 'number' ? control_weight : initialControlNetV2.weight, + beginEndStepPct, + controlMode: control_mode ?? initialControlNetV2.controlMode, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null, + processorConfig, + isProcessingImage: false, + }; + + return controlNet; +}; + +const parseAllControlNetsV2: MetadataParseFunc = async (metadata) => { + try { + const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray || undefined); + const parseResults = await Promise.allSettled(controlNetsRaw.map((cn) => parseControlNetV2(cn))); + const controlNets = parseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + return controlNets; + } catch { + return []; + } +}; + +const parseT2IAdapterV2: MetadataParseFunc = async (metadataItem) => { + const t2i_adapter_model = await getProperty(metadataItem, 't2i_adapter_model'); + const key = await getModelKey(t2i_adapter_model, 't2i_adapter'); + const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig); + + const image = zT2IAdapterField.shape.image + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'image')); + const processedImage = zT2IAdapterField.shape.image + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'processed_image')); + const weight = zT2IAdapterField.shape.weight + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'weight')); + const begin_step_percent = zT2IAdapterField.shape.begin_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'begin_step_percent')); + const end_step_percent = zT2IAdapterField.shape.end_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'end_step_percent')); + + const id = uuidv4(); + const defaultPreprocessor = t2iAdapterModel.default_settings?.preprocessor; + const processorConfig = isProcessorTypeV2(defaultPreprocessor) + ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults() + : null; + const beginEndStepPct: [number, number] = [ + begin_step_percent ?? initialT2IAdapterV2.beginEndStepPct[0], + end_step_percent ?? initialT2IAdapterV2.beginEndStepPct[1], + ]; + const imageDTO = image ? await getImageDTO(image.image_name) : null; + const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null; + + const t2iAdapter: T2IAdapterConfigV2Metadata = { + id, + type: 't2i_adapter', + model: zModelIdentifierField.parse(t2iAdapterModel), + weight: typeof weight === 'number' ? weight : initialT2IAdapterV2.weight, + beginEndStepPct, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null, + processorConfig, + isProcessingImage: false, + }; + + return t2iAdapter; +}; + +const parseAllT2IAdaptersV2: MetadataParseFunc = async (metadata) => { + try { + const t2iAdaptersRaw = await getProperty(metadata, 't2iAdapters', isArray); + const parseResults = await Promise.allSettled(t2iAdaptersRaw.map((t2iAdapter) => parseT2IAdapterV2(t2iAdapter))); + const t2iAdapters = parseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + return t2iAdapters; + } catch { + return []; + } +}; + +const parseIPAdapterV2: MetadataParseFunc = async (metadataItem) => { + const ip_adapter_model = await getProperty(metadataItem, 'ip_adapter_model'); + const key = await getModelKey(ip_adapter_model, 'ip_adapter'); + const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig); + + const image = zIPAdapterField.shape.image + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'image')); + const weight = zIPAdapterField.shape.weight + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'weight')); + const method = zIPAdapterField.shape.method + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'method')); + const begin_step_percent = zIPAdapterField.shape.begin_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'begin_step_percent')); + const end_step_percent = zIPAdapterField.shape.end_step_percent + .nullish() + .catch(null) + .parse(await getProperty(metadataItem, 'end_step_percent')); + + const id = uuidv4(); + const beginEndStepPct: [number, number] = [ + begin_step_percent ?? initialIPAdapterV2.beginEndStepPct[0], + end_step_percent ?? initialIPAdapterV2.beginEndStepPct[1], + ]; + const imageDTO = image ? await getImageDTO(image.image_name) : null; + + const ipAdapter: IPAdapterConfigV2Metadata = { + id, + type: 'ip_adapter', + model: zModelIdentifierField.parse(ipAdapterModel), + weight: typeof weight === 'number' ? weight : initialIPAdapterV2.weight, + beginEndStepPct, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + clipVisionModel: initialIPAdapterV2.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField... + method: method ?? initialIPAdapterV2.method, + }; + + return ipAdapter; +}; + +const parseAllIPAdaptersV2: MetadataParseFunc = async (metadata) => { + try { + const ipAdaptersRaw = await getProperty(metadata, 'ipAdapters', isArray); + const parseResults = await Promise.allSettled(ipAdaptersRaw.map((ipAdapter) => parseIPAdapterV2(ipAdapter))); + const ipAdapters = parseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + return ipAdapters; + } catch { + return []; + } +}; + export const parsers = { createdBy: parseCreatedBy, generationMode: parseGenerationMode, @@ -464,4 +672,10 @@ export const parsers = { t2iAdapters: parseAllT2IAdapters, ipAdapter: parseIPAdapter, ipAdapters: parseAllIPAdapters, + controlNetV2: parseControlNetV2, + controlNetsV2: parseAllControlNetsV2, + t2iAdapterV2: parseT2IAdapterV2, + t2iAdaptersV2: parseAllT2IAdaptersV2, + ipAdapterV2: parseIPAdapterV2, + ipAdaptersV2: parseAllIPAdaptersV2, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index f07b2ab8b6..a7b0cd5b2e 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -6,7 +6,12 @@ import { t2iAdaptersReset, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { + caLayerAdded, + caLayerControlNetsDeleted, + caLayerT2IAdaptersDeleted, heightChanged, + ipaLayerAdded, + ipaLayersDeleted, negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, @@ -18,9 +23,12 @@ import type { LoRA } from 'features/lora/store/loraSlice'; import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, + ControlNetConfigV2Metadata, IPAdapterConfigMetadata, + IPAdapterConfigV2Metadata, MetadataRecallFunc, T2IAdapterConfigMetadata, + T2IAdapterConfigV2Metadata, } from 'features/metadata/types'; import { modelSelected } from 'features/parameters/store/actions'; import { @@ -234,6 +242,52 @@ const recallIPAdapters: MetadataRecallFunc = (ipAdapt }); }; +//#region V2/Control Layer +const recallControlNetV2: MetadataRecallFunc = (controlNet) => { + getStore().dispatch(caLayerAdded(controlNet)); +}; + +const recallControlNetsV2: MetadataRecallFunc = (controlNets) => { + const { dispatch } = getStore(); + dispatch(caLayerControlNetsDeleted()); + if (!controlNets.length) { + return; + } + controlNets.forEach((controlNet) => { + dispatch(caLayerAdded(controlNet)); + }); +}; + +const recallT2IAdapterV2: MetadataRecallFunc = (t2iAdapter) => { + getStore().dispatch(caLayerAdded(t2iAdapter)); +}; + +const recallT2IAdaptersV2: MetadataRecallFunc = (t2iAdapters) => { + const { dispatch } = getStore(); + dispatch(caLayerT2IAdaptersDeleted()); + if (!t2iAdapters.length) { + return; + } + t2iAdapters.forEach((t2iAdapters) => { + dispatch(caLayerAdded(t2iAdapters)); + }); +}; + +const recallIPAdapterV2: MetadataRecallFunc = (ipAdapter) => { + getStore().dispatch(ipaLayerAdded(ipAdapter)); +}; + +const recallIPAdaptersV2: MetadataRecallFunc = (ipAdapters) => { + const { dispatch } = getStore(); + dispatch(ipaLayersDeleted()); + if (!ipAdapters.length) { + return; + } + ipAdapters.forEach((ipAdapter) => { + dispatch(ipaLayerAdded(ipAdapter)); + }); +}; + export const recallers = { positivePrompt: recallPositivePrompt, negativePrompt: recallNegativePrompt, @@ -268,4 +322,10 @@ export const recallers = { t2iAdapter: recallT2IAdapter, ipAdapters: recallIPAdapters, ipAdapter: recallIPAdapter, + controlNetV2: recallControlNetV2, + controlNetsV2: recallControlNetsV2, + t2iAdapterV2: recallT2IAdapterV2, + t2iAdaptersV2: recallT2IAdaptersV2, + ipAdapterV2: recallIPAdapterV2, + ipAdaptersV2: recallIPAdaptersV2, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index 66454778f2..d09321003f 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -2,9 +2,12 @@ import { getStore } from 'app/store/nanostores/store'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, + ControlNetConfigV2Metadata, IPAdapterConfigMetadata, + IPAdapterConfigV2Metadata, MetadataValidateFunc, T2IAdapterConfigMetadata, + T2IAdapterConfigV2Metadata, } from 'features/metadata/types'; import { InvalidModelConfigError } from 'features/metadata/util/modelFetchingHelpers'; import type { ParameterSDXLRefinerModel, ParameterVAEModel } from 'features/parameters/types/parameterSchemas'; @@ -108,6 +111,60 @@ const validateIPAdapters: MetadataValidateFunc = (ipA return new Promise((resolve) => resolve(validatedIPAdapters)); }; +const validateControlNetV2: MetadataValidateFunc = (controlNet) => { + validateBaseCompatibility(controlNet.model?.base, 'ControlNet incompatible with currently-selected model'); + return new Promise((resolve) => resolve(controlNet)); +}; + +const validateControlNetsV2: MetadataValidateFunc = (controlNets) => { + const validatedControlNets: ControlNetConfigV2Metadata[] = []; + controlNets.forEach((controlNet) => { + try { + validateBaseCompatibility(controlNet.model?.base, 'ControlNet incompatible with currently-selected model'); + validatedControlNets.push(controlNet); + } catch { + // This is a no-op - we want to continue validating the rest of the ControlNets, and an empty list is valid. + } + }); + return new Promise((resolve) => resolve(validatedControlNets)); +}; + +const validateT2IAdapterV2: MetadataValidateFunc = (t2iAdapter) => { + validateBaseCompatibility(t2iAdapter.model?.base, 'T2I Adapter incompatible with currently-selected model'); + return new Promise((resolve) => resolve(t2iAdapter)); +}; + +const validateT2IAdaptersV2: MetadataValidateFunc = (t2iAdapters) => { + const validatedT2IAdapters: T2IAdapterConfigV2Metadata[] = []; + t2iAdapters.forEach((t2iAdapter) => { + try { + validateBaseCompatibility(t2iAdapter.model?.base, 'T2I Adapter incompatible with currently-selected model'); + validatedT2IAdapters.push(t2iAdapter); + } catch { + // This is a no-op - we want to continue validating the rest of the T2I Adapters, and an empty list is valid. + } + }); + return new Promise((resolve) => resolve(validatedT2IAdapters)); +}; + +const validateIPAdapterV2: MetadataValidateFunc = (ipAdapter) => { + validateBaseCompatibility(ipAdapter.model?.base, 'IP Adapter incompatible with currently-selected model'); + return new Promise((resolve) => resolve(ipAdapter)); +}; + +const validateIPAdaptersV2: MetadataValidateFunc = (ipAdapters) => { + const validatedIPAdapters: IPAdapterConfigV2Metadata[] = []; + ipAdapters.forEach((ipAdapter) => { + try { + validateBaseCompatibility(ipAdapter.model?.base, 'IP Adapter incompatible with currently-selected model'); + validatedIPAdapters.push(ipAdapter); + } catch { + // This is a no-op - we want to continue validating the rest of the IP Adapters, and an empty list is valid. + } + }); + return new Promise((resolve) => resolve(validatedIPAdapters)); +}; + export const validators = { refinerModel: validateRefinerModel, vaeModel: validateVAEModel, @@ -119,4 +176,10 @@ export const validators = { t2iAdapters: validateT2IAdapters, ipAdapter: validateIPAdapter, ipAdapters: validateIPAdapters, + controlNetV2: validateControlNetV2, + controlNetsV2: validateControlNetsV2, + t2iAdapterV2: validateT2IAdapterV2, + t2iAdaptersV2: validateT2IAdaptersV2, + ipAdapterV2: validateIPAdapterV2, + ipAdaptersV2: validateIPAdaptersV2, } as const; diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts index 30f954dedb..0069bea881 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts @@ -43,7 +43,7 @@ export const usePreselectedImage = (selectedImage?: { const handleUseAllMetadata = useCallback(() => { if (selectedImageMetadata) { - parseAndRecallAllMetadata(selectedImageMetadata); + parseAndRecallAllMetadata(selectedImageMetadata, true); } }, [selectedImageMetadata]); From 1b13fee2565a62027639a8027a9a1aa87153ab95 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 18:11:09 +1000 Subject: [PATCH 41/74] fix(ui): firefox drawing lag Firefox v125.0.3 and below has a bug where `mouseenter` events are fired continually during mouse moves. The issue isn't present on FF v126.0b6 Developer Edition. It's not clear if the issue is present on FF nightly, and we're not sure if it will actually be fixed in the stable v126 release. The control layers drawing logic relied on on `mouseenter` events to create new lines, and `mousemove` to extend existing lines. On the affected version of FF, all line extensions are turned into new lines, resulting in very poor performance, noncontiguous lines, and way-too-big internal state. To resolve this, the drawing handling was updated to not use `mouseenter` at all. As a bonus, resolving this issue has resulted in simpler logic for drawing on the canvas. --- .../components/StageComponent.tsx | 30 +++---- .../controlLayers/hooks/mouseEventHooks.ts | 80 ++++++------------- .../controlLayers/store/controlLayersSlice.ts | 3 +- .../features/controlLayers/util/renderers.ts | 3 +- 4 files changed, 39 insertions(+), 77 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index ecf1121b41..c66c15d61b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -7,7 +7,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useMouseEvents } from 'features/controlLayers/hooks/mouseEventHooks'; import { $cursorPosition, - $isMouseOver, $lastMouseDownPos, $tool, isRegionalGuidanceLayer, @@ -48,10 +47,9 @@ const useStageRenderer = ( const dispatch = useAppDispatch(); const state = useAppSelector((s) => s.controlLayers.present); const tool = useStore($tool); - const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel } = useMouseEvents(); + const mouseEventHandlers = useMouseEvents(); const cursorPosition = useStore($cursorPosition); const lastMouseDownPos = useStore($lastMouseDownPos); - const isMouseOver = useStore($isMouseOver); const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor); const selectedLayerType = useAppSelector(selectSelectedLayerType); const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]); @@ -90,23 +88,21 @@ const useStageRenderer = ( if (asPreview) { return; } - stage.on('mousedown', onMouseDown); - stage.on('mouseup', onMouseUp); - stage.on('mousemove', onMouseMove); - stage.on('mouseenter', onMouseEnter); - stage.on('mouseleave', onMouseLeave); - stage.on('wheel', onMouseWheel); + stage.on('mousedown', mouseEventHandlers.onMouseDown); + stage.on('mouseup', mouseEventHandlers.onMouseUp); + stage.on('mousemove', mouseEventHandlers.onMouseMove); + stage.on('mouseleave', mouseEventHandlers.onMouseLeave); + stage.on('wheel', mouseEventHandlers.onMouseWheel); return () => { log.trace('Cleaning up stage listeners'); - stage.off('mousedown', onMouseDown); - stage.off('mouseup', onMouseUp); - stage.off('mousemove', onMouseMove); - stage.off('mouseenter', onMouseEnter); - stage.off('mouseleave', onMouseLeave); - stage.off('wheel', onMouseWheel); + stage.off('mousedown', mouseEventHandlers.onMouseDown); + stage.off('mouseup', mouseEventHandlers.onMouseUp); + stage.off('mousemove', mouseEventHandlers.onMouseMove); + stage.off('mouseleave', mouseEventHandlers.onMouseLeave); + stage.off('wheel', mouseEventHandlers.onMouseWheel); }; - }, [stage, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel]); + }, [stage, asPreview, mouseEventHandlers]); useLayoutEffect(() => { log.trace('Updating stage dimensions'); @@ -147,7 +143,6 @@ const useStageRenderer = ( state.globalMaskLayerOpacity, cursorPosition, lastMouseDownPos, - isMouseOver, state.brushSize ); }, [ @@ -159,7 +154,6 @@ const useStageRenderer = ( state.globalMaskLayerOpacity, cursorPosition, lastMouseDownPos, - isMouseOver, state.brushSize, renderers, ]); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts index e3e87d0c42..7fe81ad567 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts @@ -4,8 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { $cursorPosition, - $isMouseDown, - $isMouseOver, + $isDrawing, $lastMouseDownPos, $tool, brushSizeChanged, @@ -21,6 +20,7 @@ import { useCallback, useRef } from 'react'; const getIsFocused = (stage: Konva.Stage) => { return stage.container().contains(document.activeElement); }; +const getIsMouseDown = (e: KonvaEventObject) => e.evt.buttons === 1; export const getScaledFlooredCursorPosition = (stage: Konva.Stage) => { const pointerPosition = stage.getPointerPosition(); @@ -55,7 +55,7 @@ export const useMouseEvents = () => { const brushSize = useAppSelector((s) => s.controlLayers.present.brushSize); const onMouseDown = useCallback( - (e: KonvaEventObject) => { + (e: KonvaEventObject) => { const stage = e.target.getStage(); if (!stage) { return; @@ -64,7 +64,6 @@ export const useMouseEvents = () => { if (!pos) { return; } - $isMouseDown.set(true); $lastMouseDownPos.set(pos); if (!selectedLayerId) { return; @@ -77,18 +76,18 @@ export const useMouseEvents = () => { tool, }) ); + $isDrawing.set(true); } }, [dispatch, selectedLayerId, tool] ); const onMouseUp = useCallback( - (e: KonvaEventObject) => { + (e: KonvaEventObject) => { const stage = e.target.getStage(); if (!stage) { return; } - $isMouseDown.set(false); const pos = $cursorPosition.get(); const lastPos = $lastMouseDownPos.get(); const tool = $tool.get(); @@ -105,13 +104,14 @@ export const useMouseEvents = () => { }) ); } + $isDrawing.set(false); $lastMouseDownPos.set(null); }, [dispatch, selectedLayerId] ); const onMouseMove = useCallback( - (e: KonvaEventObject) => { + (e: KonvaEventObject) => { const stage = e.target.getStage(); if (!stage) { return; @@ -120,22 +120,29 @@ export const useMouseEvents = () => { if (!pos || !selectedLayerId) { return; } - if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) { - if (lastCursorPosRef.current) { - // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number - if (Math.hypot(lastCursorPosRef.current[0] - pos.x, lastCursorPosRef.current[1] - pos.y) < BRUSH_SPACING) { - return; + if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) { + if ($isDrawing.get()) { + // Continue the last line + if (lastCursorPosRef.current) { + // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number + if (Math.hypot(lastCursorPosRef.current[0] - pos.x, lastCursorPosRef.current[1] - pos.y) < BRUSH_SPACING) { + return; + } } + lastCursorPosRef.current = [pos.x, pos.y]; + dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: lastCursorPosRef.current })); + } else { + // Start a new line + dispatch(rgLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool })); } - lastCursorPosRef.current = [pos.x, pos.y]; - dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: lastCursorPosRef.current })); + $isDrawing.set(true); } }, [dispatch, selectedLayerId, tool] ); const onMouseLeave = useCallback( - (e: KonvaEventObject) => { + (e: KonvaEventObject) => { const stage = e.target.getStage(); if (!stage) { return; @@ -145,54 +152,17 @@ export const useMouseEvents = () => { pos && selectedLayerId && getIsFocused(stage) && - $isMouseOver.get() && - $isMouseDown.get() && + getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser') ) { dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: [pos.x, pos.y] })); } - $isMouseOver.set(false); - $isMouseDown.set(false); + $isDrawing.set(false); $cursorPosition.set(null); }, [selectedLayerId, tool, dispatch] ); - const onMouseEnter = useCallback( - (e: KonvaEventObject) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } - $isMouseOver.set(true); - const pos = syncCursorPos(stage); - if (!pos) { - return; - } - if (!getIsFocused(stage)) { - return; - } - if (e.evt.buttons !== 1) { - $isMouseDown.set(false); - } else { - $isMouseDown.set(true); - if (!selectedLayerId) { - return; - } - if (tool === 'brush' || tool === 'eraser') { - dispatch( - rgLayerLineAdded({ - layerId: selectedLayerId, - points: [pos.x, pos.y, pos.x, pos.y], - tool, - }) - ); - } - } - }, - [dispatch, selectedLayerId, tool] - ); - const onMouseWheel = useCallback( (e: KonvaEventObject) => { e.evt.preventDefault(); @@ -213,5 +183,5 @@ export const useMouseEvents = () => { [shouldInvertBrushSizeScrollDirection, brushSize, dispatch] ); - return { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel }; + return { onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 7c8d6a895c..0b17b6bc9c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -801,8 +801,7 @@ const migrateControlLayersState = (state: any): any => { return state; }; -export const $isMouseDown = atom(false); -export const $isMouseOver = atom(false); +export const $isDrawing = atom(false); export const $lastMouseDownPos = atom(null); export const $tool = atom('brush'); export const $cursorPosition = atom(null); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index 85f48baa6e..12091d3e8b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -137,7 +137,6 @@ const renderToolPreview = ( globalMaskLayerOpacity: number, cursorPos: Vector2d | null, lastMouseDownPos: Vector2d | null, - isMouseOver: boolean, brushSize: number ) => { const layerCount = stage.find(`.${RG_LAYER_NAME}`).length; @@ -161,7 +160,7 @@ const renderToolPreview = ( const toolPreviewLayer = stage.findOne(`#${TOOL_PREVIEW_LAYER_ID}`) ?? createToolPreviewLayer(stage); - if (!isMouseOver || layerCount === 0) { + if (!cursorPos || layerCount === 0) { // We can bail early if the mouse isn't over the stage or there are no layers toolPreviewLayer.visible(false); return; From 474eab6f8acb106e986aca90cf73dfff7fbfd15a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 18:28:46 +1000 Subject: [PATCH 42/74] fix(ui): clamp incoming w/h to ensure always a multiple of 8 When recalling metadata and/or using control image dimensions, it was possible to set a width or height that was not a multiple of 8, resulting in generation failures. Added a `clamp` option to the w/h actions to fix this. The option is used for all untrusted sources - everything except for the w/h number inputs, which clamp the values themselves. --- .../listeners/setDefaultSettings.ts | 6 +++--- .../components/ControlAdapterImagePreview.tsx | 5 +++-- .../ControlAdapterImagePreview.tsx | 10 ++++++---- .../IPAdapterImagePreview.tsx | 9 +++++---- .../controlLayers/store/controlLayersSlice.ts | 17 +++++++++-------- .../web/src/features/metadata/util/recallers.ts | 6 ++++-- 6 files changed, 30 insertions(+), 23 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index 6f3aa9756a..61a978d576 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -96,16 +96,16 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni dispatch(setScheduler(scheduler)); } } - + const setSizeOptions = { updateAspectRatio: true, clamp: true }; if (width) { if (isParameterWidth(width)) { - dispatch(widthChanged({ width, updateAspectRatio: true })); + dispatch(widthChanged({ width, ...setSizeOptions })); } } if (height) { if (isParameterHeight(height)) { - dispatch(heightChanged({ height, updateAspectRatio: true })); + dispatch(heightChanged({ height, ...setSizeOptions })); } } diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx index 56589fe613..1360c76240 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx @@ -96,12 +96,13 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { if (activeTabName === 'unifiedCanvas') { dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)); } else { + const options = { updateAspectRatio: true, clamp: true }; const { width, height } = calculateNewSize( controlImage.width / controlImage.height, optimalDimension * optimalDimension ); - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } }, [controlImage, activeTabName, dispatch, optimalDimension]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx index 675118c534..81bd7b14f2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx @@ -85,17 +85,19 @@ export const ControlAdapterImagePreview = memo( setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) ); } else { + const options = { updateAspectRatio: true, clamp: true }; + if (shift) { const { width, height } = controlImage; - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } else { const { width, height } = calculateNewSize( controlImage.width / controlImage.height, optimalDimension * optimalDimension ); - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } } }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx index 7de726cda5..f73f61cbbd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx @@ -51,17 +51,18 @@ export const IPAdapterImagePreview = memo( setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) ); } else { + const options = { updateAspectRatio: true, clamp: true }; if (shift) { const { width, height } = controlImage; - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } else { const { width, height } = calculateNewSize( controlImage.width / controlImage.height, optimalDimension * optimalDimension ); - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } } }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 0b17b6bc9c..9f36401e83 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -3,6 +3,7 @@ import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; +import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import type { CLIPVisionModelV2, ControlModeV2, @@ -626,20 +627,20 @@ export const controlLayersSlice = createSlice({ shouldConcatPromptsChanged: (state, action: PayloadAction) => { state.shouldConcatPrompts = action.payload; }, - widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean }>) => { - const { width, updateAspectRatio } = action.payload; - state.size.width = width; + widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { + const { width, updateAspectRatio, clamp } = action.payload; + state.size.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width; if (updateAspectRatio) { - state.size.aspectRatio.value = width / state.size.height; + state.size.aspectRatio.value = state.size.width / state.size.height; state.size.aspectRatio.id = 'Free'; state.size.aspectRatio.isLocked = false; } }, - heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean }>) => { - const { height, updateAspectRatio } = action.payload; - state.size.height = height; + heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { + const { height, updateAspectRatio, clamp } = action.payload; + state.size.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height; if (updateAspectRatio) { - state.size.aspectRatio.value = state.size.width / height; + state.size.aspectRatio.value = state.size.width / state.size.height; state.size.aspectRatio.id = 'Free'; state.size.aspectRatio.isLocked = false; } diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index a7b0cd5b2e..c04259ac62 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -110,12 +110,14 @@ const recallInitialImage: MetadataRecallFunc = async (imageDTO) => { getStore().dispatch(initialImageChanged(imageDTO)); }; +const setSizeOptions = { updateAspectRatio: true, clamp: true }; + const recallWidth: MetadataRecallFunc = (width) => { - getStore().dispatch(widthChanged({ width, updateAspectRatio: true })); + getStore().dispatch(widthChanged({ width, ...setSizeOptions })); }; const recallHeight: MetadataRecallFunc = (height) => { - getStore().dispatch(heightChanged({ height, updateAspectRatio: true })); + getStore().dispatch(heightChanged({ height, ...setSizeOptions })); }; const recallSteps: MetadataRecallFunc = (steps) => { From d55ea318ec76961b50d2f2b28c2c37af87053104 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 19:53:56 +1000 Subject: [PATCH 43/74] tidy(ui): remove unused gallery hotkeys --- .../system/components/HotkeysModal/useHotkeyData.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts index f9a0df52e4..84aa632ea9 100644 --- a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts @@ -140,16 +140,6 @@ export const useHotkeyData = (): HotkeyGroup[] => { desc: t('hotkeys.nextImage.desc'), hotkeys: [['Arrow Right']], }, - { - title: t('hotkeys.increaseGalleryThumbSize.title'), - desc: t('hotkeys.increaseGalleryThumbSize.desc'), - hotkeys: [['Shift', 'Up']], - }, - { - title: t('hotkeys.decreaseGalleryThumbSize.title'), - desc: t('hotkeys.decreaseGalleryThumbSize.desc'), - hotkeys: [['Shift', 'Down']], - }, ], }), [t] From d67480d92ca56f33a2fdc4e23cea36fa85ba7a42 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 20:52:49 +1000 Subject: [PATCH 44/74] feat(ui): add layerwrapper component --- .../components/CALayer/CALayer.tsx | 40 +++++++--------- .../components/IPALayer/IPALayer.tsx | 27 +++++------ .../components/LayerCommon/LayerWrapper.tsx | 21 +++++++++ .../components/RGLayer/RGLayer.tsx | 47 +++++++++---------- 4 files changed, 73 insertions(+), 62 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index 24de817df2..984331a050 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -5,6 +5,7 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; @@ -17,37 +18,28 @@ type Props = { export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected); - const onClickCapture = useCallback(() => { + const onClick = useCallback(() => { // Must be capture so that the layer is selected before deleting/resetting/etc dispatch(layerSelected(layerId)); }, [dispatch, layerId]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - - - - - - - - - - - {isOpen && ( - - - - )} + + + + + + + + - + {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 715e538679..02a161608d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -3,6 +3,7 @@ import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPAL import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { memo } from 'react'; type Props = { @@ -12,21 +13,19 @@ type Props = { export const IPALayer = memo(({ layerId }: Props) => { const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - - - - - - - - - {isOpen && ( - - - - )} + + + + + + - + {isOpen && ( + + + + )} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx new file mode 100644 index 0000000000..9d5fb6ea4b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx @@ -0,0 +1,21 @@ +import type { ChakraProps } from '@invoke-ai/ui-library'; +import { Flex } from '@invoke-ai/ui-library'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +type Props = PropsWithChildren<{ + onClick?: () => void; + borderColor: ChakraProps['bg']; +}>; + +export const LayerWrapper = memo(({ onClick, borderColor, children }: Props) => { + return ( + + + {children} + + + ); +}); + +LayerWrapper.displayName = 'LayerWrapper'; 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 baed22f6ca..a6bce5316e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -7,6 +7,7 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { isRegionalGuidanceLayer, layerSelected, @@ -52,32 +53,30 @@ export const RGLayer = memo(({ layerId }: Props) => { dispatch(layerSelected(layerId)); }, [dispatch, layerId]); return ( - - - - - - - {autoNegative === 'invert' && ( - - {t('controlLayers.autoNegative')} - - )} - - - - - - {isOpen && ( - - {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } - {hasPositivePrompt && } - {hasNegativePrompt && } - {hasIPAdapters && } - + + + + + + {autoNegative === 'invert' && ( + + {t('controlLayers.autoNegative')} + )} + + + + - + {isOpen && ( + + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } + {hasPositivePrompt && } + {hasNegativePrompt && } + {hasIPAdapters && } + + )} + ); }); From 1d213067e8a61969b7cc440fca185d3df0781c4d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 21:12:58 +1000 Subject: [PATCH 45/74] feat(ui): add initial image layer to CL --- invokeai/frontend/web/public/locales/en.json | 2 + .../listeners/imageDropped.ts | 19 +++ .../listeners/imageUploaded.ts | 12 ++ .../src/common/hooks/useIsReadyToEnqueue.ts | 3 +- .../components/AddLayerButton.tsx | 6 +- .../components/ControlLayersPanelContent.tsx | 4 + .../components/IILayer/IILayer.tsx | 81 +++++++++++++ .../IILayer/InitialImagePreview.tsx | 109 ++++++++++++++++++ .../components/LayerCommon/LayerMenu.tsx | 4 +- .../components/LayerCommon/LayerTitle.tsx | 2 + .../controlLayers/hooks/addLayerHooks.ts | 18 ++- .../controlLayers/store/controlLayersSlice.ts | 57 ++++++++- .../src/features/controlLayers/store/types.ts | 8 +- .../web/src/features/dnd/types/index.ts | 10 +- .../web/src/features/dnd/util/isValidDrop.ts | 2 + .../frontend/web/src/services/api/types.ts | 8 +- 16 files changed, 335 insertions(+), 10 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c80283b664..c211b4a574 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1543,6 +1543,8 @@ "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalIPAdapter": "Global $t(common.ipAdapter)", "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)", + "globalInitialImage": "Global Initial Image", + "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", "opacityFilter": "Opacity Filter", "clearProcessor": "Clear Processor", "resetProcessor": "Reset Processor to Defaults" diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 5db78ed75e..734867d0e1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -9,6 +9,7 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import { caLayerImageChanged, + iiLayerImageChanged, ipaLayerImageChanged, rgLayerIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; @@ -143,6 +144,24 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } + /** + * Image dropped on II Layer Image + */ + if ( + overData.actionType === 'SET_II_LAYER_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + const { layerId } = overData.context; + dispatch( + iiLayerImageChanged({ + layerId, + imageDTO: activeData.payload.imageDTO, + }) + ); + return; + } + /** * Image dropped on Canvas */ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index d0edfafd57..8f93d023ce 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -8,6 +8,7 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import { caLayerImageChanged, + iiLayerImageChanged, ipaLayerImageChanged, rgLayerIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; @@ -146,6 +147,17 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis ); } + if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') { + const { layerId } = postUploadAction; + dispatch(iiLayerImageChanged({ layerId, imageDTO })); + dispatch( + addToast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), + }) + ); + } + if (postUploadAction?.type === 'SET_INITIAL_IMAGE') { dispatch(initialImageChanged(imageDTO)); dispatch( diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 6073564305..d06fc259df 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -16,7 +16,6 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import i18n from 'i18next'; import { forEach } from 'lodash-es'; import { getConnectedEdges } from 'reactflow'; -import { assert } from 'tsafe'; const selector = createMemoizedSelector( [ @@ -110,7 +109,7 @@ const selector = createMemoizedSelector( } else if (l.type === 'regional_guidance_layer') { return l.ipAdapters; } - assert(false); + return []; }) .forEach((ca, i) => { const hasNoModel = !ca.model; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 3eb97dddff..3102e4afa8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,6 +1,6 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; +import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,6 +11,7 @@ export const AddLayerButton = memo(() => { const dispatch = useAppDispatch(); const [addCALayer, isAddCALayerDisabled] = useAddCALayer(); const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); + const [addIILayer, isAddIILayerDisabled] = useAddIILayer(); const addRGLayer = useCallback(() => { dispatch(rgLayerAdded()); }, [dispatch]); @@ -30,6 +31,9 @@ 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/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index ffa2856116..14bea9bc1e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -6,6 +6,7 @@ import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableCon 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 { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; @@ -54,6 +55,9 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { if (type === 'ip_adapter_layer') { return ; } + if (type === 'initial_image_layer') { + return ; + } }); LayerWrapper.displayName = 'LayerWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx new file mode 100644 index 0000000000..3c54bffb6c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -0,0 +1,81 @@ +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 { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; +import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; +import { + iiLayerImageChanged, + layerSelected, + selectIILayerOrThrow, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { IILayerImageDropData } from 'features/dnd/types'; +import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; +import { memo, useCallback, useMemo } from 'react'; +import type { IILayerImagePostUploadAction, ImageDTO } from 'services/api/types'; + +type Props = { + layerId: string; +}; + +export const IILayer = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const layer = useAppSelector((s) => selectIILayerOrThrow(s.controlLayers.present, layerId)); + const onClick = useCallback(() => { + dispatch(layerSelected(layerId)); + }, [dispatch, layerId]); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + + const onChangeImage = useCallback( + (imageDTO: ImageDTO | null) => { + dispatch(iiLayerImageChanged({ layerId, imageDTO })); + }, + [dispatch, layerId] + ); + + const droppableData = useMemo( + () => ({ + actionType: 'SET_II_LAYER_IMAGE', + context: { + layerId, + }, + id: layerId, + }), + [layerId] + ); + + const postUploadAction = useMemo( + () => ({ + layerId, + type: 'SET_II_LAYER_IMAGE', + }), + [layerId] + ); + + return ( + + + + + + + + + {isOpen && ( + + + + + )} + + ); +}); + +IILayer.displayName = 'IILayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx new file mode 100644 index 0000000000..740e81cbde --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx @@ -0,0 +1,109 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Flex, useShiftModifier } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIDndImage from 'common/components/IAIDndImage'; +import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import type { 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 } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import type { ImageDTO, PostUploadAction } from 'services/api/types'; + +type Props = { + image: ImageWithDims | null; + onChangeImage: (imageDTO: ImageDTO | null) => void; + droppableData: TypesafeDroppableData; + postUploadAction: PostUploadAction; +}; + +export const InitialImagePreview = memo(({ image, onChangeImage, droppableData, postUploadAction }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isConnected = useAppSelector((s) => s.system.isConnected); + const activeTabName = useAppSelector(activeTabNameSelector); + const optimalDimension = useAppSelector(selectOptimalDimension); + const shift = useShiftModifier(); + + const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.imageName ?? skipToken); + + const onReset = useCallback(() => { + onChangeImage(null); + }, [onChangeImage]); + + const onUseSize = useCallback(() => { + if (!imageDTO) { + return; + } + + if (activeTabName === 'unifiedCanvas') { + dispatch(setBoundingBoxDimensions({ width: imageDTO.width, height: imageDTO.height }, optimalDimension)); + } else { + const options = { updateAspectRatio: true, clamp: true }; + if (shift) { + const { width, height } = imageDTO; + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } else { + const { width, height } = calculateNewSize( + imageDTO.width / imageDTO.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } + } + }, [imageDTO, activeTabName, dispatch, optimalDimension, shift]); + + const draggableData = useMemo(() => { + if (imageDTO) { + return { + id: 'initial_image_layer', + payloadType: 'IMAGE_DTO', + payload: { imageDTO: imageDTO }, + }; + } + }, [imageDTO]); + + useEffect(() => { + if (isConnected && isErrorControlImage) { + onReset(); + } + }, [onReset, isConnected, isErrorControlImage]); + + return ( + + + + <> + : undefined} + tooltip={t('controlnet.resetControlImage')} + /> + : undefined} + tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')} + styleOverrides={useSizeStyleOverrides} + /> + + + ); +}); + +InitialImagePreview.displayName = 'InitialImagePreview'; + +const useSizeStyleOverrides: SystemStyleObject = { mt: 6 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx index b83f48188f..12074d12b8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx @@ -37,7 +37,9 @@ export const LayerMenu = memo(({ layerId }: Props) => { )} - {(layerType === 'regional_guidance_layer' || layerType === 'control_adapter_layer') && ( + {(layerType === 'regional_guidance_layer' || + layerType === 'control_adapter_layer' || + layerType === 'initial_image_layer') && ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx index ec13ff7bcc..b29c3753fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx @@ -16,6 +16,8 @@ export const LayerTitle = memo(({ type }: Props) => { return t('controlLayers.globalControlAdapter'); } else if (type === 'ip_adapter_layer') { return t('controlLayers.globalIPAdapter'); + } else if (type === 'initial_image_layer') { + return t('controlLayers.globalInitialImage'); } }, [t, type]); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 7a4e7ebc09..dcbbeb8db5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -1,5 +1,11 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { caLayerAdded, ipaLayerAdded, rgLayerIPAdapterAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { + caLayerAdded, + iiLayerAdded, + ipaLayerAdded, + isInitialImageLayer, + rgLayerIPAdapterAdded, +} from 'features/controlLayers/store/controlLayersSlice'; import { buildControlNet, buildIPAdapter, @@ -93,3 +99,13 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => { return [addIPAdapter, isDisabled] as const; }; + +export const useAddIILayer = () => { + const dispatch = useAppDispatch(); + const isDisabled = useAppSelector((s) => Boolean(s.controlLayers.present.layers.find(isInitialImageLayer))); + const addIILayer = useCallback(() => { + dispatch(iiLayerAdded(null)); + }, [dispatch]); + + return [addIILayer, isDisabled] as const; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 9f36401e83..bd130a0236 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -39,6 +39,7 @@ import type { ControlAdapterLayer, ControlLayersState, DrawingTool, + InitialImageLayer, IPAdapterLayer, Layer, RegionalGuidanceLayer, @@ -71,8 +72,13 @@ export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanc export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => layer?.type === 'control_adapter_layer'; export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => layer?.type === 'ip_adapter_layer'; -export const isRenderableLayer = (layer?: Layer): layer is RegionalGuidanceLayer | ControlAdapterLayer => - layer?.type === 'regional_guidance_layer' || layer?.type === 'control_adapter_layer'; +export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => layer?.type === 'initial_image_layer'; +export const isRenderableLayer = ( + layer?: Layer +): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer => + layer?.type === 'regional_guidance_layer' || + layer?.type === 'control_adapter_layer' || + layer?.type === 'initial_image_layer'; const resetLayer = (layer: Layer) => { if (layer.type === 'regional_guidance_layer') { layer.maskObjects = []; @@ -94,6 +100,11 @@ export const selectIPALayerOrThrow = (state: ControlLayersState, layerId: string assert(isIPAdapterLayer(layer)); return layer; }; +export const selectIILayerOrThrow = (state: ControlLayersState, layerId: string): InitialImageLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isInitialImageLayer(layer)); + return layer; +}; const selectCAOrIPALayerOrThrow = ( state: ControlLayersState, layerId: string @@ -611,6 +622,45 @@ export const controlLayersSlice = createSlice({ }, //#endregion + //#region Initial Image Layer + iiLayerAdded: { + reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { + const { layerId, imageDTO } = action.payload; + // Highlander! There can be only one! + assert(!state.layers.find(isInitialImageLayer)); + const layer: InitialImageLayer = { + id: layerId, + type: 'initial_image_layer', + x: 0, + y: 0, + bbox: null, + bboxNeedsUpdate: false, + isEnabled: true, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + isSelected: true, + }; + state.layers.push(layer); + state.selectedLayerId = layer.id; + for (const layer of state.layers.filter(isRenderableLayer)) { + if (layer.id !== layerId) { + layer.isSelected = false; + } + } + }, + prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: 'initial_image_layer', imageDTO } }), + }, + iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { + const { layerId, imageDTO } = action.payload; + const layer = selectIILayerOrThrow(state, layerId); + if (layer) { + layer.bbox = null; + layer.bboxNeedsUpdate = true; + layer.isEnabled = true; + layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + } + }, + //#endregion + //#region Globals positivePromptChanged: (state, action: PayloadAction) => { state.positivePrompt = action.payload; @@ -780,6 +830,9 @@ export const { rgLayerIPAdapterMethodChanged, rgLayerIPAdapterModelChanged, rgLayerIPAdapterCLIPVisionModelChanged, + // II Layer + iiLayerAdded, + iiLayerImageChanged, // Globals positivePromptChanged, negativePromptChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index cbf47ff3ad..efcfb0f0bc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,5 +1,6 @@ import type { ControlNetConfigV2, + ImageWithDims, IPAdapterConfigV2, T2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; @@ -73,7 +74,12 @@ export type RegionalGuidanceLayer = RenderableLayerBase & { needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object }; -export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer; +export type InitialImageLayer = RenderableLayerBase & { + type: 'initial_image_layer'; + image: ImageWithDims | null; +}; + +export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer | InitialImageLayer; export type ControlLayersState = { _version: 1; diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 7d109473ed..b8d3cfe31e 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -55,6 +55,13 @@ export type RGLayerIPAdapterImageDropData = BaseDropData & { }; }; +export type IILayerImageDropData = BaseDropData & { + actionType: 'SET_II_LAYER_IMAGE'; + context: { + layerId: string; + }; +}; + export type CanvasInitialImageDropData = BaseDropData & { actionType: 'SET_CANVAS_INITIAL_IMAGE'; }; @@ -86,7 +93,8 @@ export type TypesafeDroppableData = | RemoveFromBoardDropData | CALayerImageDropData | IPALayerImageDropData - | RGLayerIPAdapterImageDropData; + | RGLayerIPAdapterImageDropData + | IILayerImageDropData; type BaseDragData = { id: string; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index c1da111087..757a21bd5c 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -25,6 +25,8 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: return payloadType === 'IMAGE_DTO'; case 'SET_RG_LAYER_IP_ADAPTER_IMAGE': return payloadType === 'IMAGE_DTO'; + case 'SET_II_LAYER_IMAGE': + return payloadType === 'IMAGE_DTO'; case 'SET_CANVAS_INITIAL_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SET_NODES_IMAGE': diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 5b88170d41..183b81478d 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -193,6 +193,11 @@ export type RGLayerIPAdapterImagePostUploadAction = { ipAdapterId: string; }; +export type IILayerImagePostUploadAction = { + type: 'SET_II_LAYER_IMAGE'; + layerId: string; +}; + type InitialImageAction = { type: 'SET_INITIAL_IMAGE'; }; @@ -225,4 +230,5 @@ export type PostUploadAction = | AddToBatchAction | CALayerImagePostUploadAction | IPALayerImagePostUploadAction - | RGLayerIPAdapterImagePostUploadAction; + | RGLayerIPAdapterImagePostUploadAction + | IILayerImagePostUploadAction; From 75be6814bb27f3dcad95c81b541ef94d6b2c86e5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 21:22:53 +1000 Subject: [PATCH 46/74] feat(ui): add renderer for initial image --- .../controlLayers/store/controlLayersSlice.ts | 3 + .../features/controlLayers/util/renderers.ts | 108 ++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index bd130a0236..d8d159f19a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -878,6 +878,8 @@ export const RG_LAYER_NAME = 'regional_guidance_layer'; export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line'; export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group'; export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect'; +export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer'; +export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image'; export const LAYER_BBOX_NAME = 'layer.bbox'; // Getters for non-singleton layer and object IDs @@ -888,6 +890,7 @@ export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${ export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`; const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`; export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; +export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`; export const controlLayersPersistConfig: PersistConfig = { diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index 12091d3e8b..4f5def7b76 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -8,9 +8,13 @@ import { CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, getCALayerImageId, + getIILayerImageId, getLayerBboxId, getRGLayerObjectGroupId, + INITIAL_IMAGE_LAYER_IMAGE_NAME, + INITIAL_IMAGE_LAYER_NAME, isControlAdapterLayer, + isInitialImageLayer, isRegionalGuidanceLayer, isRenderableLayer, LAYER_BBOX_NAME, @@ -28,6 +32,7 @@ import { } from 'features/controlLayers/store/controlLayersSlice'; import type { ControlAdapterLayer, + InitialImageLayer, Layer, RegionalGuidanceLayer, Tool, @@ -406,6 +411,106 @@ const renderRegionalGuidanceLayer = ( } }; +const createInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLayer): Konva.Layer => { + const konvaLayer = new Konva.Layer({ + id: reduxLayer.id, + name: INITIAL_IMAGE_LAYER_NAME, + imageSmoothingEnabled: true, + }); + stage.add(konvaLayer); + return konvaLayer; +}; + +const createInitialImageLayerImage = (konvaLayer: Konva.Layer, image: HTMLImageElement): Konva.Image => { + const konvaImage = new Konva.Image({ + name: INITIAL_IMAGE_LAYER_IMAGE_NAME, + image, + }); + konvaLayer.add(konvaImage); + return konvaImage; +}; + +const updateInitialImageLayerImageAttrs = ( + stage: Konva.Stage, + konvaImage: Konva.Image, + reduxLayer: InitialImageLayer +) => { + const newWidth = stage.width() / stage.scaleX(); + const newHeight = stage.height() / stage.scaleY(); + if ( + konvaImage.width() !== newWidth || + konvaImage.height() !== newHeight || + konvaImage.visible() !== reduxLayer.isEnabled + ) { + konvaImage.setAttrs({ + // opacity: reduxLayer.opacity, + scaleX: 1, + scaleY: 1, + width: stage.width() / stage.scaleX(), + height: stage.height() / stage.scaleY(), + visible: reduxLayer.isEnabled, + }); + } + // if (konvaImage.opacity() !== reduxLayer.opacity) { + // konvaImage.opacity(reduxLayer.opacity); + // } +}; + +const updateInitialImageLayerImageSource = async ( + stage: Konva.Stage, + konvaLayer: Konva.Layer, + reduxLayer: InitialImageLayer +) => { + if (reduxLayer.image) { + const { imageName } = reduxLayer.image; + const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); + const imageDTO = await req.unwrap(); + req.unsubscribe(); + const imageEl = new Image(); + const imageId = getIILayerImageId(reduxLayer.id, imageName); + imageEl.onload = () => { + // Find the existing image or create a new one - must find using the name, bc the id may have just changed + const konvaImage = + konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`) ?? + createInitialImageLayerImage(konvaLayer, imageEl); + + // Update the image's attributes + konvaImage.setAttrs({ + id: imageId, + image: imageEl, + }); + updateInitialImageLayerImageAttrs(stage, konvaImage, reduxLayer); + imageEl.id = imageId; + }; + imageEl.src = imageDTO.image_url; + } else { + konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`)?.destroy(); + } +}; + +const renderInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLayer) => { + const konvaLayer = stage.findOne(`#${reduxLayer.id}`) ?? createInitialImageLayer(stage, reduxLayer); + const konvaImage = konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`); + const canvasImageSource = konvaImage?.image(); + let imageSourceNeedsUpdate = false; + if (canvasImageSource instanceof HTMLImageElement) { + const image = reduxLayer.image; + if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) { + imageSourceNeedsUpdate = true; + } else if (!image) { + imageSourceNeedsUpdate = true; + } + } else if (!canvasImageSource) { + imageSourceNeedsUpdate = true; + } + + if (imageSourceNeedsUpdate) { + updateInitialImageLayerImageSource(stage, konvaLayer, reduxLayer); + } else if (konvaImage) { + updateInitialImageLayerImageAttrs(stage, konvaImage, reduxLayer); + } +}; + const createControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLayer): Konva.Layer => { const konvaLayer = new Konva.Layer({ id: reduxLayer.id, @@ -546,6 +651,9 @@ const renderLayers = ( if (isControlAdapterLayer(reduxLayer)) { renderControlNetLayer(stage, reduxLayer); } + if (isInitialImageLayer(reduxLayer)) { + renderInitialImageLayer(stage, reduxLayer); + } } }; From 8b6a283eabea7d2a98e3f079a4b737b79ebcd3a7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 21:31:17 +1000 Subject: [PATCH 47/74] feat(ui): add opacity to initial image layer --- .../components/IILayer/IILayer.tsx | 2 + .../components/IILayer/IILayerOpacity.tsx | 98 +++++++++++++++++++ .../controlLayers/store/controlLayersSlice.ts | 17 ++-- .../src/features/controlLayers/store/types.ts | 1 + .../features/controlLayers/util/renderers.ts | 8 +- 5 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx 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 3c54bffb6c..772dbd7332 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -1,5 +1,6 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IILayerOpacity from 'features/controlLayers/components/IILayer/IILayerOpacity'; import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; @@ -60,6 +61,7 @@ export const IILayer = memo(({ layerId }: Props) => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx new file mode 100644 index 0000000000..e26a3634a1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx @@ -0,0 +1,98 @@ +import { + CompositeNumberInput, + CompositeSlider, + Flex, + FormControl, + FormLabel, + IconButton, + Popover, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, +} from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { stopPropagation } from 'common/util/stopPropagation'; +import { + iiLayerOpacityChanged, + isInitialImageLayer, + selectControlLayersSlice, +} from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiDropHalfFill } from 'react-icons/pi'; +import { assert } from 'tsafe'; + +type Props = { + layerId: string; +}; + +const marks = [0, 25, 50, 75, 100]; +const formatPct = (v: number | string) => `${v} %`; + +const IILayerOpacity = ({ layerId }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selectOpacity = useMemo( + () => + createSelector(selectControlLayersSlice, (controlLayers) => { + const layer = controlLayers.present.layers.filter(isInitialImageLayer).find((l) => l.id === layerId); + assert(layer, `Layer ${layerId} not found`); + return Math.round(layer.opacity * 100); + }), + [layerId] + ); + const opacity = useAppSelector(selectOpacity); + const onChangeOpacity = useCallback( + (v: number) => { + dispatch(iiLayerOpacityChanged({ layerId, opacity: v / 100 })); + }, + [dispatch, layerId] + ); + return ( + + + } + variant="ghost" + onDoubleClick={stopPropagation} + /> + + + + + + + {t('controlLayers.opacity')} + + + + + + + + ); +}; + +export default memo(IILayerOpacity); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index d8d159f19a..2cb14509d7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -631,6 +631,7 @@ export const controlLayersSlice = createSlice({ const layer: InitialImageLayer = { id: layerId, type: 'initial_image_layer', + opacity: 1, x: 0, y: 0, bbox: null, @@ -652,12 +653,15 @@ export const controlLayersSlice = createSlice({ iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; const layer = selectIILayerOrThrow(state, layerId); - if (layer) { - layer.bbox = null; - layer.bboxNeedsUpdate = true; - layer.isEnabled = true; - layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - } + layer.bbox = null; + layer.bboxNeedsUpdate = true; + layer.isEnabled = true; + layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + iiLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { + const { layerId, opacity } = action.payload; + const layer = selectIILayerOrThrow(state, layerId); + layer.opacity = opacity; }, //#endregion @@ -833,6 +837,7 @@ export const { // II Layer iiLayerAdded, iiLayerImageChanged, + iiLayerOpacityChanged, // Globals positivePromptChanged, negativePromptChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index efcfb0f0bc..cbb986bde2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -76,6 +76,7 @@ export type RegionalGuidanceLayer = RenderableLayerBase & { export type InitialImageLayer = RenderableLayerBase & { type: 'initial_image_layer'; + opacity: number; image: ImageWithDims | null; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index 4f5def7b76..3e34542e87 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -443,7 +443,7 @@ const updateInitialImageLayerImageAttrs = ( konvaImage.visible() !== reduxLayer.isEnabled ) { konvaImage.setAttrs({ - // opacity: reduxLayer.opacity, + opacity: reduxLayer.opacity, scaleX: 1, scaleY: 1, width: stage.width() / stage.scaleX(), @@ -451,9 +451,9 @@ const updateInitialImageLayerImageAttrs = ( visible: reduxLayer.isEnabled, }); } - // if (konvaImage.opacity() !== reduxLayer.opacity) { - // konvaImage.opacity(reduxLayer.opacity); - // } + if (konvaImage.opacity() !== reduxLayer.opacity) { + konvaImage.opacity(reduxLayer.opacity); + } }; const updateInitialImageLayerImageSource = async ( From 209ddc2037177e1b9d8506fbb75931c94525ef15 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 21:35:30 +1000 Subject: [PATCH 48/74] fix(ui): do not toggle layers on double click of opacity popover --- .../controlLayers/components/CALayer/CALayerOpacity.tsx | 2 +- .../controlLayers/components/IILayer/IILayerOpacity.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx index 31c8d81853..353f8e0307 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx @@ -55,7 +55,7 @@ const CALayerOpacity = ({ layerId }: Props) => { onDoubleClick={stopPropagation} /> - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx index e26a3634a1..9918dda5b8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx @@ -61,7 +61,7 @@ const IILayerOpacity = ({ layerId }: Props) => { onDoubleClick={stopPropagation} /> - + From c9886796f65df0569e9dced5a69bafd93d587aa0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 22:29:00 +1000 Subject: [PATCH 49/74] feat(ui): add image viewer overlay - Works on txt2img, canvas and workflows tabs, img2img has its own side-by-side view - In workflow editor, the is closeable only if you are in edit mode, else it's always there - Press `i` to open - Press `esc` to close - Selecting an image or changing image selection opens the viewer - When generating, if auto-switch to new image is enabled, the viewer opens when an image comes in To support this change, I organized and restructured some tab stuff. --- invokeai/frontend/web/public/locales/en.json | 11 ++- .../gallery/components/ImageViewer.tsx | 80 +++++++++++++++++++ .../features/gallery/store/gallerySlice.ts | 9 ++- .../web/src/features/gallery/store/types.ts | 1 + .../web/src/features/metadata/types.ts | 6 +- .../BottomLeftPanel/BottomLeftPanel.tsx | 2 +- .../flow/panels/MinimapPanel/MinimapPanel.tsx | 2 +- .../flow/panels/TopPanel/TopPanel.tsx | 2 +- .../components/HotkeysModal/useHotkeyData.ts | 10 +++ .../src/features/ui/components/InvokeTabs.tsx | 37 ++++----- .../components/ParametersPanelTextToImage.tsx | 4 +- .../ui/components/tabs/ImageToImageTab.tsx | 2 +- .../features/ui/components/tabs/NodesTab.tsx | 22 ++--- .../ui/components/tabs/TextToImageTab.tsx | 26 +----- .../ui/components/tabs/UnifiedCanvasTab.tsx | 3 + .../web/src/features/ui/store/tabMap.ts | 3 - .../web/src/features/ui/store/tabMap.tsx | 3 + .../web/src/features/ui/store/uiSelectors.ts | 4 +- 18 files changed, 164 insertions(+), 63 deletions(-) create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/store/tabMap.ts create mode 100644 invokeai/frontend/web/src/features/ui/store/tabMap.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c211b4a574..cacb4dfbf4 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -361,7 +361,8 @@ "bulkDownloadRequestFailed": "Problem Preparing Download", "bulkDownloadFailed": "Download Failed", "problemDeletingImages": "Problem Deleting Images", - "problemDeletingImagesDesc": "One or more images could not be deleted" + "problemDeletingImagesDesc": "One or more images could not be deleted", + "backToEditor": "Back to {{tab}} (Esc)" }, "hotkeys": { "searchHotkeys": "Search Hotkeys", @@ -584,6 +585,14 @@ "upscale": { "desc": "Upscale the current image", "title": "Upscale" + }, + "backToEditor": { + "desc": "Closes the Image Viewer and shows the Editor View (Text to Image tab only)", + "title": "Back to Editor" + }, + "openImageViewer": { + "desc": "Opens the Image Viewer (Text to Image tab only)", + "title": "Open Image Viewer" } }, "metadata": { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx new file mode 100644 index 0000000000..563d1b2285 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx @@ -0,0 +1,80 @@ +import { Button, Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import CurrentImageButtons from 'features/gallery/components/CurrentImage/CurrentImageButtons'; +import CurrentImagePreview from 'features/gallery/components/CurrentImage/CurrentImagePreview'; +import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; +import type { InvokeTabName } from 'features/ui/store/tabMap'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { memo, useCallback, useMemo } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiArrowLeftBold } from 'react-icons/pi'; + +const TAB_NAME_TO_TKEY: Record = { + txt2img: 'common.txt2img', + img2img: 'common.img2img', + unifiedCanvas: 'common.unifiedCanvas', + nodes: 'common.nodes', + modelManager: 'modelManager.modelManager', + queue: 'queue.queue', +}; + +export const ImageViewer = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen); + const activeTabName = useAppSelector(activeTabNameSelector); + const activeTabLabel = useMemo( + () => t('gallery.backToEditor', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }), + [t, activeTabName] + ); + + const onClose = useCallback(() => { + dispatch(isImageViewerOpenChanged(false)); + }, [dispatch]); + + const onOpen = useCallback(() => { + dispatch(isImageViewerOpenChanged(true)); + }, [dispatch]); + + useHotkeys('esc', onClose, { enabled: isOpen }, [isOpen]); + useHotkeys('i', onOpen, { enabled: !isOpen }, [isOpen]); + + if (!isOpen) { + return null; + } + + return ( + + + + + + ); +}); + +ImageViewer.displayName = 'ImageViewer'; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 28435d31ae..373d946469 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -21,6 +21,7 @@ const initialGalleryState: GalleryState = { boardSearchText: '', limit: INITIAL_IMAGE_LIMIT, offset: 0, + isImageViewerOpen: false, }; export const gallerySlice = createSlice({ @@ -29,9 +30,11 @@ export const gallerySlice = createSlice({ reducers: { imageSelected: (state, action: PayloadAction) => { state.selection = action.payload ? [action.payload] : []; + state.isImageViewerOpen = true; }, selectionChanged: (state, action: PayloadAction) => { state.selection = uniqBy(action.payload, (i) => i.image_name); + state.isImageViewerOpen = true; }, shouldAutoSwitchChanged: (state, action: PayloadAction) => { state.shouldAutoSwitch = action.payload; @@ -75,6 +78,9 @@ export const gallerySlice = createSlice({ alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction) => { state.alwaysShowImageSizeBadge = action.payload; }, + isImageViewerOpenChanged: (state, action: PayloadAction) => { + state.isImageViewerOpen = action.payload; + }, }, extraReducers: (builder) => { builder.addMatcher(isAnyBoardDeleted, (state, action) => { @@ -112,6 +118,7 @@ export const { boardSearchTextChanged, moreImagesLoaded, alwaysShowImageSizeBadgeChanged, + isImageViewerOpenChanged, } = gallerySlice.actions; const isAnyBoardDeleted = isAnyOf( @@ -133,5 +140,5 @@ export const galleryPersistConfig: PersistConfig = { name: gallerySlice.name, initialState: initialGalleryState, migrate: migrateGalleryState, - persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit'], + persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit', 'isImageViewerOpen'], }; diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts index dbe91392ff..0e86d2d4be 100644 --- a/invokeai/frontend/web/src/features/gallery/store/types.ts +++ b/invokeai/frontend/web/src/features/gallery/store/types.ts @@ -20,4 +20,5 @@ export type GalleryState = { offset: number; limit: number; alwaysShowImageSizeBadge: boolean; + isImageViewerOpen: boolean; }; diff --git a/invokeai/frontend/web/src/features/metadata/types.ts b/invokeai/frontend/web/src/features/metadata/types.ts index 8621031896..30a34ec0c6 100644 --- a/invokeai/frontend/web/src/features/metadata/types.ts +++ b/invokeai/frontend/web/src/features/metadata/types.ts @@ -1,5 +1,9 @@ import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlAdapters/store/types'; -import type { ControlNetConfigV2, IPAdapterConfigV2, T2IAdapterConfigV2 } from 'features/controlLayers/util/controlAdapters'; +import type { + ControlNetConfigV2, + IPAdapterConfigV2, + T2IAdapterConfigV2, +} from 'features/controlLayers/util/controlAdapters'; import type { O } from 'ts-toolbelt'; /** diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/BottomLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/BottomLeftPanel.tsx index 412ff00052..7f6b947258 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/BottomLeftPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/BottomLeftPanel.tsx @@ -5,7 +5,7 @@ import NodeOpacitySlider from './NodeOpacitySlider'; import ViewportControls from './ViewportControls'; const BottomLeftPanel = () => ( - + diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx index 8c8d803cdb..b34ae11c85 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx @@ -19,7 +19,7 @@ const MinimapPanel = () => { const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.shouldShowMinimapPanel); return ( - + {shouldShowMinimapPanel && ( { const name = useAppSelector((s) => s.workflow.name); return ( - + diff --git a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts index 84aa632ea9..806b85ca59 100644 --- a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts @@ -140,6 +140,16 @@ export const useHotkeyData = (): HotkeyGroup[] => { desc: t('hotkeys.nextImage.desc'), hotkeys: [['Arrow Right']], }, + { + title: t('hotkeys.openImageViewer.title'), + desc: t('hotkeys.openImageViewer.desc'), + hotkeys: [['I']], + }, + { + title: t('hotkeys.backToEditor.title'), + desc: t('hotkeys.backToEditor.desc'), + hotkeys: [['Esc']], + }, ], }), [t] diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 9ac5324d41..b4a2922098 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -12,10 +12,17 @@ import { selectConfigSlice } from 'features/system/store/configSlice'; import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton'; import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons'; import ParametersPanelTextToImage from 'features/ui/components/ParametersPanelTextToImage'; +import ImageToImageTab from 'features/ui/components/tabs/ImageToImageTab'; +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'; import type { InvokeTabName } from 'features/ui/store/tabMap'; +import { TAB_NUMBER_MAP } from 'features/ui/store/tabMap'; import { activeTabIndexSelector, activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { setActiveTab } from 'features/ui/store/uiSlice'; import type { CSSProperties, MouseEvent, ReactElement, ReactNode } from 'react'; @@ -28,62 +35,56 @@ import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels'; import ParametersPanel from './ParametersPanel'; -import ImageTab from './tabs/ImageToImageTab'; -import ModelManagerTab from './tabs/ModelManagerTab'; -import NodesTab from './tabs/NodesTab'; -import QueueTab from './tabs/QueueTab'; import ResizeHandle from './tabs/ResizeHandle'; -import TextToImageTab from './tabs/TextToImageTab'; -import UnifiedCanvasTab from './tabs/UnifiedCanvasTab'; -interface InvokeTabInfo { +type TabData = { id: InvokeTabName; translationKey: string; icon: ReactElement; content: ReactNode; -} +}; -const tabs: InvokeTabInfo[] = [ - { +const TAB_DATA: Record = { + txt2img: { id: 'txt2img', translationKey: 'common.txt2img', icon: , content: , }, - { + img2img: { id: 'img2img', translationKey: 'common.img2img', icon: , - content: , + content: , }, - { + unifiedCanvas: { id: 'unifiedCanvas', translationKey: 'common.unifiedCanvas', icon: , content: , }, - { + nodes: { id: 'nodes', translationKey: 'common.nodes', icon: , content: , }, - { + modelManager: { id: 'modelManager', translationKey: 'modelManager.modelManager', icon: , content: , }, - { + queue: { id: 'queue', translationKey: 'queue.queue', icon: , content: , }, -]; +}; const enabledTabsSelector = createMemoizedSelector(selectConfigSlice, (config) => - tabs.filter((tab) => !config.disabledTabs.includes(tab.id)) + TAB_NUMBER_MAP.map((tabName) => TAB_DATA[tabName]).filter((tab) => !config.disabledTabs.includes(tab.id)) ); const NO_GALLERY_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue']; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx index 2d14a50856..3073b1e66b 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx @@ -39,8 +39,8 @@ const ParametersPanelTextToImage = () => { {isSDXL ? : } - {t('common.settingsLabel')} - {controlLayersTitle} + {t('common.settingsLabel')} + {controlLayersTitle} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImageTab.tsx index 07e87d202c..4303d66e2c 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImageTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImageTab.tsx @@ -29,7 +29,7 @@ const ImageToImageTab = () => { const panelStorage = usePanelStorage(); return ( - + { const mode = useAppSelector((s) => s.workflow.mode); if (mode === 'edit') { - return ( - - - - ); - } else { return ( - - - + + + + ); } + + return ( + + + + + + ); }; export default memo(NodesTab); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx index f9b760bcd5..df0b8651bc 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx @@ -1,31 +1,13 @@ -import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; +import { Box } from '@invoke-ai/ui-library'; import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor'; -import { useControlLayersTitle } from 'features/controlLayers/hooks/useControlLayersTitle'; -import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay'; +import { ImageViewer } from 'features/gallery/components/ImageViewer'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; const TextToImageTab = () => { - const { t } = useTranslation(); - const controlLayersTitle = useControlLayersTitle(); - return ( - - - {t('common.viewer')} - {controlLayersTitle} - - - - - - - - - - - + + ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx index 0f594ed705..8c74685d2d 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx @@ -6,6 +6,7 @@ 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 { ImageViewer } from 'features/gallery/components/ImageViewer'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -27,6 +28,7 @@ const UnifiedCanvasTab = () => { return ( { > + {isValidDrop(droppableData, active) && ( )} diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.ts b/invokeai/frontend/web/src/features/ui/store/tabMap.ts deleted file mode 100644 index a25d98187b..0000000000 --- a/invokeai/frontend/web/src/features/ui/store/tabMap.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const tabMap = ['txt2img', 'img2img', 'unifiedCanvas', 'nodes', 'modelManager', 'queue'] as const; - -export type InvokeTabName = (typeof tabMap)[number]; diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.tsx b/invokeai/frontend/web/src/features/ui/store/tabMap.tsx new file mode 100644 index 0000000000..1b8f3e4812 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/store/tabMap.tsx @@ -0,0 +1,3 @@ +export const TAB_NUMBER_MAP = ['txt2img', 'img2img', 'unifiedCanvas', 'nodes', 'modelManager', 'queue'] as const; + +export type InvokeTabName = (typeof TAB_NUMBER_MAP)[number]; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts index 07f3a99224..5fbc6a41de 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts @@ -3,7 +3,7 @@ import { selectConfigSlice } from 'features/system/store/configSlice'; import { selectUiSlice } from 'features/ui/store/uiSlice'; import { isString } from 'lodash-es'; -import { tabMap } from './tabMap'; +import { TAB_NUMBER_MAP } from './tabMap'; export const activeTabNameSelector = createSelector( selectUiSlice, @@ -15,7 +15,7 @@ export const activeTabNameSelector = createSelector( ); export const activeTabIndexSelector = createSelector(selectUiSlice, selectConfigSlice, (ui, config) => { - const tabs = tabMap.filter((t) => !config.disabledTabs.includes(t)); + const tabs = TAB_NUMBER_MAP.filter((t) => !config.disabledTabs.includes(t)); const idx = tabs.indexOf(ui.activeTab); return idx === -1 ? 0 : idx; }); From dc81357152764f7078dfdcf305621455a5b5c34d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 22:29:39 +1000 Subject: [PATCH 50/74] feat(ui): add img2img via control layers to graph builders --- .../graph/addInitialImageToLinearGraph.ts | 117 ++++++++++++++++++ .../util/graph/addSeamlessToLinearGraph.ts | 2 + .../nodes/util/graph/addVAEToGraph.ts | 7 +- .../graph/buildLinearSDXLTextToImageGraph.ts | 7 +- .../util/graph/buildLinearTextToImageGraph.ts | 7 +- .../features/nodes/util/graph/constants.ts | 2 + 6 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts new file mode 100644 index 0000000000..4334a7cd31 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts @@ -0,0 +1,117 @@ +import type { RootState } from 'app/store/store'; +import { isInitialImageLayer } from 'features/controlLayers/store/controlLayersSlice'; +import type { ImageResizeInvocation, ImageToLatentsInvocation, NonNullableGraph } from 'services/api/types'; +import { assert } from 'tsafe'; + +import { IMAGE_TO_LATENTS, NOISE, RESIZE } from './constants'; + +export const addInitialImageToLinearGraph = ( + state: RootState, + graph: NonNullableGraph, + denoiseNodeId: string +): void => { + // Remove Existing UNet Connections + const { img2imgStrength, vaePrecision, model } = state.generation; + const { refinerModel, refinerStart } = state.sdxl; + const { width, height } = state.controlLayers.present.size; + const initialImageLayer = state.controlLayers.present.layers.find(isInitialImageLayer); + const initialImage = initialImageLayer?.isEnabled ? initialImageLayer?.image : null; + + if (!initialImage) { + return; + } + + const useRefinerStartEnd = model?.base === 'sdxl' && Boolean(refinerModel); + + const denoiseNode = graph.nodes[denoiseNodeId]; + assert(denoiseNode?.type === 'denoise_latents', `Missing denoise node or incorrect type: ${denoiseNode?.type}`); + + denoiseNode.denoising_start = useRefinerStartEnd ? Math.min(refinerStart, 1 - img2imgStrength) : 1 - img2imgStrength; + denoiseNode.denoising_end = useRefinerStartEnd ? refinerStart : 1; + + // We conditionally hook the image in depending on if a resize is needed + const i2lNode: ImageToLatentsInvocation = { + type: 'i2l', + id: IMAGE_TO_LATENTS, + is_intermediate: true, + use_cache: true, + fp32: vaePrecision === 'fp32', + }; + + graph.nodes[i2lNode.id] = i2lNode; + graph.edges.push({ + source: { + node_id: IMAGE_TO_LATENTS, + field: 'latents', + }, + destination: { + node_id: denoiseNode.id, + field: 'latents', + }, + }); + + if (initialImage.width !== width || initialImage.height !== height) { + // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` + + // Create a resize node, explicitly setting its image + const resizeNode: ImageResizeInvocation = { + id: RESIZE, + type: 'img_resize', + image: { + image_name: initialImage.imageName, + }, + is_intermediate: true, + width, + height, + }; + + graph.nodes[RESIZE] = resizeNode; + + // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` + graph.edges.push({ + source: { node_id: RESIZE, field: 'image' }, + destination: { + node_id: IMAGE_TO_LATENTS, + field: 'image', + }, + }); + + // The `RESIZE` node also passes its width and height to `NOISE` + graph.edges.push({ + source: { node_id: RESIZE, field: 'width' }, + destination: { + node_id: NOISE, + field: 'width', + }, + }); + + graph.edges.push({ + source: { node_id: RESIZE, field: 'height' }, + destination: { + node_id: NOISE, + field: 'height', + }, + }); + } else { + // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly + i2lNode.image = { + image_name: initialImage.imageName, + }; + + // Pass the image's dimensions to the `NOISE` node + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'width' }, + destination: { + node_id: NOISE, + field: 'width', + }, + }); + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'height' }, + destination: { + node_id: NOISE, + field: 'height', + }, + }); + } +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts index d986130d64..24e8be6546 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts @@ -7,6 +7,7 @@ import { SDXL_CANVAS_INPAINT_GRAPH, SDXL_CANVAS_OUTPAINT_GRAPH, SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH, + SDXL_CONTROL_LAYERS_GRAPH, SDXL_DENOISE_LATENTS, SDXL_IMAGE_TO_IMAGE_GRAPH, SDXL_TEXT_TO_IMAGE_GRAPH, @@ -54,6 +55,7 @@ export const addSeamlessToLinearGraph = ( let denoisingNodeId = DENOISE_LATENTS; if ( + graph.id === SDXL_CONTROL_LAYERS_GRAPH || graph.id === SDXL_TEXT_TO_IMAGE_GRAPH || graph.id === SDXL_IMAGE_TO_IMAGE_GRAPH || graph.id === SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH || diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts index 347027c539..ed705a08e6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts @@ -7,6 +7,7 @@ import { CANVAS_OUTPAINT_GRAPH, CANVAS_OUTPUT, CANVAS_TEXT_TO_IMAGE_GRAPH, + CONTROL_LAYERS_GRAPH, IMAGE_TO_IMAGE_GRAPH, IMAGE_TO_LATENTS, INPAINT_CREATE_MASK, @@ -17,11 +18,11 @@ import { SDXL_CANVAS_INPAINT_GRAPH, SDXL_CANVAS_OUTPAINT_GRAPH, SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH, + SDXL_CONTROL_LAYERS_GRAPH, SDXL_IMAGE_TO_IMAGE_GRAPH, SDXL_REFINER_SEAMLESS, SDXL_TEXT_TO_IMAGE_GRAPH, SEAMLESS, - TEXT_TO_IMAGE_GRAPH, VAE_LOADER, } from './constants'; import { upsertMetadata } from './metadata'; @@ -52,7 +53,8 @@ export const addVAEToGraph = async ( } if ( - graph.id === TEXT_TO_IMAGE_GRAPH || + graph.id === CONTROL_LAYERS_GRAPH || + graph.id === SDXL_CONTROL_LAYERS_GRAPH || graph.id === IMAGE_TO_IMAGE_GRAPH || graph.id === SDXL_TEXT_TO_IMAGE_GRAPH || graph.id === SDXL_IMAGE_TO_IMAGE_GRAPH @@ -100,6 +102,7 @@ export const addVAEToGraph = async ( } if ( + graph.id === SDXL_CONTROL_LAYERS_GRAPH || graph.id === IMAGE_TO_IMAGE_GRAPH || graph.id === SDXL_IMAGE_TO_IMAGE_GRAPH || graph.id === CANVAS_IMAGE_TO_IMAGE_GRAPH || diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts index 9134ef9de7..d3ee9e4a51 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts @@ -2,6 +2,7 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph'; +import { addInitialImageToLinearGraph } from 'features/nodes/util/graph/addInitialImageToLinearGraph'; import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; @@ -15,10 +16,10 @@ import { NEGATIVE_CONDITIONING, NOISE, POSITIVE_CONDITIONING, + SDXL_CONTROL_LAYERS_GRAPH, SDXL_DENOISE_LATENTS, SDXL_MODEL_LOADER, SDXL_REFINER_SEAMLESS, - SDXL_TEXT_TO_IMAGE_GRAPH, SEAMLESS, } from './constants'; import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; @@ -70,7 +71,7 @@ export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise // copy-pasted graph from node editor, filled in with state values & friendly node ids const graph: NonNullableGraph = { - id: SDXL_TEXT_TO_IMAGE_GRAPH, + id: SDXL_CONTROL_LAYERS_GRAPH, nodes: { [modelLoaderNodeId]: { type: 'sdxl_model_loader', @@ -241,6 +242,8 @@ export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise LATENTS_TO_IMAGE ); + addInitialImageToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); + // Add Seamless To Graph if (seamlessXAxis || seamlessYAxis) { addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts index 340a24bca4..c2ad5384e0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts @@ -2,6 +2,7 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph'; +import { addInitialImageToLinearGraph } from 'features/nodes/util/graph/addInitialImageToLinearGraph'; import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; @@ -13,6 +14,7 @@ import { addVAEToGraph } from './addVAEToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { CLIP_SKIP, + CONTROL_LAYERS_GRAPH, DENOISE_LATENTS, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, @@ -20,7 +22,6 @@ import { NOISE, POSITIVE_CONDITIONING, SEAMLESS, - TEXT_TO_IMAGE_GRAPH, } from './constants'; import { addCoreMetadataNode, getModelMetadataField } from './metadata'; @@ -66,7 +67,7 @@ export const buildLinearTextToImageGraph = async (state: RootState): Promise Date: Thu, 2 May 2024 22:37:53 +1000 Subject: [PATCH 51/74] fix(ui): destroy initial image layer after deleting --- .../frontend/web/src/features/controlLayers/util/renderers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index 3e34542e87..09db523a61 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -57,7 +57,8 @@ const STAGE_BG_DATAURL = const mapId = (object: { id: string }) => object.id; -const selectRenderableLayers = (n: Konva.Node) => n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME; +const selectRenderableLayers = (n: Konva.Node) => + n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME || n.name() === INITIAL_IMAGE_LAYER_NAME; const selectVectorMaskObjects = (node: Konva.Node) => { return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; From 7d58908e32579ce9d06db561ed178ac7538d279b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 23:11:25 +1000 Subject: [PATCH 52/74] fix(ui): fix img2img graphs w/ control layers --- .../src/features/nodes/util/graph/addVAEToGraph.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts index ed705a08e6..5482c1c680 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts @@ -102,11 +102,13 @@ export const addVAEToGraph = async ( } if ( - graph.id === SDXL_CONTROL_LAYERS_GRAPH || - graph.id === IMAGE_TO_IMAGE_GRAPH || - graph.id === SDXL_IMAGE_TO_IMAGE_GRAPH || - graph.id === CANVAS_IMAGE_TO_IMAGE_GRAPH || - graph.id === SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH + (graph.id === CONTROL_LAYERS_GRAPH || + graph.id === SDXL_CONTROL_LAYERS_GRAPH || + graph.id === IMAGE_TO_IMAGE_GRAPH || + graph.id === SDXL_IMAGE_TO_IMAGE_GRAPH || + graph.id === CANVAS_IMAGE_TO_IMAGE_GRAPH || + graph.id === SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH) && + Boolean(graph.nodes[IMAGE_TO_LATENTS]) ) { graph.edges.push({ source: { From a6ac184211c3ed0bff4c31d74129cfd9cb0e02f4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 2 May 2024 23:38:29 +1000 Subject: [PATCH 53/74] tidy(ui): excise img2img tab --- .../middleware/listenerMiddleware/index.ts | 4 - .../listeners/boardAndImagesDeleted.ts | 7 - .../listeners/enqueueRequestedLinear.ts | 16 +- .../listeners/imageDeleted.ts | 11 - .../listeners/imageDropped.ts | 14 +- .../listeners/imageToDeleteSelected.ts | 1 - .../listeners/imageUploaded.ts | 13 +- .../listeners/initialImageSelected.ts | 21 - .../src/common/hooks/useFullscreenDropzone.ts | 4 - .../web/src/common/hooks/useGlobalHotkeys.ts | 14 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 6 +- .../controlLayers/store/controlLayersSlice.ts | 2 +- .../components/DeleteImageModal.tsx | 1 - .../components/ImageUsageMessage.tsx | 1 - .../deleteImageModal/store/selectors.ts | 3 - .../features/deleteImageModal/store/types.ts | 1 - .../web/src/features/dnd/types/index.ts | 5 - .../web/src/features/dnd/util/isValidDrop.ts | 2 - .../components/Boards/DeleteBoardModal.tsx | 1 - .../CurrentImage/CurrentImageButtons.tsx | 7 +- .../SingleSelectionMenuItems.tsx | 4 +- .../gallery/components/ImageViewer.tsx | 1 - .../src/features/metadata/util/recallers.ts | 4 +- .../util/graph/addSeamlessToLinearGraph.ts | 4 - .../nodes/util/graph/addVAEToGraph.ts | 13 +- .../graph/buildLinearImageToImageGraph.ts | 369 ----------------- .../graph/buildLinearSDXLImageToImageGraph.ts | 390 ------------------ .../features/nodes/util/graph/constants.ts | 4 - .../ImageToImage/ImageToImageFit.tsx | 34 -- .../components/ImageToImage/InitialImage.tsx | 61 --- .../ImageToImage/InitialImageDisplay.tsx | 87 ---- .../parameters/hooks/usePreselectedImage.ts | 4 +- .../src/features/parameters/store/actions.ts | 3 - .../parameters/store/generationSlice.ts | 15 - .../src/features/parameters/store/types.ts | 2 - .../ImageSettingsAccordion.tsx | 4 +- .../src/features/ui/components/InvokeTabs.tsx | 9 +- .../ui/components/tabs/ImageToImageTab.tsx | 56 --- .../web/src/features/ui/store/tabMap.tsx | 2 +- .../web/src/features/ui/store/uiSlice.ts | 4 - .../frontend/web/src/services/api/types.ts | 5 - 41 files changed, 25 insertions(+), 1184 deletions(-) delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts delete mode 100644 invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageFit.tsx delete mode 100644 invokeai/frontend/web/src/features/parameters/components/ImageToImage/InitialImage.tsx delete mode 100644 invokeai/frontend/web/src/features/parameters/components/ImageToImage/InitialImageDisplay.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/ImageToImageTab.tsx diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 36040b5e41..0c0c8ed2bc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -32,7 +32,6 @@ import { addImagesStarredListener } from 'app/store/middleware/listenerMiddlewar import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred'; import { addImageToDeleteSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected'; import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded'; -import { addInitialImageSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/initialImageSelected'; import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected'; import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded'; import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged'; @@ -73,9 +72,6 @@ const startAppListening = listenerMiddleware.startListening as AppStartListening // Image uploaded addImageUploadedFulfilledListener(startAppListening); -// Image selected -addInitialImageSelectedListener(startAppListening); - // Image deleted addRequestedSingleImageDeletionListener(startAppListening); addDeleteBoardAndImagesFulfilledListener(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index 8e8d3f4b99..d7ab8430ca 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -3,7 +3,6 @@ import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; -import { clearInitialImage } from 'features/parameters/store/generationSlice'; import { imagesApi } from 'services/api/endpoints/images'; export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => { @@ -14,7 +13,6 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS // Remove all deleted images from the UI - let wasInitialImageReset = false; let wasCanvasReset = false; let wasNodeEditorReset = false; let wereControlAdaptersReset = false; @@ -23,11 +21,6 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS deleted_images.forEach((image_name) => { const imageUsage = getImageUsage(generation, canvas, nodes, controlAdapters, image_name); - if (imageUsage.isInitialImage && !wasInitialImageReset) { - dispatch(clearInitialImage()); - wasInitialImageReset = true; - } - if (imageUsage.isCanvasImage && !wasCanvasReset) { dispatch(resetCanvas()); wasCanvasReset = true; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index f923edb99a..0cbc73de35 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,8 +1,6 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; -import { buildLinearImageToImageGraph } from 'features/nodes/util/graph/buildLinearImageToImageGraph'; -import { buildLinearSDXLImageToImageGraph } from 'features/nodes/util/graph/buildLinearSDXLImageToImageGraph'; import { buildLinearSDXLTextToImageGraph } from 'features/nodes/util/graph/buildLinearSDXLTextToImageGraph'; import { buildLinearTextToImageGraph } from 'features/nodes/util/graph/buildLinearTextToImageGraph'; import { queueApi } from 'services/api/endpoints/queue'; @@ -10,7 +8,7 @@ import { queueApi } from 'services/api/endpoints/queue'; export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { startAppListening({ predicate: (action): action is ReturnType => - enqueueRequested.match(action) && (action.payload.tabName === 'txt2img' || action.payload.tabName === 'img2img'), + enqueueRequested.match(action) && action.payload.tabName === 'txt2img', effect: async (action, { getState, dispatch }) => { const state = getState(); const model = state.generation.model; @@ -19,17 +17,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let graph; if (model && model.base === 'sdxl') { - if (action.payload.tabName === 'txt2img') { - graph = await buildLinearSDXLTextToImageGraph(state); - } else { - graph = await buildLinearSDXLImageToImageGraph(state); - } + graph = await buildLinearSDXLTextToImageGraph(state); } else { - if (action.payload.tabName === 'txt2img') { - graph = await buildLinearTextToImageGraph(state); - } else { - graph = await buildLinearImageToImageGraph(state); - } + graph = await buildLinearTextToImageGraph(state); } const batchConfig = prepareLinearUIBatch(state, graph, prepend); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index 9bbbf80263..451c26629e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -14,7 +14,6 @@ import { imageSelected } from 'features/gallery/store/gallerySlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { isImageFieldInputInstance } from 'features/nodes/types/field'; import { isInvocationNode } from 'features/nodes/types/invocation'; -import { clearInitialImage } from 'features/parameters/store/generationSlice'; import { clamp, forEach } from 'lodash-es'; import { api } from 'services/api'; import { imagesApi } from 'services/api/endpoints/images'; @@ -73,11 +72,6 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt } imageDTOs.forEach((imageDTO) => { - // reset init image if we deleted it - if (getState().generation.initialImage?.imageName === imageDTO.image_name) { - dispatch(clearInitialImage()); - } - // reset control adapters that use the deleted images forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => { if ( @@ -168,11 +162,6 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt } imageDTOs.forEach((imageDTO) => { - // reset init image if we deleted it - if (getState().generation.initialImage?.imageName === imageDTO.image_name) { - dispatch(clearInitialImage()); - } - // reset control adapters that use the deleted images forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => { if ( diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 734867d0e1..9bc9635299 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -16,7 +16,7 @@ import { import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { imageSelected } from 'features/gallery/store/gallerySlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; -import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { imagesApi } from 'services/api/endpoints/images'; export const dndDropped = createAction<{ @@ -53,18 +53,6 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } - /** - * Image dropped on initial image - */ - if ( - overData.actionType === 'SET_INITIAL_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - dispatch(initialImageChanged(activeData.payload.imageDTO)); - return; - } - /** * Image dropped on ControlNet */ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts index d20c0c7c23..845c9a21f2 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts @@ -14,7 +14,6 @@ export const addImageToDeleteSelectedListener = (startAppListening: AppStartList const isImageInUse = imagesUsage.some((i) => i.isCanvasImage) || - imagesUsage.some((i) => i.isInitialImage) || imagesUsage.some((i) => i.isControlImage) || imagesUsage.some((i) => i.isNodesImage); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index 8f93d023ce..d5d74bf668 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -13,7 +13,7 @@ import { rgLayerIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; -import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice'; +import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { addToast } from 'features/system/store/systemSlice'; import { t } from 'i18next'; import { omit } from 'lodash-es'; @@ -158,17 +158,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis ); } - if (postUploadAction?.type === 'SET_INITIAL_IMAGE') { - dispatch(initialImageChanged(imageDTO)); - dispatch( - addToast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setInitialImage'), - }) - ); - return; - } - if (postUploadAction?.type === 'SET_NODES_IMAGE') { const { nodeId, fieldName } = postUploadAction; dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO })); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts deleted file mode 100644 index 735ce8367a..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { initialImageSelected } from 'features/parameters/store/actions'; -import { initialImageChanged } from 'features/parameters/store/generationSlice'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; -import { t } from 'i18next'; - -export const addInitialImageSelectedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: initialImageSelected, - effect: (action, { dispatch }) => { - if (!action.payload) { - dispatch(addToast(makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' }))); - return; - } - - dispatch(initialImageChanged(action.payload)); - dispatch(addToast(makeToast(t('toast.sentToImageToImage')))); - }, - }); -}; diff --git a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index 350e09b6e5..c51fae81d3 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -21,10 +21,6 @@ const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (ac postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' }; } - if (activeTabName === 'img2img') { - postUploadAction = { type: 'SET_INITIAL_IMAGE' }; - } - return postUploadAction; }); diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts index bbb7897575..fe65dd5983 100644 --- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -75,21 +75,13 @@ export const useGlobalHotkeys = () => { useHotkeys( '2', () => { - dispatch(setActiveTab('img2img')); + dispatch(setActiveTab('unifiedCanvas')); }, [dispatch] ); useHotkeys( '3', - () => { - dispatch(setActiveTab('unifiedCanvas')); - }, - [dispatch] - ); - - useHotkeys( - '4', () => { dispatch(setActiveTab('nodes')); }, @@ -97,7 +89,7 @@ export const useGlobalHotkeys = () => { ); useHotkeys( - '5', + '4', () => { if (isModelManagerEnabled) { dispatch(setActiveTab('modelManager')); @@ -107,7 +99,7 @@ export const useGlobalHotkeys = () => { ); useHotkeys( - isModelManagerEnabled ? '6' : '5', + isModelManagerEnabled ? '5' : '4', () => { dispatch(setActiveTab('queue')); }, diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index d06fc259df..f7c980efeb 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -28,7 +28,7 @@ const selector = createMemoizedSelector( activeTabNameSelector, ], (controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => { - const { initialImage, model } = generation; + const { model } = generation; const { positivePrompt } = controlLayers.present; const { isConnected } = system; @@ -40,10 +40,6 @@ const selector = createMemoizedSelector( reasons.push(i18n.t('parameters.invoke.systemDisconnected')); } - if (activeTabName === 'img2img' && !initialImage) { - reasons.push(i18n.t('parameters.invoke.noInitialImageSelected')); - } - if (activeTabName === 'nodes') { if (nodes.shouldValidateGraph) { if (!nodes.nodes.length) { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 2cb14509d7..6b3dfc5e6c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -627,7 +627,7 @@ export const controlLayersSlice = createSlice({ reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; // Highlander! There can be only one! - assert(!state.layers.find(isInitialImageLayer)); + state.layers = state.layers.filter((l) => isInitialImageLayer(l)); const layer: InitialImageLayer = { id: layerId, type: 'initial_image_layer', diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index d5c8f11f81..e3ee0b3852 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -38,7 +38,6 @@ const selectImageUsages = createMemoizedSelector( ); const imageUsageSummary: ImageUsage = { - isInitialImage: some(allImageUsage, (i) => i.isInitialImage), isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage), isNodesImage: some(allImageUsage, (i) => i.isNodesImage), isControlImage: some(allImageUsage, (i) => i.isControlImage), diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx index 5a6856f346..ec613409e7 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx @@ -29,7 +29,6 @@ const ImageUsageMessage = (props: Props) => { <> {topMessage} - {imageUsage.isInitialImage && {t('common.img2img')}} {imageUsage.isCanvasImage && {t('common.unifiedCanvas')}} {imageUsage.isControlImage && {t('common.controlNet')}} {imageUsage.isNodesImage && {t('common.nodeEditor')}} diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index f54f9a0dbb..b9540a3ecf 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -25,8 +25,6 @@ export const getImageUsage = ( controlAdapters: ControlAdaptersState, image_name: string ) => { - const isInitialImage = generation.initialImage?.imageName === image_name; - const isCanvasImage = canvas.layerState.objects.some((obj) => obj.kind === 'image' && obj.imageName === image_name); const isNodesImage = nodes.nodes.filter(isInvocationNode).some((node) => { @@ -41,7 +39,6 @@ export const getImageUsage = ( ); const imageUsage: ImageUsage = { - isInitialImage, isCanvasImage, isNodesImage, isControlImage, diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts index cd8f3aa5eb..f0aaf7b097 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts @@ -6,7 +6,6 @@ export type DeleteImageState = { }; export type ImageUsage = { - isInitialImage: boolean; isCanvasImage: boolean; isNodesImage: boolean; isControlImage: boolean; diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index b8d3cfe31e..4d09c759eb 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -22,10 +22,6 @@ type CurrentImageDropData = BaseDropData & { actionType: 'SET_CURRENT_IMAGE'; }; -type InitialImageDropData = BaseDropData & { - actionType: 'SET_INITIAL_IMAGE'; -}; - type ControlAdapterDropData = BaseDropData & { actionType: 'SET_CONTROL_ADAPTER_IMAGE'; context: { @@ -85,7 +81,6 @@ export type RemoveFromBoardDropData = BaseDropData & { export type TypesafeDroppableData = | CurrentImageDropData - | InitialImageDropData | ControlAdapterDropData | CanvasInitialImageDropData | NodesImageDropData diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 757a21bd5c..b701c72947 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -15,8 +15,6 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: switch (actionType) { case 'SET_CURRENT_IMAGE': return payloadType === 'IMAGE_DTO'; - case 'SET_INITIAL_IMAGE': - return payloadType === 'IMAGE_DTO'; case 'SET_CONTROL_ADAPTER_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SET_CA_LAYER_IMAGE': diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 6581033aaa..5f01fd9f29 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -50,7 +50,6 @@ const DeleteBoardModal = (props: Props) => { ); const imageUsageSummary: ImageUsage = { - isInitialImage: some(allImageUsage, (i) => i.isInitialImage), isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage), isNodesImage: some(allImageUsage, (i) => i.isNodesImage), isControlImage: some(allImageUsage, (i) => i.isControlImage), diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx index 880fdbca6c..2e4519af5e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx @@ -4,6 +4,7 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppToaster } from 'app/components/Toaster'; import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems'; @@ -13,7 +14,6 @@ import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers'; import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings'; -import { initialImageSelected } from 'features/parameters/store/actions'; import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { selectSystemSlice } from 'features/system/store/systemSlice'; @@ -86,8 +86,11 @@ const CurrentImageButtons = () => { useHotkeys('d', handleUseSize, [handleUseSize]); const handleSendToImageToImage = useCallback(() => { + if (!imageDTO) { + return; + } dispatch(sentImageToImg2Img()); - dispatch(initialImageSelected(imageDTO)); + dispatch(iiLayerAdded(imageDTO)); }, [dispatch, imageDTO]); useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index aff74481ca..1dfd839b31 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -7,10 +7,10 @@ import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard'; import { useDownloadImage } from 'common/hooks/useDownloadImage'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice'; +import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions'; -import { initialImageSelected } from 'features/parameters/store/actions'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { setActiveTab } from 'features/ui/store/uiSlice'; @@ -72,7 +72,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const handleSendToImageToImage = useCallback(() => { dispatch(sentImageToImg2Img()); - dispatch(initialImageSelected(imageDTO)); + dispatch(iiLayerAdded(imageDTO)); }, [dispatch, imageDTO]); const handleSendToCanvas = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx index 563d1b2285..92b0394abd 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx @@ -12,7 +12,6 @@ import { PiArrowLeftBold } from 'react-icons/pi'; const TAB_NAME_TO_TKEY: Record = { txt2img: 'common.txt2img', - img2img: 'common.img2img', unifiedCanvas: 'common.unifiedCanvas', nodes: 'common.nodes', modelManager: 'modelManager.modelManager', diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index c04259ac62..b29d937159 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -10,6 +10,7 @@ import { caLayerControlNetsDeleted, caLayerT2IAdaptersDeleted, heightChanged, + iiLayerAdded, ipaLayerAdded, ipaLayersDeleted, negativePrompt2Changed, @@ -32,7 +33,6 @@ import type { } from 'features/metadata/types'; import { modelSelected } from 'features/parameters/store/actions'; import { - initialImageChanged, setCfgRescaleMultiplier, setCfgScale, setImg2imgStrength, @@ -107,7 +107,7 @@ const recallScheduler: MetadataRecallFunc = (scheduler) => { }; const recallInitialImage: MetadataRecallFunc = async (imageDTO) => { - getStore().dispatch(initialImageChanged(imageDTO)); + getStore().dispatch(iiLayerAdded(imageDTO)); }; const setSizeOptions = { updateAspectRatio: true, clamp: true }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts index 24e8be6546..d6fcd411a4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts @@ -9,8 +9,6 @@ import { SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH, SDXL_CONTROL_LAYERS_GRAPH, SDXL_DENOISE_LATENTS, - SDXL_IMAGE_TO_IMAGE_GRAPH, - SDXL_TEXT_TO_IMAGE_GRAPH, SEAMLESS, VAE_LOADER, } from './constants'; @@ -56,8 +54,6 @@ export const addSeamlessToLinearGraph = ( if ( graph.id === SDXL_CONTROL_LAYERS_GRAPH || - graph.id === SDXL_TEXT_TO_IMAGE_GRAPH || - graph.id === SDXL_IMAGE_TO_IMAGE_GRAPH || graph.id === SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH || graph.id === SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH || graph.id === SDXL_CANVAS_INPAINT_GRAPH || diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts index 5482c1c680..f464723381 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts @@ -8,7 +8,6 @@ import { CANVAS_OUTPUT, CANVAS_TEXT_TO_IMAGE_GRAPH, CONTROL_LAYERS_GRAPH, - IMAGE_TO_IMAGE_GRAPH, IMAGE_TO_LATENTS, INPAINT_CREATE_MASK, INPAINT_IMAGE, @@ -19,9 +18,7 @@ import { SDXL_CANVAS_OUTPAINT_GRAPH, SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH, SDXL_CONTROL_LAYERS_GRAPH, - SDXL_IMAGE_TO_IMAGE_GRAPH, SDXL_REFINER_SEAMLESS, - SDXL_TEXT_TO_IMAGE_GRAPH, SEAMLESS, VAE_LOADER, } from './constants'; @@ -52,13 +49,7 @@ export const addVAEToGraph = async ( }; } - if ( - graph.id === CONTROL_LAYERS_GRAPH || - graph.id === SDXL_CONTROL_LAYERS_GRAPH || - graph.id === IMAGE_TO_IMAGE_GRAPH || - graph.id === SDXL_TEXT_TO_IMAGE_GRAPH || - graph.id === SDXL_IMAGE_TO_IMAGE_GRAPH - ) { + if (graph.id === CONTROL_LAYERS_GRAPH || graph.id === SDXL_CONTROL_LAYERS_GRAPH) { graph.edges.push({ source: { node_id: isSeamlessEnabled @@ -104,8 +95,6 @@ export const addVAEToGraph = async ( if ( (graph.id === CONTROL_LAYERS_GRAPH || graph.id === SDXL_CONTROL_LAYERS_GRAPH || - graph.id === IMAGE_TO_IMAGE_GRAPH || - graph.id === SDXL_IMAGE_TO_IMAGE_GRAPH || graph.id === CANVAS_IMAGE_TO_IMAGE_GRAPH || graph.id === SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH) && Boolean(graph.nodes[IMAGE_TO_LATENTS]) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts deleted file mode 100644 index 0ca121b667..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; -import { - type ImageResizeInvocation, - type ImageToLatentsInvocation, - isNonRefinerMainModelConfig, - type NonNullableGraph, -} from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLoRAsToGraph } from './addLoRAsToGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; -import { - CLIP_SKIP, - DENOISE_LATENTS, - IMAGE_TO_IMAGE_GRAPH, - IMAGE_TO_LATENTS, - LATENTS_TO_IMAGE, - MAIN_MODEL_LOADER, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - RESIZE, - SEAMLESS, -} from './constants'; -import { addCoreMetadataNode, getModelMetadataField } from './metadata'; - -/** - * Builds the Image to Image tab graph. - */ -export const buildLinearImageToImageGraph = async (state: RootState): Promise => { - const log = logger('nodes'); - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - seed, - steps, - initialImage, - img2imgStrength: strength, - shouldFitToWidthHeight, - clipSkip, - shouldUseCpuNoise, - vaePrecision, - seamlessXAxis, - seamlessYAxis, - } = state.generation; - const { positivePrompt, negativePrompt } = state.controlLayers.present; - const { width, height } = state.controlLayers.present.size; - - /** - * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the - * full graph here as a template. Then use the parameters from app state and set friendlier node - * ids. - * - * The only thing we need extra logic for is handling randomized seed, control net, and for img2img, - * the `fit` param. These are added to the graph at the end. - */ - - if (!initialImage) { - log.error('No initial image found in state'); - throw new Error('No initial image found in state'); - } - - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } - - const fp32 = vaePrecision === 'fp32'; - const is_intermediate = true; - - let modelLoaderNodeId = MAIN_MODEL_LOADER; - - const use_cpu = shouldUseCpuNoise; - - // copy-pasted graph from node editor, filled in with state values & friendly node ids - const graph: NonNullableGraph = { - id: IMAGE_TO_IMAGE_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'main_model_loader', - id: modelLoaderNodeId, - model, - is_intermediate, - }, - [CLIP_SKIP]: { - type: 'clip_skip', - id: CLIP_SKIP, - skipped_layers: clipSkip, - is_intermediate, - }, - [POSITIVE_CONDITIONING]: { - type: 'compel', - id: POSITIVE_CONDITIONING, - prompt: positivePrompt, - is_intermediate, - }, - [NEGATIVE_CONDITIONING]: { - type: 'compel', - id: NEGATIVE_CONDITIONING, - prompt: negativePrompt, - is_intermediate, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - use_cpu, - seed, - is_intermediate, - }, - [LATENTS_TO_IMAGE]: { - type: 'l2i', - id: LATENTS_TO_IMAGE, - fp32, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - }, - [DENOISE_LATENTS]: { - type: 'denoise_latents', - id: DENOISE_LATENTS, - cfg_scale, - cfg_rescale_multiplier, - scheduler, - steps, - denoising_start: 1 - strength, - denoising_end: 1, - is_intermediate, - }, - [IMAGE_TO_LATENTS]: { - type: 'i2l', - id: IMAGE_TO_LATENTS, - // must be set manually later, bc `fit` parameter may require a resize node inserted - // image: { - // image_name: initialImage.image_name, - // }, - fp32, - is_intermediate, - use_cache: false, - }, - }, - edges: [ - // Connect Model Loader to UNet and CLIP Skip - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: CLIP_SKIP, - field: 'clip', - }, - }, - // Connect CLIP Skip to Conditioning - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: CLIP_SKIP, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - // Connect everything to Denoise Latents - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'noise', - }, - }, - { - source: { - node_id: IMAGE_TO_LATENTS, - field: 'latents', - }, - destination: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - }, - // Decode denoised latents to image - { - source: { - node_id: DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - ], - }; - - // handle `fit` - if (shouldFitToWidthHeight && (initialImage.width !== width || initialImage.height !== height)) { - // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` - - // Create a resize node, explicitly setting its image - const resizeNode: ImageResizeInvocation = { - id: RESIZE, - type: 'img_resize', - image: { - image_name: initialImage.imageName, - }, - is_intermediate: true, - width, - height, - }; - - graph.nodes[RESIZE] = resizeNode; - - // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` - graph.edges.push({ - source: { node_id: RESIZE, field: 'image' }, - destination: { - node_id: IMAGE_TO_LATENTS, - field: 'image', - }, - }); - - // The `RESIZE` node also passes its width and height to `NOISE` - graph.edges.push({ - source: { node_id: RESIZE, field: 'width' }, - destination: { - node_id: NOISE, - field: 'width', - }, - }); - - graph.edges.push({ - source: { node_id: RESIZE, field: 'height' }, - destination: { - node_id: NOISE, - field: 'height', - }, - }); - } else { - // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly - (graph.nodes[IMAGE_TO_LATENTS] as ImageToLatentsInvocation).image = { - image_name: initialImage.imageName, - }; - - // Pass the image's dimensions to the `NOISE` node - graph.edges.push({ - source: { node_id: IMAGE_TO_LATENTS, field: 'width' }, - destination: { - node_id: NOISE, - field: 'width', - }, - }); - graph.edges.push({ - source: { node_id: IMAGE_TO_LATENTS, field: 'height' }, - destination: { - node_id: NOISE, - field: 'height', - }, - }); - } - - const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); - - addCoreMetadataNode( - graph, - { - generation_mode: 'img2img', - cfg_scale, - cfg_rescale_multiplier, - height, - width, - positive_prompt: positivePrompt, - negative_prompt: negativePrompt, - model: getModelMetadataField(modelConfig), - seed, - steps, - rand_device: use_cpu ? 'cpu' : 'cuda', - scheduler, - clip_skip: clipSkip, - strength, - init_image: initialImage.imageName, - }, - LATENTS_TO_IMAGE - ); - - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } - - // optionally add custom VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // add LoRA support - await addLoRAsToGraph(state, graph, DENOISE_LATENTS, modelLoaderNodeId); - - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, DENOISE_LATENTS); - - // Add IP Adapter - await addIPAdapterToLinearGraph(state, graph, DENOISE_LATENTS); - await addT2IAdaptersToLinearGraph(state, graph, DENOISE_LATENTS); - - // NSFW & watermark - must be last thing added to graph - if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph); - } - - if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph); - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts deleted file mode 100644 index 31c70d3dde..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { RootState } from 'app/store/store'; -import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; -import { - type ImageResizeInvocation, - type ImageToLatentsInvocation, - isNonRefinerMainModelConfig, - type NonNullableGraph, -} from 'services/api/types'; - -import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; -import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; -import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; -import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; -import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; -import { addT2IAdaptersToLinearGraph } from './addT2IAdapterToLinearGraph'; -import { addVAEToGraph } from './addVAEToGraph'; -import { addWatermarkerToGraph } from './addWatermarkerToGraph'; -import { - IMAGE_TO_LATENTS, - LATENTS_TO_IMAGE, - NEGATIVE_CONDITIONING, - NOISE, - POSITIVE_CONDITIONING, - RESIZE, - SDXL_DENOISE_LATENTS, - SDXL_IMAGE_TO_IMAGE_GRAPH, - SDXL_MODEL_LOADER, - SDXL_REFINER_SEAMLESS, - SEAMLESS, -} from './constants'; -import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; -import { addCoreMetadataNode, getModelMetadataField } from './metadata'; - -/** - * Builds the Image to Image tab graph. - */ -export const buildLinearSDXLImageToImageGraph = async (state: RootState): Promise => { - const log = logger('nodes'); - const { - model, - cfgScale: cfg_scale, - cfgRescaleMultiplier: cfg_rescale_multiplier, - scheduler, - seed, - steps, - initialImage, - shouldFitToWidthHeight, - shouldUseCpuNoise, - vaePrecision, - seamlessXAxis, - seamlessYAxis, - img2imgStrength: strength, - } = state.generation; - const { positivePrompt, negativePrompt } = state.controlLayers.present; - const { width, height } = state.controlLayers.present.size; - - const { refinerModel, refinerStart } = state.sdxl; - - /** - * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the - * full graph here as a template. Then use the parameters from app state and set friendlier node - * ids. - * - * The only thing we need extra logic for is handling randomized seed, control net, and for img2img, - * the `fit` param. These are added to the graph at the end. - */ - - if (!initialImage) { - log.error('No initial image found in state'); - throw new Error('No initial image found in state'); - } - - if (!model) { - log.error('No model found in state'); - throw new Error('No model found in state'); - } - - const fp32 = vaePrecision === 'fp32'; - const is_intermediate = true; - - // Model Loader ID - let modelLoaderNodeId = SDXL_MODEL_LOADER; - - const use_cpu = shouldUseCpuNoise; - - // Construct Style Prompt - const { positiveStylePrompt, negativeStylePrompt } = getSDXLStylePrompts(state); - - // copy-pasted graph from node editor, filled in with state values & friendly node ids - const graph: NonNullableGraph = { - id: SDXL_IMAGE_TO_IMAGE_GRAPH, - nodes: { - [modelLoaderNodeId]: { - type: 'sdxl_model_loader', - id: modelLoaderNodeId, - model, - is_intermediate, - }, - [POSITIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: POSITIVE_CONDITIONING, - prompt: positivePrompt, - style: positiveStylePrompt, - is_intermediate, - }, - [NEGATIVE_CONDITIONING]: { - type: 'sdxl_compel_prompt', - id: NEGATIVE_CONDITIONING, - prompt: negativePrompt, - style: negativeStylePrompt, - is_intermediate, - }, - [NOISE]: { - type: 'noise', - id: NOISE, - use_cpu, - seed, - is_intermediate, - }, - [LATENTS_TO_IMAGE]: { - type: 'l2i', - id: LATENTS_TO_IMAGE, - fp32, - is_intermediate: getIsIntermediate(state), - board: getBoardField(state), - }, - [SDXL_DENOISE_LATENTS]: { - type: 'denoise_latents', - id: SDXL_DENOISE_LATENTS, - cfg_scale, - cfg_rescale_multiplier, - scheduler, - steps, - denoising_start: refinerModel ? Math.min(refinerStart, 1 - strength) : 1 - strength, - denoising_end: refinerModel ? refinerStart : 1, - is_intermediate, - }, - [IMAGE_TO_LATENTS]: { - type: 'i2l', - id: IMAGE_TO_LATENTS, - // must be set manually later, bc `fit` parameter may require a resize node inserted - // image: { - // image_name: initialImage.image_name, - // }, - fp32, - is_intermediate, - use_cache: false, - }, - }, - edges: [ - // Connect Model Loader to UNet, CLIP & VAE - { - source: { - node_id: modelLoaderNodeId, - field: 'unet', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'unet', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip2', - }, - destination: { - node_id: POSITIVE_CONDITIONING, - field: 'clip2', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip', - }, - }, - { - source: { - node_id: modelLoaderNodeId, - field: 'clip2', - }, - destination: { - node_id: NEGATIVE_CONDITIONING, - field: 'clip2', - }, - }, - // Connect everything to Denoise Latents - { - source: { - node_id: POSITIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'positive_conditioning', - }, - }, - { - source: { - node_id: NEGATIVE_CONDITIONING, - field: 'conditioning', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'negative_conditioning', - }, - }, - { - source: { - node_id: NOISE, - field: 'noise', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'noise', - }, - }, - { - source: { - node_id: IMAGE_TO_LATENTS, - field: 'latents', - }, - destination: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - }, - // Decode Denoised Latents To Image - { - source: { - node_id: SDXL_DENOISE_LATENTS, - field: 'latents', - }, - destination: { - node_id: LATENTS_TO_IMAGE, - field: 'latents', - }, - }, - ], - }; - - // handle `fit` - if (shouldFitToWidthHeight && (initialImage.width !== width || initialImage.height !== height)) { - // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` - - // Create a resize node, explicitly setting its image - const resizeNode: ImageResizeInvocation = { - id: RESIZE, - type: 'img_resize', - image: { - image_name: initialImage.imageName, - }, - is_intermediate: true, - width, - height, - }; - - graph.nodes[RESIZE] = resizeNode; - - // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` - graph.edges.push({ - source: { node_id: RESIZE, field: 'image' }, - destination: { - node_id: IMAGE_TO_LATENTS, - field: 'image', - }, - }); - - // The `RESIZE` node also passes its width and height to `NOISE` - graph.edges.push({ - source: { node_id: RESIZE, field: 'width' }, - destination: { - node_id: NOISE, - field: 'width', - }, - }); - - graph.edges.push({ - source: { node_id: RESIZE, field: 'height' }, - destination: { - node_id: NOISE, - field: 'height', - }, - }); - } else { - // We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly - (graph.nodes[IMAGE_TO_LATENTS] as ImageToLatentsInvocation).image = { - image_name: initialImage.imageName, - }; - - // Pass the image's dimensions to the `NOISE` node - graph.edges.push({ - source: { node_id: IMAGE_TO_LATENTS, field: 'width' }, - destination: { - node_id: NOISE, - field: 'width', - }, - }); - graph.edges.push({ - source: { node_id: IMAGE_TO_LATENTS, field: 'height' }, - destination: { - node_id: NOISE, - field: 'height', - }, - }); - } - - const modelConfig = await fetchModelConfigWithTypeGuard(model.key, isNonRefinerMainModelConfig); - - addCoreMetadataNode( - graph, - { - generation_mode: 'sdxl_img2img', - cfg_scale, - cfg_rescale_multiplier, - height, - width, - positive_prompt: positivePrompt, - negative_prompt: negativePrompt, - model: getModelMetadataField(modelConfig), - seed, - steps, - rand_device: use_cpu ? 'cpu' : 'cuda', - scheduler, - strength, - init_image: initialImage.imageName, - positive_style_prompt: positiveStylePrompt, - negative_style_prompt: negativeStylePrompt, - }, - LATENTS_TO_IMAGE - ); - - // Add Seamless To Graph - if (seamlessXAxis || seamlessYAxis) { - addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); - modelLoaderNodeId = SEAMLESS; - } - - // Add Refiner if enabled - if (refinerModel) { - await addSDXLRefinerToGraph(state, graph, SDXL_DENOISE_LATENTS); - if (seamlessXAxis || seamlessYAxis) { - modelLoaderNodeId = SDXL_REFINER_SEAMLESS; - } - } - - // optionally add custom VAE - await addVAEToGraph(state, graph, modelLoaderNodeId); - - // Add LoRA Support - await addSDXLLoRAsToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId); - - // add controlnet, mutating `graph` - await addControlNetToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // Add IP Adapter - await addIPAdapterToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - await addT2IAdaptersToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); - - // NSFW & watermark - must be last thing added to graph - if (state.system.shouldUseNSFWChecker) { - // must add before watermarker! - addNSFWCheckerToGraph(state, graph); - } - - if (state.system.shouldUseWatermarker) { - // must add after nsfw checker! - addWatermarkerToGraph(state, graph); - } - - return graph; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts b/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts index fdb789dab3..53d7d742ab 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts @@ -57,14 +57,10 @@ export const NEGATIVE_CONDITIONING_COLLECT = 'negative_conditioning_collect'; // friendly graph ids export const CONTROL_LAYERS_GRAPH = 'control_layers_graph'; export const SDXL_CONTROL_LAYERS_GRAPH = 'sdxl_control_layers_graph'; -export const TEXT_TO_IMAGE_GRAPH = 'text_to_image_graph'; -export const IMAGE_TO_IMAGE_GRAPH = 'image_to_image_graph'; export const CANVAS_TEXT_TO_IMAGE_GRAPH = 'canvas_text_to_image_graph'; export const CANVAS_IMAGE_TO_IMAGE_GRAPH = 'canvas_image_to_image_graph'; export const CANVAS_INPAINT_GRAPH = 'canvas_inpaint_graph'; export const CANVAS_OUTPAINT_GRAPH = 'canvas_outpaint_graph'; -export const SDXL_TEXT_TO_IMAGE_GRAPH = 'sdxl_text_to_image_graph'; -export const SDXL_IMAGE_TO_IMAGE_GRAPH = 'sxdl_image_to_image_graph'; export const SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH = 'sdxl_canvas_text_to_image_graph'; export const SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH = 'sdxl_canvas_image_to_image_graph'; export const SDXL_CANVAS_INPAINT_GRAPH = 'sdxl_canvas_inpaint_graph'; diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageFit.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageFit.tsx deleted file mode 100644 index a772daa177..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageFit.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import type { RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setShouldFitToWidthHeight } from 'features/parameters/store/generationSlice'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -const ImageToImageFit = () => { - const dispatch = useAppDispatch(); - - const shouldFitToWidthHeight = useAppSelector((state: RootState) => state.generation.shouldFitToWidthHeight); - - const handleChangeFit = useCallback( - (e: ChangeEvent) => { - dispatch(setShouldFitToWidthHeight(e.target.checked)); - }, - [dispatch] - ); - - const { t } = useTranslation(); - - return ( - - - {t('parameters.imageFit')} - - - - ); -}; - -export default memo(ImageToImageFit); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/InitialImage.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/InitialImage.tsx deleted file mode 100644 index f70ea70616..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/InitialImage.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIDndImage from 'common/components/IAIDndImage'; -import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { clearInitialImage, selectGenerationSlice } from 'features/parameters/store/generationSlice'; -import { memo, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; - -const selectInitialImage = createMemoizedSelector(selectGenerationSlice, (generation) => generation.initialImage); - -const InitialImage = () => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const initialImage = useAppSelector(selectInitialImage); - const isConnected = useAppSelector((s) => s.system.isConnected); - - const { currentData: imageDTO, isError } = useGetImageDTOQuery(initialImage?.imageName ?? skipToken); - - const draggableData = useMemo(() => { - if (imageDTO) { - return { - id: 'initial-image', - payloadType: 'IMAGE_DTO', - payload: { imageDTO }, - }; - } - }, [imageDTO]); - - const droppableData = useMemo( - () => ({ - id: 'initial-image', - actionType: 'SET_INITIAL_IMAGE', - }), - [] - ); - - useEffect(() => { - if (isError && isConnected) { - // The image doesn't exist, reset init image - dispatch(clearInitialImage()); - } - }, [dispatch, isConnected, isError]); - - return ( - } - dataTestId="initial-image" - /> - ); -}; - -export default memo(InitialImage); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/InitialImageDisplay.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/InitialImageDisplay.tsx deleted file mode 100644 index 68e981b7f5..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/InitialImageDisplay.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; -import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers'; -import { clearInitialImage, selectGenerationSlice } from 'features/parameters/store/generationSlice'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiRulerBold, PiUploadSimpleBold } from 'react-icons/pi'; -import type { PostUploadAction } from 'services/api/types'; - -import InitialImage from './InitialImage'; - -const selectInitialImage = createMemoizedSelector(selectGenerationSlice, (generation) => generation.initialImage); - -const postUploadAction: PostUploadAction = { - type: 'SET_INITIAL_IMAGE', -}; - -const InitialImageDisplay = () => { - const { t } = useTranslation(); - const initialImage = useAppSelector(selectInitialImage); - const dispatch = useAppDispatch(); - - const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({ - postUploadAction, - }); - - const handleReset = useCallback(() => { - dispatch(clearInitialImage()); - }, [dispatch]); - - const handleUseSizeInitialImage = useCallback(() => { - if (initialImage) { - parseAndRecallImageDimensions(initialImage); - } - }, [initialImage]); - - useHotkeys('shift+d', handleUseSizeInitialImage, [initialImage]); - - return ( - - - - {t('metadata.initImage')} - - - } - {...getUploadButtonProps()} - /> - } - onClick={handleUseSizeInitialImage} - isDisabled={!initialImage} - /> - } - onClick={handleReset} - isDisabled={!initialImage} - /> - - - - - ); -}; - -export default memo(InitialImageDisplay); diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts index 0069bea881..ca7d00d507 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts @@ -2,8 +2,8 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppToaster } from 'app/components/Toaster'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; +import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers'; -import { initialImageSelected } from 'features/parameters/store/actions'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { t } from 'i18next'; @@ -37,7 +37,7 @@ export const usePreselectedImage = (selectedImage?: { const handleSendToImg2Img = useCallback(() => { if (selectedImageDto) { - dispatch(initialImageSelected(selectedImageDto)); + dispatch(iiLayerAdded(selectedImageDto)); } }, [dispatch, selectedImageDto]); diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts index 3b43129720..f913245e82 100644 --- a/invokeai/frontend/web/src/features/parameters/store/actions.ts +++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts @@ -1,8 +1,5 @@ import { createAction } from '@reduxjs/toolkit'; import type { ParameterModel } from 'features/parameters/types/parameterSchemas'; -import type { ImageDTO } from 'services/api/types'; - -export const initialImageSelected = createAction('generation/initialImageSelected'); export const modelSelected = createAction('generation/modelSelected'); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 18180455ce..573e9c1bbe 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -16,7 +16,6 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { configChanged } from 'features/system/store/configSlice'; import { clamp } from 'lodash-es'; import type { RgbaColor } from 'react-colorful'; -import type { ImageDTO } from 'services/api/types'; import type { GenerationState } from './types'; @@ -34,7 +33,6 @@ const initialGenerationState: GenerationState = { canvasCoherenceMinDenoise: 0, canvasCoherenceEdgeSize: 16, seed: 0, - shouldFitToWidthHeight: true, shouldRandomizeSeed: true, steps: 50, model: null, @@ -86,15 +84,9 @@ export const generationSlice = createSlice({ setSeamlessYAxis: (state, action: PayloadAction) => { state.seamlessYAxis = action.payload; }, - setShouldFitToWidthHeight: (state, action: PayloadAction) => { - state.shouldFitToWidthHeight = action.payload; - }, setShouldRandomizeSeed: (state, action: PayloadAction) => { state.shouldRandomizeSeed = action.payload; }, - clearInitialImage: (state) => { - state.initialImage = undefined; - }, setMaskBlur: (state, action: PayloadAction) => { state.maskBlur = action.payload; }, @@ -107,10 +99,6 @@ export const generationSlice = createSlice({ setCanvasCoherenceMinDenoise: (state, action: PayloadAction) => { state.canvasCoherenceMinDenoise = action.payload; }, - initialImageChanged: (state, action: PayloadAction) => { - const { image_name, width, height } = action.payload; - state.initialImage = { imageName: image_name, width, height }; - }, modelChanged: { reducer: ( state, @@ -195,7 +183,6 @@ export const generationSlice = createSlice({ }); export const { - clearInitialImage, setCfgScale, setCfgRescaleMultiplier, setImg2imgStrength, @@ -207,10 +194,8 @@ export const { setCanvasCoherenceEdgeSize, setCanvasCoherenceMinDenoise, setSeed, - setShouldFitToWidthHeight, setShouldRandomizeSeed, setSteps, - initialImageChanged, modelChanged, vaeSelected, setSeamlessXAxis, diff --git a/invokeai/frontend/web/src/features/parameters/store/types.ts b/invokeai/frontend/web/src/features/parameters/store/types.ts index 9314f8d076..51ab6146cf 100644 --- a/invokeai/frontend/web/src/features/parameters/store/types.ts +++ b/invokeai/frontend/web/src/features/parameters/store/types.ts @@ -20,7 +20,6 @@ export interface GenerationState { cfgRescaleMultiplier: ParameterCFGRescaleMultiplier; img2imgStrength: ParameterStrength; infillMethod: string; - initialImage?: { imageName: string; width: number; height: number }; iterations: number; scheduler: ParameterScheduler; maskBlur: number; @@ -29,7 +28,6 @@ export interface GenerationState { canvasCoherenceMinDenoise: ParameterStrength; canvasCoherenceEdgeSize: number; seed: ParameterSeed; - shouldFitToWidthHeight: boolean; shouldRandomizeSeed: boolean; steps: ParameterSteps; model: ParameterModel | null; 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 bb9cfd36ce..e97bdca45a 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -9,7 +9,6 @@ import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing'; import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight'; import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth'; -import ImageToImageFit from 'features/parameters/components/ImageToImage/ImageToImageFit'; import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput'; import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize'; @@ -94,8 +93,7 @@ export const ImageSettingsAccordion = memo(() => { - {(activeTabName === 'img2img' || activeTabName === 'unifiedCanvas') && } - {activeTabName === 'img2img' && } + {activeTabName === 'unifiedCanvas' && } {activeTabName === 'txt2img' && !isSDXL && } {activeTabName === 'unifiedCanvas' && ( <> diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index b4a2922098..6503ac0893 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -12,7 +12,6 @@ import { selectConfigSlice } from 'features/system/store/configSlice'; import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton'; import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons'; import ParametersPanelTextToImage from 'features/ui/components/ParametersPanelTextToImage'; -import ImageToImageTab from 'features/ui/components/tabs/ImageToImageTab'; import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab'; import NodesTab from 'features/ui/components/tabs/NodesTab'; import QueueTab from 'features/ui/components/tabs/QueueTab'; @@ -30,7 +29,7 @@ import { memo, useCallback, useMemo, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { PiFlowArrowBold } from 'react-icons/pi'; -import { RiBox2Line, RiBrushLine, RiImage2Line, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri'; +import { RiBox2Line, RiBrushLine, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri'; import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels'; @@ -51,12 +50,6 @@ const TAB_DATA: Record = { icon: , content: , }, - img2img: { - id: 'img2img', - translationKey: 'common.img2img', - icon: , - content: , - }, unifiedCanvas: { id: 'unifiedCanvas', translationKey: 'common.unifiedCanvas', diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImageTab.tsx deleted file mode 100644 index 4303d66e2c..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImageTab.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay'; -import InitialImageDisplay from 'features/parameters/components/ImageToImage/InitialImageDisplay'; -import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; -import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; -import type { CSSProperties } from 'react'; -import { memo, useCallback, useRef } from 'react'; -import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; -import { Panel, PanelGroup } from 'react-resizable-panels'; - -const panelGroupStyles: CSSProperties = { - height: '100%', - width: '100%', -}; -const panelStyles: CSSProperties = { - position: 'relative', -}; - -const ImageToImageTab = () => { - const panelGroupRef = useRef(null); - - const handleDoubleClickHandle = useCallback(() => { - if (!panelGroupRef.current) { - return; - } - panelGroupRef.current.setLayout([50, 50]); - }, []); - - const panelStorage = usePanelStorage(); - - return ( - - - - - - - - - - - - - - - - ); -}; - -export default memo(ImageToImageTab); diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.tsx b/invokeai/frontend/web/src/features/ui/store/tabMap.tsx index 1b8f3e4812..2b249a2a87 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 = ['txt2img', 'img2img', 'unifiedCanvas', 'nodes', 'modelManager', 'queue'] as const; +export const TAB_NUMBER_MAP = ['txt2img', 'unifiedCanvas', 'nodes', 'modelManager', 'queue'] as const; export type InvokeTabName = (typeof TAB_NUMBER_MAP)[number]; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 69c8eaf7cf..221d2a1217 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -2,7 +2,6 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { workflowLoadRequested } from 'features/nodes/store/actions'; -import { initialImageChanged } from 'features/parameters/store/generationSlice'; import type { InvokeTabName } from './tabMap'; import type { UIState } from './uiTypes'; @@ -43,9 +42,6 @@ export const uiSlice = createSlice({ }, }, extraReducers(builder) { - builder.addCase(initialImageChanged, (state) => { - state.activeTab = 'img2img'; - }); builder.addCase(workflowLoadRequested, (state) => { state.activeTab = 'nodes'; }); diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 183b81478d..a153780712 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -198,10 +198,6 @@ export type IILayerImagePostUploadAction = { layerId: string; }; -type InitialImageAction = { - type: 'SET_INITIAL_IMAGE'; -}; - type NodesAction = { type: 'SET_NODES_IMAGE'; nodeId: string; @@ -223,7 +219,6 @@ type AddToBatchAction = { export type PostUploadAction = | ControlAdapterAction - | InitialImageAction | NodesAction | CanvasInitialImageAction | ToastAction From 7c1f1076b42e9472aea4f2c616a12b41807e5f35 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 06:27:25 +1000 Subject: [PATCH 54/74] feat(ui): rename tabs - "Text to Image" -> "Generation" - "Unified Canvas" -> "Canvas" - "Model Manager" -> "Models" --- invokeai/frontend/web/public/locales/en.json | 9 +++++++++ .../web/src/features/ui/components/InvokeTabs.tsx | 10 +++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index cacb4dfbf4..0ad7432ca2 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1557,5 +1557,14 @@ "opacityFilter": "Opacity Filter", "clearProcessor": "Clear Processor", "resetProcessor": "Reset Processor to Defaults" + }, + "ui": { + "tabs": { + "generation": "Generation", + "canvas": "Canvas", + "workflows": "Workflows", + "models": "Models", + "queue": "Queue" + } } } diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 6503ac0893..471046e188 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -46,31 +46,31 @@ type TabData = { const TAB_DATA: Record = { txt2img: { id: 'txt2img', - translationKey: 'common.txt2img', + translationKey: 'ui.tabs.generation', icon: , content: , }, unifiedCanvas: { id: 'unifiedCanvas', - translationKey: 'common.unifiedCanvas', + translationKey: 'ui.tabs.canvas', icon: , content: , }, nodes: { id: 'nodes', - translationKey: 'common.nodes', + translationKey: 'ui.tabs.workflows', icon: , content: , }, modelManager: { id: 'modelManager', - translationKey: 'modelManager.modelManager', + translationKey: 'ui.tabs.models', icon: , content: , }, queue: { id: 'queue', - translationKey: 'queue.queue', + translationKey: 'ui.tabs.queue', icon: , content: , }, From 0f7fdabe9bcf25ce06f14dd57cd575b7d996d0cd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 06:52:15 +1000 Subject: [PATCH 55/74] feat(ui): rename tab identifiers - "txt2img" -> "generation" - "unifiedCanvas" -> "canvas" - "modelManager" -> "models" - "nodes" -> "workflows" - Add UI slice migration setting the active tab to "generation" --- .../frontend/web/src/app/logging/logger.ts | 3 +-- .../listeners/enqueueRequestedCanvas.ts | 2 +- .../listeners/enqueueRequestedLinear.ts | 2 +- .../listeners/enqueueRequestedNodes.ts | 2 +- .../src/common/hooks/useFullscreenDropzone.ts | 2 +- .../web/src/common/hooks/useGlobalHotkeys.ts | 8 +++---- .../src/common/hooks/useIsReadyToEnqueue.ts | 6 ++--- .../features/canvas/hooks/useCanvasHotkeys.ts | 4 ++-- .../components/ControlAdapterConfig.tsx | 2 +- .../components/ControlAdapterImagePreview.tsx | 2 +- .../ControlAdapterImagePreview.tsx | 2 +- .../IPAdapterImagePreview.tsx | 2 +- .../IILayer/InitialImagePreview.tsx | 2 +- .../dnd/hooks/useScaledCenteredModifer.ts | 2 +- .../SingleSelectionMenuItems.tsx | 4 ++-- .../ImageMetadataActions.tsx | 12 +++++----- .../gallery/components/ImageViewer.tsx | 10 ++++---- .../gallery/hooks/useGalleryHotkeys.ts | 2 +- .../features/gallery/hooks/useImageActions.ts | 4 ++-- .../hooks/useStarterModelsToast.tsx | 2 +- .../util/graph/addControlNetToLinearGraph.ts | 4 ++-- .../nodes/util/graph/addHrfToGraph.ts | 2 +- .../util/graph/addIPAdapterToLinearGraph.ts | 4 ++-- .../util/graph/addT2IAdapterToLinearGraph.ts | 4 ++-- .../nodes/util/graph/graphBuilderUtils.ts | 2 +- .../NavigateToModelManagerButton.tsx | 4 ++-- .../parameters/hooks/usePreselectedImage.ts | 2 +- .../ImageSettingsAccordion.tsx | 10 ++++---- .../ImageSizeLinear.tsx | 2 +- .../src/features/ui/components/InvokeTabs.tsx | 24 +++++++++---------- .../ui/components/ParametersPanel.tsx | 4 ++-- .../components/ParametersPanelTextToImage.tsx | 4 ++-- .../web/src/features/ui/store/tabMap.tsx | 2 +- .../web/src/features/ui/store/uiSelectors.ts | 2 +- .../web/src/features/ui/store/uiSlice.ts | 10 +++++--- .../web/src/features/ui/store/uiTypes.ts | 2 +- 36 files changed, 80 insertions(+), 77 deletions(-) diff --git a/invokeai/frontend/web/src/app/logging/logger.ts b/invokeai/frontend/web/src/app/logging/logger.ts index ca7a24201a..c0de4e3685 100644 --- a/invokeai/frontend/web/src/app/logging/logger.ts +++ b/invokeai/frontend/web/src/app/logging/logger.ts @@ -20,8 +20,7 @@ export type LoggerNamespace = | 'models' | 'config' | 'canvas' - | 'txt2img' - | 'img2img' + | 'generation' | 'nodes' | 'system' | 'socketio' diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts index f38020b8ea..cdcc99ade2 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts @@ -30,7 +30,7 @@ import type { ImageDTO } from 'services/api/types'; export const addEnqueueRequestedCanvasListener = (startAppListening: AppStartListening) => { startAppListening({ predicate: (action): action is ReturnType => - enqueueRequested.match(action) && action.payload.tabName === 'unifiedCanvas', + enqueueRequested.match(action) && action.payload.tabName === 'canvas', effect: async (action, { getState, dispatch }) => { const log = logger('queue'); const { prepend } = action.payload; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 0cbc73de35..bc0fcb5ef0 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -8,7 +8,7 @@ import { queueApi } from 'services/api/endpoints/queue'; export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { startAppListening({ predicate: (action): action is ReturnType => - enqueueRequested.match(action) && action.payload.tabName === 'txt2img', + enqueueRequested.match(action) && action.payload.tabName === 'generation', effect: async (action, { getState, dispatch }) => { const state = getState(); const model = state.generation.model; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index e33f7c964a..8d39daaef8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -8,7 +8,7 @@ import type { BatchConfig } from 'services/api/types'; export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) => { startAppListening({ predicate: (action): action is ReturnType => - enqueueRequested.match(action) && action.payload.tabName === 'nodes', + enqueueRequested.match(action) && action.payload.tabName === 'workflows', effect: async (action, { getState, dispatch }) => { const state = getState(); const { nodes, edges } = state.nodes; diff --git a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index c51fae81d3..0334294e98 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -17,7 +17,7 @@ const accept: Accept = { const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => { let postUploadAction: PostUploadAction = { type: 'TOAST' }; - if (activeTabName === 'unifiedCanvas') { + if (activeTabName === 'canvas') { postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' }; } diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts index fe65dd5983..9ba044199f 100644 --- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -67,7 +67,7 @@ export const useGlobalHotkeys = () => { useHotkeys( '1', () => { - dispatch(setActiveTab('txt2img')); + dispatch(setActiveTab('generation')); }, [dispatch] ); @@ -75,7 +75,7 @@ export const useGlobalHotkeys = () => { useHotkeys( '2', () => { - dispatch(setActiveTab('unifiedCanvas')); + dispatch(setActiveTab('canvas')); }, [dispatch] ); @@ -83,7 +83,7 @@ export const useGlobalHotkeys = () => { useHotkeys( '3', () => { - dispatch(setActiveTab('nodes')); + dispatch(setActiveTab('workflows')); }, [dispatch] ); @@ -92,7 +92,7 @@ export const useGlobalHotkeys = () => { '4', () => { if (isModelManagerEnabled) { - dispatch(setActiveTab('modelManager')); + dispatch(setActiveTab('models')); } }, [dispatch, isModelManagerEnabled] diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index f7c980efeb..2aac5b8e72 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -40,7 +40,7 @@ const selector = createMemoizedSelector( reasons.push(i18n.t('parameters.invoke.systemDisconnected')); } - if (activeTabName === 'nodes') { + if (activeTabName === 'workflows') { if (nodes.shouldValidateGraph) { if (!nodes.nodes.length) { reasons.push(i18n.t('parameters.invoke.noNodesInGraph')); @@ -93,8 +93,8 @@ const selector = createMemoizedSelector( reasons.push(i18n.t('parameters.invoke.noModelSelected')); } - if (activeTabName === 'txt2img') { - // Handling for Control Layers - only exists on txt2img tab now + if (activeTabName === 'generation') { + // Handling for generation tab controlLayers.present.layers .filter((l) => l.isEnabled) .flatMap((l) => { diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts index e915259201..ec833c5f3d 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts @@ -75,7 +75,7 @@ const useInpaintingCanvasHotkeys = () => { const onKeyDown = useCallback( (e: KeyboardEvent) => { - if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'unifiedCanvas') { + if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') { return; } if ($toolStash.get() || $tool.get() === 'move') { @@ -90,7 +90,7 @@ const useInpaintingCanvasHotkeys = () => { ); const onKeyUp = useCallback( (e: KeyboardEvent) => { - if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'unifiedCanvas') { + if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') { return; } if (!$toolStash.get() || $tool.get() !== 'move') { diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx index fcc816d75f..c13783cddd 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx @@ -76,7 +76,7 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => { - {activeTabName === 'unifiedCanvas' && } + {activeTabName === 'canvas' && } { return; } - if (activeTabName === 'unifiedCanvas') { + if (activeTabName === 'canvas') { dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)); } else { const options = { updateAspectRatio: true, clamp: true }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx index 81bd7b14f2..e6c6aae286 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx @@ -80,7 +80,7 @@ export const ControlAdapterImagePreview = memo( return; } - if (activeTabName === 'unifiedCanvas') { + if (activeTabName === 'canvas') { dispatch( setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx index f73f61cbbd..83dd250cd0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx @@ -46,7 +46,7 @@ export const IPAdapterImagePreview = memo( return; } - if (activeTabName === 'unifiedCanvas') { + if (activeTabName === 'canvas') { dispatch( setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx index 740e81cbde..e355d5db86 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx @@ -43,7 +43,7 @@ export const InitialImagePreview = memo(({ image, onChangeImage, droppableData, return; } - if (activeTabName === 'unifiedCanvas') { + if (activeTabName === 'canvas') { dispatch(setBoundingBoxDimensions({ width: imageDTO.width, height: imageDTO.height }, optimalDimension)); } else { const options = { updateAspectRatio: true, clamp: true }; diff --git a/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts b/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts index 2816fc4830..f3f0c50f03 100644 --- a/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts +++ b/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts @@ -7,7 +7,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { useCallback } from 'react'; const selectZoom = createSelector([selectNodesSlice, activeTabNameSelector], (nodes, activeTabName) => - activeTabName === 'nodes' ? nodes.viewport.zoom : 1 + activeTabName === 'workflows' ? nodes.viewport.zoom : 1 ); /** diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 1dfd839b31..7bfb4050fb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -45,7 +45,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const toaster = useAppToaster(); - const isCanvasEnabled = useFeatureStatus('unifiedCanvas'); + const isCanvasEnabled = useFeatureStatus('canvas'); const customStarUi = useStore($customStarUI); const { downloadImage } = useDownloadImage(); @@ -78,7 +78,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const handleSendToCanvas = useCallback(() => { dispatch(sentImageToCanvas()); flushSync(() => { - dispatch(setActiveTab('unifiedCanvas')); + dispatch(setActiveTab('canvas')); }); dispatch(setInitialCanvasImage(imageDTO, optimalDimension)); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index 007a89f1a1..c73f5b1817 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -52,12 +52,12 @@ const ImageMetadataActions = (props: Props) => { - {activeTabName !== 'txt2img' && } - {activeTabName !== 'txt2img' && } - {activeTabName !== 'txt2img' && } - {activeTabName === 'txt2img' && } - {activeTabName === 'txt2img' && } - {activeTabName === 'txt2img' && } + {activeTabName !== 'generation' && } + {activeTabName !== 'generation' && } + {activeTabName !== 'generation' && } + {activeTabName === 'generation' && } + {activeTabName === 'generation' && } + {activeTabName === 'generation' && } ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx index 92b0394abd..d1cc108ef2 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx @@ -11,11 +11,11 @@ import { useTranslation } from 'react-i18next'; import { PiArrowLeftBold } from 'react-icons/pi'; const TAB_NAME_TO_TKEY: Record = { - txt2img: 'common.txt2img', - unifiedCanvas: 'common.unifiedCanvas', - nodes: 'common.nodes', - modelManager: 'modelManager.modelManager', - queue: 'queue.queue', + generation: 'ui.tabs.generation', + canvas: 'ui.tabs.canvas', + workflows: 'ui.tabs.workflows', + models: 'ui.tabs.models', + queue: 'ui.tabs.queue', }; export const ImageViewer = memo(() => { diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts index 75df186dd7..1efc317e3a 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts @@ -14,7 +14,7 @@ export const useGalleryHotkeys = () => { const isStaging = useAppSelector(isStagingSelector); // block navigation on Unified Canvas tab when staging new images const canNavigateGallery = useMemo(() => { - return activeTabName !== 'unifiedCanvas' || !isStaging; + return activeTabName !== 'canvas' || !isStaging; }, [activeTabName, isStaging]); const { diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts index 3f0f0bdf91..727752d79e 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts @@ -43,12 +43,12 @@ export const useImageActions = (image_name?: string) => { }, [metadata]); const recallAll = useCallback(() => { - parseAndRecallAllMetadata(metadata, activeTabName === 'txt2img'); + parseAndRecallAllMetadata(metadata, activeTabName === 'generation'); }, [activeTabName, metadata]); const remix = useCallback(() => { // Recalls all metadata parameters except seed - parseAndRecallAllMetadata(metadata, activeTabName === 'txt2img', ['seed']); + parseAndRecallAllMetadata(metadata, activeTabName === 'generation', ['seed']); }, [activeTabName, metadata]); const recallSeed = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx index 6abc633ac8..6d3d759883 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx @@ -39,7 +39,7 @@ const ToastDescription = () => { const toast = useToast(); const onClick = useCallback(() => { - dispatch(setActiveTab('modelManager')); + dispatch(setActiveTab('models')); toast.close(TOAST_ID); }, [dispatch, toast]); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts index 363d97badf..531c88335b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts @@ -31,9 +31,9 @@ export const addControlNetToLinearGraph = async ( } ); - // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper. + // The generation tab has special handling - its control adapters are set up in the Control Layers graph helper. const activeTabName = activeTabNameSelector(state); - assert(activeTabName !== 'txt2img', 'Tried to use addControlNetToLinearGraph on txt2img tab'); + assert(activeTabName !== 'generation', 'Tried to use addControlNetToLinearGraph on generation tab'); if (controlNets.length) { // Even though denoise_latents' control input is collection or scalar, keep it simple and always use a collect diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts index 5abf07740a..d6709f7058 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts @@ -106,7 +106,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = if (!state.hrf.hrfEnabled || state.config.disabledSDFeatures.includes('hrf')) { return; } - const log = logger('txt2img'); + const log = logger('generation'); const { vae, seamlessXAxis, seamlessYAxis } = state.generation; const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts index 12ba4e12a8..2cf93100eb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts @@ -20,9 +20,9 @@ export const addIPAdapterToLinearGraph = async ( graph: NonNullableGraph, baseNodeId: string ): Promise => { - // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper. + // The generation tab has special handling - its control adapters are set up in the Control Layers graph helper. const activeTabName = activeTabNameSelector(state); - assert(activeTabName !== 'txt2img', 'Tried to use addT2IAdaptersToLinearGraph on txt2img tab'); + assert(activeTabName !== 'generation', 'Tried to use addT2IAdaptersToLinearGraph on generation tab'); const ipAdapters = selectValidIPAdapters(state.controlAdapters).filter(({ model, controlImage, isEnabled }) => { const hasModel = Boolean(model); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts index ddd87256f4..ee21bbff1b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts @@ -20,9 +20,9 @@ export const addT2IAdaptersToLinearGraph = async ( graph: NonNullableGraph, baseNodeId: string ): Promise => { - // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper. + // The generation tab has special handling - its control adapters are set up in the Control Layers graph helper. const activeTabName = activeTabNameSelector(state); - assert(activeTabName !== 'txt2img', 'Tried to use addT2IAdaptersToLinearGraph on txt2img tab'); + assert(activeTabName !== 'generation', 'Tried to use addT2IAdaptersToLinearGraph on generation tab'); const t2iAdapters = selectValidT2IAdapters(state.controlAdapters).filter( ({ model, processedControlImage, processorType, controlImage, isEnabled }) => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts index 5abdc408e8..55795a092c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -31,7 +31,7 @@ export const getSDXLStylePrompts = (state: RootState): { positiveStylePrompt: st */ export const getIsIntermediate = (state: RootState) => { const activeTabName = activeTabNameSelector(state); - if (activeTabName === 'unifiedCanvas') { + if (activeTabName === 'canvas') { return !state.canvas.shouldAutoSave; } return false; diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx index 733fb83826..0924c0c099 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx @@ -10,10 +10,10 @@ export const NavigateToModelManagerButton = memo((props: Omit s.config.disabledTabs); - const shouldShowButton = useMemo(() => !disabledTabs.includes('modelManager'), [disabledTabs]); + const shouldShowButton = useMemo(() => !disabledTabs.includes('models'), [disabledTabs]); const handleClick = useCallback(() => { - dispatch(setActiveTab('modelManager')); + dispatch(setActiveTab('models')); }, [dispatch]); if (!shouldShowButton) { diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts index ca7d00d507..20d771d75d 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts @@ -25,7 +25,7 @@ export const usePreselectedImage = (selectedImage?: { const handleSendToCanvas = useCallback(() => { if (selectedImageDto) { dispatch(setInitialCanvasImage(selectedImageDto, optimalDimension)); - dispatch(setActiveTab('unifiedCanvas')); + dispatch(setActiveTab('canvas')); toaster({ title: t('toast.sentToUnifiedCanvas'), status: 'info', 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 e97bdca45a..e9a9263605 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -31,7 +31,7 @@ const selector = createMemoizedSelector( const badges: string[] = []; const isSDXL = model?.base === 'sdxl'; - if (activeTabName === 'unifiedCanvas') { + if (activeTabName === 'canvas') { const { aspectRatio, boundingBoxDimensions: { width, height }, @@ -85,7 +85,7 @@ export const ImageSettingsAccordion = memo(() => { onToggle={onToggleAccordion} > - {activeTabName === 'unifiedCanvas' ? : } + {activeTabName === 'canvas' ? : } @@ -93,9 +93,9 @@ export const ImageSettingsAccordion = memo(() => { - {activeTabName === 'unifiedCanvas' && } - {activeTabName === 'txt2img' && !isSDXL && } - {activeTabName === 'unifiedCanvas' && ( + {activeTabName === 'canvas' && } + {activeTabName === 'generation' && !isSDXL && } + {activeTabName === 'canvas' && ( <> diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx index 0b28200ca2..ddf4997a16 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx @@ -50,7 +50,7 @@ export const ImageSizeLinear = memo(() => { aspectRatioState={aspectRatioState} heightComponent={} widthComponent={} - previewComponent={tab === 'txt2img' ? : } + previewComponent={tab === 'generation' ? : } onChangeAspectRatioState={onChangeAspectRatioState} onChangeWidth={onChangeWidth} onChangeHeight={onChangeHeight} diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 471046e188..1968c64161 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -44,26 +44,26 @@ type TabData = { }; const TAB_DATA: Record = { - txt2img: { - id: 'txt2img', + generation: { + id: 'generation', translationKey: 'ui.tabs.generation', icon: , content: , }, - unifiedCanvas: { - id: 'unifiedCanvas', + canvas: { + id: 'canvas', translationKey: 'ui.tabs.canvas', icon: , content: , }, - nodes: { - id: 'nodes', + workflows: { + id: 'workflows', translationKey: 'ui.tabs.workflows', icon: , content: , }, - modelManager: { - id: 'modelManager', + models: { + id: 'models', translationKey: 'ui.tabs.models', icon: , content: , @@ -80,8 +80,8 @@ const enabledTabsSelector = createMemoizedSelector(selectConfigSlice, (config) = TAB_NUMBER_MAP.map((tabName) => TAB_DATA[tabName]).filter((tab) => !config.disabledTabs.includes(tab.id)) ); -const NO_GALLERY_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue']; -const NO_OPTIONS_PANEL_TABS: InvokeTabName[] = ['modelManager', 'queue']; +const NO_GALLERY_PANEL_TABS: InvokeTabName[] = ['models', 'queue']; +const NO_OPTIONS_PANEL_TABS: InvokeTabName[] = ['models', 'queue']; const panelStyles: CSSProperties = { height: '100%', width: '100%' }; const GALLERY_MIN_SIZE_PX = 310; const GALLERY_MIN_SIZE_PCT = 20; @@ -291,10 +291,10 @@ export default memo(InvokeTabs); const ParametersPanelComponent = memo(() => { const activeTabName = useAppSelector(activeTabNameSelector); - if (activeTabName === 'nodes') { + if (activeTabName === 'workflows') { return ; } - if (activeTabName === 'txt2img') { + if (activeTabName === 'generation') { return ; } return ; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx index b8d35976e3..e8f73fd786 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx @@ -34,8 +34,8 @@ const ParametersPanel = () => { {isSDXL ? : } - {activeTabName !== 'txt2img' && } - {activeTabName === 'unifiedCanvas' && } + {activeTabName !== 'generation' && } + {activeTabName === 'canvas' && } {isSDXL && } diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx index 3073b1e66b..79a9b5a03c 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx @@ -48,8 +48,8 @@ const ParametersPanelTextToImage = () => { - {activeTabName !== 'txt2img' && } - {activeTabName === 'unifiedCanvas' && } + {activeTabName !== 'generation' && } + {activeTabName === 'canvas' && } {isSDXL && } diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.tsx b/invokeai/frontend/web/src/features/ui/store/tabMap.tsx index 2b249a2a87..526a55b069 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 = ['txt2img', 'unifiedCanvas', 'nodes', 'modelManager', 'queue'] as const; +export const TAB_NUMBER_MAP = ['generation', 'canvas', 'workflows', 'models', 'queue'] as const; export type InvokeTabName = (typeof TAB_NUMBER_MAP)[number]; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts index 5fbc6a41de..05cefe43c5 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts @@ -11,7 +11,7 @@ export const activeTabNameSelector = createSelector( * Previously `activeTab` was an integer, but now it's a string. * Default to first tab in case user has integer. */ - (ui) => (isString(ui.activeTab) ? ui.activeTab : 'txt2img') + (ui) => (isString(ui.activeTab) ? ui.activeTab : 'generation') ); export const activeTabIndexSelector = createSelector(selectUiSlice, selectConfigSlice, (ui, config) => { diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 221d2a1217..2146db974c 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -7,8 +7,8 @@ import type { InvokeTabName } from './tabMap'; import type { UIState } from './uiTypes'; const initialUIState: UIState = { - _version: 1, - activeTab: 'txt2img', + _version: 2, + activeTab: 'generation', shouldShowImageDetails: false, shouldShowProgressInViewer: true, panels: {}, @@ -43,7 +43,7 @@ export const uiSlice = createSlice({ }, extraReducers(builder) { builder.addCase(workflowLoadRequested, (state) => { - state.activeTab = 'nodes'; + state.activeTab = 'workflows'; }); }, }); @@ -64,6 +64,10 @@ const migrateUIState = (state: any): any => { if (!('_version' in state)) { state._version = 1; } + if (state._version === 1) { + state.activeTab = 'generation'; + state._version = 2; + } return state; }; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 2cf9fd8aa6..c72043190f 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -4,7 +4,7 @@ export interface UIState { /** * Slice schema version. */ - _version: 1; + _version: 2; /** * The currently active tab. */ From 94a73d5377c9596a85c132dcfe77db7a82a46d38 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 07:04:14 +1000 Subject: [PATCH 56/74] feat(ui): update mm-related translations --- invokeai/frontend/web/public/locales/en.json | 11 +++++++++-- .../modelManagerV2/hooks/useStarterModelsToast.tsx | 2 +- .../MainModel/NavigateToModelManagerButton.tsx | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 0ad7432ca2..cfcb433db2 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -93,6 +93,7 @@ "folder": "Folder", "format": "format", "githubLabel": "Github", + "goTo": "Go to", "hotkeysLabel": "Hotkeys", "imageFailedToLoad": "Unable to Load Image", "img2img": "Image To Image", @@ -140,7 +141,8 @@ "blue": "Blue", "alpha": "Alpha", "selected": "Selected", - "viewer": "Viewer" + "viewer": "Viewer", + "tab": "Tab" }, "controlnet": { "controlAdapter_one": "Control Adapter", @@ -1561,10 +1563,15 @@ "ui": { "tabs": { "generation": "Generation", + "generationTab": "$t(ui.tabs.generation) $t(common.tab)", "canvas": "Canvas", + "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", "workflows": "Workflows", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", "models": "Models", - "queue": "Queue" + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "queue": "Queue", + "queueTab": "$t(ui.tabs.queue) $t(common.tab)" } } } diff --git a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx index 6d3d759883..6106264b78 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx @@ -47,7 +47,7 @@ const ToastDescription = () => { {t('modelManager.noModelsInstalledDesc1')}{' '} ); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx index 0924c0c099..268674d8c3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx @@ -23,8 +23,8 @@ export const NavigateToModelManagerButton = memo((props: Omit} - tooltip={t('modelManager.modelManager')} - aria-label={t('modelManager.modelManager')} + tooltip={`${t('common.goTo')} ${t('ui.tabs.modelsTab')}`} + aria-label={`${t('common.goTo')} ${t('ui.tabs.modelsTab')}`} onClick={handleClick} size="sm" variant="ghost" From 5734a97c55e7f96c08dbba6a0908acaea3916336 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 08:41:03 +1000 Subject: [PATCH 57/74] fix(ui): do not attempt drawing when invalid layer type selected --- .../controlLayers/hooks/mouseEventHooks.ts | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts index 7fe81ad567..889d2c0c2e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts @@ -49,6 +49,13 @@ const BRUSH_SPACING = 20; export const useMouseEvents = () => { const dispatch = useAppDispatch(); const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId); + const selectedLayerType = useAppSelector((s) => { + const selectedLayer = s.controlLayers.present.layers.find((l) => l.id === s.controlLayers.present.selectedLayerId); + if (!selectedLayer) { + return null; + } + return selectedLayer.type; + }); const tool = useStore($tool); const lastCursorPosRef = useRef<[number, number] | null>(null); const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); @@ -61,13 +68,10 @@ export const useMouseEvents = () => { return; } const pos = syncCursorPos(stage); - if (!pos) { + if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { return; } $lastMouseDownPos.set(pos); - if (!selectedLayerId) { - return; - } if (tool === 'brush' || tool === 'eraser') { dispatch( rgLayerLineAdded({ @@ -79,7 +83,7 @@ export const useMouseEvents = () => { $isDrawing.set(true); } }, - [dispatch, selectedLayerId, tool] + [dispatch, selectedLayerId, selectedLayerType, tool] ); const onMouseUp = useCallback( @@ -89,9 +93,12 @@ export const useMouseEvents = () => { return; } const pos = $cursorPosition.get(); + if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + return; + } const lastPos = $lastMouseDownPos.get(); const tool = $tool.get(); - if (pos && lastPos && selectedLayerId && tool === 'rect') { + if (lastPos && selectedLayerId && tool === 'rect') { dispatch( rgLayerRectAdded({ layerId: selectedLayerId, @@ -107,7 +114,7 @@ export const useMouseEvents = () => { $isDrawing.set(false); $lastMouseDownPos.set(null); }, - [dispatch, selectedLayerId] + [dispatch, selectedLayerId, selectedLayerType] ); const onMouseMove = useCallback( @@ -117,7 +124,7 @@ export const useMouseEvents = () => { return; } const pos = syncCursorPos(stage); - if (!pos || !selectedLayerId) { + if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { return; } if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) { @@ -138,7 +145,7 @@ export const useMouseEvents = () => { $isDrawing.set(true); } }, - [dispatch, selectedLayerId, tool] + [dispatch, selectedLayerId, selectedLayerType, tool] ); const onMouseLeave = useCallback( @@ -148,25 +155,25 @@ export const useMouseEvents = () => { return; } const pos = syncCursorPos(stage); - if ( - pos && - selectedLayerId && - getIsFocused(stage) && - getIsMouseDown(e) && - (tool === 'brush' || tool === 'eraser') - ) { + if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + return; + } + if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) { dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: [pos.x, pos.y] })); } $isDrawing.set(false); $cursorPosition.set(null); }, - [selectedLayerId, tool, dispatch] + [selectedLayerId, selectedLayerType, tool, dispatch] ); const onMouseWheel = useCallback( (e: KonvaEventObject) => { e.evt.preventDefault(); + if (selectedLayerType !== 'regional_guidance_layer' || (tool !== 'brush' && tool !== 'eraser')) { + return; + } // checking for ctrl key is pressed or not, // so that brush size can be controlled using ctrl + scroll up/down @@ -180,7 +187,7 @@ export const useMouseEvents = () => { dispatch(brushSizeChanged(calculateNewBrushSize(brushSize, delta))); } }, - [shouldInvertBrushSizeScrollDirection, brushSize, dispatch] + [selectedLayerType, tool, shouldInvertBrushSizeScrollDirection, dispatch, brushSize] ); return { onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel }; From c05e52ebae9b5e6385ec987e875ff0c71c288d0c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 09:27:21 +1000 Subject: [PATCH 58/74] fix(ui): do not delete all layers when using image as initial image --- .../web/src/features/controlLayers/store/controlLayersSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 6b3dfc5e6c..a1b5e0ebc8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -627,7 +627,7 @@ export const controlLayersSlice = createSlice({ reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; // Highlander! There can be only one! - state.layers = state.layers.filter((l) => isInitialImageLayer(l)); + state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); const layer: InitialImageLayer = { id: layerId, type: 'initial_image_layer', From 33617fc06a229b721c5b93621730bc0445fc84df Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 09:37:52 +1000 Subject: [PATCH 59/74] feat(ui): rework image viewer - Rework styling - Replace "CurrentImageDisplay" entirely - Add a super short fade to reduce jarring transition - Make the viewer a singleton component, overlaid on everything else - reduces change when switching tabs --- .../CurrentImage/CurrentImageDisplay.tsx | 24 --- .../gallery/components/ImageViewer.tsx | 79 ------- .../ImageViewer/BackToEditorButton.tsx | 24 +++ .../CurrentImageButtons.tsx | 193 +++++++----------- .../CurrentImagePreview.tsx | 3 +- .../components/ImageViewer/ImageViewer.tsx | 90 ++++++++ .../ProgressImage.tsx | 0 .../ToggleMetadataViewerButton.tsx | 42 ++++ .../ImageViewer/ToggleProgressButton.tsx | 29 +++ .../components/ImageViewer/useImageViewer.tsx | 31 +++ .../src/features/ui/components/InvokeTabs.tsx | 4 +- .../features/ui/components/tabs/NodesTab.tsx | 24 +-- .../ui/components/tabs/TextToImageTab.tsx | 2 - .../ui/components/tabs/UnifiedCanvasTab.tsx | 2 - 14 files changed, 294 insertions(+), 253 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageDisplay.tsx delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/BackToEditorButton.tsx rename invokeai/frontend/web/src/features/gallery/components/{CurrentImage => ImageViewer}/CurrentImageButtons.tsx (51%) rename invokeai/frontend/web/src/features/gallery/components/{CurrentImage => ImageViewer}/CurrentImagePreview.tsx (98%) create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx rename invokeai/frontend/web/src/features/gallery/components/{CurrentImage => ImageViewer}/ProgressImage.tsx (100%) create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageDisplay.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageDisplay.tsx deleted file mode 100644 index f4b707b859..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageDisplay.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Flex } from '@invoke-ai/ui-library'; -import { memo } from 'react'; - -import CurrentImageButtons from './CurrentImageButtons'; -import CurrentImagePreview from './CurrentImagePreview'; - -const CurrentImageDisplay = () => { - return ( - - - - - ); -}; - -export default memo(CurrentImageDisplay); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx deleted file mode 100644 index d1cc108ef2..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Button, Flex } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import CurrentImageButtons from 'features/gallery/components/CurrentImage/CurrentImageButtons'; -import CurrentImagePreview from 'features/gallery/components/CurrentImage/CurrentImagePreview'; -import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import type { InvokeTabName } from 'features/ui/store/tabMap'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback, useMemo } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiArrowLeftBold } from 'react-icons/pi'; - -const TAB_NAME_TO_TKEY: Record = { - generation: 'ui.tabs.generation', - canvas: 'ui.tabs.canvas', - workflows: 'ui.tabs.workflows', - models: 'ui.tabs.models', - queue: 'ui.tabs.queue', -}; - -export const ImageViewer = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen); - const activeTabName = useAppSelector(activeTabNameSelector); - const activeTabLabel = useMemo( - () => t('gallery.backToEditor', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }), - [t, activeTabName] - ); - - const onClose = useCallback(() => { - dispatch(isImageViewerOpenChanged(false)); - }, [dispatch]); - - const onOpen = useCallback(() => { - dispatch(isImageViewerOpenChanged(true)); - }, [dispatch]); - - useHotkeys('esc', onClose, { enabled: isOpen }, [isOpen]); - useHotkeys('i', onOpen, { enabled: !isOpen }, [isOpen]); - - if (!isOpen) { - return null; - } - - return ( - - - - - - ); -}); - -ImageViewer.displayName = 'ImageViewer'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/BackToEditorButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/BackToEditorButton.tsx new file mode 100644 index 0000000000..660840b568 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/BackToEditorButton.tsx @@ -0,0 +1,24 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowLeftBold } from 'react-icons/pi'; + +import { TAB_NAME_TO_TKEY, useImageViewer } from './useImageViewer'; + +export const BackToEditorButton = () => { + const { t } = useTranslation(); + const { onClose } = useImageViewer(); + const activeTabName = useAppSelector(activeTabNameSelector); + const tooltip = useMemo( + () => t('gallery.backToEditor', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }), + [t, activeTabName] + ); + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx similarity index 51% rename from invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx rename to invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index 2e4519af5e..f93f48e51b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -1,7 +1,6 @@ -import { ButtonGroup, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; +import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppToaster } from 'app/components/Toaster'; import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; @@ -17,7 +16,6 @@ import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUps import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { selectSystemSlice } from 'features/system/store/systemSlice'; -import { setShouldShowImageDetails, setShouldShowProgressInViewer } from 'features/ui/store/uiSlice'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -27,8 +25,6 @@ import { PiAsteriskBold, PiDotsThreeOutlineFill, PiFlowArrowBold, - PiHourglassHighBold, - PiInfoBold, PiPlantBold, PiQuotesBold, PiRulerBold, @@ -48,15 +44,12 @@ const selectShouldDisableToolbarButtons = createSelector( const CurrentImageButtons = () => { const dispatch = useAppDispatch(); const isConnected = useAppSelector((s) => s.system.isConnected); - const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails); - const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer); const lastSelectedImage = useAppSelector(selectLastSelectedImage); const selection = useAppSelector((s) => s.gallery.selection); const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons); const isUpscalingEnabled = useFeatureStatus('upscaling'); const isQueueMutationInProgress = useIsQueueMutationInProgress(); - const toaster = useAppToaster(); const { t } = useTranslation(); const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken); @@ -120,28 +113,6 @@ const CurrentImageButtons = () => { [isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected] ); - const handleClickShowImageDetails = useCallback( - () => dispatch(setShouldShowImageDetails(!shouldShowImageDetails)), - [dispatch, shouldShowImageDetails] - ); - - useHotkeys( - 'i', - () => { - if (imageDTO) { - handleClickShowImageDetails(); - } else { - toaster({ - title: t('toast.metadataLoadFailed'), - status: 'error', - duration: 2500, - isClosable: true, - }); - } - }, - [imageDTO, shouldShowImageDetails, toaster] - ); - useHotkeys( 'delete', () => { @@ -150,106 +121,80 @@ const CurrentImageButtons = () => { [dispatch, imageDTO] ); - const handleClickProgressImagesToggle = useCallback(() => { - dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer)); - }, [dispatch, shouldShowProgressInViewer]); - return ( <> - - - - } - /> - {imageDTO && } - - + + + } + /> + {imageDTO && } + + - - } - tooltip={`${t('nodes.loadWorkflow')} (W)`} - aria-label={`${t('nodes.loadWorkflow')} (W)`} - isDisabled={!imageDTO?.has_workflow} - onClick={handleLoadWorkflow} - isLoading={getAndLoadEmbeddedWorkflowResult.isLoading} - /> - } - tooltip={`${t('parameters.remixImage')} (R)`} - aria-label={`${t('parameters.remixImage')} (R)`} - isDisabled={!hasMetadata} - onClick={remix} - /> - } - tooltip={`${t('parameters.usePrompt')} (P)`} - aria-label={`${t('parameters.usePrompt')} (P)`} - isDisabled={!hasPrompts} - onClick={recallPrompts} - /> - } - tooltip={`${t('parameters.useSeed')} (S)`} - aria-label={`${t('parameters.useSeed')} (S)`} - isDisabled={!hasSeed} - onClick={recallSeed} - /> - } - tooltip={`${t('parameters.useSize')} (D)`} - aria-label={`${t('parameters.useSize')} (D)`} - onClick={handleUseSize} - /> - } - tooltip={`${t('parameters.useAll')} (A)`} - aria-label={`${t('parameters.useAll')} (A)`} - isDisabled={!hasMetadata} - onClick={recallAll} - /> - + + } + tooltip={`${t('nodes.loadWorkflow')} (W)`} + aria-label={`${t('nodes.loadWorkflow')} (W)`} + isDisabled={!imageDTO?.has_workflow} + onClick={handleLoadWorkflow} + isLoading={getAndLoadEmbeddedWorkflowResult.isLoading} + /> + } + tooltip={`${t('parameters.remixImage')} (R)`} + aria-label={`${t('parameters.remixImage')} (R)`} + isDisabled={!hasMetadata} + onClick={remix} + /> + } + tooltip={`${t('parameters.usePrompt')} (P)`} + aria-label={`${t('parameters.usePrompt')} (P)`} + isDisabled={!hasPrompts} + onClick={recallPrompts} + /> + } + tooltip={`${t('parameters.useSeed')} (S)`} + aria-label={`${t('parameters.useSeed')} (S)`} + isDisabled={!hasSeed} + onClick={recallSeed} + /> + } + tooltip={`${t('parameters.useSize')} (D)`} + aria-label={`${t('parameters.useSize')} (D)`} + onClick={handleUseSize} + /> + } + tooltip={`${t('parameters.useAll')} (A)`} + aria-label={`${t('parameters.useAll')} (A)`} + isDisabled={!hasMetadata} + onClick={recallAll} + /> + - {isUpscalingEnabled && ( - - {isUpscalingEnabled && } - - )} - - - } - tooltip={`${t('parameters.info')} (I)`} - aria-label={`${t('parameters.info')} (I)`} - isChecked={shouldShowImageDetails} - onClick={handleClickShowImageDetails} - /> + {isUpscalingEnabled && ( + + {isUpscalingEnabled && } + )} - - } - isChecked={shouldShowProgressInViewer} - onClick={handleClickProgressImagesToggle} - /> - - - - - - + + + ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx similarity index 98% rename from invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx rename to invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx index 02863daa2f..3f54974449 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx @@ -5,7 +5,6 @@ import { useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import ProgressImage from 'features/gallery/components/CurrentImage/ProgressImage'; import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer'; import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; @@ -17,6 +16,8 @@ import { useTranslation } from 'react-i18next'; import { PiImageBold } from 'react-icons/pi'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import ProgressImage from './ProgressImage'; + const selectLastSelectedImageName = createSelector( selectLastSelectedImage, (lastSelectedImage) => lastSelectedImage?.image_name diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx new file mode 100644 index 0000000000..9f3e7c5902 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -0,0 +1,90 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton'; +import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; +import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; +import type { InvokeTabName } from 'features/ui/store/tabMap'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import type { AnimationProps } from 'framer-motion'; +import { AnimatePresence, motion } from 'framer-motion'; +import { memo, useMemo } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; + +import { BackToEditorButton } from './BackToEditorButton'; +import CurrentImageButtons from './CurrentImageButtons'; +import CurrentImagePreview from './CurrentImagePreview'; + +const initial: AnimationProps['initial'] = { + opacity: 0, +}; +const animate: AnimationProps['animate'] = { + opacity: 1, + transition: { duration: 0.07 }, +}; +const exit: AnimationProps['exit'] = { + opacity: 0, + transition: { duration: 0.07 }, +}; + +const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows']; + +export const ImageViewer = memo(() => { + const { isOpen, onToggle, onClose } = useImageViewer(); + const activeTabName = useAppSelector(activeTabNameSelector); + const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]); + const shouldShowViewer = useMemo(() => { + if (!isViewerEnabled) { + return false; + } + return isOpen; + }, [isOpen, isViewerEnabled]); + + useHotkeys('shift+s', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]); + useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]); + + return ( + + {shouldShowViewer && ( + + + + + + + + + + + + + + + + + + + + )} + + ); +}); + +ImageViewer.displayName = 'ImageViewer'; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/ProgressImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx similarity index 100% rename from invokeai/frontend/web/src/features/gallery/components/CurrentImage/ProgressImage.tsx rename to invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx new file mode 100644 index 0000000000..a298ebda56 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx @@ -0,0 +1,42 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppToaster } from 'app/components/Toaster'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; +import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiInfoBold } from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; + +export const ToggleMetadataViewerButton = memo(() => { + const dispatch = useAppDispatch(); + const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails); + const lastSelectedImage = useAppSelector(selectLastSelectedImage); + const toaster = useAppToaster(); + const { t } = useTranslation(); + + const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken); + + const toggleMetadataViewer = useCallback( + () => dispatch(setShouldShowImageDetails(!shouldShowImageDetails)), + [dispatch, shouldShowImageDetails] + ); + + useHotkeys('i', toggleMetadataViewer, { enabled: Boolean(imageDTO) }, [imageDTO, shouldShowImageDetails, toaster]); + + return ( + } + tooltip={`${t('parameters.info')} (I)`} + aria-label={`${t('parameters.info')} (I)`} + onClick={toggleMetadataViewer} + isDisabled={!imageDTO} + variant="outline" + colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'} + /> + ); +}); + +ToggleMetadataViewerButton.displayName = 'ToggleMetadataViewerButton'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx new file mode 100644 index 0000000000..994a8bf10e --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx @@ -0,0 +1,29 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiHourglassHighBold } from 'react-icons/pi'; + +export const ToggleProgressButton = memo(() => { + const dispatch = useAppDispatch(); + const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer); + const { t } = useTranslation(); + + const onClick = useCallback(() => { + dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer)); + }, [dispatch, shouldShowProgressInViewer]); + + return ( + } + onClick={onClick} + variant="outline" + colorScheme={shouldShowProgressInViewer ? 'invokeBlue' : 'base'} + /> + ); +}); + +ToggleProgressButton.displayName = 'ToggleProgressButton'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx new file mode 100644 index 0000000000..17a9dc9922 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx @@ -0,0 +1,31 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; +import type { InvokeTabName } from 'features/ui/store/tabMap'; +import { useCallback } from 'react'; + +export const TAB_NAME_TO_TKEY: Record = { + generation: 'ui.tabs.generation', + canvas: 'ui.tabs.canvas', + workflows: 'ui.tabs.workflows', + models: 'ui.tabs.models', + queue: 'ui.tabs.queue', +}; + +export const useImageViewer = () => { + const dispatch = useAppDispatch(); + const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen); + + const onClose = useCallback(() => { + dispatch(isImageViewerOpenChanged(false)); + }, [dispatch]); + + const onOpen = useCallback(() => { + dispatch(isImageViewerOpenChanged(true)); + }, [dispatch]); + + const onToggle = useCallback(() => { + dispatch(isImageViewerOpenChanged(!isOpen)); + }, [dispatch, isOpen]); + + return { isOpen, onOpen, onClose, onToggle }; +}; diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 1968c64161..5e37a2d8c8 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -4,6 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { $customNavComponent } from 'app/store/nanostores/customNavComponent'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent'; +import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer'; import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup'; import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent'; import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu'; @@ -253,10 +254,11 @@ const InvokeTabs = () => { /> )} - + {tabPanels} + {shouldShowGalleryPanel && ( <> diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx index 81f0810192..2ee21bfadf 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx @@ -1,30 +1,14 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay'; -import { ImageViewer } from 'features/gallery/components/ImageViewer'; +import { Box } from '@invoke-ai/ui-library'; import NodeEditor from 'features/nodes/components/NodeEditor'; import { memo } from 'react'; import { ReactFlowProvider } from 'reactflow'; const NodesTab = () => { - const mode = useAppSelector((s) => s.workflow.mode); - - if (mode === 'edit') { - return ( - - - - - - - ); - } - return ( - - - + + + ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx index df0b8651bc..74845a9ca9 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx @@ -1,13 +1,11 @@ import { Box } from '@invoke-ai/ui-library'; import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor'; -import { ImageViewer } from 'features/gallery/components/ImageViewer'; import { memo } from 'react'; const TextToImageTab = () => { return ( - ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx index 8c74685d2d..3e0d9b35d4 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx @@ -6,7 +6,6 @@ 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 { ImageViewer } from 'features/gallery/components/ImageViewer'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -42,7 +41,6 @@ const UnifiedCanvasTab = () => { > - {isValidDrop(droppableData, active) && ( )} From 98664fc46f4d8b744725143e626c98a38f659840 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 09:50:20 +1000 Subject: [PATCH 60/74] fix(ui): gallery prev/next buttons animations --- .../ImageViewer/CurrentImagePreview.tsx | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx index 3f54974449..3ba3f2e372 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx @@ -24,6 +24,7 @@ const selectLastSelectedImageName = createSelector( ); const CurrentImagePreview = () => { + const { t } = useTranslation(); const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails); const imageName = useAppSelector(selectLastSelectedImageName); const hasDenoiseProgress = useAppSelector((s) => Boolean(s.system.denoiseProgress)); @@ -51,26 +52,18 @@ const CurrentImagePreview = () => { // Show and hide the next/prev buttons on mouse move const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState(false); - const timeoutId = useRef(0); - - const { t } = useTranslation(); - - const handleMouseOver = useCallback(() => { + const onMouseMove = useCallback(() => { setShouldShowNextPrevButtons(true); window.clearTimeout(timeoutId.current); - }, []); - - const handleMouseOut = useCallback(() => { timeoutId.current = window.setTimeout(() => { setShouldShowNextPrevButtons(false); - }, 500); + }, 1000); }, []); return ( { /> )} {shouldShowImageDetails && imageDTO && ( - + )} - {!shouldShowImageDetails && imageDTO && shouldShowNextPrevButtons && ( + {shouldShowNextPrevButtons && imageDTO && ( @@ -115,11 +108,11 @@ const initial: AnimationProps['initial'] = { }; const animate: AnimationProps['animate'] = { opacity: 1, - transition: { duration: 0.1 }, + transition: { duration: 0.07 }, }; const exit: AnimationProps['exit'] = { opacity: 0, - transition: { duration: 0.1 }, + transition: { duration: 0.07 }, }; const motionStyles: CSSProperties = { position: 'absolute', From 20e628297cbee3ad99c73a403285e79d18cc7be0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 09:53:47 +1000 Subject: [PATCH 61/74] fix(ui): smoother animations in current image preview --- .../ImageViewer/CurrentImagePreview.tsx | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx index 3ba3f2e372..37fada0b78 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx @@ -10,7 +10,6 @@ import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButto import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import type { AnimationProps } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion'; -import type { CSSProperties } from 'react'; import { memo, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiImageBold } from 'react-icons/pi'; @@ -85,16 +84,40 @@ const CurrentImagePreview = () => { dataTestId="image-preview" /> )} - {shouldShowImageDetails && imageDTO && ( - - - - )} + + {shouldShowImageDetails && imageDTO && ( + + + + )} + {shouldShowNextPrevButtons && imageDTO && ( - + - + )} @@ -106,18 +129,15 @@ export default memo(CurrentImagePreview); const initial: AnimationProps['initial'] = { opacity: 0, }; -const animate: AnimationProps['animate'] = { +const animateArrows: AnimationProps['animate'] = { opacity: 1, transition: { duration: 0.07 }, }; +const animateMetadata: AnimationProps['animate'] = { + opacity: 0.8, + transition: { duration: 0.07 }, +}; const exit: AnimationProps['exit'] = { opacity: 0, transition: { duration: 0.07 }, }; -const motionStyles: CSSProperties = { - position: 'absolute', - top: '0', - width: '100%', - height: '100%', - pointerEvents: 'none', -}; From e354fee4f487031ddc5c1d17c16c0d5d81f11014 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 09:58:06 +1000 Subject: [PATCH 62/74] fix(ui): add img2img metadata to graphs --- .../nodes/util/graph/addInitialImageToLinearGraph.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts index 4334a7cd31..603708f15b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts @@ -1,5 +1,6 @@ import type { RootState } from 'app/store/store'; import { isInitialImageLayer } from 'features/controlLayers/store/controlLayersSlice'; +import { upsertMetadata } from 'features/nodes/util/graph/metadata'; import type { ImageResizeInvocation, ImageToLatentsInvocation, NonNullableGraph } from 'services/api/types'; import { assert } from 'tsafe'; @@ -21,7 +22,8 @@ export const addInitialImageToLinearGraph = ( return; } - const useRefinerStartEnd = model?.base === 'sdxl' && Boolean(refinerModel); + const isSDXL = model?.base === 'sdxl'; + const useRefinerStartEnd = isSDXL && Boolean(refinerModel); const denoiseNode = graph.nodes[denoiseNodeId]; assert(denoiseNode?.type === 'denoise_latents', `Missing denoise node or incorrect type: ${denoiseNode?.type}`); @@ -114,4 +116,10 @@ export const addInitialImageToLinearGraph = ( }, }); } + + upsertMetadata(graph, { + generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img', + strength: img2imgStrength, + init_image: initialImage.imageName, + }); }; From 4c7be037020d25150dbb198c0826e7351c026a96 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 10:05:43 +1000 Subject: [PATCH 63/74] tidy(ui): rename generation tab graph builders --- .../listeners/enqueueRequestedLinear.ts | 8 ++++---- ...nearTextToImageGraph.ts => buildGenerationTabGraph.ts} | 2 +- ...TextToImageGraph.ts => buildGenerationTabSDXLGraph.ts} | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename invokeai/frontend/web/src/features/nodes/util/graph/{buildLinearTextToImageGraph.ts => buildGenerationTabGraph.ts} (98%) rename invokeai/frontend/web/src/features/nodes/util/graph/{buildLinearSDXLTextToImageGraph.ts => buildGenerationTabSDXLGraph.ts} (98%) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index bc0fcb5ef0..557220c449 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,8 +1,8 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph'; +import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; -import { buildLinearSDXLTextToImageGraph } from 'features/nodes/util/graph/buildLinearSDXLTextToImageGraph'; -import { buildLinearTextToImageGraph } from 'features/nodes/util/graph/buildLinearTextToImageGraph'; import { queueApi } from 'services/api/endpoints/queue'; export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { @@ -17,9 +17,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) let graph; if (model && model.base === 'sdxl') { - graph = await buildLinearSDXLTextToImageGraph(state); + graph = await buildGenerationTabSDXLGraph(state); } else { - graph = await buildLinearTextToImageGraph(state); + graph = await buildGenerationTabGraph(state); } const batchConfig = prepareLinearUIBatch(state, graph, prepend); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts similarity index 98% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts index c2ad5384e0..6c04b25770 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts @@ -25,7 +25,7 @@ import { } from './constants'; import { addCoreMetadataNode, getModelMetadataField } from './metadata'; -export const buildLinearTextToImageGraph = async (state: RootState): Promise => { +export const buildGenerationTabGraph = async (state: RootState): Promise => { const log = logger('nodes'); const { model, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts similarity index 98% rename from invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts index d3ee9e4a51..900e993602 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabSDXLGraph.ts @@ -25,7 +25,7 @@ import { import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; import { addCoreMetadataNode, getModelMetadataField } from './metadata'; -export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise => { +export const buildGenerationTabSDXLGraph = async (state: RootState): Promise => { const log = logger('nodes'); const { model, @@ -224,7 +224,7 @@ export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise addCoreMetadataNode( graph, { - generation_mode: 'sdxl_txt2img', + generation_mode: 'txt2img', cfg_scale, cfg_rescale_multiplier, height, From 85dd78b8df4c6660d0b8c23cc26fff7c2f8a5b3f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 10:44:07 +1000 Subject: [PATCH 64/74] fix(ui): handle deleting images in use in generation tab --- .../listeners/boardAndImagesDeleted.ts | 11 +- .../listeners/imageDeleted.ts | 163 +++++++++--------- .../components/DeleteImageModal.tsx | 9 +- .../components/ImageUsageMessage.tsx | 5 +- .../deleteImageModal/store/selectors.ts | 37 +++- .../features/deleteImageModal/store/types.ts | 1 + .../components/Boards/DeleteBoardModal.tsx | 9 +- 7 files changed, 139 insertions(+), 96 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index d7ab8430ca..a0b07b9419 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -1,6 +1,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { imagesApi } from 'services/api/endpoints/images'; @@ -16,10 +17,11 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS let wasCanvasReset = false; let wasNodeEditorReset = false; let wereControlAdaptersReset = false; + let wereControlLayersReset = false; - const { generation, canvas, nodes, controlAdapters } = getState(); + const { canvas, nodes, controlAdapters, controlLayers } = getState(); deleted_images.forEach((image_name) => { - const imageUsage = getImageUsage(generation, canvas, nodes, controlAdapters, image_name); + const imageUsage = getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name); if (imageUsage.isCanvasImage && !wasCanvasReset) { dispatch(resetCanvas()); @@ -35,6 +37,11 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS dispatch(controlAdaptersReset()); wereControlAdaptersReset = true; } + + if (imageUsage.isControlLayerImage && !wereControlLayersReset) { + dispatch(allLayersDeleted()); + wereControlLayersReset = true; + } }); }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index 451c26629e..95d17da653 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import type { AppDispatch, RootState } from 'app/store/store'; import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { controlAdapterImageChanged, @@ -7,6 +8,13 @@ import { selectControlAdapterAll, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; +import { + isControlAdapterLayer, + isInitialImageLayer, + isIPAdapterLayer, + isRegionalGuidanceLayer, + layerDeleted, +} from 'features/controlLayers/store/controlLayersSlice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; @@ -17,8 +25,79 @@ import { isInvocationNode } from 'features/nodes/types/invocation'; import { clamp, forEach } from 'lodash-es'; import { api } from 'services/api'; import { imagesApi } from 'services/api/endpoints/images'; +import type { ImageDTO } from 'services/api/types'; import { imagesSelectors } from 'services/api/util'; +const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { + state.nodes.nodes.forEach((node) => { + if (!isInvocationNode(node)) { + return; + } + + forEach(node.data.inputs, (input) => { + if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) { + dispatch( + fieldImageValueChanged({ + nodeId: node.data.id, + fieldName: input.name, + value: undefined, + }) + ); + } + }); + }); +}; + +const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { + forEach(selectControlAdapterAll(state.controlAdapters), (ca) => { + if ( + ca.controlImage === imageDTO.image_name || + (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name) + ) { + dispatch( + controlAdapterImageChanged({ + id: ca.id, + controlImage: null, + }) + ); + dispatch( + controlAdapterProcessedImageChanged({ + id: ca.id, + processedControlImage: null, + }) + ); + } + }); +}; + +const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { + state.controlLayers.present.layers.forEach((l) => { + if (isRegionalGuidanceLayer(l)) { + if (l.ipAdapters.some((ipa) => ipa.image?.imageName === imageDTO.image_name)) { + dispatch(layerDeleted(l.id)); + } + } + if (isControlAdapterLayer(l)) { + if ( + l.controlAdapter.image?.imageName === imageDTO.image_name || + l.controlAdapter.processedImage?.imageName === imageDTO.image_name + ) { + dispatch(layerDeleted(l.id)); + } + } + if (isIPAdapterLayer(l)) { + if (l.ipAdapter.image?.imageName === imageDTO.image_name) { + dispatch(layerDeleted(l.id)); + } + } + if (isInitialImageLayer(l)) { + if (l.image?.imageName === imageDTO.image_name) { + dispatch(layerDeleted(l.id)); + } + } + }); +}; + export const addRequestedSingleImageDeletionListener = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: imageDeletionConfirmed, @@ -72,45 +151,9 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt } imageDTOs.forEach((imageDTO) => { - // reset control adapters that use the deleted images - forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => { - if ( - ca.controlImage === imageDTO.image_name || - (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name) - ) { - dispatch( - controlAdapterImageChanged({ - id: ca.id, - controlImage: null, - }) - ); - dispatch( - controlAdapterProcessedImageChanged({ - id: ca.id, - processedControlImage: null, - }) - ); - } - }); - - // reset nodes that use the deleted images - getState().nodes.nodes.forEach((node) => { - if (!isInvocationNode(node)) { - return; - } - - forEach(node.data.inputs, (input) => { - if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) { - dispatch( - fieldImageValueChanged({ - nodeId: node.data.id, - fieldName: input.name, - value: undefined, - }) - ); - } - }); - }); + deleteControlAdapterImages(state, dispatch, imageDTO); + deleteNodesImages(state, dispatch, imageDTO); + deleteControlLayerImages(state, dispatch, imageDTO); }); // Delete from server @@ -162,45 +205,9 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt } imageDTOs.forEach((imageDTO) => { - // reset control adapters that use the deleted images - forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => { - if ( - ca.controlImage === imageDTO.image_name || - (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name) - ) { - dispatch( - controlAdapterImageChanged({ - id: ca.id, - controlImage: null, - }) - ); - dispatch( - controlAdapterProcessedImageChanged({ - id: ca.id, - processedControlImage: null, - }) - ); - } - }); - - // reset nodes that use the deleted images - getState().nodes.nodes.forEach((node) => { - if (!isInvocationNode(node)) { - return; - } - - forEach(node.data.inputs, (input) => { - if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) { - dispatch( - fieldImageValueChanged({ - nodeId: node.data.id, - fieldName: input.name, - value: undefined, - }) - ); - } - }); - }); + deleteControlAdapterImages(state, dispatch, imageDTO); + deleteNodesImages(state, dispatch, imageDTO); + deleteControlLayerImages(state, dispatch, imageDTO); }); } catch { // no-op diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index e3ee0b3852..f4b7438dff 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -3,6 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors'; import { @@ -12,7 +13,6 @@ import { } from 'features/deleteImageModal/store/slice'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice'; import { some } from 'lodash-es'; import type { ChangeEvent } from 'react'; @@ -24,23 +24,24 @@ import ImageUsageMessage from './ImageUsageMessage'; const selectImageUsages = createMemoizedSelector( [ selectDeleteImageModalSlice, - selectGenerationSlice, selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, + selectControlLayersSlice, selectImageUsage, ], - (deleteImageModal, generation, canvas, nodes, controlAdapters, imagesUsage) => { + (deleteImageModal, canvas, nodes, controlAdapters, controlLayers, imagesUsage) => { const { imagesToDelete } = deleteImageModal; const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) => - getImageUsage(generation, canvas, nodes, controlAdapters, image_name) + getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name) ); const imageUsageSummary: ImageUsage = { isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage), isNodesImage: some(allImageUsage, (i) => i.isNodesImage), isControlImage: some(allImageUsage, (i) => i.isControlImage), + isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage), }; return { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx index ec613409e7..d76716d01d 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx @@ -29,9 +29,10 @@ const ImageUsageMessage = (props: Props) => { <> {topMessage} - {imageUsage.isCanvasImage && {t('common.unifiedCanvas')}} + {imageUsage.isCanvasImage && {t('ui.tabs.canvasTab')}} {imageUsage.isControlImage && {t('common.controlNet')}} - {imageUsage.isNodesImage && {t('common.nodeEditor')}} + {imageUsage.isNodesImage && {t('ui.tabs.workflowsTab')}} + {imageUsage.isControlLayerImage && {t('ui.tabs.generationTab')}} {bottomMessage} diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index b9540a3ecf..ce989de7b1 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -7,22 +7,28 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { ControlAdaptersState } from 'features/controlAdapters/store/types'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; +import { + isControlAdapterLayer, + isInitialImageLayer, + isIPAdapterLayer, + isRegionalGuidanceLayer, + selectControlLayersSlice, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlLayersState } from 'features/controlLayers/store/types'; import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import type { NodesState } from 'features/nodes/store/types'; import { isImageFieldInputInstance } from 'features/nodes/types/field'; import { isInvocationNode } from 'features/nodes/types/invocation'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; -import type { GenerationState } from 'features/parameters/store/types'; import { some } from 'lodash-es'; import type { ImageUsage } from './types'; export const getImageUsage = ( - generation: GenerationState, canvas: CanvasState, nodes: NodesState, controlAdapters: ControlAdaptersState, + controlLayers: ControlLayersState, image_name: string ) => { const isCanvasImage = canvas.layerState.objects.some((obj) => obj.kind === 'image' && obj.imageName === image_name); @@ -38,10 +44,29 @@ export const getImageUsage = ( (ca) => ca.controlImage === image_name || (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === image_name) ); + const isControlLayerImage = controlLayers.layers.some((l) => { + if (isRegionalGuidanceLayer(l)) { + return l.ipAdapters.some((ipa) => ipa.image?.imageName === image_name); + } + if (isControlAdapterLayer(l)) { + return ( + l.controlAdapter.image?.imageName === image_name || l.controlAdapter.processedImage?.imageName === image_name + ); + } + if (isIPAdapterLayer(l)) { + return l.ipAdapter.image?.imageName === image_name; + } + if (isInitialImageLayer(l)) { + return l.image?.imageName === image_name; + } + return false; + }); + const imageUsage: ImageUsage = { isCanvasImage, isNodesImage, isControlImage, + isControlLayerImage, }; return imageUsage; @@ -49,11 +74,11 @@ export const getImageUsage = ( export const selectImageUsage = createMemoizedSelector( selectDeleteImageModalSlice, - selectGenerationSlice, selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, - (deleteImageModal, generation, canvas, nodes, controlAdapters) => { + selectControlLayersSlice, + (deleteImageModal, canvas, nodes, controlAdapters, controlLayers) => { const { imagesToDelete } = deleteImageModal; if (!imagesToDelete.length) { @@ -61,7 +86,7 @@ export const selectImageUsage = createMemoizedSelector( } const imagesUsage = imagesToDelete.map((i) => - getImageUsage(generation, canvas, nodes, controlAdapters, i.image_name) + getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, i.image_name) ); return imagesUsage; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts index f0aaf7b097..2cc3dd90b4 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts @@ -9,4 +9,5 @@ export type ImageUsage = { isCanvasImage: boolean; isNodesImage: boolean; isControlImage: boolean; + isControlLayerImage: boolean; }; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 5f01fd9f29..377636d0d0 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -15,11 +15,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { some } from 'lodash-es'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -43,16 +43,17 @@ const DeleteBoardModal = (props: Props) => { const selectImageUsageSummary = useMemo( () => createMemoizedSelector( - [selectGenerationSlice, selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice], - (generation, canvas, nodes, controlAdapters) => { + [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectControlLayersSlice], + (canvas, nodes, controlAdapters, controlLayers) => { const allImageUsage = (boardImageNames ?? []).map((imageName) => - getImageUsage(generation, canvas, nodes, controlAdapters, imageName) + getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName) ); const imageUsageSummary: ImageUsage = { isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage), isNodesImage: some(allImageUsage, (i) => i.isNodesImage), isControlImage: some(allImageUsage, (i) => i.isControlImage), + isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage), }; return imageUsageSummary; From f05ac5a7a516d3f41be45ffe275516ab02fd7a9d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 11:02:26 +1000 Subject: [PATCH 65/74] chore(ui): bump @invoke-ai/ui-library --- invokeai/frontend/web/package.json | 2 +- invokeai/frontend/web/pnpm-lock.yaml | 586 +++++++-------------------- 2 files changed, 146 insertions(+), 442 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 2730367fe5..96db090386 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -58,7 +58,7 @@ "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/inter": "^5.0.17", - "@invoke-ai/ui-library": "^0.0.21", + "@invoke-ai/ui-library": "^0.0.25", "@nanostores/react": "^0.7.2", "@reduxjs/toolkit": "2.2.2", "@roarr/browser-log-writer": "^1.3.0", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 9910e32391..2e5442479f 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: dependencies: '@chakra-ui/react': specifier: ^2.8.2 - version: 2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0) + version: 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) '@chakra-ui/react-use-size': specifier: ^2.1.0 version: 2.1.0(react@18.2.0) @@ -30,8 +30,8 @@ dependencies: specifier: ^5.0.17 version: 5.0.17 '@invoke-ai/ui-library': - specifier: ^0.0.21 - version: 0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.2)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) + specifier: ^0.0.25 + version: 0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.3)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) '@nanostores/react': specifier: ^0.7.2 version: 0.7.2(nanostores@0.10.0)(react@18.2.0) @@ -306,7 +306,7 @@ packages: '@jridgewell/trace-mapping': 0.3.25 dev: true - /@ark-ui/anatomy@1.3.0(@internationalized/date@3.5.2): + /@ark-ui/anatomy@1.3.0(@internationalized/date@3.5.3): resolution: {integrity: sha512-1yG2MrzUlix6KthjQMCNiHnkXrWwEdFAX6D+HqGJaNu0XvaGul2J+wDNtjsdX+gxiWu1nXXEEOAWlFVYMUf65w==} dependencies: '@zag-js/accordion': 0.32.1 @@ -318,7 +318,7 @@ packages: '@zag-js/color-utils': 0.32.1 '@zag-js/combobox': 0.32.1 '@zag-js/date-picker': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2) + '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3) '@zag-js/dialog': 0.32.1 '@zag-js/editable': 0.32.1 '@zag-js/file-upload': 0.32.1 @@ -345,13 +345,13 @@ packages: - '@internationalized/date' dev: false - /@ark-ui/react@1.3.0(@internationalized/date@3.5.2)(react-dom@18.2.0)(react@18.2.0): + /@ark-ui/react@1.3.0(@internationalized/date@3.5.3)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-JHjNoIX50+mUCTaEGMjfGQWGGi31pKsV646jZJlR/1xohpYJigzg8BvO97cTsVk8fwtur+cm11gz3Nf7f5QUnA==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' dependencies: - '@ark-ui/anatomy': 1.3.0(@internationalized/date@3.5.2) + '@ark-ui/anatomy': 1.3.0(@internationalized/date@3.5.3) '@zag-js/accordion': 0.32.1 '@zag-js/avatar': 0.32.1 '@zag-js/carousel': 0.32.1 @@ -361,7 +361,7 @@ packages: '@zag-js/combobox': 0.32.1 '@zag-js/core': 0.32.1 '@zag-js/date-picker': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2) + '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3) '@zag-js/dialog': 0.32.1 '@zag-js/editable': 0.32.1 '@zag-js/file-upload': 0.32.1 @@ -1681,7 +1681,7 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0): + /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -1694,9 +1694,9 @@ packages: '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false @@ -1848,16 +1848,6 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.3)(react@18.2.0): - resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==} - peerDependencies: - '@emotion/react': '>=10.0.35' - react: '>=18' - dependencies: - '@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0) - react: 18.2.0 - dev: false - /@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.4)(react@18.2.0): resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==} peerDependencies: @@ -1905,18 +1895,6 @@ packages: resolution: {integrity: sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==} dev: false - /@chakra-ui/focus-lock@2.1.0(@types/react@18.2.59)(react@18.2.0): - resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/dom-utils': 2.1.0 - react: 18.2.0 - react-focus-lock: 2.11.1(@types/react@18.2.59)(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - dev: false - /@chakra-ui/focus-lock@2.1.0(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==} peerDependencies: @@ -1924,7 +1902,7 @@ packages: dependencies: '@chakra-ui/dom-utils': 2.1.0 react: 18.2.0 - react-focus-lock: 2.11.2(@types/react@18.2.73)(react@18.2.0) + react-focus-lock: 2.11.1(@types/react@18.2.73)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false @@ -2100,59 +2078,6 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0): - resolution: {integrity: sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.2.0) - '@chakra-ui/descendant': 3.1.0(react@18.2.0) - '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-outside-click': 2.2.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - dev: false - - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.59)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0) - aria-hidden: 1.2.3 - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.7(@types/react@18.2.59)(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - dev: false - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} peerDependencies: @@ -2170,11 +2095,37 @@ packages: '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) - aria-hidden: 1.2.4 + aria-hidden: 1.2.3 framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.9(@types/react@18.2.73)(react@18.2.0) + react-remove-scroll: 2.5.7(@types/react@18.2.73)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} + peerDependencies: + '@chakra-ui/system': '>=2.0.0' + framer-motion: '>=4.0.0' + react: '>=18' + react-dom: '>=18' + dependencies: + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) + '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-types': 2.0.7(react@18.2.0) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) + aria-hidden: 1.2.3 + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.7(@types/react@18.2.73)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false @@ -2248,7 +2199,7 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0): + /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2266,8 +2217,8 @@ packages: '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.2.0) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false @@ -2305,25 +2256,6 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/provider@2.4.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==} - peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.3)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - /@chakra-ui/provider@2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==} peerDependencies: @@ -2554,77 +2486,6 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} - peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0) - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/counter': 2.1.0(react@18.2.0) - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.3)(react@18.2.0) - '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.59)(react@18.2.0) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/hooks': 2.2.1(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/live-region': 2.1.0(react@18.2.0) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0) - '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0) - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0) - '@chakra-ui/utils': 2.0.15 - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} peerDependencies: @@ -2696,6 +2557,77 @@ packages: - '@types/react' dev: false + /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} + peerDependencies: + '@emotion/react': ^11.0.0 + '@emotion/styled': ^11.0.0 + framer-motion: '>=4.0.0' + react: '>=18' + react-dom: '>=18' + dependencies: + '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/counter': 2.1.0(react@18.2.0) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.2.0) + '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/hooks': 2.2.1(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/live-region': 2.1.0(react@18.2.0) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.2.0) + '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/react-env': 3.1.0(react@18.2.0) + '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/styled-system': 2.9.2 + '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) + '@chakra-ui/theme-utils': 2.0.21 + '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/utils': 2.0.15 + '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): resolution: {integrity: sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==} peerDependencies: @@ -2814,7 +2746,7 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0): + /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2823,30 +2755,11 @@ packages: dependencies: '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false - /@chakra-ui/system@2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} - peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - react: '>=18' - dependencies: - '@chakra-ui/color-mode': 2.2.0(react@18.2.0) - '@chakra-ui/object-utils': 2.1.0 - '@chakra-ui/react-utils': 2.0.12(react@18.2.0) - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0) - react: 18.2.0 - react-fast-compare: 3.2.2 - dev: false - /@chakra-ui/system@2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0): resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} peerDependencies: @@ -2975,7 +2888,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==} peerDependencies: '@chakra-ui/system': 2.6.2 @@ -2991,9 +2904,9 @@ packages: '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -3020,7 +2933,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -3036,8 +2949,8 @@ packages: '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -3064,17 +2977,6 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/transition@2.1.0(framer-motion@11.0.6)(react@18.2.0): - resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==} - peerDependencies: - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - dev: false - /@chakra-ui/utils@2.0.15: resolution: {integrity: sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==} dependencies: @@ -3198,12 +3100,6 @@ packages: dev: false optional: true - /@emotion/is-prop-valid@1.2.1: - resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==} - dependencies: - '@emotion/memoize': 0.8.1 - dev: false - /@emotion/is-prop-valid@1.2.2: resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} dependencies: @@ -3220,27 +3116,6 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false - /@emotion/react@11.11.3(@types/react@18.2.59)(react@18.2.0): - resolution: {integrity: sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==} - peerDependencies: - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.23.9 - '@emotion/babel-plugin': 11.11.0 - '@emotion/cache': 11.11.0 - '@emotion/serialize': 1.1.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) - '@emotion/utils': 1.2.1 - '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.2.59 - hoist-non-react-statics: 3.3.2 - react: 18.2.0 - dev: false - /@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==} peerDependencies: @@ -3276,27 +3151,6 @@ packages: resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} dev: false - /@emotion/styled@11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0): - resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} - peerDependencies: - '@emotion/react': ^11.0.0-rc.0 - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.23.9 - '@emotion/babel-plugin': 11.11.0 - '@emotion/is-prop-valid': 1.2.1 - '@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0) - '@emotion/serialize': 1.1.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) - '@emotion/utils': 1.2.1 - '@types/react': 18.2.59 - react: 18.2.0 - dev: false - /@emotion/styled@11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} peerDependencies: @@ -3663,16 +3517,16 @@ packages: resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} dev: true - /@internationalized/date@3.5.2: - resolution: {integrity: sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==} + /@internationalized/date@3.5.3: + resolution: {integrity: sha512-X9bi8NAEHAjD8yzmPYT2pdJsbe+tYSEBAfowtlxJVJdZR3aK8Vg7ZUT1Fm5M47KLzp/M1p1VwAaeSma3RT7biw==} dependencies: - '@swc/helpers': 0.5.7 + '@swc/helpers': 0.5.11 dev: false /@internationalized/number@3.5.1: resolution: {integrity: sha512-N0fPU/nz15SwR9IbfJ5xaS9Ss/O5h1sVXMZf43vc9mxEG48ovglvvzBjF53aHlq20uoR6c+88CrIXipU/LSzwg==} dependencies: - '@swc/helpers': 0.5.7 + '@swc/helpers': 0.5.11 dev: false /@invoke-ai/eslint-config-react@0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.3): @@ -3709,14 +3563,14 @@ packages: prettier: 3.2.5 dev: true - /@invoke-ai/ui-library@0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.2)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-tCvgkBPDt0gNq+8IcR03e/Mw7R8Mb/SMXTqx3FEIxlTQEo93A/D38dKXeDCzTdx4sQ+sknfB+JLBbHs6sg5hhQ==} + /@invoke-ai/ui-library@0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.3)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Fmjdlu62NXHgairYXGjcuCrxPEAl1G6Q6ban8g3excF6pDDdBeS7CmSNCyEDMxnSIOZrQlI04OhaMB17Imi9Uw==} peerDependencies: '@fontsource-variable/inter': ^5.0.16 react: ^18.2.0 react-dom: ^18.2.0 dependencies: - '@ark-ui/react': 1.3.0(@internationalized/date@3.5.2)(react-dom@18.2.0)(react@18.2.0) + '@ark-ui/react': 1.3.0(@internationalized/date@3.5.3)(react-dom@18.2.0)(react@18.2.0) '@chakra-ui/anatomy': 2.2.2 '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) @@ -5381,8 +5235,8 @@ packages: resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} dev: true - /@swc/helpers@0.5.7: - resolution: {integrity: sha512-BVvNZhx362+l2tSwSuyEUV4h7+jk9raNdoTSdLfwTshXJSaGmYKluGRJznziCI3KX02Z19DdsQrdfrpXAU3Hfg==} + /@swc/helpers@0.5.11: + resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} dependencies: tslib: 2.6.2 dev: false @@ -5844,10 +5698,6 @@ packages: resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==} dev: true - /@types/prop-types@15.7.11: - resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} - dev: false - /@types/prop-types@15.7.12: resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -5877,14 +5727,6 @@ packages: '@types/react': 18.2.73 dev: false - /@types/react@18.2.59: - resolution: {integrity: sha512-DE+F6BYEC8VtajY85Qr7mmhTd/79rJKIHCg99MU9SWPB4xvLb6D1za2vYflgZfmPqQVEr6UqJTnLXEwzpVPuOg==} - dependencies: - '@types/prop-types': 15.7.11 - '@types/scheduler': 0.16.8 - csstype: 3.1.3 - dev: false - /@types/react@18.2.73: resolution: {integrity: sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==} dependencies: @@ -5895,10 +5737,6 @@ packages: resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} dev: true - /@types/scheduler@0.16.8: - resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - dev: false - /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} dev: true @@ -6405,10 +6243,10 @@ packages: /@zag-js/date-picker@0.32.1: resolution: {integrity: sha512-n/hYmF+/R4+NuyfPRzCgeuLT6LJihKSuKzK29STPWy3sC/tBBHiqhNv1/4UKbatHUJXdBW2XF+N8Rw08RffcFQ==} dependencies: - '@internationalized/date': 3.5.2 + '@internationalized/date': 3.5.3 '@zag-js/anatomy': 0.32.1 '@zag-js/core': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2) + '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3) '@zag-js/dismissable': 0.32.1 '@zag-js/dom-event': 0.32.1 '@zag-js/dom-query': 0.32.1 @@ -6420,12 +6258,12 @@ packages: '@zag-js/utils': 0.32.1 dev: false - /@zag-js/date-utils@0.32.1(@internationalized/date@3.5.2): + /@zag-js/date-utils@0.32.1(@internationalized/date@3.5.3): resolution: {integrity: sha512-dbBDRSVr5pRUw3rXndyGuSshZiWqQI5JQO4D2KIFGkXzorj6WzoOpcO910Z7AdM/9cCAMpCjUrka8d8o9BpJBg==} peerDependencies: '@internationalized/date': '>=3.0.0' dependencies: - '@internationalized/date': 3.5.2 + '@internationalized/date': 3.5.3 dev: false /@zag-js/dialog@0.32.1: @@ -6999,13 +6837,6 @@ packages: tslib: 2.6.2 dev: false - /aria-hidden@1.2.4: - resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} - engines: {node: '>=10'} - dependencies: - tslib: 2.6.2 - dev: false - /aria-query@5.1.3: resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} dependencies: @@ -9026,13 +8857,6 @@ packages: tslib: 2.6.2 dev: false - /focus-lock@1.3.4: - resolution: {integrity: sha512-Gv0N3mvej3pD+HWkNryrF8sExzEHqhQ6OSFxD4DPxm9n5HGCaHme98ZMBZroNEAJcsdtHxk+skvThGKyUeoEGA==} - engines: {node: '>=10'} - dependencies: - tslib: 2.6.2 - dev: false - /focus-trap@7.5.4: resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==} dependencies: @@ -9095,24 +8919,6 @@ packages: tslib: 2.6.2 dev: false - /framer-motion@11.0.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-BpO3mWF8UwxzO3Ca5AmSkrg14QYTeJa9vKgoLOoBdBdTPj0e81i1dMwnX6EQJXRieUx20uiDBXq8bA6y7N6b8Q==} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - tslib: 2.6.2 - optionalDependencies: - '@emotion/is-prop-valid': 0.8.8 - dev: false - /framesync@6.1.2: resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} dependencies: @@ -11485,7 +11291,7 @@ packages: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false - /react-focus-lock@2.11.1(@types/react@18.2.59)(react@18.2.0): + /react-focus-lock@2.11.1(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -11495,31 +11301,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@types/react': 18.2.59 + '@types/react': 18.2.73 focus-lock: 1.3.3 prop-types: 15.8.1 react: 18.2.0 react-clientside-effect: 1.2.6(react@18.2.0) - use-callback-ref: 1.3.1(@types/react@18.2.59)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.59)(react@18.2.0) - dev: false - - /react-focus-lock@2.11.2(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-DDTbEiov0+RthESPVSTIdAWPPKic+op3sCcP+icbMRobvQNt7LuAlJ3KoarqQv5sCgKArru3kXmlmFTa27/CdQ==} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.24.1 - '@types/react': 18.2.73 - focus-lock: 1.3.4 - prop-types: 15.8.1 - react: 18.2.0 - react-clientside-effect: 1.2.6(react@18.2.0) - use-callback-ref: 1.3.2(@types/react@18.2.73)(react@18.2.0) + use-callback-ref: 1.3.1(@types/react@18.2.73)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0) dev: false @@ -11634,25 +11421,9 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false - /react-remove-scroll-bar@2.3.5(@types/react@18.2.59)(react@18.2.0): + /react-remove-scroll-bar@2.3.5(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.59 - react: 18.2.0 - react-style-singleton: 2.2.1(@types/react@18.2.59)(react@18.2.0) - tslib: 2.6.2 - dev: false - - /react-remove-scroll-bar@2.3.6(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} - engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -11666,28 +11437,9 @@ packages: tslib: 2.6.2 dev: false - /react-remove-scroll@2.5.7(@types/react@18.2.59)(react@18.2.0): + /react-remove-scroll@2.5.7(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.59 - react: 18.2.0 - react-remove-scroll-bar: 2.3.5(@types/react@18.2.59)(react@18.2.0) - react-style-singleton: 2.2.1(@types/react@18.2.59)(react@18.2.0) - tslib: 2.6.2 - use-callback-ref: 1.3.1(@types/react@18.2.59)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.59)(react@18.2.0) - dev: false - - /react-remove-scroll@2.5.9(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-bvHCLBrFfM2OgcrpPY2YW84sPdS2o2HKWJUf1xGyGLnSoEnOTOBpahIarjRuYtN0ryahCeP242yf+5TrBX/pZA==} - engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -11697,10 +11449,10 @@ packages: dependencies: '@types/react': 18.2.73 react: 18.2.0 - react-remove-scroll-bar: 2.3.6(@types/react@18.2.73)(react@18.2.0) + react-remove-scroll-bar: 2.3.5(@types/react@18.2.73)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.2.73)(react@18.2.0) tslib: 2.6.2 - use-callback-ref: 1.3.2(@types/react@18.2.73)(react@18.2.0) + use-callback-ref: 1.3.1(@types/react@18.2.73)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0) dev: false @@ -11756,23 +11508,6 @@ packages: - '@types/react' dev: false - /react-style-singleton@2.2.1(@types/react@18.2.59)(react@18.2.0): - resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.59 - get-nonce: 1.0.1 - invariant: 2.2.4 - react: 18.2.0 - tslib: 2.6.2 - dev: false - /react-style-singleton@2.2.1(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -13288,24 +13023,9 @@ packages: punycode: 2.3.1 dev: true - /use-callback-ref@1.3.1(@types/react@18.2.59)(react@18.2.0): + /use-callback-ref@1.3.1(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.59 - react: 18.2.0 - tslib: 2.6.2 - dev: false - - /use-callback-ref@1.3.2(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} - engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -13358,22 +13078,6 @@ packages: react: 18.2.0 dev: false - /use-sidecar@1.1.2(@types/react@18.2.59)(react@18.2.0): - resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.59 - detect-node-es: 1.1.0 - react: 18.2.0 - tslib: 2.6.2 - dev: false - /use-sidecar@1.1.2(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} From c30df7ce79eabc8a67c7aaae77572bfafcaf4131 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 11:24:49 +1000 Subject: [PATCH 66/74] feat(ui): style settings/control layers tabs --- .../components/ParametersPanelTextToImage.tsx | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx index 79a9b5a03c..6cb9c6d762 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx @@ -1,3 +1,4 @@ +import type { ChakraProps } from '@invoke-ai/ui-library'; import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; @@ -23,6 +24,29 @@ const overlayScrollbarsStyles: CSSProperties = { width: '100%', }; +const unselectedStyles: ChakraProps['sx'] = { + bg: 'none', + color: 'base.300', + fontWeight: 'semibold', + fontSize: 'sm', + w: '50%', + borderWidth: 1, + borderRadius: 'base', +}; + +const selectedStyles: ChakraProps['sx'] = { + color: 'invokeBlue.300', + borderColor: 'invokeBlueAlpha.400', + _hover: { + color: 'invokeBlue.200', + }, +}; + +const hoverStyles: ChakraProps['sx'] = { + bg: 'base.850', + color: 'base.100', +}; + const ParametersPanelTextToImage = () => { const { t } = useTranslation(); const activeTabName = useAppSelector(activeTabNameSelector); @@ -37,14 +61,17 @@ const ParametersPanelTextToImage = () => { {isSDXL ? : } - - - {t('common.settingsLabel')} - {controlLayersTitle} + + + + {t('common.settingsLabel')} + + + {controlLayersTitle} + - - + @@ -54,7 +81,7 @@ const ParametersPanelTextToImage = () => { - + From 2baa33730a926e19fcafe066de46764bf6e1491a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 11:28:52 +1000 Subject: [PATCH 67/74] fix(ui): fix control layer list layout --- .../controlLayers/components/ControlLayersPanelContent.tsx | 4 ++-- .../src/features/ui/components/ParametersPanelTextToImage.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 14bea9bc1e..b78beb3df8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -22,13 +22,13 @@ const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, export const ControlLayersPanelContent = memo(() => { const layerIdTypePairs = useAppSelector(selectLayerIdTypePairs); return ( - + - + {layerIdTypePairs.map(({ id, type }) => ( ))} diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx index 6cb9c6d762..abd78d00e4 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanelTextToImage.tsx @@ -71,7 +71,7 @@ const ParametersPanelTextToImage = () => { - + @@ -81,7 +81,7 @@ const ParametersPanelTextToImage = () => { - + From eb36e834b2a950c8bf6fc8442c2a311fcbd92b54 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 11:34:01 +1000 Subject: [PATCH 68/74] feat(ui): add fallback when no layers exist --- invokeai/frontend/web/public/locales/en.json | 3 ++- .../components/ControlLayersPanelContent.tsx | 20 ++++++++++++------- .../features/controlLayers/util/renderers.ts | 3 ++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index cfcb433db2..d122892d39 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1558,7 +1558,8 @@ "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", "opacityFilter": "Opacity Filter", "clearProcessor": "Clear Processor", - "resetProcessor": "Reset Processor to Defaults" + "resetProcessor": "Reset Processor to Defaults", + "noLayersAdded": "No Layers Added" }, "ui": { "tabs": { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index b78beb3df8..1dd79d0220 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -2,6 +2,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; import { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; @@ -13,6 +14,7 @@ import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLay import type { Layer } from 'features/controlLayers/store/types'; import { partition } from 'lodash-es'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer); @@ -20,6 +22,7 @@ const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, }); export const ControlLayersPanelContent = memo(() => { + const { t } = useTranslation(); const layerIdTypePairs = useAppSelector(selectLayerIdTypePairs); return ( @@ -27,13 +30,16 @@ export const ControlLayersPanelContent = memo(() => { - - - {layerIdTypePairs.map(({ id, type }) => ( - - ))} - - + {layerIdTypePairs.length > 0 && ( + + + {layerIdTypePairs.map(({ id, type }) => ( + + ))} + + + )} + {layerIdTypePairs.length === 0 && } ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index 09db523a61..b437c95650 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -40,6 +40,7 @@ import type { VectorMaskRect, } from 'features/controlLayers/store/types'; import { getLayerBboxFast, getLayerBboxPixels } from 'features/controlLayers/util/bbox'; +import { t } from 'i18next'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { debounce } from 'lodash-es'; @@ -819,7 +820,7 @@ const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => { y: 0, align: 'center', verticalAlign: 'middle', - text: 'No Layers Added', + text: t('controlLayers.noLayersAdded'), fontFamily: '"Inter Variable", sans-serif', fontStyle: '600', fill: 'white', From 2062cfe84a96f2076108b69517d22fee567a58e8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 11:35:53 +1000 Subject: [PATCH 69/74] fix(ui): cursor when no renderable layers added --- .../frontend/web/src/features/controlLayers/util/renderers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index b437c95650..36083d2d92 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -146,7 +146,7 @@ const renderToolPreview = ( lastMouseDownPos: Vector2d | null, brushSize: number ) => { - const layerCount = stage.find(`.${RG_LAYER_NAME}`).length; + const layerCount = stage.find(selectRenderableLayers).length; // Update the stage's pointer style if (layerCount === 0) { // We have no layers, so we should not render any tool From fdfc379a84751b986c6f8dc79e21e9ffee43ad52 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 11:46:15 +1000 Subject: [PATCH 70/74] fix(ui): layer counts --- .../hooks/useControlLayersTitle.ts | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts index c42a27f28f..bf0fa661a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts @@ -1,20 +1,43 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { + isControlAdapterLayer, + isInitialImageLayer, + isIPAdapterLayer, + isRegionalGuidanceLayer, + selectControlLayersSlice, +} from 'features/controlLayers/store/controlLayersSlice'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => { - const validLayers = controlLayers.present.layers - .filter(isRegionalGuidanceLayer) - .filter((l) => l.isEnabled) - .filter((l) => { + let count = 0; + controlLayers.present.layers.forEach((l) => { + if (isRegionalGuidanceLayer(l)) { const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); - const hasAtLeastOneImagePrompt = l.ipAdapters.length > 0; - return hasTextPrompt || hasAtLeastOneImagePrompt; - }); + const hasAtLeastOneImagePrompt = l.ipAdapters.filter((ipa) => Boolean(ipa.image)).length > 0; + if (hasTextPrompt || hasAtLeastOneImagePrompt) { + count += 1; + } + } + if (isControlAdapterLayer(l)) { + if (l.controlAdapter.image || l.controlAdapter.processedImage) { + count += 1; + } + } + if (isIPAdapterLayer(l)) { + if (l.ipAdapter.image) { + count += 1; + } + } + if (isInitialImageLayer(l)) { + if (l.image) { + count += 1; + } + } + }); - return validLayers.length; + return count; }); export const useControlLayersTitle = () => { From d9b92d19f95bf05798aec2af8958d9e9f19acf4f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 12:13:35 +1000 Subject: [PATCH 71/74] feat(ui): clearer viewer/editor context switching --- invokeai/frontend/web/public/locales/en.json | 3 +- .../IAICanvasToolbar/IAICanvasToolbar.tsx | 181 ++++++++++-------- .../components/ControlLayersToolbar.tsx | 21 +- .../ImageViewer/BackToEditorButton.tsx | 24 --- .../components/ImageViewer/EditorButton.tsx | 37 ++++ .../components/ImageViewer/ImageViewer.tsx | 7 +- .../components/ImageViewer/ViewerButton.tsx | 17 ++ .../components/ImageViewer/useImageViewer.tsx | 9 - .../flow/panels/TopPanel/TopPanel.tsx | 2 + 9 files changed, 174 insertions(+), 127 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/BackToEditorButton.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/EditorButton.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index d122892d39..a6d60fa281 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -88,6 +88,7 @@ "negativePrompt": "Negative Prompt", "discordLabel": "Discord", "dontAskMeAgain": "Don't ask me again", + "editor": "Editor", "error": "Error", "file": "File", "folder": "Folder", @@ -364,7 +365,7 @@ "bulkDownloadFailed": "Download Failed", "problemDeletingImages": "Problem Deleting Images", "problemDeletingImagesDesc": "One or more images could not be deleted", - "backToEditor": "Back to {{tab}} (Esc)" + "switchTo": "Switch to {{ tab }} (Z)" }, "hotkeys": { "searchHotkeys": "Search Hotkeys", diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx index 686577b4a7..15d38b9f76 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx @@ -22,6 +22,7 @@ import { } from 'features/canvas/store/canvasSlice'; import type { CanvasLayer } from 'features/canvas/store/canvasTypes'; import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes'; +import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton'; import { memo, useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -219,97 +220,107 @@ const IAICanvasToolbar = () => { const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]); return ( - - - - - - + + + + + + + + + + - - + + - - } - isChecked={tool === 'move' || isStaging} - onClick={handleSelectMoveTool} - /> - : } - onClick={handleSetShouldShowBoundingBox} - isDisabled={isStaging} - /> - } - onClick={handleClickResetCanvasView} - /> - - - - } - onClick={handleMergeVisible} - isDisabled={isStaging} - /> - } - onClick={handleSaveToGallery} - isDisabled={isStaging} - /> - {isClipboardAPIAvailable && ( + } - onClick={handleCopyImageToClipboard} + aria-label={`${t('unifiedCanvas.move')} (V)`} + tooltip={`${t('unifiedCanvas.move')} (V)`} + icon={} + isChecked={tool === 'move' || isStaging} + onClick={handleSelectMoveTool} + /> + : } + onClick={handleSetShouldShowBoundingBox} isDisabled={isStaging} /> - )} - } - onClick={handleDownloadAsImage} - isDisabled={isStaging} - /> - - - - - + } + onClick={handleClickResetCanvasView} + /> + - - } - isDisabled={isStaging} - {...getUploadButtonProps()} - /> - - } - onClick={handleResetCanvas} - colorScheme="error" - isDisabled={isStaging} - /> - - - - + + } + onClick={handleMergeVisible} + isDisabled={isStaging} + /> + } + onClick={handleSaveToGallery} + isDisabled={isStaging} + /> + {isClipboardAPIAvailable && ( + } + onClick={handleCopyImageToClipboard} + isDisabled={isStaging} + /> + )} + } + onClick={handleDownloadAsImage} + isDisabled={isStaging} + /> + + + + + + + + } + isDisabled={isStaging} + {...getUploadButtonProps()} + /> + + } + onClick={handleResetCanvas} + colorScheme="error" + isDisabled={isStaging} + /> + + + + + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 15a74a332a..b78910700d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -4,15 +4,26 @@ import { BrushSize } 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'; +import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton'; import { memo } from 'react'; export const ControlLayersToolbar = memo(() => { return ( - - - - - + + + + + + + + + + + + + + + ); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/BackToEditorButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/BackToEditorButton.tsx deleted file mode 100644 index 660840b568..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/BackToEditorButton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Button } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowLeftBold } from 'react-icons/pi'; - -import { TAB_NAME_TO_TKEY, useImageViewer } from './useImageViewer'; - -export const BackToEditorButton = () => { - const { t } = useTranslation(); - const { onClose } = useImageViewer(); - const activeTabName = useAppSelector(activeTabNameSelector); - const tooltip = useMemo( - () => t('gallery.backToEditor', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }), - [t, activeTabName] - ); - - return ( - - ); -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/EditorButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/EditorButton.tsx new file mode 100644 index 0000000000..d6cbfbb124 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/EditorButton.tsx @@ -0,0 +1,37 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import type { InvokeTabName } from 'features/ui/store/tabMap'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useImageViewer } from './useImageViewer'; + +export const TAB_NAME_TO_TKEY: Record = { + generation: 'ui.tabs.generationTab', + canvas: 'ui.tabs.canvasTab', + workflows: 'ui.tabs.workflowsTab', + models: 'ui.tabs.modelsTab', + queue: 'ui.tabs.queueTab', +}; + +export const TAB_NAME_TO_TKEY_SHORT: Record = { + generation: 'ui.tabs.generation', + canvas: 'ui.tabs.canvas', + workflows: 'ui.tabs.workflows', + models: 'ui.tabs.models', + queue: 'ui.tabs.queue', +}; + +export const EditorButton = () => { + const { t } = useTranslation(); + const { onClose } = useImageViewer(); + const activeTabName = useAppSelector(activeTabNameSelector); + const tooltip = useMemo(() => t('gallery.switchTo', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }), [t, activeTabName]); + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx index 9f3e7c5902..874464f938 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -10,9 +10,9 @@ import { AnimatePresence, motion } from 'framer-motion'; import { memo, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; -import { BackToEditorButton } from './BackToEditorButton'; import CurrentImageButtons from './CurrentImageButtons'; import CurrentImagePreview from './CurrentImagePreview'; +import { EditorButton } from './EditorButton'; const initial: AnimationProps['initial'] = { opacity: 0, @@ -39,13 +39,14 @@ export const ImageViewer = memo(() => { return isOpen; }, [isOpen, isViewerEnabled]); - useHotkeys('shift+s', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]); + useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]); useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]); return ( {shouldShowViewer && ( { - + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx new file mode 100644 index 0000000000..a57ae9d1ee --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx @@ -0,0 +1,17 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useImageViewer } from './useImageViewer'; + +export const ViewerButton = () => { + const { t } = useTranslation(); + const { onOpen } = useImageViewer(); + const tooltip = useMemo(() => t('gallery.switchTo', { tab: t('common.viewer') }), [t]); + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx index 17a9dc9922..57b3697b7e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx @@ -1,16 +1,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import type { InvokeTabName } from 'features/ui/store/tabMap'; import { useCallback } from 'react'; -export const TAB_NAME_TO_TKEY: Record = { - generation: 'ui.tabs.generation', - canvas: 'ui.tabs.canvas', - workflows: 'ui.tabs.workflows', - models: 'ui.tabs.models', - queue: 'ui.tabs.queue', -}; - export const useImageViewer = () => { const dispatch = useAppDispatch(); const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx index 93856a21c4..2a08fb840e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx @@ -1,5 +1,6 @@ import { Flex, Spacer } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton'; import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton'; import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton'; import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton'; @@ -22,6 +23,7 @@ const TopCenterPanel = () => { + ); }; From 36f01988e8f3a7770c1b7932464a20f3228a66ba Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 12:30:59 +1000 Subject: [PATCH 72/74] chore(ui): lint --- .../features/gallery/components/ImageViewer/EditorButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/EditorButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/EditorButton.tsx index d6cbfbb124..2e10d057f8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/EditorButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/EditorButton.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { useImageViewer } from './useImageViewer'; -export const TAB_NAME_TO_TKEY: Record = { +const TAB_NAME_TO_TKEY: Record = { generation: 'ui.tabs.generationTab', canvas: 'ui.tabs.canvasTab', workflows: 'ui.tabs.workflowsTab', @@ -15,7 +15,7 @@ export const TAB_NAME_TO_TKEY: Record = { queue: 'ui.tabs.queueTab', }; -export const TAB_NAME_TO_TKEY_SHORT: Record = { +const TAB_NAME_TO_TKEY_SHORT: Record = { generation: 'ui.tabs.generation', canvas: 'ui.tabs.canvas', workflows: 'ui.tabs.workflows', From 579d436934f0dea4e6e7cb2556f6bb7199575cad Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 3 May 2024 13:06:04 +1000 Subject: [PATCH 73/74] fix(ui): floating param/gallery buttons --- .../web/src/features/ui/components/FloatingGalleryButton.tsx | 2 +- .../features/ui/components/FloatingParametersPanelButtons.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx index c2d8d9addb..6ea62981a5 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx @@ -17,7 +17,7 @@ const FloatingGalleryButton = (props: Props) => { return ( - + { direction="column" gap={2} h={48} + zIndex={11} > Date: Fri, 3 May 2024 13:12:11 +1000 Subject: [PATCH 74/74] Update invokeai_version.py --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index 7c223b74a7..afcedcd6bb 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.2.0a4" +__version__ = "4.2.0b1"