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") diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 4ad63f4f89..3d1439f7db 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 @@ -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 @@ -521,9 +523,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 +828,7 @@ class DenoiseLatentsInvocation(BaseInvocation): denoising_start: float, denoising_end: float, seed: int, - ) -> Tuple[int, List[int], int]: + ) -> 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") @@ -853,13 +856,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 = {} + scheduler_step_kwargs: Dict[str, Any] = {} scheduler_step_signature = inspect.signature(scheduler.step) 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 isinstance(scheduler, TCDScheduler): + scheduler_step_kwargs.update({"eta": 1.0}) return num_inference_steps, timesteps, init_timestep, scheduler_step_kwargs diff --git a/invokeai/backend/stable_diffusion/schedulers/schedulers.py b/invokeai/backend/stable_diffusion/schedulers/schedulers.py index c824d94dca..3a55d52d4a 100644 --- a/invokeai/backend/stable_diffusion/schedulers/schedulers.py +++ b/invokeai/backend/stable_diffusion/schedulers/schedulers.py @@ -13,6 +13,7 @@ from diffusers import ( LCMScheduler, LMSDiscreteScheduler, PNDMScheduler, + TCDScheduler, UniPCMultistepScheduler, ) @@ -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, {}), } diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 9e661e0737..96db090386 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", @@ -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'} diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 885a937de3..a6d60fa281 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -88,11 +88,13 @@ "negativePrompt": "Negative Prompt", "discordLabel": "Discord", "dontAskMeAgain": "Don't ask me again", + "editor": "Editor", "error": "Error", "file": "File", "folder": "Folder", "format": "format", "githubLabel": "Github", + "goTo": "Go to", "hotkeysLabel": "Hotkeys", "imageFailedToLoad": "Unable to Load Image", "img2img": "Image To Image", @@ -140,7 +142,8 @@ "blue": "Blue", "alpha": "Alpha", "selected": "Selected", - "viewer": "Viewer" + "viewer": "Viewer", + "tab": "Tab" }, "controlnet": { "controlAdapter_one": "Control Adapter", @@ -225,7 +228,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)", @@ -361,7 +364,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", + "switchTo": "Switch to {{ tab }} (Z)" }, "hotkeys": { "searchHotkeys": "Search Hotkeys", @@ -584,6 +588,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": { @@ -917,6 +929,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.", @@ -1542,6 +1555,25 @@ "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" + "globalInitialImage": "Global Initial Image", + "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", + "opacityFilter": "Opacity Filter", + "clearProcessor": "Clear Processor", + "resetProcessor": "Reset Processor to Defaults", + "noLayersAdded": "No Layers Added" + }, + "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", + "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/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/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index ac039c2df6..0c0c8ed2bc 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,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 { addControlLayersToControlAdapterBridge } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; +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'; @@ -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); @@ -158,5 +154,4 @@ addUpscaleRequestedListener(startAppListening); addDynamicPromptsListener(startAppListening); addSetDefaultSettingsListener(startAppListening); - -addControlLayersToControlAdapterBridge(startAppListening); +addControlAdapterPreprocessor(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..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,9 +1,9 @@ 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 { clearInitialImage } from 'features/parameters/store/generationSlice'; import { imagesApi } from 'services/api/endpoints/images'; export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => { @@ -14,19 +14,14 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS // Remove all deleted images from the UI - let wasInitialImageReset = false; 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); - - if (imageUsage.isInitialImage && !wasInitialImageReset) { - dispatch(clearInitialImage()); - wasInitialImageReset = true; - } + const imageUsage = getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name); if (imageUsage.isCanvasImage && !wasCanvasReset) { dispatch(resetCanvas()); @@ -42,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/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/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts new file mode 100644 index 0000000000..7d5aa27f20 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -0,0 +1,156 @@ +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 { 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'; +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'; +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, 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); + + // 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 ( + layerDoesNotExist || + layerHasNoImage || + layerHasNoProcessorConfig || + areImageAndProcessorUnchanged || + layerIsAlreadyProcessingImage + ) { + 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 = CA_PROCESSOR_DATA[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/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge.ts deleted file mode 100644 index bc14277f88..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 { - controlAdapterLayerAdded, - ipAdapterLayerAdded, - layerDeleted, - maskLayerIPAdapterAdded, - maskLayerIPAdapterDeleted, - regionalGuidanceLayerAdded, -} 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(regionalGuidanceLayerAdded({ 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(ipAdapterLayerAdded({ 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(controlAdapterLayerAdded({ 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(maskLayerIPAdapterAdded({ layerId, ipAdapterId })); - }, - }); - - startAppListening({ - actionCreator: guidanceLayerIPAdapterDeleted, - effect: (action, { dispatch }) => { - const { layerId, ipAdapterId } = action.payload; - dispatch(controlAdapterRemoved({ id: ipAdapterId })); - dispatch(maskLayerIPAdapterDeleted({ layerId, ipAdapterId })); - }, - }); -}; 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/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 f923edb99a..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,16 +1,14 @@ 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 { 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'; 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 === 'generation', 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 buildGenerationTabSDXLGraph(state); } else { - if (action.payload.tabName === 'txt2img') { - graph = await buildLinearTextToImageGraph(state); - } else { - graph = await buildLinearImageToImageGraph(state); - } + graph = await buildGenerationTabGraph(state); } const batchConfig = prepareLinearUIBatch(state, graph, prepend); 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/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index 9bbbf80263..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'; @@ -14,12 +22,82 @@ 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'; +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, @@ -73,50 +151,9 @@ 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 ( - 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 @@ -168,50 +205,9 @@ 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 ( - 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/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 307e3487dd..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 @@ -7,10 +7,16 @@ import { controlAdapterImageChanged, controlAdapterIsEnabledChanged, } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { + caLayerImageChanged, + iiLayerImageChanged, + 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'; -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<{ @@ -47,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 */ @@ -71,7 +65,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => dispatch( controlAdapterImageChanged({ id, - controlImage: activeData.payload.imageDTO, + controlImage: activeData.payload.imageDTO.image_name, }) ); dispatch( @@ -83,6 +77,79 @@ 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 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/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 a2ca4baeb1..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 @@ -6,8 +6,14 @@ import { controlAdapterImageChanged, controlAdapterIsEnabledChanged, } from 'features/controlAdapters/store/controlAdaptersSlice'; +import { + caLayerImageChanged, + iiLayerImageChanged, + ipaLayerImageChanged, + 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'; @@ -96,7 +102,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis dispatch( controlAdapterImageChanged({ id, - controlImage: imageDTO, + controlImage: imageDTO.image_name, }) ); dispatch( @@ -108,15 +114,48 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis return; } - if (postUploadAction?.type === 'SET_INITIAL_IMAGE') { - dispatch(initialImageChanged(imageDTO)); + if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') { + const { layerId } = postUploadAction; + dispatch(caLayerImageChanged({ layerId, imageDTO })); dispatch( addToast({ ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setInitialImage'), + 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_II_LAYER_IMAGE') { + const { layerId } = postUploadAction; + dispatch(iiLayerImageChanged({ layerId, imageDTO })); + dispatch( + addToast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), }) ); - return; } if (postUploadAction?.type === 'SET_NODES_IMAGE') { 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/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/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/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index 350e09b6e5..0334294e98 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -17,14 +17,10 @@ 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' }; } - 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..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('img2img')); + dispatch(setActiveTab('canvas')); }, [dispatch] ); @@ -83,31 +83,23 @@ export const useGlobalHotkeys = () => { useHotkeys( '3', () => { - dispatch(setActiveTab('unifiedCanvas')); + dispatch(setActiveTab('workflows')); }, [dispatch] ); useHotkeys( '4', - () => { - dispatch(setActiveTab('nodes')); - }, - [dispatch] - ); - - useHotkeys( - '5', () => { if (isModelManagerEnabled) { - dispatch(setActiveTab('modelManager')); + dispatch(setActiveTab('models')); } }, [dispatch, isModelManagerEnabled] ); 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 d765e987eb..2aac5b8e72 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,11 +40,7 @@ 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 (activeTabName === 'workflows') { if (nodes.shouldValidateGraph) { if (!nodes.nodes.length) { reasons.push(i18n.t('parameters.invoke.noNodesInGraph')); @@ -97,71 +93,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 = controlLayers.present.layers + if (activeTabName === 'generation') { + // Handling for generation tab + controlLayers.present.layers .filter((l) => l.isEnabled) - .flatMap((layer) => { - if (layer.type === 'regional_guidance_layer') { - return layer.ipAdapterIds; + .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; } - if (layer.type === 'control_adapter_layer') { - return [layer.controlNetId]; + return []; + }) + .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; + + if (hasNoModel) { + reasons.push( + i18n.t('parameters.invoke.noModelForControlAdapter', { + number: i + 1, + }) + ); } - if (layer.type === 'ip_adapter_layer') { - return [layer.ipAdapterId]; + 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, + }) + ); } }); - - 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]; - } - }); - 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/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/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/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) => { diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx index 032e46f477..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' && } { - {controlAdapterType === 'ip_adapter' && } + diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx index 56589fe613..bf1c7dce9f 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx @@ -93,15 +93,16 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { return; } - if (activeTabName === 'unifiedCanvas') { + if (activeTabName === 'canvas') { 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/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 0c1ac20200..8ec397f99c 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'; @@ -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); @@ -481,8 +412,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/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/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index b521153239..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,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, useAddIILayer, 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,11 @@ 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 [addIILayer, isAddIILayerDisabled] = useAddIILayer(); + const addRGLayer = useCallback(() => { + dispatch(rgLayerAdded()); }, [dispatch]); return ( @@ -24,15 +22,18 @@ 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')} + } onClick={addIILayer} isDisabled={isAddIILayerDisabled}> + {t('controlLayers.globalInitialImageLayer')} + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index 88eac207b2..26d9c8ce69 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -1,11 +1,11 @@ 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, - maskLayerNegativePromptChanged, - maskLayerPositivePromptChanged, + rgLayerNegativePromptChanged, + rgLayerPositivePromptChanged, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; import { useCallback, useMemo } from 'react'; @@ -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) => { @@ -33,13 +34,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, layerId]); - const addIPAdapter = useCallback(() => { - dispatch(guidanceLayerIPAdapterAdded(layerId)); + dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); }, [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 new file mode 100644 index 0000000000..984331a050 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -0,0 +1,46 @@ +import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CALayerControlAdapterWrapper } from 'features/controlLayers/components/CALayer/CALayerControlAdapterWrapper'; +import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; +import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; +import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; +import { 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'; + +import CALayerOpacity from './CALayerOpacity'; + +type Props = { + layerId: string; +}; + +export const CALayer = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected); + 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 && ( + + + + )} + + ); +}); + +CALayer.displayName = 'CALayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx new file mode 100644 index 0000000000..8ff1f9711f --- /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, + selectCALayerOrThrow, +} from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { CALayerImageDropData } from 'features/dnd/types'; +import { memo, useCallback, useMemo } from 'react'; +import type { + CALayerImagePostUploadAction, + ControlNetModelConfig, + ImageDTO, + T2IAdapterModelConfig, +} from 'services/api/types'; + +type Props = { + layerId: string; +}; + +export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const controlAdapter = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).controlAdapter); + + const onChangeBeginEndStepPct = useCallback( + (beginEndStepPct: [number, number]) => { + dispatch( + caOrIPALayerBeginEndStepPctChanged({ + layerId, + beginEndStepPct, + }) + ); + }, + [dispatch, layerId] + ); + + const onChangeControlMode = useCallback( + (controlMode: ControlModeV2) => { + 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/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx similarity index 88% rename from invokeai/frontend/web/src/features/controlLayers/components/CALayerOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx index a6107da1ec..353f8e0307 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/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, caLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -34,13 +34,13 @@ 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] ); const onChangeFilter = useCallback( (e: ChangeEvent) => { - dispatch(isFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked })); + dispatch(caLayerIsFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked })); }, [dispatch, layerId] ); @@ -55,7 +55,7 @@ const CALayerOpacity = ({ layerId }: Props) => { onDoubleClick={stopPropagation} /> - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayerListItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayerListItem.tsx deleted file mode 100644 index f97546c4fe..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayerListItem.tsx +++ /dev/null @@ -1,71 +0,0 @@ -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 { - isControlAdapterLayer, - layerSelected, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { assert } from 'tsafe'; - -type Props = { - layerId: string; -}; - -export const CALayerListItem = 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 onClickCapture = 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 && ( - - - - )} - - - ); -}); - -CALayerListItem.displayName = 'CALayerListItem'; 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..c28c40ecc1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx @@ -0,0 +1,117 @@ +import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; +import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox'; +import type { + ControlModeV2, + ControlNetConfigV2, + ProcessorConfig, + T2IAdapterConfigV2, +} from 'features/controlLayers/util/controlAdapters'; +import type { TypesafeDroppableData } from 'features/dnd/types'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretUpBold } from 'react-icons/pi'; +import { useToggle } from 'react-use'; +import type { ControlNetModelConfig, ImageDTO, PostUploadAction, T2IAdapterModelConfig } from 'services/api/types'; + +import { ControlAdapterBeginEndStepPct } from './ControlAdapterBeginEndStepPct'; +import { ControlAdapterControlModeSelect } from './ControlAdapterControlModeSelect'; +import { ControlAdapterImagePreview } from './ControlAdapterImagePreview'; +import { ControlAdapterProcessorConfig } from './ControlAdapterProcessorConfig'; +import { ControlAdapterProcessorTypeSelect } from './ControlAdapterProcessorTypeSelect'; +import { ControlAdapterWeight } from './ControlAdapterWeight'; + +type Props = { + controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; + onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; + onChangeControlMode: (controlMode: ControlModeV2) => void; + onChangeWeight: (weight: number) => void; + onChangeProcessorConfig: (processorConfig: ProcessorConfig | null) => void; + onChangeModel: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; + onChangeImage: (imageDTO: ImageDTO | null) => void; + 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/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx new file mode 100644 index 0000000000..9da9ce50a0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterBeginEndStepPct.tsx @@ -0,0 +1,43 @@ +import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + beginEndStepPct: [number, number]; + onChange: (beginEndStepPct: [number, number]) => void; +}; + +const formatPct = (v: number) => `${Math.round(v * 100)}%`; +const ariaLabel = ['Begin Step %', 'End Step %']; + +export const ControlAdapterBeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => { + const { t } = useTranslation(); + const onReset = useCallback(() => { + onChange([0, 1]); + }, [onChange]); + + return ( + + + {t('controlnet.beginEndStepPercentShort')} + + + + ); +}); + +ControlAdapterBeginEndStepPct.displayName = 'ControlAdapterBeginEndStepPct'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterControlMode.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx similarity index 52% rename from invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterControlMode.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx index 6b5d34c106..2c35ce51b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterControlMode.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx @@ -1,24 +1,19 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } 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 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 = { - id: string; + controlMode: ControlModeV2; + onChange: (controlMode: ControlModeV2) => void; }; -const ParamControlAdapterControlMode = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const controlMode = useControlAdapterControlMode(id); - const dispatch = useAppDispatch(); +export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => { const { t } = useTranslation(); - const CONTROL_MODE_DATA = useMemo( () => [ { label: t('controlnet.balanced'), value: 'balanced' }, @@ -31,17 +26,10 @@ const ParamControlAdapterControlMode = ({ id }: Props) => { const handleControlModeChange = useCallback( (v) => { - if (!v) { - return; - } - dispatch( - controlAdapterControlModeChanged({ - id, - controlMode: v.value as ControlMode, - }) - ); + assert(isControlModeV2(v?.value)); + onChange(v.value); }, - [id, dispatch] + [onChange] ); const value = useMemo( @@ -54,13 +42,19 @@ const ParamControlAdapterControlMode = ({ id }: Props) => { } return ( - + {t('controlnet.control')} - + ); -}; +}); -export default memo(ParamControlAdapterControlMode); +ControlAdapterControlModeSelect.displayName = 'ControlAdapterControlModeSelect'; 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..e6c6aae286 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx @@ -0,0 +1,217 @@ +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 { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIDndImage from 'common/components/IAIDndImage'; +import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlNetConfigV2, T2IAdapterConfigV2 } from 'features/controlLayers/util/controlAdapters'; +import type { 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 = { + controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; + onChangeImage: (imageDTO: ImageDTO | null) => void; + droppableData: TypesafeDroppableData; + postUploadAction: PostUploadAction; +}; + +export const ControlAdapterImagePreview = memo( + ({ 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 shift = useShiftModifier(); + + const [isMouseOverImage, setIsMouseOverImage] = useState(false); + + const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( + controlAdapter.image?.imageName ?? skipToken + ); + const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( + controlAdapter.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 === 'canvas') { + dispatch( + setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) + ); + } else { + const options = { updateAspectRatio: true, clamp: true }; + + if (shift) { + const { width, height } = controlImage; + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } else { + const { width, height } = calculateNewSize( + controlImage.width / controlImage.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } + } + }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); + + const handleMouseEnter = useCallback(() => { + setIsMouseOverImage(true); + }, []); + + const handleMouseLeave = useCallback(() => { + setIsMouseOverImage(false); + }, []); + + const draggableData = useMemo(() => { + if (controlImage) { + return { + id: controlAdapter.id, + payloadType: 'IMAGE_DTO', + payload: { imageDTO: controlImage }, + }; + } + }, [controlImage, controlAdapter.id]); + + const shouldShowProcessedImage = + controlImage && + processedControlImage && + !isMouseOverImage && + !controlAdapter.isProcessingImage && + controlAdapter.processorConfig !== null; + + 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} + /> + + + {controlAdapter.isProcessingImage && ( + + + + )} + + ); + } +); + +ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview'; + +const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 }; +const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx new file mode 100644 index 0000000000..a4b1d6b744 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx @@ -0,0 +1,62 @@ +import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +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 = { + modelKey: string | null; + onChange: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; +}; + +export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeModel }: Props) => { + const { t } = useTranslation(); + const currentBaseModel = useAppSelector((s) => s.generation.model?.base); + const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels(); + const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]); + + const _onChange = useCallback( + (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null) => { + if (!modelConfig) { + return; + } + onChangeModel(modelConfig); + }, + [onChangeModel] + ); + + 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 ( + + + + + + ); +}); + +ControlAdapterModelCombobox.displayName = 'ControlAdapterModelCombobox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx new file mode 100644 index 0000000000..034dc5454e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorConfig.tsx @@ -0,0 +1,85 @@ +import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { memo } from 'react'; + +import { CannyProcessor } from './processors/CannyProcessor'; +import { ColorMapProcessor } from './processors/ColorMapProcessor'; +import { ContentShuffleProcessor } from './processors/ContentShuffleProcessor'; +import { DepthAnythingProcessor } from './processors/DepthAnythingProcessor'; +import { DWOpenposeProcessor } from './processors/DWOpenposeProcessor'; +import { HedProcessor } from './processors/HedProcessor'; +import { LineartProcessor } from './processors/LineartProcessor'; +import { MediapipeFaceProcessor } from './processors/MediapipeFaceProcessor'; +import { MidasDepthProcessor } from './processors/MidasDepthProcessor'; +import { MlsdImageProcessor } from './processors/MlsdImageProcessor'; +import { PidiProcessor } from './processors/PidiProcessor'; + +type Props = { + config: ProcessorConfig | null; + onChange: (config: ProcessorConfig | null) => void; +}; + +export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) => { + 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; + } +}); + +ControlAdapterProcessorConfig.displayName = 'ControlAdapterProcessorConfig'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx new file mode 100644 index 0000000000..5598b81787 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx @@ -0,0 +1,70 @@ +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 { useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/util/controlAdapters'; +import { 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 = { + config: ProcessorConfig | null; + onChange: (config: ProcessorConfig | null) => void; +}; + +const selectDisabledProcessors = createMemoizedSelector( + configSelector, + (config) => config.sd.disabledControlNetProcessors +); + +export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Props) => { + const { t } = useTranslation(); + const disabledProcessors = useAppSelector(selectDisabledProcessors); + const options = useMemo(() => { + return map(CA_PROCESSOR_DATA, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( + (o) => !includes(disabledProcessors, o.value) + ); + }, [disabledProcessors, t]); + + const _onChange = useCallback( + (v) => { + if (!v) { + onChange(null); + } else { + assert(isProcessorTypeV2(v.value)); + onChange(CA_PROCESSOR_DATA[v.value].buildDefaults()); + } + }, + [onChange] + ); + const clearProcessor = useCallback(() => { + onChange(null); + }, [onChange]); + const value = useMemo(() => options.find((o) => o.value === config?.type) ?? null, [options, config?.type]); + + return ( + + + + {t('controlnet.processor')} + + + + } + variant="ghost" + size="sm" + /> + + ); +}); + +ControlAdapterProcessorTypeSelect.displayName = 'ControlAdapterProcessorTypeSelect'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterWeight.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx similarity index 60% rename from invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterWeight.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx index 5e456fc792..4bb7bb3911 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterWeight.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterWeight.tsx @@ -1,24 +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 { 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 { memo, useCallback } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -type ParamControlAdapterWeightProps = { - id: string; +type Props = { + weight: number; + onChange: (weight: number) => void; }; const formatValue = (v: number) => v.toFixed(2); +const marks = [0, 1, 2]; -const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => { +export const ControlAdapterWeight = memo(({ weight, onChange }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useControlAdapterIsEnabled(id); - const weight = useControlAdapterWeight(id); 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); @@ -27,20 +22,8 @@ const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => { 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(controlAdapterWeightChanged({ id, weight })); - }, - [dispatch, id] - ); - - if (isNil(weight)) { - // should never happen - return null; - } - return ( - + {t('controlnet.weight')} @@ -67,8 +50,6 @@ const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => { /> ); -}; +}); -export default memo(ParamControlAdapterWeight); - -const marks = [0, 1, 2]; +ControlAdapterWeight.displayName = 'ControlAdapterWeight'; 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..86ed77ce36 --- /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 { 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: IPAdapterConfigV2; + onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; + onChangeWeight: (weight: number) => void; + onChangeIPMethod: (method: IPMethodV2) => void; + onChangeModel: (modelConfig: IPAdapterModelConfig) => void; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => 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..83dd250cd0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx @@ -0,0 +1,115 @@ +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 === 'canvas') { + dispatch( + setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) + ); + } else { + const options = { updateAspectRatio: true, clamp: true }; + if (shift) { + const { width, height } = controlImage; + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } else { + const { width, height } = calculateNewSize( + controlImage.width / controlImage.height, + optimalDimension * optimalDimension + ); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); + } + } + }, [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/ControlAndIPAdapter/IPAdapterMethod.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx new file mode 100644 index 0000000000..4f6a468fc3 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/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 { 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: IPMethodV2; + onChange: (method: IPMethodV2) => void; +}; + +export const IPAdapterMethod = memo(({ method, onChange }: Props) => { + const { t } = useTranslation(); + const options: { label: string; value: IPMethodV2 }[] = 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(isIPMethodV2(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/ControlAndIPAdapter/IPAdapterModelSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx new file mode 100644 index 0000000000..b0541dca2c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.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 { 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'; +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: CLIPVisionModelV2; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; +}; + +export const IPAdapterModelSelect = 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(isCLIPVisionModelV2(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' && ( + + + + )} + + ); + } +); + +IPAdapterModelSelect.displayName = 'IPAdapterModelSelect'; 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 new file mode 100644 index 0000000000..ef6e4160d6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx @@ -0,0 +1,67 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; +import { CA_PROCESSOR_DATA, type CannyProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; +const DEFAULTS = CA_PROCESSOR_DATA['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/ControlAndIPAdapter/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx new file mode 100644 index 0000000000..6faa00dd14 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx @@ -0,0 +1,47 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; +import { CA_PROCESSOR_DATA, type ColorMapProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ProcessorWrapper from './ProcessorWrapper'; + +type Props = ProcessorComponentProps; +const DEFAULTS = CA_PROCESSOR_DATA['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/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx new file mode 100644 index 0000000000..c03efd27c6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx @@ -0,0 +1,79 @@ +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 { 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 = CA_PROCESSOR_DATA['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/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx new file mode 100644 index 0000000000..3bbe813dcc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx @@ -0,0 +1,62 @@ +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 { CA_PROCESSOR_DATA } 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 = CA_PROCESSOR_DATA['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/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx new file mode 100644 index 0000000000..00993789b1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/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/ControlAndIPAdapter/processors/types'; +import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } 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 = CA_PROCESSOR_DATA['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/ControlAndIPAdapter/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx new file mode 100644 index 0000000000..1ca75eae2f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/HedProcessor.tsx @@ -0,0 +1,32 @@ +import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +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'; +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/ControlAndIPAdapter/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx new file mode 100644 index 0000000000..aeb4121a36 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/LineartProcessor.tsx @@ -0,0 +1,32 @@ +import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +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'; +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/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx new file mode 100644 index 0000000000..0f45d83ef0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx @@ -0,0 +1,73 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; +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 = CA_PROCESSOR_DATA['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/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx new file mode 100644 index 0000000000..1ce728984c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx @@ -0,0 +1,76 @@ +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 { 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 = CA_PROCESSOR_DATA['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/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx new file mode 100644 index 0000000000..b6eef311ef --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx @@ -0,0 +1,76 @@ +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 { 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 = CA_PROCESSOR_DATA['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/ControlAndIPAdapter/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx new file mode 100644 index 0000000000..e7d559a1b4 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/PidiProcessor.tsx @@ -0,0 +1,43 @@ +import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +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'; +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/ControlAndIPAdapter/processors/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ProcessorWrapper.tsx new file mode 100644 index 0000000000..2b2468703b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/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/ControlAndIPAdapter/processors/types.ts b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/types.ts new file mode 100644 index 0000000000..48a0942678 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/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/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index e2865be356..1dd79d0220 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -2,16 +2,19 @@ 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 { 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 { 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'; 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); @@ -19,20 +22,24 @@ const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, }); export const ControlLayersPanelContent = memo(() => { + const { t } = useTranslation(); const layerIdTypePairs = useAppSelector(selectLayerIdTypePairs); return ( - + - - - {layerIdTypePairs.map(({ id, type }) => ( - - ))} - - + {layerIdTypePairs.length > 0 && ( + + + {layerIdTypePairs.map(({ id, type }) => ( + + ))} + + + )} + {layerIdTypePairs.length === 0 && } ); }); @@ -46,13 +53,16 @@ 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 ; + } + if (type === 'initial_image_layer') { + return ; } }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 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/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/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx new file mode 100644 index 0000000000..772dbd7332 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -0,0 +1,83 @@ +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'; +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/IILayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayerOpacity.tsx new file mode 100644 index 0000000000..9918dda5b8 --- /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/components/IILayer/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx new file mode 100644 index 0000000000..e355d5db86 --- /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 === 'canvas') { + 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/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx new file mode 100644 index 0000000000..02a161608d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -0,0 +1,32 @@ +import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; +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'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; +import { memo } from 'react'; + +type Props = { + layerId: string; +}; + +export const IPALayer = memo(({ layerId }: Props) => { + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + return ( + + + + + + + + {isOpen && ( + + + + )} + + ); +}); + +IPALayer.displayName = 'IPALayer'; 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..9f99710dac --- /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, + selectIPALayerOrThrow, +} from 'features/controlLayers/store/controlLayersSlice'; +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'; + +type Props = { + layerId: string; +}; + +export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const ipAdapter = useAppSelector((s) => selectIPALayerOrThrow(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: IPMethodV2) => { + dispatch(ipaLayerMethodChanged({ layerId, method })); + }, + [dispatch, layerId] + ); + + const onChangeModel = useCallback( + (modelConfig: IPAdapterModelConfig) => { + dispatch(ipaLayerModelChanged({ layerId, modelConfig })); + }, + [dispatch, layerId] + ); + + const onChangeCLIPVisionModel = useCallback( + (clipVisionModel: CLIPVisionModelV2) => { + 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/IPLayerListItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPLayerListItem.tsx deleted file mode 100644 index bdc54373a0..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPLayerListItem.tsx +++ /dev/null @@ -1,47 +0,0 @@ -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 { isIPAdapterLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useMemo } from 'react'; -import { assert } from 'tsafe'; - -type Props = { - layerId: string; -}; - -export const IPLayerListItem = 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 ( - - - - - - - - - {isOpen && ( - - - - )} - - - ); -}); - -IPLayerListItem.displayName = 'IPLayerListItem'; 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 84% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerDeleteButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerDeleteButton.tsx index 0c74b2a9ea..0cd7d83dfe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/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 ( { )} - {(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/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 78% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerMenuRGActions.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx index 6c2bb4c26b..172709ec14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerMenuRGActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx @@ -1,11 +1,11 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { isRegionalGuidanceLayer, - maskLayerNegativePromptChanged, - maskLayerPositivePromptChanged, + rgLayerNegativePromptChanged, + rgLayerPositivePromptChanged, selectControlLayersSlice, } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback, useMemo } from 'react'; @@ -18,6 +18,7 @@ type Props = { layerId: string }; export const LayerMenuRGActions = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); + const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const selectValidActions = useMemo( () => createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { @@ -32,13 +33,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, layerId]); - const addIPAdapter = useCallback(() => { - dispatch(guidanceLayerIPAdapterAdded(layerId)); + dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); }, [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/LayerTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx similarity index 89% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerTitle.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx index ec13ff7bcc..b29c3753fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/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/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/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 new file mode 100644 index 0000000000..a6bce5316e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -0,0 +1,83 @@ +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 { 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 { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; +import { + isRegionalGuidanceLayer, + layerSelected, + selectControlLayersSlice, +} from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { assert } from 'tsafe'; + +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 RGLayer = memo(({ layerId }: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selector = useMemo( + () => + createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + const layer = controlLayers.present.layers.find((l) => l.id === layerId); + assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); + return { + color: rgbColorToString(layer.previewColor), + hasPositivePrompt: layer.positivePrompt !== null, + hasNegativePrompt: layer.negativePrompt !== null, + hasIPAdapters: layer.ipAdapters.length > 0, + isSelected: layerId === controlLayers.present.selectedLayerId, + autoNegative: layer.autoNegative, + }; + }), + [layerId] + ); + const { autoNegative, color, hasPositivePrompt, hasNegativePrompt, hasIPAdapters, isSelected } = + useAppSelector(selector); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const onClick = useCallback(() => { + dispatch(layerSelected(layerId)); + }, [dispatch, layerId]); + return ( + + + + + + {autoNegative === 'invert' && ( + + {t('controlLayers.autoNegative')} + + )} + + + + + + {isOpen && ( + + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } + {hasPositivePrompt && } + {hasNegativePrompt && } + {hasIPAdapters && } + + )} + + ); +}); + +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 91% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerAutoNegativeCheckbox.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx index 6f03d4b28d..89edb58d2f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/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/RGLayerColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx similarity index 95% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerColorPicker.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx index e76ab57a51..624047caf3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/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/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx new file mode 100644 index 0000000000..578d3789bf --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx @@ -0,0 +1,45 @@ +import { Divider, Flex } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +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 = { + layerId: string; +}; + +export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { + const selectIPAdapterIds = useMemo( + () => + createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); + assert(layer, `Layer ${layerId} not found`); + return layer.ipAdapters; + }), + [layerId] + ); + const ipAdapters = useAppSelector(selectIPAdapterIds); + + if (ipAdapters.length === 0) { + return null; + } + + return ( + <> + {ipAdapters.map(({ id }, index) => ( + + {index > 0 && ( + + + + )} + + + ))} + + ); +}); + +RGLayerIPAdapterList.displayName = 'RGLayerIPAdapterList'; 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..f7be62eb0a --- /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, + selectRGLayerIPAdapterOrThrow, +} from 'features/controlLayers/store/controlLayersSlice'; +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'; +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) => selectRGLayerIPAdapterOrThrow(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: IPMethodV2) => { + 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: CLIPVisionModelV2) => { + 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/components/RGLayerNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx similarity index 90% 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..ba02aa9242 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx @@ -1,8 +1,8 @@ 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 { 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/RGLayerPositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx similarity index 90% 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..6f85ea077c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx @@ -1,8 +1,8 @@ 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 { 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/RGLayerPromptDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx similarity index 82% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayerPromptDeleteButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx index 9a32bb68ad..62a4ddfaeb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/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/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'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayerIPAdapterList.tsx deleted file mode 100644 index 464bd41897..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerIPAdapterList.tsx +++ /dev/null @@ -1,80 +0,0 @@ -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 { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { PiTrashSimpleBold } from 'react-icons/pi'; -import { assert } from 'tsafe'; - -type Props = { - layerId: string; -}; - -export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { - const selectIPAdapterIds = useMemo( - () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); - assert(layer, `Layer ${layerId} not found`); - return layer.ipAdapterIds; - }), - [layerId] - ); - const ipAdapterIds = useAppSelector(selectIPAdapterIds); - - if (ipAdapterIds.length === 0) { - return null; - } - - return ( - <> - {ipAdapterIds.map((id, index) => ( - - {index > 0 && ( - - - - )} - - - ))} - - ); -}); - -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(guidanceLayerIPAdapterDeleted({ 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/RGLayerListItem.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayerListItem.tsx deleted file mode 100644 index 3c126cabaa..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayerListItem.tsx +++ /dev/null @@ -1,84 +0,0 @@ -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 { - isRegionalGuidanceLayer, - layerSelected, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { assert } from 'tsafe'; - -import { AddPromptButtons } from './AddPromptButtons'; - -type Props = { - layerId: string; -}; - -export const RGLayerListItem = memo(({ layerId }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selector = useMemo( - () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); - return { - color: rgbColorToString(layer.previewColor), - hasPositivePrompt: layer.positivePrompt !== null, - hasNegativePrompt: layer.negativePrompt !== null, - hasIPAdapters: layer.ipAdapterIds.length > 0, - isSelected: layerId === controlLayers.present.selectedLayerId, - autoNegative: layer.autoNegative, - }; - }), - [layerId] - ); - const { autoNegative, color, hasPositivePrompt, hasNegativePrompt, hasIPAdapters, isSelected } = - useAppSelector(selector); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - const onClick = useCallback(() => { - dispatch(layerSelected(layerId)); - }, [dispatch, layerId]); - return ( - - - - - - - {autoNegative === 'invert' && ( - - {t('controlLayers.autoNegative')} - - )} - - - - - - {isOpen && ( - - {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } - {hasPositivePrompt && } - {hasNegativePrompt && } - {hasIPAdapters && } - - )} - - - ); -}); - -RGLayerListItem.displayName = 'RGLayerListItem'; 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/components/controlAdapterOverrides/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterImagePreview.tsx deleted file mode 100644 index b3094e5599..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterImagePreview.tsx +++ /dev/null @@ -1,237 +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 { useControlAdapterControlImage } from 'features/controlAdapters/hooks/useControlAdapterControlImage'; -import { useControlAdapterProcessedControlImage } from 'features/controlAdapters/hooks/useControlAdapterProcessedControlImage'; -import { useControlAdapterProcessorType } from 'features/controlAdapters/hooks/useControlAdapterProcessorType'; -import { - controlAdapterImageChanged, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { TypesafeDraggableData, 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 { PostUploadAction } from 'services/api/types'; - -type Props = { - id: string; - isSmall?: boolean; -}; - -const selectPendingControlImages = createMemoizedSelector( - selectControlAdaptersSlice, - (controlAdapters) => controlAdapters.pendingControlImages -); - -const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const controlImageName = useControlAdapterControlImage(id); - const processedControlImageName = useControlAdapterProcessedControlImage(id); - const processorType = useControlAdapterProcessorType(id); - 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( - controlImageName ?? skipToken - ); - - const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - processedControlImageName ?? skipToken - ); - - const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); - const [addToBoard] = useAddImageToBoardMutation(); - const [removeFromBoard] = useRemoveImageFromBoardMutation(); - const handleResetControlImage = useCallback(() => { - dispatch(controlAdapterImageChanged({ id, controlImage: null })); - }, [id, dispatch]); - - 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, - payloadType: 'IMAGE_DTO', - payload: { imageDTO: controlImage }, - }; - } - }, [controlImage, id]); - - const droppableData = useMemo( - () => ({ - id, - actionType: 'SET_CONTROL_ADAPTER_IMAGE', - context: { id }, - }), - [id] - ); - - const postUploadAction = useMemo(() => ({ type: 'SET_CONTROL_ADAPTER_IMAGE', id }), [id]); - - const shouldShowProcessedImage = - controlImage && - processedControlImage && - !isMouseOverImage && - !pendingControlImages.includes(id) && - processorType !== 'none'; - - 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(id) && ( - - - - )} - - ); -}; - -export default memo(ControlAdapterImagePreview); - -const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 }; -const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig.tsx b/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig.tsx deleted file mode 100644 index 29a3502d37..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; -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 { 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'; - -const ControlAdapterLayerConfig = (props: { id: string }) => { - const { id } = props; - const controlAdapterType = useControlAdapterType(id); - const { t } = useTranslation(); - const [isExpanded, toggleIsExpanded] = useToggle(false); - - return ( - - - - {' '} - - - {controlAdapterType !== 'ip_adapter' && ( - - } - /> - )} - - - - {controlAdapterType === 'ip_adapter' && } - {controlAdapterType === 'controlnet' && } - - - - - - - - {isExpanded && ( - <> - - - - - )} - - ); -}; - -export default memo(ControlAdapterLayerConfig); 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/components/controlAdapterOverrides/ParamControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/ParamControlAdapterModel.tsx deleted file mode 100644 index 73a7d695b3..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/controlAdapterOverrides/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/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts new file mode 100644 index 0000000000..dcbbeb8db5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -0,0 +1,111 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + caLayerAdded, + iiLayerAdded, + ipaLayerAdded, + isInitialImageLayer, + rgLayerIPAdapterAdded, +} from 'features/controlLayers/store/controlLayersSlice'; +import { + buildControlNet, + buildIPAdapter, + buildT2IAdapter, + CA_PROCESSOR_DATA, + isProcessorTypeV2, +} 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 = isProcessorTypeV2(defaultPreprocessor) + ? CA_PROCESSOR_DATA[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; +}; + +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/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts index bab7ef263f..889d2c0c2e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts @@ -4,14 +4,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { $cursorPosition, - $isMouseDown, - $isMouseOver, + $isDrawing, $lastMouseDownPos, $tool, brushSizeChanged, - maskLayerLineAdded, - maskLayerPointsAdded, - maskLayerRectAdded, + rgLayerLineAdded, + rgLayerPointsAdded, + rgLayerRectAdded, } from 'features/controlLayers/store/controlLayersSlice'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -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(); @@ -49,52 +49,58 @@ 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); const brushSize = useAppSelector((s) => s.controlLayers.present.brushSize); const onMouseDown = useCallback( - (e: KonvaEventObject) => { + (e: KonvaEventObject) => { const stage = e.target.getStage(); if (!stage) { return; } const pos = syncCursorPos(stage); - if (!pos) { + if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { return; } - $isMouseDown.set(true); $lastMouseDownPos.set(pos); - if (!selectedLayerId) { - return; - } if (tool === 'brush' || tool === 'eraser') { dispatch( - maskLayerLineAdded({ + rgLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool, }) ); + $isDrawing.set(true); } }, - [dispatch, selectedLayerId, tool] + [dispatch, selectedLayerId, selectedLayerType, tool] ); const onMouseUp = useCallback( - (e: KonvaEventObject) => { + (e: KonvaEventObject) => { const stage = e.target.getStage(); if (!stage) { return; } - $isMouseDown.set(false); 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( - maskLayerRectAdded({ + rgLayerRectAdded({ layerId: selectedLayerId, rect: { x: Math.min(pos.x, lastPos.x), @@ -105,98 +111,69 @@ export const useMouseEvents = () => { }) ); } + $isDrawing.set(false); $lastMouseDownPos.set(null); }, - [dispatch, selectedLayerId] + [dispatch, selectedLayerId, selectedLayerType] ); const onMouseMove = useCallback( - (e: KonvaEventObject) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } - const pos = syncCursorPos(stage); - 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; - } - } - lastCursorPosRef.current = [pos.x, pos.y]; - dispatch(maskLayerPointsAdded({ layerId: selectedLayerId, point: lastCursorPosRef.current })); - } - }, - [dispatch, selectedLayerId, tool] - ); - - const onMouseLeave = useCallback( - (e: KonvaEventObject) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } - const pos = syncCursorPos(stage); - if ( - pos && - selectedLayerId && - getIsFocused(stage) && - $isMouseOver.get() && - $isMouseDown.get() && - (tool === 'brush' || tool === 'eraser') - ) { - dispatch(maskLayerPointsAdded({ layerId: selectedLayerId, point: [pos.x, pos.y] })); - } - $isMouseOver.set(false); - $isMouseDown.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) { + if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { 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( - maskLayerLineAdded({ - layerId: selectedLayerId, - points: [pos.x, pos.y, pos.x, pos.y], - tool, - }) - ); + 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 })); } + $isDrawing.set(true); } }, - [dispatch, selectedLayerId, tool] + [dispatch, selectedLayerId, selectedLayerType, tool] + ); + + const onMouseLeave = useCallback( + (e: KonvaEventObject) => { + const stage = e.target.getStage(); + if (!stage) { + return; + } + const pos = syncCursorPos(stage); + 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, 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 @@ -210,8 +187,8 @@ export const useMouseEvents = () => { dispatch(brushSizeChanged(calculateNewBrushSize(brushSize, delta))); } }, - [shouldInvertBrushSizeScrollDirection, brushSize, dispatch] + [selectedLayerType, tool, shouldInvertBrushSizeScrollDirection, dispatch, brushSize] ); - return { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel }; + return { onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel }; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts index 93c8bec8a6..bf0fa661a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts @@ -1,23 +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) => { - if (!controlLayers.present.isEnabled) { - return 0; - } - 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.ipAdapterIds.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 = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 6d351d4d0d..a1b5e0ebc8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -3,12 +3,23 @@ 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 { roundDownToMultiple } from 'common/util/roundDownToMultiple'; +import type { + CLIPVisionModelV2, + ControlModeV2, + ControlNetConfigV2, + IPAdapterConfigV2, + IPMethodV2, + ProcessorConfig, + T2IAdapterConfigV2, +} from 'features/controlLayers/util/controlAdapters'; import { - controlAdapterImageChanged, - controlAdapterProcessedImageChanged, - isAnyControlAdapterAdded, -} from 'features/controlAdapters/store/controlAdaptersSlice'; + buildControlAdapterProcessorV2, + 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'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; @@ -20,6 +31,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, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; @@ -27,6 +39,7 @@ import type { ControlAdapterLayer, ControlLayersState, DrawingTool, + InitialImageLayer, IPAdapterLayer, Layer, RegionalGuidanceLayer, @@ -41,13 +54,11 @@ export const initialControlLayersState: ControlLayersState = { brushSize: 100, layers: [], globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity - isEnabled: true, positivePrompt: '', negativePrompt: '', positivePrompt2: '', negativePrompt2: '', shouldConcatPrompts: true, - initialImage: null, size: { width: 512, height: 512, @@ -61,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 = []; @@ -72,14 +88,50 @@ const resetLayer = (layer: Layer) => { layer.bboxNeedsUpdate = false; return; } +}; - if (layer.type === 'control_adapter_layer') { - // TODO - } +export const selectCALayerOrThrow = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isControlAdapterLayer(layer)); + return layer; +}; +export const selectIPALayerOrThrow = (state: ControlLayersState, layerId: string): IPAdapterLayer => { + const layer = state.layers.find((l) => l.id === layerId); + 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 +): ControlAdapterLayer | IPAdapterLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isControlAdapterLayer(layer) || isIPAdapterLayer(layer)); + return layer; +}; +const selectRGLayerOrThrow = (state: ControlLayersState, layerId: string): RegionalGuidanceLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isRegionalGuidanceLayer(layer)); + return layer; +}; +export const selectRGLayerIPAdapterOrThrow = ( + state: ControlLayersState, + layerId: string, + ipAdapterId: string +): IPAdapterConfigV2 => { + 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 vmLayers = state.layers.filter(isRegionalGuidanceLayer); - const lastColor = vmLayers[vmLayers.length - 1]?.previewColor; + const rgLayers = state.layers.filter(isRegionalGuidanceLayer); + const lastColor = rgLayers[rgLayers.length - 1]?.previewColor; return LayerColors.next(lastColor); }; @@ -87,71 +139,7 @@ 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; - }, + //#region Any Layer Type layerSelected: (state, action: PayloadAction) => { for (const layer of state.layers.filter(isRenderableLayer)) { if (layer.id === action.payload) { @@ -235,144 +223,449 @@ 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; - } + allLayersDeleted: (state) => { + state.layers = []; + state.selectedLayerId = null; }, //#endregion //#region CA Layers - isFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => { + caLayerAdded: { + reducer: ( + state, + action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2 }> + ) => { + 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: ControlNetConfigV2 | T2IAdapterConfigV2) => ({ + payload: { layerId: uuidv4(), controlAdapter }, + }), + }, + caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { + const { layerId, imageDTO } = action.payload; + const layer = selectCALayerOrThrow(state, layerId); + layer.bbox = null; + layer.bboxNeedsUpdate = true; + layer.isEnabled = true; + 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; + const layer = selectCALayerOrThrow(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 = selectCALayerOrThrow(state, layerId); + if (!modelConfig) { + layer.controlAdapter.model = null; + 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 = 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. + layer.controlAdapter.processedImage = null; + layer.controlAdapter.processorConfig = candidateProcessorConfig; + } + }, + caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlModeV2 }>) => { + const { layerId, controlMode } = action.payload; + const layer = selectCALayerOrThrow(state, layerId); + assert(layer.controlAdapter.type === 'controlnet'); + layer.controlAdapter.controlMode = controlMode; + }, + caLayerProcessorConfigChanged: ( + state, + action: PayloadAction<{ layerId: string; processorConfig: ProcessorConfig | null }> + ) => { + 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; - const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); - if (layer) { - layer.isFilterEnabled = isFilterEnabled; + const layer = selectCALayerOrThrow(state, layerId); + layer.isFilterEnabled = isFilterEnabled; + }, + caLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { + const { layerId, opacity } = action.payload; + 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; + }, + 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 + ipaLayerAdded: { + reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { + const { layerId, ipAdapter } = action.payload; + const layer: IPAdapterLayer = { + id: getIPALayerId(layerId), + type: 'ip_adapter_layer', + isEnabled: true, + ipAdapter, + }; + state.layers.push(layer); + }, + 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: IPMethodV2 }>) => { + const { layerId, method } = action.payload; + const layer = selectIPALayerOrThrow(state, layerId); + layer.ipAdapter.method = method; + }, + ipaLayerModelChanged: ( + state, + action: PayloadAction<{ + layerId: string; + modelConfig: IPAdapterModelConfig | null; + }> + ) => { + const { layerId, modelConfig } = action.payload; + const layer = selectIPALayerOrThrow(state, layerId); + if (!modelConfig) { + layer.ipAdapter.model = null; + return; + } + layer.ipAdapter.model = zModelIdentifierField.parse(modelConfig); + }, + ipaLayerCLIPVisionModelChanged: ( + state, + action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModelV2 }> + ) => { + const { layerId, clipVisionModel } = action.payload; + 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 + caOrIPALayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { + const { layerId, weight } = action.payload; + const layer = selectCAOrIPALayerOrThrow(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 = selectCAOrIPALayerOrThrow(state, layerId); + if (layer.type === 'control_adapter_layer') { + layer.controlAdapter.beginEndStepPct = beginEndStepPct; + } else { + layer.ipAdapter.beginEndStepPct = beginEndStepPct; } }, //#endregion - //#region Mask Layers - maskLayerPositivePromptChanged: (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; - } - }, - maskLayerNegativePromptChanged: (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; - } - }, - maskLayerIPAdapterAdded: (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); - } - }, - 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 }>) => { - const { layerId, color } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (layer?.type === 'regional_guidance_layer') { - layer.previewColor = color; - } - }, - maskLayerLineAdded: { - reducer: ( - state, - action: PayloadAction< - { layerId: string; points: [number, number, number, number]; tool: DrawingTool }, - string, - { uuid: 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; + //#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 = selectRGLayerOrThrow(state, layerId); + layer.positivePrompt = prompt; + }, + rgLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { + const { layerId, prompt } = action.payload; + const layer = selectRGLayerOrThrow(state, layerId); + layer.negativePrompt = prompt; + }, + rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { + const { layerId, color } = action.payload; + const layer = selectRGLayerOrThrow(state, layerId); + layer.previewColor = color; + }, + rgLayerLineAdded: { + reducer: ( + state, + action: PayloadAction<{ + layerId: string; + points: [number, number, number, number]; + tool: DrawingTool; + lineUuid: string; + }> + ) => { + const { layerId, points, tool, lineUuid } = action.payload; + const layer = selectRGLayerOrThrow(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 = selectRGLayerOrThrow(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 = selectRGLayerOrThrow(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 = selectRGLayerOrThrow(state, layerId); + layer.autoNegative = autoNegative; + }, + rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { + const { layerId, ipAdapter } = action.payload; + const layer = selectRGLayerOrThrow(state, layerId); + layer.ipAdapters.push(ipAdapter); + }, + rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { + const { layerId, ipAdapterId } = action.payload; + const layer = selectRGLayerOrThrow(state, layerId); + layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); + }, + rgLayerIPAdapterImageChanged: ( + state, + action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }> + ) => { + const { layerId, ipAdapterId, imageDTO } = action.payload; + const ipAdapter = selectRGLayerIPAdapterOrThrow(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 = selectRGLayerIPAdapterOrThrow(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 = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); + ipAdapter.beginEndStepPct = beginEndStepPct; + }, + rgLayerIPAdapterMethodChanged: ( + state, + action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethodV2 }> + ) => { + const { layerId, ipAdapterId, method } = action.payload; + const ipAdapter = selectRGLayerIPAdapterOrThrow(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 = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); + if (!modelConfig) { + ipAdapter.model = null; + return; } + ipAdapter.model = zModelIdentifierField.parse(modelConfig); + }, + rgLayerIPAdapterCLIPVisionModelChanged: ( + state, + action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> + ) => { + const { layerId, ipAdapterId, clipVisionModel } = action.payload; + const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); + ipAdapter.clipVisionModel = clipVisionModel; }, //#endregion - //#region Base Layer + //#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! + state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); + const layer: InitialImageLayer = { + id: layerId, + type: 'initial_image_layer', + opacity: 1, + 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); + 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 + + //#region Globals positivePromptChanged: (state, action: PayloadAction) => { state.positivePrompt = action.payload; }, @@ -388,20 +681,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; } @@ -409,18 +702,12 @@ 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); }, 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)) { @@ -451,36 +738,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); + // } + // }); }, }); @@ -516,36 +781,64 @@ 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, - regionalGuidanceLayerAdded, - ipAdapterLayerAdded, - controlAdapterLayerAdded, - layerOpacityChanged, - // CA layer actions - isFilterEnabledChanged, - // Mask layer actions - maskLayerLineAdded, - maskLayerPointsAdded, - maskLayerRectAdded, - maskLayerNegativePromptChanged, - maskLayerPositivePromptChanged, - maskLayerIPAdapterAdded, - maskLayerIPAdapterDeleted, - maskLayerAutoNegativeChanged, - maskLayerPreviewColorChanged, - // Base layer actions + allLayersDeleted, + // CA Layers + caLayerAdded, + caLayerImageChanged, + caLayerProcessedImageChanged, + caLayerModelChanged, + caLayerControlModeChanged, + caLayerProcessorConfigChanged, + caLayerIsFilterEnabledChanged, + caLayerOpacityChanged, + caLayerIsProcessingImageChanged, + caLayerControlNetsDeleted, + caLayerT2IAdaptersDeleted, + // IPA Layers + ipaLayerAdded, + ipaLayerImageChanged, + ipaLayerMethodChanged, + ipaLayerModelChanged, + ipaLayerCLIPVisionModelChanged, + ipaLayersDeleted, + // CA or IPA Layers + caOrIPALayerWeightChanged, + caOrIPALayerBeginEndStepPctChanged, + // RG Layers + rgLayerAdded, + rgLayerPositivePromptChanged, + rgLayerNegativePromptChanged, + rgLayerPreviewColorChanged, + rgLayerLineAdded, + rgLayerPointsAdded, + rgLayerRectAdded, + rgLayerAutoNegativeChanged, + rgLayerIPAdapterAdded, + rgLayerIPAdapterDeleted, + rgLayerIPAdapterImageChanged, + rgLayerIPAdapterWeightChanged, + rgLayerIPAdapterBeginEndStepPctChanged, + rgLayerIPAdapterMethodChanged, + rgLayerIPAdapterModelChanged, + rgLayerIPAdapterCLIPVisionModelChanged, + // II Layer + iiLayerAdded, + iiLayerImageChanged, + iiLayerOpacityChanged, + // Globals positivePromptChanged, negativePromptChanged, positivePrompt2Changed, @@ -554,27 +847,12 @@ export const { widthChanged, heightChanged, aspectRatioChanged, - // General actions brushSizeChanged, globalMaskLayerOpacityChanged, undo, 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 */ @@ -582,8 +860,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); @@ -600,24 +877,26 @@ 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 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 -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}`; +export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; +const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`; export const controlLayersPersistConfig: PersistConfig = { name: controlLayersSlice.name, @@ -631,9 +910,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) @@ -645,13 +928,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; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 58b25f967b..cbb986bde2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,3 +1,9 @@ +import type { + ControlNetConfigV2, + ImageWithDims, + IPAdapterConfigV2, + T2IAdapterConfigV2, +} from 'features/controlLayers/util/controlAdapters'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { ParameterAutoNegative, @@ -47,15 +53,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: ControlNetConfigV2 | T2IAdapterConfigV2; }; export type IPAdapterLayer = LayerBase & { - type: 'ip_adapter_layer'; // technically, also t2i adapter layer - ipAdapterId: string; + type: 'ip_adapter_layer'; + ipAdapter: IPAdapterConfigV2; }; export type RegionalGuidanceLayer = RenderableLayerBase & { @@ -63,13 +68,19 @@ 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: 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 }; -export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer; +export type InitialImageLayer = RenderableLayerBase & { + type: 'initial_image_layer'; + opacity: number; + image: ImageWithDims | null; +}; + +export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer | InitialImageLayer; export type ControlLayersState = { _version: 1; @@ -77,13 +88,11 @@ export type ControlLayersState = { layers: Layer[]; brushSize: number; globalMaskLayerOpacity: number; - isEnabled: boolean; positivePrompt: ParameterPositivePrompt; negativePrompt: ParameterNegativePrompt; 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.test.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts new file mode 100644 index 0000000000..880514bf7c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts @@ -0,0 +1,23 @@ +import type { S } from 'services/api/types'; +import type { Equals } from 'tsafe'; +import { assert } from 'tsafe'; +import { describe, test } from 'vitest'; + +import type { + CLIPVisionModelV2, + ControlModeV2, + DepthAnythingModelSize, + IPMethodV2, + ProcessorConfig, + ProcessorTypeV2, +} from './controlAdapters'; + +describe('Control Adapter Types', () => { + test('ProcessorType', () => assert>()); + test('IP Adapter Method', () => assert, IPMethodV2>>()); + test('CLIP Vision Model', () => + 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 new file mode 100644 index 0000000000..2964a2eb6c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -0,0 +1,483 @@ +import { deepClone } from 'common/util/deepClone'; +import type { + ParameterControlNetModel, + ParameterIPAdapterModel, + ParameterT2IAdapterModel, +} from 'features/parameters/types/parameterSchemas'; +import { merge, omit } from 'lodash-es'; +import type { + BaseModelType, + CannyImageProcessorInvocation, + ColorMapImageProcessorInvocation, + ContentShuffleImageProcessorInvocation, + ControlNetModelConfig, + DepthAnythingImageProcessorInvocation, + DWOpenposeImageProcessorInvocation, + Graph, + HedImageProcessorInvocation, + ImageDTO, + LineartAnimeImageProcessorInvocation, + LineartImageProcessorInvocation, + MediapipeFaceProcessorInvocation, + MidasDepthImageProcessorInvocation, + MlsdImageProcessorInvocation, + NormalbaeImageProcessorInvocation, + PidiImageProcessorInvocation, + T2IAdapterModelConfig, + ZoeDepthImageProcessorInvocation, +} from 'services/api/types'; +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< + Pick +>; +export type ContentShuffleProcessorConfig = Required< + Pick +>; +export type DepthAnythingProcessorConfig = Required< + Pick +>; +export type HedProcessorConfig = Required>; +type LineartAnimeProcessorConfig = Required>; +export type LineartProcessorConfig = Required>; +export type MediapipeFaceProcessorConfig = Required< + Pick +>; +export type MidasDepthProcessorConfig = Required< + Pick +>; +export type MlsdProcessorConfig = Required>; +type NormalbaeProcessorConfig = Required>; +export type DWOpenposeProcessorConfig = Required< + Pick +>; +export type PidiProcessorConfig = Required>; +type ZoeDepthProcessorConfig = Required>; + +export type ProcessorConfig = + | CannyProcessorConfig + | ColorMapProcessorConfig + | ContentShuffleProcessorConfig + | DepthAnythingProcessorConfig + | HedProcessorConfig + | LineartAnimeProcessorConfig + | LineartProcessorConfig + | MediapipeFaceProcessorConfig + | MidasDepthProcessorConfig + | MlsdProcessorConfig + | NormalbaeProcessorConfig + | DWOpenposeProcessorConfig + | PidiProcessorConfig + | ZoeDepthProcessorConfig; + +export type ImageWithDims = { + imageName: string; + width: number; + height: number; +}; + +type ControlAdapterBase = { + id: string; + weight: number; + image: ImageWithDims | null; + processedImage: ImageWithDims | null; + isProcessingImage: boolean; + processorConfig: ProcessorConfig | null; + beginEndStepPct: [number, number]; +}; + +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 ControlNetConfigV2 = ControlAdapterBase & { + type: 'controlnet'; + model: ParameterControlNetModel | null; + controlMode: ControlModeV2; +}; +export const isControlNetConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is ControlNetConfigV2 => + ca.type === 'controlnet'; + +export type T2IAdapterConfigV2 = ControlAdapterBase & { + type: 't2i_adapter'; + model: ParameterT2IAdapterModel | null; +}; +export const isT2IAdapterConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is T2IAdapterConfigV2 => + ca.type === 't2i_adapter'; + +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 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 IPAdapterConfigV2 = { + id: string; + type: 'ip_adapter'; + weight: number; + method: IPMethodV2; + image: ImageWithDims | null; + model: ParameterIPAdapterModel | null; + clipVisionModel: CLIPVisionModelV2; + beginEndStepPct: [number, number]; +}; + +const zProcessorTypeV2 = z.enum([ + 'canny_image_processor', + 'color_map_image_processor', + 'content_shuffle_image_processor', + 'depth_anything_image_processor', + 'hed_image_processor', + 'lineart_anime_image_processor', + 'lineart_image_processor', + 'mediapipe_face_processor', + 'midas_depth_image_processor', + 'mlsd_image_processor', + 'normalbae_image_processor', + 'dw_openpose_image_processor', + 'pidi_image_processor', + 'zoe_depth_image_processor', +]); +export type ProcessorTypeV2 = z.infer; +export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success; + +type ProcessorData = { + type: T; + labelTKey: string; + descriptionTKey: string; + buildDefaults(baseModel?: BaseModelType): Extract; + buildNode( + image: ImageWithDims, + config: Extract + ): Extract; +}; + +const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); + +type CAProcessorsData = { + [key in ProcessorTypeV2]: ProcessorData; +}; +/** + * A dict of ControlNet processors, including: + * - label translation key + * - description translation key + * - a builder to create default values for the config + * - a builder to create the node for the config + * + * TODO: Generate from the OpenAPI schema + */ +export const CA_PROCESSOR_DATA: CAProcessorsData = { + canny_image_processor: { + type: 'canny_image_processor', + labelTKey: 'controlnet.canny', + descriptionTKey: 'controlnet.cannyDescription', + buildDefaults: () => ({ + id: 'canny_image_processor', + type: 'canny_image_processor', + low_threshold: 100, + high_threshold: 200, + }), + buildNode: (image, config) => ({ + ...config, + type: 'canny_image_processor', + image: { image_name: image.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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: 'zoe_depth_image_processor', + type: 'zoe_depth_image_processor', + }), + buildNode: (image, config) => ({ + ...config, + image: { image_name: image.imageName }, + }), + }, +}; + +export const initialControlNetV2: Omit = { + type: 'controlnet', + model: null, + weight: 1, + beginEndStepPct: [0, 1], + controlMode: 'balanced', + image: null, + processedImage: null, + isProcessingImage: false, + processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), +}; + +export const initialT2IAdapterV2: Omit = { + type: 't2i_adapter', + model: null, + weight: 1, + beginEndStepPct: [0, 1], + image: null, + processedImage: null, + isProcessingImage: false, + processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), +}; + +export const initialIPAdapterV2: Omit = { + type: 'ip_adapter', + image: null, + model: null, + beginEndStepPct: [0, 1], + method: 'full', + clipVisionModel: 'ViT-H', + weight: 1, +}; + +export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfigV2 => { + return merge(deepClone(initialControlNetV2), { id, ...overrides }); +}; + +export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfigV2 => { + return merge(deepClone(initialT2IAdapterV2), { id, ...overrides }); +}; + +export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfigV2 => { + return merge(deepClone(initialIPAdapterV2), { id, ...overrides }); +}; + +export const buildControlAdapterProcessorV2 = ( + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig +): ProcessorConfig | null => { + const defaultPreprocessor = modelConfig.default_settings?.preprocessor; + if (!isProcessorTypeV2(defaultPreprocessor)) { + return null; + } + const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base); + return processorConfig; +}; + +export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({ + imageName: image_name, + width, + height, +}); + +export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfigV2): ControlNetConfigV2 => { + return { + ...deepClone(t2iAdapter), + type: 'controlnet', + controlMode: initialControlNetV2.controlMode, + }; +}; + +export const controlNetToT2IAdapter = (controlNet: ControlNetConfigV2): T2IAdapterConfigV2 => { + return { + ...omit(deepClone(controlNet), 'controlMode'), + type: 't2i_adapter', + }; +}; diff --git a/invokeai/frontend/web/src/features/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..36083d2d92 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -5,20 +5,24 @@ import { $tool, BACKGROUND_LAYER_ID, BACKGROUND_RECT_ID, - CONTROLNET_LAYER_IMAGE_NAME, - CONTROLNET_LAYER_NAME, - getControlNetLayerImageId, + CA_LAYER_IMAGE_NAME, + CA_LAYER_NAME, + getCALayerImageId, + getIILayerImageId, getLayerBboxId, - getRegionalGuidanceLayerObjectGroupId, + getRGLayerObjectGroupId, + INITIAL_IMAGE_LAYER_IMAGE_NAME, + INITIAL_IMAGE_LAYER_NAME, isControlAdapterLayer, + isInitialImageLayer, 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, @@ -28,6 +32,7 @@ import { } from 'features/controlLayers/store/controlLayersSlice'; import type { ControlAdapterLayer, + InitialImageLayer, Layer, RegionalGuidanceLayer, Tool, @@ -35,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'; @@ -53,10 +59,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 || n.name() === INITIAL_IMAGE_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; }; /** @@ -138,10 +144,9 @@ const renderToolPreview = ( globalMaskLayerOpacity: number, cursorPos: Vector2d | null, lastMouseDownPos: Vector2d | null, - isMouseOver: boolean, brushSize: number ) => { - const layerCount = stage.find(`.${regional_guidance_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 @@ -162,7 +167,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; @@ -233,7 +238,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 +270,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 +290,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 +312,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 +352,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. @@ -408,10 +413,110 @@ 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, - name: CONTROLNET_LAYER_NAME, + name: CA_LAYER_NAME, imageSmoothingEnabled: true, }); stage.add(konvaLayer); @@ -420,7 +525,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); @@ -432,32 +537,32 @@ 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)); + 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 imageId = getControlNetLayerImageId(reduxLayer.id, imageName); - image.onload = () => { + const imageEl = new Image(); + const imageId = getCALayerImageId(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(`.${CONTROLNET_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(`.${CONTROLNET_LAYER_IMAGE_NAME}`)?.destroy(); + konvaLayer.findOne(`.${CA_LAYER_IMAGE_NAME}`)?.destroy(); } }; @@ -497,16 +602,14 @@ 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) - ) { + const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image; + if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) { imageSourceNeedsUpdate = true; - } else if (!reduxLayer.imageName) { + } else if (!image) { imageSourceNeedsUpdate = true; } } else if (!canvasImageSource) { @@ -550,6 +653,9 @@ const renderLayers = ( if (isControlAdapterLayer(reduxLayer)) { renderControlNetLayer(stage, reduxLayer); } + if (isInitialImageLayer(reduxLayer)) { + renderInitialImageLayer(stage, reduxLayer); + } } }; @@ -714,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', diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index d5c8f11f81..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,24 +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 = { - isInitialImage: some(allImageUsage, (i) => i.isInitialImage), 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 5a6856f346..d76716d01d 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/ImageUsageMessage.tsx @@ -29,10 +29,10 @@ const ImageUsageMessage = (props: Props) => { <> {topMessage} - {imageUsage.isInitialImage && {t('common.img2img')}} - {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 f54f9a0dbb..ce989de7b1 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -7,26 +7,30 @@ 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 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) => { @@ -40,11 +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 = { - isInitialImage, isCanvasImage, isNodesImage, isControlImage, + isControlLayerImage, }; return imageUsage; @@ -52,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) { @@ -64,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 cd8f3aa5eb..2cc3dd90b4 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/types.ts @@ -6,8 +6,8 @@ export type DeleteImageState = { }; export type ImageUsage = { - isInitialImage: boolean; isCanvasImage: boolean; isNodesImage: boolean; isControlImage: boolean; + isControlLayerImage: boolean; }; 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/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index b2b7820762..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: { @@ -33,6 +29,35 @@ type ControlAdapterDropData = BaseDropData & { }; }; +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 IILayerImageDropData = BaseDropData & { + actionType: 'SET_II_LAYER_IMAGE'; + context: { + layerId: string; + }; +}; + export type CanvasInitialImageDropData = BaseDropData & { actionType: 'SET_CANVAS_INITIAL_IMAGE'; }; @@ -56,12 +81,15 @@ export type RemoveFromBoardDropData = BaseDropData & { export type TypesafeDroppableData = | CurrentImageDropData - | InitialImageDropData | ControlAdapterDropData | CanvasInitialImageDropData | NodesImageDropData | AddToBoardDropData - | RemoveFromBoardDropData; + | RemoveFromBoardDropData + | CALayerImageDropData + | IPALayerImageDropData + | 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 c2c9de3f0c..b701c72947 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -15,10 +15,16 @@ 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': + 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_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/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 6581033aaa..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,17 +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 = { - isInitialImage: some(allImageUsage, (i) => i.isInitialImage), 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; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx deleted file mode 100644 index 880fdbca6c..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import { ButtonGroup, Flex, 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 { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; -import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; -import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems'; -import { useImageActions } from 'features/gallery/hooks/useImageActions'; -import { sentImageToImg2Img } from 'features/gallery/store/actions'; -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'; -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'; -import { useTranslation } from 'react-i18next'; -import { - PiArrowsCounterClockwiseBold, - PiAsteriskBold, - PiDotsThreeOutlineFill, - PiFlowArrowBold, - PiHourglassHighBold, - PiInfoBold, - PiPlantBold, - PiQuotesBold, - PiRulerBold, -} from 'react-icons/pi'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; - -const selectShouldDisableToolbarButtons = createSelector( - selectSystemSlice, - selectGallerySlice, - selectLastSelectedImage, - (system, gallery, lastSelectedImage) => { - const hasProgressImage = Boolean(system.denoiseProgress?.progress_image); - return hasProgressImage || !lastSelectedImage; - } -); - -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); - - const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } = - useImageActions(lastSelectedImage?.image_name); - - const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({}); - - const handleLoadWorkflow = useCallback(() => { - if (!lastSelectedImage || !lastSelectedImage.has_workflow) { - return; - } - getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name); - }, [getAndLoadEmbeddedWorkflow, lastSelectedImage]); - - useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]); - useHotkeys('a', recallAll, [recallAll]); - useHotkeys('s', recallSeed, [recallSeed]); - useHotkeys('p', recallPrompts, [recallPrompts]); - useHotkeys('r', remix, [remix]); - - const handleUseSize = useCallback(() => { - parseAndRecallImageDimensions(lastSelectedImage); - }, [lastSelectedImage]); - - useHotkeys('d', handleUseSize, [handleUseSize]); - - const handleSendToImageToImage = useCallback(() => { - dispatch(sentImageToImg2Img()); - dispatch(initialImageSelected(imageDTO)); - }, [dispatch, imageDTO]); - - useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]); - - const handleClickUpscale = useCallback(() => { - if (!imageDTO) { - return; - } - dispatch(upscaleRequested({ imageDTO })); - }, [dispatch, imageDTO]); - - const handleDelete = useCallback(() => { - if (!imageDTO) { - return; - } - dispatch(imagesToDeleteSelected(selection)); - }, [dispatch, imageDTO, selection]); - - useHotkeys( - 'Shift+U', - () => { - handleClickUpscale(); - }, - { - enabled: () => Boolean(isUpscalingEnabled && !shouldDisableToolbarButtons && isConnected), - }, - [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', - () => { - handleDelete(); - }, - [dispatch, imageDTO] - ); - - const handleClickProgressImagesToggle = useCallback(() => { - dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer)); - }, [dispatch, shouldShowProgressInViewer]); - - return ( - <> - - - - } - /> - {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} - /> - - - {isUpscalingEnabled && ( - - {isUpscalingEnabled && } - - )} - - - } - tooltip={`${t('parameters.info')} (I)`} - aria-label={`${t('parameters.info')} (I)`} - isChecked={shouldShowImageDetails} - onClick={handleClickShowImageDetails} - /> - - - - } - isChecked={shouldShowProgressInViewer} - onClick={handleClickProgressImagesToggle} - /> - - - - - - - - ); -}; - -export default memo(CurrentImageButtons); 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/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index aff74481ca..7bfb4050fb 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'; @@ -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(); @@ -72,13 +72,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const handleSendToImageToImage = useCallback(() => { dispatch(sentImageToImg2Img()); - dispatch(initialImageSelected(imageDTO)); + dispatch(iiLayerAdded(imageDTO)); }, [dispatch, imageDTO]); 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 ce75ea62e0..c73f5b1817 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 !== 'generation' && } + {activeTabName !== 'generation' && } + {activeTabName !== 'generation' && } + {activeTabName === 'generation' && } + {activeTabName === 'generation' && } + {activeTabName === 'generation' && } ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx new file mode 100644 index 0000000000..f93f48e51b --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -0,0 +1,202 @@ +import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { skipToken } from '@reduxjs/toolkit/query'; +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'; +import { useImageActions } from 'features/gallery/hooks/useImageActions'; +import { sentImageToImg2Img } from 'features/gallery/store/actions'; +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 { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress'; +import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import { selectSystemSlice } from 'features/system/store/systemSlice'; +import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { + PiArrowsCounterClockwiseBold, + PiAsteriskBold, + PiDotsThreeOutlineFill, + PiFlowArrowBold, + PiPlantBold, + PiQuotesBold, + PiRulerBold, +} from 'react-icons/pi'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; + +const selectShouldDisableToolbarButtons = createSelector( + selectSystemSlice, + selectGallerySlice, + selectLastSelectedImage, + (system, gallery, lastSelectedImage) => { + const hasProgressImage = Boolean(system.denoiseProgress?.progress_image); + return hasProgressImage || !lastSelectedImage; + } +); + +const CurrentImageButtons = () => { + const dispatch = useAppDispatch(); + const isConnected = useAppSelector((s) => s.system.isConnected); + const lastSelectedImage = useAppSelector(selectLastSelectedImage); + const selection = useAppSelector((s) => s.gallery.selection); + const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons); + + const isUpscalingEnabled = useFeatureStatus('upscaling'); + const isQueueMutationInProgress = useIsQueueMutationInProgress(); + const { t } = useTranslation(); + + const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken); + + const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } = + useImageActions(lastSelectedImage?.image_name); + + const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({}); + + const handleLoadWorkflow = useCallback(() => { + if (!lastSelectedImage || !lastSelectedImage.has_workflow) { + return; + } + getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name); + }, [getAndLoadEmbeddedWorkflow, lastSelectedImage]); + + useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]); + useHotkeys('a', recallAll, [recallAll]); + useHotkeys('s', recallSeed, [recallSeed]); + useHotkeys('p', recallPrompts, [recallPrompts]); + useHotkeys('r', remix, [remix]); + + const handleUseSize = useCallback(() => { + parseAndRecallImageDimensions(lastSelectedImage); + }, [lastSelectedImage]); + + useHotkeys('d', handleUseSize, [handleUseSize]); + + const handleSendToImageToImage = useCallback(() => { + if (!imageDTO) { + return; + } + dispatch(sentImageToImg2Img()); + dispatch(iiLayerAdded(imageDTO)); + }, [dispatch, imageDTO]); + + useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]); + + const handleClickUpscale = useCallback(() => { + if (!imageDTO) { + return; + } + dispatch(upscaleRequested({ imageDTO })); + }, [dispatch, imageDTO]); + + const handleDelete = useCallback(() => { + if (!imageDTO) { + return; + } + dispatch(imagesToDeleteSelected(selection)); + }, [dispatch, imageDTO, selection]); + + useHotkeys( + 'Shift+U', + () => { + handleClickUpscale(); + }, + { + enabled: () => Boolean(isUpscalingEnabled && !shouldDisableToolbarButtons && isConnected), + }, + [isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected] + ); + + useHotkeys( + 'delete', + () => { + handleDelete(); + }, + [dispatch, imageDTO] + ); + + return ( + <> + + + } + /> + {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} + /> + + + {isUpscalingEnabled && ( + + {isUpscalingEnabled && } + + )} + + + + + + ); +}; + +export default memo(CurrentImageButtons); 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 73% 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..37fada0b78 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx @@ -5,24 +5,25 @@ 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'; 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'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import ProgressImage from './ProgressImage'; + const selectLastSelectedImageName = createSelector( selectLastSelectedImage, (lastSelectedImage) => lastSelectedImage?.image_name ); const CurrentImagePreview = () => { + const { t } = useTranslation(); const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails); const imageName = useAppSelector(selectLastSelectedImageName); const hasDenoiseProgress = useAppSelector((s) => Boolean(s.system.denoiseProgress)); @@ -50,26 +51,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 ( { dataTestId="image-preview" /> )} - {shouldShowImageDetails && imageDTO && ( - - - - )} - {!shouldShowImageDetails && imageDTO && shouldShowNextPrevButtons && ( - + {shouldShowImageDetails && imageDTO && ( + + + + )} + + + {shouldShowNextPrevButtons && imageDTO && ( + - + )} @@ -112,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.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.1 }, -}; -const motionStyles: CSSProperties = { - position: 'absolute', - top: '0', - width: '100%', - height: '100%', - pointerEvents: 'none', + transition: { duration: 0.07 }, }; 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..2e10d057f8 --- /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'; + +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', +}; + +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 new file mode 100644 index 0000000000..874464f938 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -0,0 +1,91 @@ +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 CurrentImageButtons from './CurrentImageButtons'; +import CurrentImagePreview from './CurrentImagePreview'; +import { EditorButton } from './EditorButton'; + +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('z', 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/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 new file mode 100644 index 0000000000..57b3697b7e --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx @@ -0,0 +1,22 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; +import { useCallback } from 'react'; + +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/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 c3ae0cea5f..727752d79e 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 === 'generation'); + }, [activeTabName, metadata]); const remix = useCallback(() => { // Recalls all metadata parameters except seed - parseAndRecallAllMetadata(metadata, ['seed']); - }, [metadata]); + parseAndRecallAllMetadata(metadata, activeTabName === 'generation', ['seed']); + }, [activeTabName, metadata]); const recallSeed = useCallback(() => { handlers.seed.parse(metadata).then((seed) => { 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/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..30a34ec0c6 100644 --- a/invokeai/frontend/web/src/features/metadata/types.ts +++ b/invokeai/frontend/web/src/features/metadata/types.ts @@ -1,4 +1,9 @@ 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 +140,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 5d2bd78784..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, @@ -286,9 +297,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 +361,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(), }; @@ -432,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, @@ -468,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..b29d937159 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -6,7 +6,13 @@ import { t2iAdaptersReset, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { + caLayerAdded, + caLayerControlNetsDeleted, + caLayerT2IAdaptersDeleted, heightChanged, + iiLayerAdded, + ipaLayerAdded, + ipaLayersDeleted, negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, @@ -18,13 +24,15 @@ 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 { - initialImageChanged, setCfgRescaleMultiplier, setCfgScale, setImg2imgStrength, @@ -99,15 +107,17 @@ const recallScheduler: MetadataRecallFunc = (scheduler) => { }; const recallInitialImage: MetadataRecallFunc = async (imageDTO) => { - getStore().dispatch(initialImageChanged(imageDTO)); + getStore().dispatch(iiLayerAdded(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) => { @@ -234,6 +244,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 +324,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/modelManagerV2/hooks/useStarterModelsToast.tsx b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx index 6abc633ac8..6106264b78 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]); @@ -47,7 +47,7 @@ const ToastDescription = () => { {t('modelManager.noModelsInstalledDesc1')}{' '} ); 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 ( - + @@ -22,6 +23,7 @@ const TopCenterPanel = () => { + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts index e570054258..ca44259995 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/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts index a7236af3cc..da13fed9f5 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 ControlNetConfigV2, + type ImageWithDims, + type IPAdapterConfigV2, + isControlNetConfigV2, + isT2IAdapterConfigV2, + type ProcessorConfig, + type T2IAdapterConfigV2, +} 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: ControlNetConfigV2): 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: ControlNetConfigV2[], + 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: T2IAdapterConfigV2): 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: T2IAdapterConfigV2[], + 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: IPAdapterConfigV2): 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: IPAdapterConfigV2[], + 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(isControlNetConfigV2) + .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(isT2IAdapterConfigV2) + .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: IPAdapterConfigV2[] = 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..531c88335b 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 generation 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 !== '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 2c53fb3827..2cf93100eb 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 generation tab has special handling - its control adapters are set up in the Control Layers graph helper. + const activeTabName = activeTabNameSelector(state); + assert(activeTabName !== 'generation', 'Tried to use addT2IAdaptersToLinearGraph on generation 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/addInitialImageToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts new file mode 100644 index 0000000000..603708f15b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts @@ -0,0 +1,125 @@ +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'; + +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 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}`); + + 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', + }, + }); + } + + upsertMetadata(graph, { + generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img', + strength: img2imgStrength, + init_image: initialImage.imageName, + }); +}; 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..d6fcd411a4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts @@ -7,9 +7,8 @@ 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, SEAMLESS, VAE_LOADER, } from './constants'; @@ -54,8 +53,7 @@ export const addSeamlessToLinearGraph = ( let denoisingNodeId = DENOISE_LATENTS; if ( - graph.id === SDXL_TEXT_TO_IMAGE_GRAPH || - graph.id === SDXL_IMAGE_TO_IMAGE_GRAPH || + graph.id === SDXL_CONTROL_LAYERS_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/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts index 1632449724..ee21bbff1b 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 generation tab has special handling - its control adapters are set up in the Control Layers graph helper. + const activeTabName = activeTabNameSelector(state); + assert(activeTabName !== 'generation', 'Tried to use addT2IAdaptersToLinearGraph on generation 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/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts index 347027c539..f464723381 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts @@ -7,7 +7,7 @@ import { CANVAS_OUTPAINT_GRAPH, CANVAS_OUTPUT, CANVAS_TEXT_TO_IMAGE_GRAPH, - IMAGE_TO_IMAGE_GRAPH, + CONTROL_LAYERS_GRAPH, IMAGE_TO_LATENTS, INPAINT_CREATE_MASK, INPAINT_IMAGE, @@ -17,11 +17,9 @@ import { SDXL_CANVAS_INPAINT_GRAPH, SDXL_CANVAS_OUTPAINT_GRAPH, SDXL_CANVAS_TEXT_TO_IMAGE_GRAPH, - SDXL_IMAGE_TO_IMAGE_GRAPH, + SDXL_CONTROL_LAYERS_GRAPH, SDXL_REFINER_SEAMLESS, - SDXL_TEXT_TO_IMAGE_GRAPH, SEAMLESS, - TEXT_TO_IMAGE_GRAPH, VAE_LOADER, } from './constants'; import { upsertMetadata } from './metadata'; @@ -51,12 +49,7 @@ export const addVAEToGraph = async ( }; } - if ( - graph.id === TEXT_TO_IMAGE_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 @@ -100,10 +93,11 @@ export const addVAEToGraph = async ( } if ( - 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 === CANVAS_IMAGE_TO_IMAGE_GRAPH || + graph.id === SDXL_CANVAS_IMAGE_TO_IMAGE_GRAPH) && + Boolean(graph.nodes[IMAGE_TO_LATENTS]) ) { graph.edges.push({ source: { 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 91% 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 ea59d7e41d..6c04b25770 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts @@ -2,20 +2,19 @@ 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'; -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 { CLIP_SKIP, + CONTROL_LAYERS_GRAPH, DENOISE_LATENTS, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, @@ -23,11 +22,10 @@ import { NOISE, POSITIVE_CONDITIONING, SEAMLESS, - TEXT_TO_IMAGE_GRAPH, } 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, @@ -69,7 +67,7 @@ export const buildLinearTextToImageGraph = async (state: RootState): Promise => { +export const buildGenerationTabSDXLGraph = async (state: RootState): Promise => { const log = logger('nodes'); const { model, @@ -73,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', @@ -226,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, @@ -244,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); @@ -264,14 +264,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/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 9866d836db..53d7d742ab 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts @@ -55,14 +55,12 @@ export const POSITIVE_CONDITIONING_COLLECT = 'positive_conditioning_collect'; export const NEGATIVE_CONDITIONING_COLLECT = 'negative_conditioning_collect'; // friendly graph ids -export const TEXT_TO_IMAGE_GRAPH = 'text_to_image_graph'; -export const IMAGE_TO_IMAGE_GRAPH = 'image_to_image_graph'; +export const CONTROL_LAYERS_GRAPH = 'control_layers_graph'; +export const SDXL_CONTROL_LAYERS_GRAPH = 'sdxl_control_layers_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/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/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/components/MainModel/NavigateToModelManagerButton.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx index 733fb83826..268674d8c3 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) { @@ -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" diff --git a/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts b/invokeai/frontend/web/src/features/parameters/hooks/usePreselectedImage.ts index 30f954dedb..20d771d75d 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'; @@ -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', @@ -37,13 +37,13 @@ export const usePreselectedImage = (selectedImage?: { const handleSendToImg2Img = useCallback(() => { if (selectedImageDto) { - dispatch(initialImageSelected(selectedImageDto)); + dispatch(iiLayerAdded(selectedImageDto)); } }, [dispatch, selectedImageDto]); const handleUseAllMetadata = useCallback(() => { if (selectedImageMetadata) { - parseAndRecallAllMetadata(selectedImageMetadata); + parseAndRecallAllMetadata(selectedImageMetadata, true); } }, [selectedImageMetadata]); 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/parameters/types/constants.ts b/invokeai/frontend/web/src/features/parameters/types/constants.ts index 5bdfe9937b..6d7b4f9248 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)); 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(); 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..e9a9263605 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'; @@ -32,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 }, @@ -86,7 +85,7 @@ export const ImageSettingsAccordion = memo(() => { onToggle={onToggleAccordion} > - {activeTabName === 'unifiedCanvas' ? : } + {activeTabName === 'canvas' ? : } @@ -94,10 +93,9 @@ export const ImageSettingsAccordion = memo(() => { - {(activeTabName === 'img2img' || activeTabName === 'unifiedCanvas') && } - {activeTabName === 'img2img' && } - {activeTabName === 'txt2img' && !isSDXL && } - {activeTabName === 'unifiedCanvas' && ( + {activeTabName === 'canvas' && } + {activeTabName === 'generation' && !isSDXL && } + {activeTabName === 'canvas' && ( <> diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeCanvas.tsx 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..ddf4997a16 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] @@ -44,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/system/components/HotkeysModal/useHotkeyData.ts b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts index f9a0df52e4..806b85ca59 100644 --- a/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts +++ b/invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts @@ -141,14 +141,14 @@ export const useHotkeyData = (): HotkeyGroup[] => { hotkeys: [['Arrow Right']], }, { - title: t('hotkeys.increaseGalleryThumbSize.title'), - desc: t('hotkeys.increaseGalleryThumbSize.desc'), - hotkeys: [['Shift', 'Up']], + title: t('hotkeys.openImageViewer.title'), + desc: t('hotkeys.openImageViewer.desc'), + hotkeys: [['I']], }, { - title: t('hotkeys.decreaseGalleryThumbSize.title'), - desc: t('hotkeys.decreaseGalleryThumbSize.desc'), - hotkeys: [['Shift', 'Down']], + title: t('hotkeys.backToEditor.title'), + desc: t('hotkeys.backToEditor.desc'), + hotkeys: [['Esc']], }, ], }), 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} > = { + generation: { + id: 'generation', + translationKey: 'ui.tabs.generation', icon: , content: , }, - { - id: 'img2img', - translationKey: 'common.img2img', - icon: , - content: , - }, - { - id: 'unifiedCanvas', - translationKey: 'common.unifiedCanvas', + canvas: { + id: 'canvas', + translationKey: 'ui.tabs.canvas', icon: , content: , }, - { - id: 'nodes', - translationKey: 'common.nodes', + workflows: { + id: 'workflows', + translationKey: 'ui.tabs.workflows', icon: , content: , }, - { - id: 'modelManager', - translationKey: 'modelManager.modelManager', + models: { + id: 'models', + translationKey: 'ui.tabs.models', icon: , content: , }, - { + queue: { id: 'queue', - translationKey: 'queue.queue', + translationKey: 'ui.tabs.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']; -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; @@ -259,10 +254,11 @@ const InvokeTabs = () => { /> )} - + {tabPanels} + {shouldShowGalleryPanel && ( <> @@ -297,10 +293,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 2d14a50856..abd78d00e4 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,24 +61,27 @@ const ParametersPanelTextToImage = () => { {isSDXL ? : } - - - {t('common.settingsLabel')} - {controlLayersTitle} + + + + {t('common.settingsLabel')} + + + {controlLayersTitle} + - - + - {activeTabName !== 'txt2img' && } - {activeTabName === 'unifiedCanvas' && } + {activeTabName !== 'generation' && } + {activeTabName === 'canvas' && } {isSDXL && } - + 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 07e87d202c..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/components/tabs/NodesTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx index a707327d5d..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,28 +1,16 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay'; +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 ( + - ); - } else { - 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..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,31 +1,11 @@ -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 { 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..3e0d9b35d4 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvasTab.tsx @@ -27,6 +27,7 @@ const UnifiedCanvasTab = () => { return ( (isString(ui.activeTab) ? ui.activeTab : 'txt2img') + (ui) => (isString(ui.activeTab) ? ui.activeTab : 'generation') ); 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; }); diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 69c8eaf7cf..2146db974c 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -2,14 +2,13 @@ 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'; const initialUIState: UIState = { - _version: 1, - activeTab: 'txt2img', + _version: 2, + activeTab: 'generation', shouldShowImageDetails: false, shouldShowProgressInViewer: true, panels: {}, @@ -43,11 +42,8 @@ export const uiSlice = createSlice({ }, }, extraReducers(builder) { - builder.addCase(initialImageChanged, (state) => { - state.activeTab = 'img2img'; - }); builder.addCase(workflowLoadRequested, (state) => { - state.activeTab = 'nodes'; + state.activeTab = 'workflows'; }); }, }); @@ -68,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. */ 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; + } +}; 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/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 diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 7dde5fb624..a153780712 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -177,8 +177,25 @@ type ControlAdapterAction = { id: string; }; -type InitialImageAction = { - type: 'SET_INITIAL_IMAGE'; +export type CALayerImagePostUploadAction = { + type: 'SET_CA_LAYER_IMAGE'; + layerId: string; +}; + +export type IPALayerImagePostUploadAction = { + type: 'SET_IPA_LAYER_IMAGE'; + layerId: string; +}; + +export type RGLayerIPAdapterImagePostUploadAction = { + type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE'; + layerId: string; + ipAdapterId: string; +}; + +export type IILayerImagePostUploadAction = { + type: 'SET_II_LAYER_IMAGE'; + layerId: string; }; type NodesAction = { @@ -202,8 +219,11 @@ type AddToBatchAction = { export type PostUploadAction = | ControlAdapterAction - | InitialImageAction | NodesAction | CanvasInitialImageAction | ToastAction - | AddToBatchAction; + | AddToBatchAction + | CALayerImagePostUploadAction + | IPALayerImagePostUploadAction + | RGLayerIPAdapterImagePostUploadAction + | IILayerImagePostUploadAction; 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"