diff --git a/README.md b/README.md index f540e7be75..41de4882ee 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Invoke is a leading creative engine built to empower professionals and enthusiasts alike. Generate and create stunning visual media using the latest AI-driven technologies. Invoke offers an industry leading web-based UI, and serves as the foundation for multiple commercial products. -[Installation][installation docs] - [Documentation and Tutorials][docs home] - [Bug Reports][github issues] - [Contributing][contributing docs] +[Installation and Updates][installation docs] - [Documentation and Tutorials][docs home] - [Bug Reports][github issues] - [Contributing][contributing docs]
diff --git a/docs/features/TRAINING.md b/docs/features/TRAINING.md index 7be9aff0f2..47f8557889 100644 --- a/docs/features/TRAINING.md +++ b/docs/features/TRAINING.md @@ -4,278 +4,6 @@ title: Training # :material-file-document: Training -# Textual Inversion Training -## **Personalizing Text-to-Image Generation** +Invoke Training has moved to its own repository, with a dedicated UI for accessing common scripts like Textual Inversion and LoRA training. -You may personalize the generated images to provide your own styles or objects -by training a new LDM checkpoint and introducing a new vocabulary to the fixed -model as a (.pt) embeddings file. Alternatively, you may use or train -HuggingFace Concepts embeddings files (.bin) from - and its associated -notebooks. - -## **Hardware and Software Requirements** - -You will need a GPU to perform training in a reasonable length of -time, and at least 12 GB of VRAM. We recommend using the [`xformers` -library](../installation/070_INSTALL_XFORMERS.md) to accelerate the -training process further. During training, about ~8 GB is temporarily -needed in order to store intermediate models, checkpoints and logs. - -## **Preparing for Training** - -To train, prepare a folder that contains 3-5 images that illustrate -the object or concept. It is good to provide a variety of examples or -poses to avoid overtraining the system. Format these images as PNG -(preferred) or JPG. You do not need to resize or crop the images in -advance, but for more control you may wish to do so. - -Place the training images in a directory on the machine InvokeAI runs -on. We recommend placing them in a subdirectory of the -`text-inversion-training-data` folder located in the InvokeAI root -directory, ordinarily `~/invokeai` (Linux/Mac), or -`C:\Users\your_name\invokeai` (Windows). For example, to create an -embedding for the "psychedelic" style, you'd place the training images -into the directory -`~invokeai/text-inversion-training-data/psychedelic`. - -## **Launching Training Using the Console Front End** - -InvokeAI 2.3 and higher comes with a text console-based training front -end. From within the `invoke.sh`/`invoke.bat` Invoke launcher script, -start training tool selecting choice (3): - -```sh -1 "Generate images with a browser-based interface" -2 "Explore InvokeAI nodes using a command-line interface" -3 "Textual inversion training" -4 "Merge models (diffusers type only)" -5 "Download and install models" -6 "Change InvokeAI startup options" -7 "Re-run the configure script to fix a broken install or to complete a major upgrade" -8 "Open the developer console" -9 "Update InvokeAI" -``` - -Alternatively, you can select option (8) or from the command line, with the InvokeAI virtual environment active, -you can then launch the front end with the command `invokeai-ti --gui`. - -This will launch a text-based front end that will look like this: - -
-![ti-frontend](../assets/textual-inversion/ti-frontend.png) -
- -The interface is keyboard-based. Move from field to field using -control-N (^N) to move to the next field and control-P (^P) to the -previous one. and work as well. Once a field is -active, use the cursor keys. In a checkbox group, use the up and down -cursor keys to move from choice to choice, and to select a -choice. In a scrollbar, use the left and right cursor keys to increase -and decrease the value of the scroll. In textfields, type the desired -values. - -The number of parameters may look intimidating, but in most cases the -predefined defaults work fine. The red circled fields in the above -illustration are the ones you will adjust most frequently. - -### Model Name - -This will list all the diffusers models that are currently -installed. Select the one you wish to use as the basis for your -embedding. Be aware that if you use a SD-1.X-based model for your -training, you will only be able to use this embedding with other -SD-1.X-based models. Similarly, if you train on SD-2.X, you will only -be able to use the embeddings with models based on SD-2.X. - -### Trigger Term - -This is the prompt term you will use to trigger the embedding. Type a -single word or phrase you wish to use as the trigger, example -"psychedelic" (without angle brackets). Within InvokeAI, you will then -be able to activate the trigger using the syntax ``. - -### Initializer - -This is a single character that is used internally during the training -process as a placeholder for the trigger term. It defaults to "*" and -can usually be left alone. - -### Resume from last saved checkpoint - -As training proceeds, textual inversion will write a series of -intermediate files that can be used to resume training from where it -was left off in the case of an interruption. This checkbox will be -automatically selected if you provide a previously used trigger term -and at least one checkpoint file is found on disk. - -Note that as of 20 January 2023, resume does not seem to be working -properly due to an issue with the upstream code. - -### Data Training Directory - -This is the location of the images to be used for training. When you -select a trigger term like "my-trigger", the frontend will prepopulate -this field with `~/invokeai/text-inversion-training-data/my-trigger`, -but you can change the path to wherever you want. - -### Output Destination Directory - -This is the location of the logs, checkpoint files, and embedding -files created during training. When you select a trigger term like -"my-trigger", the frontend will prepopulate this field with -`~/invokeai/text-inversion-output/my-trigger`, but you can change the -path to wherever you want. - -### Image resolution - -The images in the training directory will be automatically scaled to -the value you use here. For best results, you will want to use the -same default resolution of the underlying model (512 pixels for -SD-1.5, 768 for the larger version of SD-2.1). - -### Center crop images - -If this is selected, your images will be center cropped to make them -square before resizing them to the desired resolution. Center cropping -can indiscriminately cut off the top of subjects' heads for portrait -aspect images, so if you have images like this, you may wish to use a -photoeditor to manually crop them to a square aspect ratio. - -### Mixed precision - -Select the floating point precision for the embedding. "no" will -result in a full 32-bit precision, "fp16" will provide 16-bit -precision, and "bf16" will provide mixed precision (only available -when XFormers is used). - -### Max training steps - -How many steps the training will take before the model converges. Most -training sets will converge with 2000-3000 steps. - -### Batch size - -This adjusts how many training images are processed simultaneously in -each step. Higher values will cause the training process to run more -quickly, but use more memory. The default size will run with GPUs with -as little as 12 GB. - -### Learning rate - -The rate at which the system adjusts its internal weights during -training. Higher values risk overtraining (getting the same image each -time), and lower values will take more steps to train a good -model. The default of 0.0005 is conservative; you may wish to increase -it to 0.005 to speed up training. - -### Scale learning rate by number of GPUs, steps and batch size - -If this is selected (the default) the system will adjust the provided -learning rate to improve performance. - -### Use xformers acceleration - -This will activate XFormers memory-efficient attention. You need to -have XFormers installed for this to have an effect. - -### Learning rate scheduler - -This adjusts how the learning rate changes over the course of -training. The default "constant" means to use a constant learning rate -for the entire training session. The other values scale the learning -rate according to various formulas. - -Only "constant" is supported by the XFormers library. - -### Gradient accumulation steps - -This is a parameter that allows you to use bigger batch sizes than -your GPU's VRAM would ordinarily accommodate, at the cost of some -performance. - -### Warmup steps - -If "constant_with_warmup" is selected in the learning rate scheduler, -then this provides the number of warmup steps. Warmup steps have a -very low learning rate, and are one way of preventing early -overtraining. - -## The training run - -Start the training run by advancing to the OK button (bottom right) -and pressing . A series of progress messages will be displayed -as the training process proceeds. This may take an hour or two, -depending on settings and the speed of your system. Various log and -checkpoint files will be written into the output directory (ordinarily -`~/invokeai/text-inversion-output/my-model/`) - -At the end of successful training, the system will copy the file -`learned_embeds.bin` into the InvokeAI root directory's `embeddings` -directory, using a subdirectory named after the trigger token. For -example, if the trigger token was `psychedelic`, then look for the -embeddings file in -`~/invokeai/embeddings/psychedelic/learned_embeds.bin` - -You may now launch InvokeAI and try out a prompt that uses the trigger -term. For example `a plate of banana sushi in style`. - -## **Training with the Command-Line Script** - -Training can also be done using a traditional command-line script. It -can be launched from within the "developer's console", or from the -command line after activating InvokeAI's virtual environment. - -It accepts a large number of arguments, which can be summarized by -passing the `--help` argument: - -```sh -invokeai-ti --help -``` - -Typical usage is shown here: -```sh -invokeai-ti \ - --model=stable-diffusion-1.5 \ - --resolution=512 \ - --learnable_property=style \ - --initializer_token='*' \ - --placeholder_token='' \ - --train_data_dir=/home/lstein/invokeai/training-data/psychedelic \ - --output_dir=/home/lstein/invokeai/text-inversion-training/psychedelic \ - --scale_lr \ - --train_batch_size=8 \ - --gradient_accumulation_steps=4 \ - --max_train_steps=3000 \ - --learning_rate=0.0005 \ - --resume_from_checkpoint=latest \ - --lr_scheduler=constant \ - --mixed_precision=fp16 \ - --only_save_embeds -``` - -## Troubleshooting - -### `Cannot load embedding for . It was trained on a model with token dimension 1024, but the current model has token dimension 768` - -Messages like this indicate you trained the embedding on a different base model than the currently selected one. - -For example, in the error above, the training was done on SD2.1 (768x768) but it was used on SD1.5 (512x512). - -## Reading - -For more information on textual inversion, please see the following -resources: - -* The [textual inversion repository](https://github.com/rinongal/textual_inversion) and - associated paper for details and limitations. -* [HuggingFace's textual inversion training - page](https://huggingface.co/docs/diffusers/training/text_inversion) -* [HuggingFace example script - documentation](https://github.com/huggingface/diffusers/tree/main/examples/textual_inversion) - (Note that this script is similar to, but not identical, to - `textual_inversion`, but produces embed files that are completely compatible. - ---- - -copyright (c) 2023, Lincoln Stein and the InvokeAI Development Team +You can find more by visiting the repo at https://github.com/invoke-ai/invoke-training diff --git a/docs/installation/010_INSTALL_AUTOMATED.md b/docs/installation/010_INSTALL_AUTOMATED.md index 5e2db65d7b..9eb8620321 100644 --- a/docs/installation/010_INSTALL_AUTOMATED.md +++ b/docs/installation/010_INSTALL_AUTOMATED.md @@ -1,8 +1,10 @@ -# Automatic Install +# Automatic Install & Updates -The installer is used for both new installs and updates. +**The same packaged installer file can be used for both new installs and updates.** +Using the installer for updates will leave everything you've added since installation, and just update the core libraries used to run Invoke. +Simply use the same path you installed to originally. -Both release and pre-release versions can be installed using it. It also supports install a wheel if needed. +Both release and pre-release versions can be installed using the installer. It also supports install through a wheel if needed. Be sure to review the [installation requirements] and ensure your system has everything it needs to install Invoke. diff --git a/docs/installation/INSTALLATION.md b/docs/installation/INSTALLATION.md index 5c121e6170..267376f197 100644 --- a/docs/installation/INSTALLATION.md +++ b/docs/installation/INSTALLATION.md @@ -1,4 +1,4 @@ -# Installation Overview +# Installation and Updating Overview Before installing, review the [installation requirements] to ensure your system is set up properly. @@ -6,14 +6,21 @@ See the [FAQ] for frequently-encountered installation issues. If you need more help, join our [discord] or [create an issue]. -

Automatic Install

+

Automatic Install & Updates

✅ The automatic install is the best way to run InvokeAI. Check out the [installation guide] to get started. +⬆️ The same installer is also the best way to update InvokeAI - Simply rerun it for the same folder you installed to. + +The installation process simply manages installation for the core libraries & application dependencies that run Invoke. +Any models, images, or other assets in the Invoke root folder won't be affected by the installation process. +

Manual Install

If you are familiar with python and want more control over the packages that are installed, you can [install InvokeAI manually via PyPI]. +Updates are managed by reinstalling the latest version through PyPi. +

Developer Install

If you want to contribute to InvokeAI, consult the [developer install guide]. diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 2730367fe5..96db090386 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -58,7 +58,7 @@ "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/inter": "^5.0.17", - "@invoke-ai/ui-library": "^0.0.21", + "@invoke-ai/ui-library": "^0.0.25", "@nanostores/react": "^0.7.2", "@reduxjs/toolkit": "2.2.2", "@roarr/browser-log-writer": "^1.3.0", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 9910e32391..2e5442479f 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: dependencies: '@chakra-ui/react': specifier: ^2.8.2 - version: 2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0) + version: 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) '@chakra-ui/react-use-size': specifier: ^2.1.0 version: 2.1.0(react@18.2.0) @@ -30,8 +30,8 @@ dependencies: specifier: ^5.0.17 version: 5.0.17 '@invoke-ai/ui-library': - specifier: ^0.0.21 - version: 0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.2)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) + specifier: ^0.0.25 + version: 0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.3)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) '@nanostores/react': specifier: ^0.7.2 version: 0.7.2(nanostores@0.10.0)(react@18.2.0) @@ -306,7 +306,7 @@ packages: '@jridgewell/trace-mapping': 0.3.25 dev: true - /@ark-ui/anatomy@1.3.0(@internationalized/date@3.5.2): + /@ark-ui/anatomy@1.3.0(@internationalized/date@3.5.3): resolution: {integrity: sha512-1yG2MrzUlix6KthjQMCNiHnkXrWwEdFAX6D+HqGJaNu0XvaGul2J+wDNtjsdX+gxiWu1nXXEEOAWlFVYMUf65w==} dependencies: '@zag-js/accordion': 0.32.1 @@ -318,7 +318,7 @@ packages: '@zag-js/color-utils': 0.32.1 '@zag-js/combobox': 0.32.1 '@zag-js/date-picker': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2) + '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3) '@zag-js/dialog': 0.32.1 '@zag-js/editable': 0.32.1 '@zag-js/file-upload': 0.32.1 @@ -345,13 +345,13 @@ packages: - '@internationalized/date' dev: false - /@ark-ui/react@1.3.0(@internationalized/date@3.5.2)(react-dom@18.2.0)(react@18.2.0): + /@ark-ui/react@1.3.0(@internationalized/date@3.5.3)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-JHjNoIX50+mUCTaEGMjfGQWGGi31pKsV646jZJlR/1xohpYJigzg8BvO97cTsVk8fwtur+cm11gz3Nf7f5QUnA==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' dependencies: - '@ark-ui/anatomy': 1.3.0(@internationalized/date@3.5.2) + '@ark-ui/anatomy': 1.3.0(@internationalized/date@3.5.3) '@zag-js/accordion': 0.32.1 '@zag-js/avatar': 0.32.1 '@zag-js/carousel': 0.32.1 @@ -361,7 +361,7 @@ packages: '@zag-js/combobox': 0.32.1 '@zag-js/core': 0.32.1 '@zag-js/date-picker': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2) + '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3) '@zag-js/dialog': 0.32.1 '@zag-js/editable': 0.32.1 '@zag-js/file-upload': 0.32.1 @@ -1681,7 +1681,7 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0): + /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -1694,9 +1694,9 @@ packages: '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false @@ -1848,16 +1848,6 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.3)(react@18.2.0): - resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==} - peerDependencies: - '@emotion/react': '>=10.0.35' - react: '>=18' - dependencies: - '@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0) - react: 18.2.0 - dev: false - /@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.4)(react@18.2.0): resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==} peerDependencies: @@ -1905,18 +1895,6 @@ packages: resolution: {integrity: sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==} dev: false - /@chakra-ui/focus-lock@2.1.0(@types/react@18.2.59)(react@18.2.0): - resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/dom-utils': 2.1.0 - react: 18.2.0 - react-focus-lock: 2.11.1(@types/react@18.2.59)(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - dev: false - /@chakra-ui/focus-lock@2.1.0(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==} peerDependencies: @@ -1924,7 +1902,7 @@ packages: dependencies: '@chakra-ui/dom-utils': 2.1.0 react: 18.2.0 - react-focus-lock: 2.11.2(@types/react@18.2.73)(react@18.2.0) + react-focus-lock: 2.11.1(@types/react@18.2.73)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false @@ -2100,59 +2078,6 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0): - resolution: {integrity: sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.2.0) - '@chakra-ui/descendant': 3.1.0(react@18.2.0) - '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-outside-click': 2.2.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - dev: false - - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.59)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0) - aria-hidden: 1.2.3 - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.7(@types/react@18.2.59)(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - dev: false - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} peerDependencies: @@ -2170,11 +2095,37 @@ packages: '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) - aria-hidden: 1.2.4 + aria-hidden: 1.2.3 framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.9(@types/react@18.2.73)(react@18.2.0) + react-remove-scroll: 2.5.7(@types/react@18.2.73)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} + peerDependencies: + '@chakra-ui/system': '>=2.0.0' + framer-motion: '>=4.0.0' + react: '>=18' + react-dom: '>=18' + dependencies: + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) + '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-types': 2.0.7(react@18.2.0) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) + aria-hidden: 1.2.3 + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.7(@types/react@18.2.73)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false @@ -2248,7 +2199,7 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0): + /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2266,8 +2217,8 @@ packages: '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.2.0) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false @@ -2305,25 +2256,6 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/provider@2.4.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==} - peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.3)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - /@chakra-ui/provider@2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==} peerDependencies: @@ -2554,77 +2486,6 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} - peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0) - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/counter': 2.1.0(react@18.2.0) - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.3)(react@18.2.0) - '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.59)(react@18.2.0) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/hooks': 2.2.1(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/live-region': 2.1.0(react@18.2.0) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0) - '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0) - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0) - '@chakra-ui/utils': 2.0.15 - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} peerDependencies: @@ -2696,6 +2557,77 @@ packages: - '@types/react' dev: false + /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} + peerDependencies: + '@emotion/react': ^11.0.0 + '@emotion/styled': ^11.0.0 + framer-motion: '>=4.0.0' + react: '>=18' + react-dom: '>=18' + dependencies: + '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/counter': 2.1.0(react@18.2.0) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.2.0) + '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/hooks': 2.2.1(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/live-region': 2.1.0(react@18.2.0) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.2.0) + '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/react-env': 3.1.0(react@18.2.0) + '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/styled-system': 2.9.2 + '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) + '@chakra-ui/theme-utils': 2.0.21 + '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/utils': 2.0.15 + '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): resolution: {integrity: sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==} peerDependencies: @@ -2814,7 +2746,7 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0): + /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2823,30 +2755,11 @@ packages: dependencies: '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false - /@chakra-ui/system@2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} - peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - react: '>=18' - dependencies: - '@chakra-ui/color-mode': 2.2.0(react@18.2.0) - '@chakra-ui/object-utils': 2.1.0 - '@chakra-ui/react-utils': 2.0.12(react@18.2.0) - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0) - react: 18.2.0 - react-fast-compare: 3.2.2 - dev: false - /@chakra-ui/system@2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0): resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} peerDependencies: @@ -2975,7 +2888,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==} peerDependencies: '@chakra-ui/system': 2.6.2 @@ -2991,9 +2904,9 @@ packages: '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -3020,7 +2933,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -3036,8 +2949,8 @@ packages: '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -3064,17 +2977,6 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/transition@2.1.0(framer-motion@11.0.6)(react@18.2.0): - resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==} - peerDependencies: - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - dev: false - /@chakra-ui/utils@2.0.15: resolution: {integrity: sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==} dependencies: @@ -3198,12 +3100,6 @@ packages: dev: false optional: true - /@emotion/is-prop-valid@1.2.1: - resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==} - dependencies: - '@emotion/memoize': 0.8.1 - dev: false - /@emotion/is-prop-valid@1.2.2: resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} dependencies: @@ -3220,27 +3116,6 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false - /@emotion/react@11.11.3(@types/react@18.2.59)(react@18.2.0): - resolution: {integrity: sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==} - peerDependencies: - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.23.9 - '@emotion/babel-plugin': 11.11.0 - '@emotion/cache': 11.11.0 - '@emotion/serialize': 1.1.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) - '@emotion/utils': 1.2.1 - '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.2.59 - hoist-non-react-statics: 3.3.2 - react: 18.2.0 - dev: false - /@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==} peerDependencies: @@ -3276,27 +3151,6 @@ packages: resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} dev: false - /@emotion/styled@11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0): - resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} - peerDependencies: - '@emotion/react': ^11.0.0-rc.0 - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.23.9 - '@emotion/babel-plugin': 11.11.0 - '@emotion/is-prop-valid': 1.2.1 - '@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0) - '@emotion/serialize': 1.1.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) - '@emotion/utils': 1.2.1 - '@types/react': 18.2.59 - react: 18.2.0 - dev: false - /@emotion/styled@11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} peerDependencies: @@ -3663,16 +3517,16 @@ packages: resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} dev: true - /@internationalized/date@3.5.2: - resolution: {integrity: sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==} + /@internationalized/date@3.5.3: + resolution: {integrity: sha512-X9bi8NAEHAjD8yzmPYT2pdJsbe+tYSEBAfowtlxJVJdZR3aK8Vg7ZUT1Fm5M47KLzp/M1p1VwAaeSma3RT7biw==} dependencies: - '@swc/helpers': 0.5.7 + '@swc/helpers': 0.5.11 dev: false /@internationalized/number@3.5.1: resolution: {integrity: sha512-N0fPU/nz15SwR9IbfJ5xaS9Ss/O5h1sVXMZf43vc9mxEG48ovglvvzBjF53aHlq20uoR6c+88CrIXipU/LSzwg==} dependencies: - '@swc/helpers': 0.5.7 + '@swc/helpers': 0.5.11 dev: false /@invoke-ai/eslint-config-react@0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.3): @@ -3709,14 +3563,14 @@ packages: prettier: 3.2.5 dev: true - /@invoke-ai/ui-library@0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.2)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-tCvgkBPDt0gNq+8IcR03e/Mw7R8Mb/SMXTqx3FEIxlTQEo93A/D38dKXeDCzTdx4sQ+sknfB+JLBbHs6sg5hhQ==} + /@invoke-ai/ui-library@0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.3)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Fmjdlu62NXHgairYXGjcuCrxPEAl1G6Q6ban8g3excF6pDDdBeS7CmSNCyEDMxnSIOZrQlI04OhaMB17Imi9Uw==} peerDependencies: '@fontsource-variable/inter': ^5.0.16 react: ^18.2.0 react-dom: ^18.2.0 dependencies: - '@ark-ui/react': 1.3.0(@internationalized/date@3.5.2)(react-dom@18.2.0)(react@18.2.0) + '@ark-ui/react': 1.3.0(@internationalized/date@3.5.3)(react-dom@18.2.0)(react@18.2.0) '@chakra-ui/anatomy': 2.2.2 '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) @@ -5381,8 +5235,8 @@ packages: resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} dev: true - /@swc/helpers@0.5.7: - resolution: {integrity: sha512-BVvNZhx362+l2tSwSuyEUV4h7+jk9raNdoTSdLfwTshXJSaGmYKluGRJznziCI3KX02Z19DdsQrdfrpXAU3Hfg==} + /@swc/helpers@0.5.11: + resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} dependencies: tslib: 2.6.2 dev: false @@ -5844,10 +5698,6 @@ packages: resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==} dev: true - /@types/prop-types@15.7.11: - resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} - dev: false - /@types/prop-types@15.7.12: resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -5877,14 +5727,6 @@ packages: '@types/react': 18.2.73 dev: false - /@types/react@18.2.59: - resolution: {integrity: sha512-DE+F6BYEC8VtajY85Qr7mmhTd/79rJKIHCg99MU9SWPB4xvLb6D1za2vYflgZfmPqQVEr6UqJTnLXEwzpVPuOg==} - dependencies: - '@types/prop-types': 15.7.11 - '@types/scheduler': 0.16.8 - csstype: 3.1.3 - dev: false - /@types/react@18.2.73: resolution: {integrity: sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==} dependencies: @@ -5895,10 +5737,6 @@ packages: resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} dev: true - /@types/scheduler@0.16.8: - resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - dev: false - /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} dev: true @@ -6405,10 +6243,10 @@ packages: /@zag-js/date-picker@0.32.1: resolution: {integrity: sha512-n/hYmF+/R4+NuyfPRzCgeuLT6LJihKSuKzK29STPWy3sC/tBBHiqhNv1/4UKbatHUJXdBW2XF+N8Rw08RffcFQ==} dependencies: - '@internationalized/date': 3.5.2 + '@internationalized/date': 3.5.3 '@zag-js/anatomy': 0.32.1 '@zag-js/core': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2) + '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3) '@zag-js/dismissable': 0.32.1 '@zag-js/dom-event': 0.32.1 '@zag-js/dom-query': 0.32.1 @@ -6420,12 +6258,12 @@ packages: '@zag-js/utils': 0.32.1 dev: false - /@zag-js/date-utils@0.32.1(@internationalized/date@3.5.2): + /@zag-js/date-utils@0.32.1(@internationalized/date@3.5.3): resolution: {integrity: sha512-dbBDRSVr5pRUw3rXndyGuSshZiWqQI5JQO4D2KIFGkXzorj6WzoOpcO910Z7AdM/9cCAMpCjUrka8d8o9BpJBg==} peerDependencies: '@internationalized/date': '>=3.0.0' dependencies: - '@internationalized/date': 3.5.2 + '@internationalized/date': 3.5.3 dev: false /@zag-js/dialog@0.32.1: @@ -6999,13 +6837,6 @@ packages: tslib: 2.6.2 dev: false - /aria-hidden@1.2.4: - resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} - engines: {node: '>=10'} - dependencies: - tslib: 2.6.2 - dev: false - /aria-query@5.1.3: resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} dependencies: @@ -9026,13 +8857,6 @@ packages: tslib: 2.6.2 dev: false - /focus-lock@1.3.4: - resolution: {integrity: sha512-Gv0N3mvej3pD+HWkNryrF8sExzEHqhQ6OSFxD4DPxm9n5HGCaHme98ZMBZroNEAJcsdtHxk+skvThGKyUeoEGA==} - engines: {node: '>=10'} - dependencies: - tslib: 2.6.2 - dev: false - /focus-trap@7.5.4: resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==} dependencies: @@ -9095,24 +8919,6 @@ packages: tslib: 2.6.2 dev: false - /framer-motion@11.0.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-BpO3mWF8UwxzO3Ca5AmSkrg14QYTeJa9vKgoLOoBdBdTPj0e81i1dMwnX6EQJXRieUx20uiDBXq8bA6y7N6b8Q==} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - tslib: 2.6.2 - optionalDependencies: - '@emotion/is-prop-valid': 0.8.8 - dev: false - /framesync@6.1.2: resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} dependencies: @@ -11485,7 +11291,7 @@ packages: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false - /react-focus-lock@2.11.1(@types/react@18.2.59)(react@18.2.0): + /react-focus-lock@2.11.1(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -11495,31 +11301,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@types/react': 18.2.59 + '@types/react': 18.2.73 focus-lock: 1.3.3 prop-types: 15.8.1 react: 18.2.0 react-clientside-effect: 1.2.6(react@18.2.0) - use-callback-ref: 1.3.1(@types/react@18.2.59)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.59)(react@18.2.0) - dev: false - - /react-focus-lock@2.11.2(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-DDTbEiov0+RthESPVSTIdAWPPKic+op3sCcP+icbMRobvQNt7LuAlJ3KoarqQv5sCgKArru3kXmlmFTa27/CdQ==} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.24.1 - '@types/react': 18.2.73 - focus-lock: 1.3.4 - prop-types: 15.8.1 - react: 18.2.0 - react-clientside-effect: 1.2.6(react@18.2.0) - use-callback-ref: 1.3.2(@types/react@18.2.73)(react@18.2.0) + use-callback-ref: 1.3.1(@types/react@18.2.73)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0) dev: false @@ -11634,25 +11421,9 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false - /react-remove-scroll-bar@2.3.5(@types/react@18.2.59)(react@18.2.0): + /react-remove-scroll-bar@2.3.5(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.59 - react: 18.2.0 - react-style-singleton: 2.2.1(@types/react@18.2.59)(react@18.2.0) - tslib: 2.6.2 - dev: false - - /react-remove-scroll-bar@2.3.6(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} - engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -11666,28 +11437,9 @@ packages: tslib: 2.6.2 dev: false - /react-remove-scroll@2.5.7(@types/react@18.2.59)(react@18.2.0): + /react-remove-scroll@2.5.7(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.59 - react: 18.2.0 - react-remove-scroll-bar: 2.3.5(@types/react@18.2.59)(react@18.2.0) - react-style-singleton: 2.2.1(@types/react@18.2.59)(react@18.2.0) - tslib: 2.6.2 - use-callback-ref: 1.3.1(@types/react@18.2.59)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.59)(react@18.2.0) - dev: false - - /react-remove-scroll@2.5.9(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-bvHCLBrFfM2OgcrpPY2YW84sPdS2o2HKWJUf1xGyGLnSoEnOTOBpahIarjRuYtN0ryahCeP242yf+5TrBX/pZA==} - engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -11697,10 +11449,10 @@ packages: dependencies: '@types/react': 18.2.73 react: 18.2.0 - react-remove-scroll-bar: 2.3.6(@types/react@18.2.73)(react@18.2.0) + react-remove-scroll-bar: 2.3.5(@types/react@18.2.73)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.2.73)(react@18.2.0) tslib: 2.6.2 - use-callback-ref: 1.3.2(@types/react@18.2.73)(react@18.2.0) + use-callback-ref: 1.3.1(@types/react@18.2.73)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0) dev: false @@ -11756,23 +11508,6 @@ packages: - '@types/react' dev: false - /react-style-singleton@2.2.1(@types/react@18.2.59)(react@18.2.0): - resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.59 - get-nonce: 1.0.1 - invariant: 2.2.4 - react: 18.2.0 - tslib: 2.6.2 - dev: false - /react-style-singleton@2.2.1(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -13288,24 +13023,9 @@ packages: punycode: 2.3.1 dev: true - /use-callback-ref@1.3.1(@types/react@18.2.59)(react@18.2.0): + /use-callback-ref@1.3.1(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.59 - react: 18.2.0 - tslib: 2.6.2 - dev: false - - /use-callback-ref@1.3.2(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} - engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -13358,22 +13078,6 @@ packages: react: 18.2.0 dev: false - /use-sidecar@1.1.2(@types/react@18.2.59)(react@18.2.0): - resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.59 - detect-node-es: 1.1.0 - react: 18.2.0 - tslib: 2.6.2 - dev: false - /use-sidecar@1.1.2(@types/react@18.2.73)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c80283b664..37a2a7a5da 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", @@ -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": { @@ -1522,7 +1534,7 @@ "moveForward": "Move Forward", "moveBackward": "Move Backward", "brushSize": "Brush Size", - "controlLayers": "Control Layers (BETA)", + "controlLayers": "Control Layers", "globalMaskOpacity": "Global Mask Opacity", "autoNegative": "Auto Negative", "toggleVisibility": "Toggle Layer Visibility", @@ -1543,8 +1555,25 @@ "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalIPAdapter": "Global $t(common.ipAdapter)", "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)", + "globalInitialImage": "Global Initial Image", + "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", "opacityFilter": "Opacity Filter", "clearProcessor": "Clear Processor", - "resetProcessor": "Reset Processor to Defaults" + "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 36040b5e41..0c0c8ed2bc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -32,7 +32,6 @@ import { addImagesStarredListener } from 'app/store/middleware/listenerMiddlewar import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred'; import { addImageToDeleteSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected'; import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded'; -import { addInitialImageSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/initialImageSelected'; import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected'; import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded'; import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged'; @@ -73,9 +72,6 @@ const startAppListening = listenerMiddleware.startListening as AppStartListening // Image uploaded addImageUploadedFulfilledListener(startAppListening); -// Image selected -addInitialImageSelectedListener(startAppListening); - // Image deleted addRequestedSingleImageDeletionListener(startAppListening); addDeleteBoardAndImagesFulfilledListener(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index 8e8d3f4b99..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 index 50395dc9dc..7d5aa27f20 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -10,7 +10,7 @@ import { caLayerProcessorConfigChanged, isControlAdapterLayer, } from 'features/controlLayers/store/controlLayersSlice'; -import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { isImageOutput } from 'features/nodes/types/common'; import { addToast } from 'features/system/store/systemSlice'; import { t } from 'i18next'; @@ -76,7 +76,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni } // @ts-expect-error: TS isn't able to narrow the typing of buildNode and `config` will error... - const processorNode = CONTROLNET_PROCESSORS[config.type].buildNode(image, config); + const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config); const enqueueBatchArg: BatchConfig = { prepend: true, batch: { diff --git a/invokeai/frontend/web/src/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/galleryImageClicked.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts index 67c6d076ee..6b8c9b4ea3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts @@ -1,7 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; -import { selectionChanged } from 'features/gallery/store/gallerySlice'; +import { isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice'; import { imagesApi } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import { imagesSelectors } from 'services/api/util'; @@ -62,6 +62,7 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen } else { dispatch(selectionChanged([imageDTO])); } + dispatch(isImageViewerOpenChanged(true)); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index 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 de2ac3a39a..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 @@ -9,13 +9,14 @@ import { } 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<{ @@ -52,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 */ @@ -76,7 +65,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => dispatch( controlAdapterImageChanged({ id, - controlImage: activeData.payload.imageDTO, + controlImage: activeData.payload.imageDTO.image_name, }) ); dispatch( @@ -143,6 +132,24 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => return; } + /** + * Image dropped on II Layer Image + */ + if ( + overData.actionType === 'SET_II_LAYER_IMAGE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + const { layerId } = overData.context; + dispatch( + iiLayerImageChanged({ + layerId, + imageDTO: activeData.payload.imageDTO, + }) + ); + return; + } + /** * Image dropped on Canvas */ diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/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 fd568ef1bd..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 @@ -8,11 +8,12 @@ import { } 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'; @@ -101,7 +102,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis dispatch( controlAdapterImageChanged({ id, - controlImage: imageDTO, + controlImage: imageDTO.image_name, }) ); dispatch( @@ -146,15 +147,15 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis ); } - if (postUploadAction?.type === 'SET_INITIAL_IMAGE') { - dispatch(initialImageChanged(imageDTO)); + if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') { + const { layerId } = postUploadAction; + dispatch(iiLayerImageChanged({ layerId, imageDTO })); dispatch( addToast({ ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setInitialImage'), + 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 6073564305..2aac5b8e72 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -16,7 +16,6 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import i18n from 'i18next'; import { forEach } from 'lodash-es'; import { getConnectedEdges } from 'reactflow'; -import { assert } from 'tsafe'; const selector = createMemoizedSelector( [ @@ -29,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; @@ -41,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')); @@ -98,8 +93,8 @@ const selector = createMemoizedSelector( reasons.push(i18n.t('parameters.invoke.noModelSelected')); } - if (activeTabName === 'txt2img') { - // Handling for Control Layers - only exists on txt2img tab now + if (activeTabName === 'generation') { + // Handling for generation tab controlLayers.present.layers .filter((l) => l.isEnabled) .flatMap((l) => { @@ -110,7 +105,7 @@ const selector = createMemoizedSelector( } else if (l.type === 'regional_guidance_layer') { return l.ipAdapters; } - assert(false); + return []; }) .forEach((ca, i) => { const hasNoModel = !ca.model; diff --git a/invokeai/frontend/web/src/features/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/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 f8afde677c..8ec397f99c 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts @@ -7,7 +7,7 @@ import { buildControlAdapter } from 'features/controlAdapters/util/buildControlA import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { merge, uniq } from 'lodash-es'; -import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; +import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; import { socketInvocationError } from 'services/events/actions'; import { v4 as uuidv4 } from 'uuid'; @@ -134,46 +134,23 @@ export const controlAdaptersSlice = createSlice({ const { id, isEnabled } = action.payload; caAdapter.updateOne(state, { id, changes: { isEnabled } }); }, - controlAdapterImageChanged: (state, action: PayloadAction<{ id: string; controlImage: ImageDTO | null }>) => { + controlAdapterImageChanged: ( + state, + action: PayloadAction<{ + id: string; + controlImage: string | null; + }> + ) => { const { id, controlImage } = action.payload; const ca = selectControlAdapterById(state, id); if (!ca) { return; } - if (isControlNetOrT2IAdapter(ca)) { - if (controlImage) { - const { image_name, width, height } = controlImage; - const processorNode = deepClone(ca.processorNode); - const minDim = Math.min(controlImage.width, controlImage.height); - if ('detect_resolution' in processorNode) { - processorNode.detect_resolution = minDim; - } - if ('image_resolution' in processorNode) { - processorNode.image_resolution = minDim; - } - if ('resolution' in processorNode) { - processorNode.resolution = minDim; - } - caAdapter.updateOne(state, { - id, - changes: { - processorNode, - controlImage: image_name, - controlImageDimensions: { width, height }, - processedControlImage: null, - }, - }); - } else { - caAdapter.updateOne(state, { - id, - changes: { controlImage: null, controlImageDimensions: null, processedControlImage: null }, - }); - } - } else { - // ip adapter - caAdapter.updateOne(state, { id, changes: { controlImage: controlImage?.image_name ?? null } }); - } + caAdapter.updateOne(state, { + id, + changes: { controlImage, processedControlImage: null }, + }); if (controlImage !== null && isControlNetOrT2IAdapter(ca) && ca.processorType !== 'none') { state.pendingControlImages.push(id); @@ -183,7 +160,7 @@ export const controlAdaptersSlice = createSlice({ state, action: PayloadAction<{ id: string; - processedControlImage: ImageDTO | null; + processedControlImage: string | null; }> ) => { const { id, processedControlImage } = action.payload; @@ -196,24 +173,12 @@ export const controlAdaptersSlice = createSlice({ return; } - if (processedControlImage) { - const { image_name, width, height } = processedControlImage; - caAdapter.updateOne(state, { - id, - changes: { - processedControlImage: image_name, - processedControlImageDimensions: { width, height }, - }, - }); - } else { - caAdapter.updateOne(state, { - id, - changes: { - processedControlImage: null, - processedControlImageDimensions: null, - }, - }); - } + caAdapter.updateOne(state, { + id, + changes: { + processedControlImage, + }, + }); state.pendingControlImages = state.pendingControlImages.filter((pendingId) => pendingId !== id); }, @@ -227,7 +192,7 @@ export const controlAdaptersSlice = createSlice({ state, action: PayloadAction<{ id: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig | null; + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig; }> ) => { const { id, modelConfig } = action.payload; @@ -236,11 +201,6 @@ export const controlAdaptersSlice = createSlice({ return; } - if (modelConfig === null) { - caAdapter.updateOne(state, { id, changes: { model: null } }); - return; - } - const model = zModelIdentifierField.parse(modelConfig); if (!isControlNetOrT2IAdapter(cn)) { @@ -248,36 +208,22 @@ export const controlAdaptersSlice = createSlice({ return; } + const update: Update = { + id, + changes: { model, shouldAutoConfig: true }, + }; + + update.changes.processedControlImage = null; + if (modelConfig.type === 'ip_adapter') { // should never happen... return; } - // We always update the model - const update: Update = { id, changes: { model } }; - - // Build the default processor for this model const processor = buildControlAdapterProcessor(modelConfig); - if (processor.processorType !== cn.processorNode.type) { - // If the processor type has changed, update the processor node - update.changes.shouldAutoConfig = true; - update.changes.processedControlImage = null; - update.changes.processorType = processor.processorType; - update.changes.processorNode = processor.processorNode; + update.changes.processorType = processor.processorType; + update.changes.processorNode = processor.processorNode; - if (cn.controlImageDimensions) { - const minDim = Math.min(cn.controlImageDimensions.width, cn.controlImageDimensions.height); - if ('detect_resolution' in update.changes.processorNode) { - update.changes.processorNode.detect_resolution = minDim; - } - if ('image_resolution' in update.changes.processorNode) { - update.changes.processorNode.image_resolution = minDim; - } - if ('resolution' in update.changes.processorNode) { - update.changes.processorNode.resolution = minDim; - } - } - } caAdapter.updateOne(state, update); }, controlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { @@ -394,23 +340,8 @@ export const controlAdaptersSlice = createSlice({ if (update.changes.shouldAutoConfig && modelConfig) { const processor = buildControlAdapterProcessor(modelConfig); - if (processor.processorType !== cn.processorNode.type) { - update.changes.processorType = processor.processorType; - update.changes.processorNode = processor.processorNode; - // Copy image resolution settings, urgh - if (cn.controlImageDimensions) { - const minDim = Math.min(cn.controlImageDimensions.width, cn.controlImageDimensions.height); - if ('detect_resolution' in update.changes.processorNode) { - update.changes.processorNode.detect_resolution = minDim; - } - if ('image_resolution' in update.changes.processorNode) { - update.changes.processorNode.image_resolution = minDim; - } - if ('resolution' in update.changes.processorNode) { - update.changes.processorNode.resolution = minDim; - } - } - } + update.changes.processorType = processor.processorType; + update.changes.processorNode = processor.processorNode; } caAdapter.updateOne(state, update); diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts b/invokeai/frontend/web/src/features/controlAdapters/store/types.ts index 80af59cd01..7e2f18af5c 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/store/types.ts @@ -225,9 +225,7 @@ export type ControlNetConfig = { controlMode: ControlMode; resizeMode: ResizeMode; controlImage: string | null; - controlImageDimensions: { width: number; height: number } | null; processedControlImage: string | null; - processedControlImageDimensions: { width: number; height: number } | null; processorType: ControlAdapterProcessorType; processorNode: RequiredControlAdapterProcessorNode; shouldAutoConfig: boolean; @@ -243,9 +241,7 @@ export type T2IAdapterConfig = { endStepPct: number; resizeMode: ResizeMode; controlImage: string | null; - controlImageDimensions: { width: number; height: number } | null; processedControlImage: string | null; - processedControlImageDimensions: { width: number; height: number } | null; processorType: ControlAdapterProcessorType; processorNode: RequiredControlAdapterProcessorNode; shouldAutoConfig: boolean; diff --git a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts b/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts index 7c9c28e2b3..ad7bdba363 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts @@ -20,9 +20,7 @@ export const initialControlNet: Omit = { controlMode: 'balanced', resizeMode: 'just_resize', controlImage: null, - controlImageDimensions: null, processedControlImage: null, - processedControlImageDimensions: null, processorType: 'canny_image_processor', processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation, shouldAutoConfig: true, @@ -37,9 +35,7 @@ export const initialT2IAdapter: Omit = { endStepPct: 1, resizeMode: 'just_resize', controlImage: null, - controlImageDimensions: null, processedControlImage: null, - processedControlImageDimensions: null, processorType: 'canny_image_processor', processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation, shouldAutoConfig: true, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 3eb97dddff..3102e4afa8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,6 +1,6 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; +import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,6 +11,7 @@ export const AddLayerButton = memo(() => { const dispatch = useAppDispatch(); const [addCALayer, isAddCALayerDisabled] = useAddCALayer(); const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); + const [addIILayer, isAddIILayerDisabled] = useAddIILayer(); const addRGLayer = useCallback(() => { dispatch(rgLayerAdded()); }, [dispatch]); @@ -30,6 +31,9 @@ export const AddLayerButton = memo(() => { } onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> {t('controlLayers.globalIPAdapterLayer')} + } onClick={addIILayer} isDisabled={isAddIILayerDisabled}> + {t('controlLayers.globalInitialImageLayer')} + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index 24de817df2..984331a050 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -5,6 +5,7 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; @@ -17,37 +18,28 @@ type Props = { export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected); - const onClickCapture = useCallback(() => { + const onClick = useCallback(() => { // Must be capture so that the layer is selected before deleting/resetting/etc dispatch(layerSelected(layerId)); }, [dispatch, layerId]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - - - - - - - - - - - {isOpen && ( - - - - )} + + + + + + + + - + {isOpen && ( + + + + )} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx index 6793a33f69..8ff1f9711f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx @@ -9,7 +9,7 @@ import { caOrIPALayerWeightChanged, selectCALayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; -import type { ControlMode, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import type { CALayerImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; import type { @@ -40,7 +40,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { ); const onChangeControlMode = useCallback( - (controlMode: ControlMode) => { + (controlMode: ControlModeV2) => { dispatch( caLayerControlModeChanged({ layerId, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx index 31c8d81853..353f8e0307 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx @@ -55,7 +55,7 @@ const CALayerOpacity = ({ layerId }: Props) => { onDoubleClick={stopPropagation} /> - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx index 087f634d73..c28c40ecc1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapter.tsx @@ -1,10 +1,10 @@ import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox'; import type { - ControlMode, - ControlNetConfig, + ControlModeV2, + ControlNetConfigV2, ProcessorConfig, - T2IAdapterConfig, + T2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import type { TypesafeDroppableData } from 'features/dnd/types'; import { memo } from 'react'; @@ -21,9 +21,9 @@ import { ControlAdapterProcessorTypeSelect } from './ControlAdapterProcessorType import { ControlAdapterWeight } from './ControlAdapterWeight'; type Props = { - controlAdapter: ControlNetConfig | T2IAdapterConfig; + controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; - onChangeControlMode: (controlMode: ControlMode) => void; + onChangeControlMode: (controlMode: ControlModeV2) => void; onChangeWeight: (weight: number) => void; onChangeProcessorConfig: (processorConfig: ProcessorConfig | null) => void; onChangeModel: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx index 34f4c85467..2c35ce51b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterControlModeSelect.tsx @@ -1,15 +1,15 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import type { ControlMode } from 'features/controlLayers/util/controlAdapters'; -import { isControlMode } from 'features/controlLayers/util/controlAdapters'; +import type { ControlModeV2 } from 'features/controlLayers/util/controlAdapters'; +import { isControlModeV2 } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; type Props = { - controlMode: ControlMode; - onChange: (controlMode: ControlMode) => void; + controlMode: ControlModeV2; + onChange: (controlMode: ControlModeV2) => void; }; export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => { @@ -26,7 +26,7 @@ export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: const handleControlModeChange = useCallback( (v) => { - assert(isControlMode(v?.value)); + assert(isControlModeV2(v?.value)); onChange(v.value); }, [onChange] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx index 7def6b2b56..e6c6aae286 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx @@ -6,7 +6,7 @@ import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { ControlNetConfig, T2IAdapterConfig } from 'features/controlLayers/util/controlAdapters'; +import type { ControlNetConfigV2, T2IAdapterConfigV2 } from 'features/controlLayers/util/controlAdapters'; import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; @@ -23,7 +23,7 @@ import { import type { ImageDTO, PostUploadAction } from 'services/api/types'; type Props = { - controlAdapter: ControlNetConfig | T2IAdapterConfig; + controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; onChangeImage: (imageDTO: ImageDTO | null) => void; droppableData: TypesafeDroppableData; postUploadAction: PostUploadAction; @@ -80,22 +80,24 @@ export const ControlAdapterImagePreview = memo( return; } - if (activeTabName === 'unifiedCanvas') { + 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, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } else { const { width, height } = calculateNewSize( controlImage.width / controlImage.height, optimalDimension * optimalDimension ); - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } } }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx index 46e6131353..5598b81787 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterProcessorTypeSelect.tsx @@ -4,7 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS, isProcessorType } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/util/controlAdapters'; import { configSelector } from 'features/system/store/configSelectors'; import { includes, map } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; @@ -26,7 +26,7 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro const { t } = useTranslation(); const disabledProcessors = useAppSelector(selectDisabledProcessors); const options = useMemo(() => { - return map(CONTROLNET_PROCESSORS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( + return map(CA_PROCESSOR_DATA, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter( (o) => !includes(disabledProcessors, o.value) ); }, [disabledProcessors, t]); @@ -36,8 +36,8 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro if (!v) { onChange(null); } else { - assert(isProcessorType(v.value)); - onChange(CONTROLNET_PROCESSORS[v.value].buildDefaults()); + assert(isProcessorTypeV2(v.value)); + onChange(CA_PROCESSOR_DATA[v.value].buildDefaults()); } }, [onChange] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx index a0aa7d79a1..86ed77ce36 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapter.tsx @@ -4,18 +4,18 @@ import { ControlAdapterWeight } from 'features/controlLayers/components/ControlA import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview'; import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod'; import { IPAdapterModelSelect } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect'; -import type { CLIPVisionModel, IPAdapterConfig, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { CLIPVisionModelV2, IPAdapterConfigV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { TypesafeDroppableData } from 'features/dnd/types'; import { memo } from 'react'; import type { ImageDTO, IPAdapterModelConfig, PostUploadAction } from 'services/api/types'; type Props = { - ipAdapter: IPAdapterConfig; + ipAdapter: IPAdapterConfigV2; onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; onChangeWeight: (weight: number) => void; - onChangeIPMethod: (method: IPMethod) => void; + onChangeIPMethod: (method: IPMethodV2) => void; onChangeModel: (modelConfig: IPAdapterModelConfig) => void; - onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; onChangeImage: (imageDTO: ImageDTO | null) => void; droppableData: TypesafeDroppableData; postUploadAction: PostUploadAction; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx index 7de726cda5..83dd250cd0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview.tsx @@ -46,22 +46,23 @@ export const IPAdapterImagePreview = memo( return; } - if (activeTabName === 'unifiedCanvas') { + 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, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } else { const { width, height } = calculateNewSize( controlImage.width / controlImage.height, optimalDimension * optimalDimension ); - dispatch(widthChanged({ width, updateAspectRatio: true })); - dispatch(heightChanged({ height, updateAspectRatio: true })); + dispatch(widthChanged({ width, ...options })); + dispatch(heightChanged({ height, ...options })); } } }, [controlImage, activeTabName, dispatch, optimalDimension, shift]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx index 70fd63f9c0..4f6a468fc3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod.tsx @@ -1,20 +1,20 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import type { IPMethod } from 'features/controlLayers/util/controlAdapters'; -import { isIPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; +import { isIPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { assert } from 'tsafe'; type Props = { - method: IPMethod; - onChange: (method: IPMethod) => void; + method: IPMethodV2; + onChange: (method: IPMethodV2) => void; }; export const IPAdapterMethod = memo(({ method, onChange }: Props) => { const { t } = useTranslation(); - const options: { label: string; value: IPMethod }[] = useMemo( + const options: { label: string; value: IPMethodV2 }[] = useMemo( () => [ { label: t('controlnet.full'), value: 'full' }, { label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' }, @@ -24,7 +24,7 @@ export const IPAdapterMethod = memo(({ method, onChange }: Props) => { ); const _onChange = useCallback( (v) => { - assert(isIPMethod(v?.value)); + assert(isIPMethodV2(v?.value)); onChange(v.value); }, [onChange] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx index e47bcd5182..b0541dca2c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect.tsx @@ -2,8 +2,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import type { CLIPVisionModel } from 'features/controlLayers/util/controlAdapters'; -import { isCLIPVisionModel } from 'features/controlLayers/util/controlAdapters'; +import type { CLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters'; +import { isCLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useIPAdapterModels } from 'services/api/hooks/modelsByType'; @@ -18,8 +18,8 @@ const CLIP_VISION_OPTIONS = [ type Props = { modelKey: string | null; onChangeModel: (modelConfig: IPAdapterModelConfig) => void; - clipVisionModel: CLIPVisionModel; - onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void; + clipVisionModel: CLIPVisionModelV2; + onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void; }; export const IPAdapterModelSelect = memo( @@ -41,7 +41,7 @@ export const IPAdapterModelSelect = memo( const _onChangeCLIPVisionModel = useCallback( (v) => { - assert(isCLIPVisionModel(v?.value)); + assert(isCLIPVisionModelV2(v?.value)); onChangeCLIPVisionModel(v.value); }, [onChangeCLIPVisionModel] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx index 999c8c7764..ef6e4160d6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/CannyProcessor.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { type CannyProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { 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 = CONTROLNET_PROCESSORS['canny_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['canny_image_processor'].buildDefaults(); export const CannyProcessor = ({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx index bf2d0d5d6d..6faa00dd14 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ColorMapProcessor.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { type ColorMapProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { 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 = CONTROLNET_PROCESSORS['color_map_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['color_map_image_processor'].buildDefaults(); export const ColorMapProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx index 041ab0ac9a..c03efd27c6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/ContentShuffleProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { ContentShuffleProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['content_shuffle_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['content_shuffle_image_processor'].buildDefaults(); export const ContentShuffleProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx index 70d608f7a9..3bbe813dcc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DWOpenposeProcessor.tsx @@ -1,7 +1,7 @@ import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { DWOpenposeProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['dw_openpose_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['dw_openpose_image_processor'].buildDefaults(); export const DWOpenposeProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx index d2e14a17f9..00993789b1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx @@ -2,14 +2,14 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['depth_anything_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['depth_anything_image_processor'].buildDefaults(); export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx index 72f0d52dc5..0f45d83ef0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MediapipeFaceProcessor.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; -import { CONTROLNET_PROCESSORS, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['mediapipe_face_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['mediapipe_face_processor'].buildDefaults(); export const MediapipeFaceProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx index 9078d14a53..1ce728984c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MidasDepthProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { MidasDepthProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['midas_depth_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['midas_depth_image_processor'].buildDefaults(); export const MidasDepthProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx index 5fc0c21ecd..b6eef311ef 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/MlsdImageProcessor.tsx @@ -1,14 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { MlsdProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters'; +import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CONTROLNET_PROCESSORS['mlsd_image_processor'].buildDefaults(); +const DEFAULTS = CA_PROCESSOR_DATA['mlsd_image_processor'].buildDefaults(); export const MlsdImageProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index ffa2856116..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 { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; +import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; 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 && } ); }); @@ -54,6 +61,9 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { if (type === 'ip_adapter_layer') { return ; } + if (type === 'initial_image_layer') { + return ; + } }); LayerWrapper.displayName = 'LayerWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/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/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 index 715e538679..02a161608d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -3,6 +3,7 @@ import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPAL import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { memo } from 'react'; type Props = { @@ -12,21 +13,19 @@ type Props = { export const IPALayer = memo(({ layerId }: Props) => { const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); return ( - - - - - - - - - {isOpen && ( - - - - )} + + + + + + - + {isOpen && ( + + + + )} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx index b8dfae6c03..9f99710dac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx @@ -9,7 +9,7 @@ import { ipaLayerModelChanged, selectIPALayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; -import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { IPALayerImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; @@ -42,7 +42,7 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { ); const onChangeIPMethod = useCallback( - (method: IPMethod) => { + (method: IPMethodV2) => { dispatch(ipaLayerMethodChanged({ layerId, method })); }, [dispatch, layerId] @@ -56,7 +56,7 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { ); const onChangeCLIPVisionModel = useCallback( - (clipVisionModel: CLIPVisionModel) => { + (clipVisionModel: CLIPVisionModelV2) => { dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel })); }, [dispatch, layerId] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx index b83f48188f..12074d12b8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenu.tsx @@ -37,7 +37,9 @@ export const LayerMenu = memo(({ layerId }: Props) => { )} - {(layerType === 'regional_guidance_layer' || layerType === 'control_adapter_layer') && ( + {(layerType === 'regional_guidance_layer' || + layerType === 'control_adapter_layer' || + layerType === 'initial_image_layer') && ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx index ec13ff7bcc..b29c3753fc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx @@ -16,6 +16,8 @@ export const LayerTitle = memo(({ type }: Props) => { return t('controlLayers.globalControlAdapter'); } else if (type === 'ip_adapter_layer') { return t('controlLayers.globalIPAdapter'); + } else if (type === 'initial_image_layer') { + return t('controlLayers.globalInitialImage'); } }, [t, type]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx new file mode 100644 index 0000000000..9d5fb6ea4b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx @@ -0,0 +1,21 @@ +import type { ChakraProps } from '@invoke-ai/ui-library'; +import { Flex } from '@invoke-ai/ui-library'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +type Props = PropsWithChildren<{ + onClick?: () => void; + borderColor: ChakraProps['bg']; +}>; + +export const LayerWrapper = memo(({ onClick, borderColor, children }: Props) => { + return ( + + + {children} + + + ); +}); + +LayerWrapper.displayName = 'LayerWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index baed22f6ca..a6bce5316e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -7,6 +7,7 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { isRegionalGuidanceLayer, layerSelected, @@ -52,32 +53,30 @@ export const RGLayer = memo(({ layerId }: Props) => { dispatch(layerSelected(layerId)); }, [dispatch, layerId]); return ( - - - - - - - {autoNegative === 'invert' && ( - - {t('controlLayers.autoNegative')} - - )} - - - - - - {isOpen && ( - - {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } - {hasPositivePrompt && } - {hasNegativePrompt && } - {hasIPAdapters && } - + + + + + + {autoNegative === 'invert' && ( + + {t('controlLayers.autoNegative')} + )} + + + + - + {isOpen && ( + + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } + {hasPositivePrompt && } + {hasNegativePrompt && } + {hasIPAdapters && } + + )} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx index 015cf75e4d..f7be62eb0a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx @@ -11,7 +11,7 @@ import { rgLayerIPAdapterWeightChanged, selectRGLayerIPAdapterOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; -import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; import type { RGLayerIPAdapterImageDropData } from 'features/dnd/types'; import { memo, useCallback, useMemo } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; @@ -51,7 +51,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu ); const onChangeIPMethod = useCallback( - (method: IPMethod) => { + (method: IPMethodV2) => { dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method })); }, [dispatch, ipAdapterId, layerId] @@ -65,7 +65,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu ); const onChangeCLIPVisionModel = useCallback( - (clipVisionModel: CLIPVisionModel) => { + (clipVisionModel: CLIPVisionModelV2) => { dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel })); }, [dispatch, ipAdapterId, layerId] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index ecf1121b41..d0d693a5f2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -6,8 +6,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useMouseEvents } from 'features/controlLayers/hooks/mouseEventHooks'; import { - $cursorPosition, - $isMouseOver, + $lastCursorPos, $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 cursorPosition = useStore($cursorPosition); + const mouseEventHandlers = useMouseEvents(); + const lastCursorPos = useStore($lastCursorPos); 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'); @@ -145,9 +141,8 @@ const useStageRenderer = ( selectedLayerIdColor, selectedLayerType, state.globalMaskLayerOpacity, - cursorPosition, + lastCursorPos, lastMouseDownPos, - isMouseOver, state.brushSize ); }, [ @@ -157,9 +152,8 @@ const useStageRenderer = ( selectedLayerIdColor, selectedLayerType, state.globalMaskLayerOpacity, - cursorPosition, + lastCursorPos, lastMouseDownPos, - isMouseOver, state.brushSize, renderers, ]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 53535b4248..f97a0f35e5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -4,9 +4,9 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { $tool, + layerReset, selectControlLayersSlice, selectedLayerDeleted, - selectedLayerReset, } from 'features/controlLayers/store/controlLayersSlice'; import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -22,6 +22,7 @@ export const ToolChooser: React.FC = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const isDisabled = useAppSelector(selectIsDisabled); + const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId); const tool = useStore($tool); const setToolToBrush = useCallback(() => { @@ -42,8 +43,11 @@ export const ToolChooser: React.FC = () => { useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]); const resetSelectedLayer = useCallback(() => { - dispatch(selectedLayerReset()); - }, [dispatch]); + if (selectedLayerId === null) { + return; + } + dispatch(layerReset(selectedLayerId)); + }, [dispatch, selectedLayerId]); useHotkeys('shift+c', resetSelectedLayer); const deleteSelectedLayer = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 17f0d4bf2d..dcbbeb8db5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -1,11 +1,17 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { caLayerAdded, ipaLayerAdded, rgLayerIPAdapterAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { + caLayerAdded, + iiLayerAdded, + ipaLayerAdded, + isInitialImageLayer, + rgLayerIPAdapterAdded, +} from 'features/controlLayers/store/controlLayersSlice'; import { buildControlNet, buildIPAdapter, buildT2IAdapter, - CONTROLNET_PROCESSORS, - isProcessorType, + CA_PROCESSOR_DATA, + isProcessorTypeV2, } from 'features/controlLayers/util/controlAdapters'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { useCallback, useMemo } from 'react'; @@ -30,8 +36,8 @@ export const useAddCALayer = () => { const id = uuidv4(); const defaultPreprocessor = model.default_settings?.preprocessor; - const processorConfig = isProcessorType(defaultPreprocessor) - ? CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(baseModel) + const processorConfig = isProcessorTypeV2(defaultPreprocessor) + ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(baseModel) : null; const builder = model.type === 'controlnet' ? buildControlNet : buildT2IAdapter; @@ -93,3 +99,13 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => { return [addIPAdapter, isDisabled] as const; }; + +export const useAddIILayer = () => { + const dispatch = useAppDispatch(); + const isDisabled = useAppSelector((s) => Boolean(s.controlLayers.present.layers.find(isInitialImageLayer))); + const addIILayer = useCallback(() => { + dispatch(iiLayerAdded(null)); + }, [dispatch]); + + return [addIILayer, isDisabled] as const; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts index b4880d1dc6..f2054779d4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts @@ -1,4 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { isControlAdapterLayer, @@ -69,7 +70,7 @@ export const useLayerType = (layerId: string) => { export const useLayerOpacity = (layerId: string) => { const selectLayer = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts index e3e87d0c42..8f69c165ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts @@ -3,9 +3,8 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { - $cursorPosition, - $isMouseDown, - $isMouseOver, + $isDrawing, + $lastCursorPos, $lastMouseDownPos, $tool, brushSizeChanged, @@ -16,16 +15,41 @@ import { import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; import type { Vector2d } from 'konva/lib/types'; -import { useCallback, useRef } from 'react'; +import { clamp } from 'lodash-es'; +import { useCallback, useMemo, useRef } from 'react'; const getIsFocused = (stage: Konva.Stage) => { return stage.container().contains(document.activeElement); }; +const getIsMouseDown = (e: KonvaEventObject) => e.evt.buttons === 1; + +const SNAP_PX = 10; + +export const snapPosToStage = (pos: Vector2d, stage: Konva.Stage) => { + const snappedPos = { ...pos }; + // Get the normalized threshold for snapping to the edge of the stage + const thresholdX = SNAP_PX / stage.scaleX(); + const thresholdY = SNAP_PX / stage.scaleY(); + const stageWidth = stage.width() / stage.scaleX(); + const stageHeight = stage.height() / stage.scaleY(); + // Snap to the edge of the stage if within threshold + if (pos.x - thresholdX < 0) { + snappedPos.x = 0; + } else if (pos.x + thresholdX > stageWidth) { + snappedPos.x = Math.floor(stageWidth); + } + if (pos.y - thresholdY < 0) { + snappedPos.y = 0; + } else if (pos.y + thresholdY > stageHeight) { + snappedPos.y = Math.floor(stageHeight); + } + return snappedPos; +}; export const getScaledFlooredCursorPosition = (stage: Konva.Stage) => { const pointerPosition = stage.getPointerPosition(); const stageTransform = stage.getAbsoluteTransform().copy(); - if (!pointerPosition || !stageTransform) { + if (!pointerPosition) { return; } const scaledCursorPosition = stageTransform.invert().point(pointerPosition); @@ -40,33 +64,41 @@ const syncCursorPos = (stage: Konva.Stage): Vector2d | null => { if (!pos) { return null; } - $cursorPosition.set(pos); + $lastCursorPos.set(pos); return pos; }; -const BRUSH_SPACING = 20; +const BRUSH_SPACING_PCT = 10; +const MIN_BRUSH_SPACING_PX = 5; +const MAX_BRUSH_SPACING_PX = 15; 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 brushSpacingPx = useMemo( + () => clamp(brushSize / BRUSH_SPACING_PCT, MIN_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX), + [brushSize] + ); const onMouseDown = useCallback( - (e: KonvaEventObject) => { + (e: KonvaEventObject) => { const stage = e.target.getStage(); if (!stage) { return; } const pos = syncCursorPos(stage); - if (!pos) { - return; - } - $isMouseDown.set(true); - $lastMouseDownPos.set(pos); - if (!selectedLayerId) { + if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { return; } if (tool === 'brush' || tool === 'eraser') { @@ -77,126 +109,105 @@ export const useMouseEvents = () => { tool, }) ); + $isDrawing.set(true); + $lastMouseDownPos.set(pos); + } else if (tool === 'rect') { + $lastMouseDownPos.set(snapPosToStage(pos, stage)); } }, - [dispatch, selectedLayerId, tool] + [dispatch, selectedLayerId, selectedLayerType, tool] ); const onMouseUp = useCallback( - (e: KonvaEventObject) => { - const stage = e.target.getStage(); - if (!stage) { - return; - } - $isMouseDown.set(false); - const pos = $cursorPosition.get(); - const lastPos = $lastMouseDownPos.get(); - const tool = $tool.get(); - if (pos && lastPos && selectedLayerId && tool === 'rect') { - dispatch( - rgLayerRectAdded({ - layerId: selectedLayerId, - rect: { - x: Math.min(pos.x, lastPos.x), - y: Math.min(pos.y, lastPos.y), - width: Math.abs(pos.x - lastPos.x), - height: Math.abs(pos.y - lastPos.y), - }, - }) - ); - } - $lastMouseDownPos.set(null); - }, - [dispatch, selectedLayerId] - ); - - 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(rgLayerPointsAdded({ 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(rgLayerPointsAdded({ 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 = $lastCursorPos.get(); + if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') { + return; + } + const lastPos = $lastMouseDownPos.get(); + const tool = $tool.get(); + if (lastPos && selectedLayerId && tool === 'rect') { + const snappedPos = snapPosToStage(pos, stage); + dispatch( + rgLayerRectAdded({ + layerId: selectedLayerId, + rect: { + x: Math.min(snappedPos.x, lastPos.x), + y: Math.min(snappedPos.y, lastPos.y), + width: Math.abs(snappedPos.x - lastPos.x), + height: Math.abs(snappedPos.y - lastPos.y), + }, + }) + ); + } + $isDrawing.set(false); + $lastMouseDownPos.set(null); + }, + [dispatch, selectedLayerId, selectedLayerType] + ); + + const onMouseMove = useCallback( + (e: KonvaEventObject) => { + const stage = e.target.getStage(); + if (!stage) { + return; + } 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( - rgLayerLineAdded({ - 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) < brushSpacingPx) { + 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] + [brushSpacingPx, 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); + $lastCursorPos.set(null); + $lastMouseDownPos.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 +221,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 c42a27f28f..bf0fa661a9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts @@ -1,20 +1,43 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { + isControlAdapterLayer, + isInitialImageLayer, + isIPAdapterLayer, + isRegionalGuidanceLayer, + selectControlLayersSlice, +} from 'features/controlLayers/store/controlLayersSlice'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => { - const validLayers = controlLayers.present.layers - .filter(isRegionalGuidanceLayer) - .filter((l) => l.isEnabled) - .filter((l) => { + let count = 0; + controlLayers.present.layers.forEach((l) => { + if (isRegionalGuidanceLayer(l)) { const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); - const hasAtLeastOneImagePrompt = l.ipAdapters.length > 0; - return hasTextPrompt || hasAtLeastOneImagePrompt; - }); + const hasAtLeastOneImagePrompt = l.ipAdapters.filter((ipa) => Boolean(ipa.image)).length > 0; + if (hasTextPrompt || hasAtLeastOneImagePrompt) { + count += 1; + } + } + if (isControlAdapterLayer(l)) { + if (l.controlAdapter.image || l.controlAdapter.processedImage) { + count += 1; + } + } + if (isIPAdapterLayer(l)) { + if (l.ipAdapter.image) { + count += 1; + } + } + if (isInitialImageLayer(l)) { + if (l.image) { + count += 1; + } + } + }); - return validLayers.length; + return count; }); export const useControlLayersTitle = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index c27a98c826..1ef90ead3a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -3,17 +3,18 @@ import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; +import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import type { - CLIPVisionModel, - ControlMode, - ControlNetConfig, - IPAdapterConfig, - IPMethod, + CLIPVisionModelV2, + ControlModeV2, + ControlNetConfigV2, + IPAdapterConfigV2, + IPMethodV2, ProcessorConfig, - T2IAdapterConfig, + T2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import { - buildControlAdapterProcessor, + buildControlAdapterProcessorV2, controlNetToT2IAdapter, imageDTOToImageWithDims, t2iAdapterToControlNet, @@ -38,6 +39,7 @@ import type { ControlAdapterLayer, ControlLayersState, DrawingTool, + InitialImageLayer, IPAdapterLayer, Layer, RegionalGuidanceLayer, @@ -70,18 +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'; -const resetLayer = (layer: Layer) => { - if (layer.type === 'regional_guidance_layer') { - layer.maskObjects = []; - layer.bbox = null; - layer.isEnabled = true; - layer.needsPixelBbox = false; - layer.bboxNeedsUpdate = false; - return; - } -}; +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'; export const selectCALayerOrThrow = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { const layer = state.layers.find((l) => l.id === layerId); @@ -93,6 +90,11 @@ export const selectIPALayerOrThrow = (state: ControlLayersState, layerId: string assert(isIPAdapterLayer(layer)); return layer; }; +export const selectIILayerOrThrow = (state: ControlLayersState, layerId: string): InitialImageLayer => { + const layer = state.layers.find((l) => l.id === layerId); + assert(isInitialImageLayer(layer)); + return layer; +}; const selectCAOrIPALayerOrThrow = ( state: ControlLayersState, layerId: string @@ -110,7 +112,7 @@ export const selectRGLayerIPAdapterOrThrow = ( state: ControlLayersState, layerId: string, ipAdapterId: string -): IPAdapterConfig => { +): IPAdapterConfigV2 => { const layer = state.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer)); const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); @@ -151,6 +153,9 @@ export const controlLayersSlice = createSlice({ layer.x = x; layer.y = y; } + if (isRegionalGuidanceLayer(layer)) { + layer.uploadedMaskImage = null; + } }, layerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => { const { layerId, bbox } = action.payload; @@ -161,14 +166,21 @@ export const controlLayersSlice = createSlice({ if (bbox === null && layer.type === 'regional_guidance_layer') { // The layer was fully erased, empty its objects to prevent accumulation of invisible objects layer.maskObjects = []; + layer.uploadedMaskImage = null; layer.needsPixelBbox = false; } } }, layerReset: (state, action: PayloadAction) => { const layer = state.layers.find((l) => l.id === action.payload); - if (layer) { - resetLayer(layer); + // TODO(psyche): Should other layer types also have reset functionality? + if (isRegionalGuidanceLayer(layer)) { + layer.maskObjects = []; + layer.bbox = null; + layer.isEnabled = true; + layer.needsPixelBbox = false; + layer.bboxNeedsUpdate = false; + layer.uploadedMaskImage = null; } }, layerDeleted: (state, action: PayloadAction) => { @@ -201,12 +213,6 @@ export const controlLayersSlice = createSlice({ moveToFront(renderableLayers, cb); state.layers = [...ipAdapterLayers, ...renderableLayers]; }, - selectedLayerReset: (state) => { - const layer = state.layers.find((l) => l.id === state.selectedLayerId); - if (layer) { - resetLayer(layer); - } - }, selectedLayerDeleted: (state) => { state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId); state.selectedLayerId = state.layers[0]?.id ?? null; @@ -221,7 +227,7 @@ export const controlLayersSlice = createSlice({ caLayerAdded: { reducer: ( state, - action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }> + action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2 }> ) => { const { layerId, controlAdapter } = action.payload; const layer: ControlAdapterLayer = { @@ -245,7 +251,7 @@ export const controlLayersSlice = createSlice({ } } }, - prepare: (controlAdapter: ControlNetConfig | T2IAdapterConfig) => ({ + prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({ payload: { layerId: uuidv4(), controlAdapter }, }), }, @@ -297,7 +303,7 @@ export const controlLayersSlice = createSlice({ layer.controlAdapter = controlNetToT2IAdapter(layer.controlAdapter); } - const candidateProcessorConfig = buildControlAdapterProcessor(modelConfig); + const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig); if (candidateProcessorConfig?.type !== layer.controlAdapter.processorConfig?.type) { // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth // model. We need to use the new processor. @@ -305,7 +311,7 @@ export const controlLayersSlice = createSlice({ layer.controlAdapter.processorConfig = candidateProcessorConfig; } }, - caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlMode }>) => { + caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlModeV2 }>) => { const { layerId, controlMode } = action.payload; const layer = selectCALayerOrThrow(state, layerId); assert(layer.controlAdapter.type === 'controlnet'); @@ -340,11 +346,17 @@ export const controlLayersSlice = createSlice({ const layer = selectCALayerOrThrow(state, layerId); layer.controlAdapter.isProcessingImage = isProcessingImage; }, + caLayerControlNetsDeleted: (state) => { + state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 'controlnet'); + }, + caLayerT2IAdaptersDeleted: (state) => { + state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 't2i_adapter'); + }, //#endregion //#region IP Adapter Layers ipaLayerAdded: { - reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { + reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { const { layerId, ipAdapter } = action.payload; const layer: IPAdapterLayer = { id: getIPALayerId(layerId), @@ -354,14 +366,14 @@ export const controlLayersSlice = createSlice({ }; state.layers.push(layer); }, - prepare: (ipAdapter: IPAdapterConfig) => ({ payload: { layerId: uuidv4(), ipAdapter } }), + prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }), }, ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethod }>) => { + ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethodV2 }>) => { const { layerId, method } = action.payload; const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.method = method; @@ -383,12 +395,15 @@ export const controlLayersSlice = createSlice({ }, ipaLayerCLIPVisionModelChanged: ( state, - action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }> + action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModelV2 }> ) => { const { layerId, clipVisionModel } = action.payload; const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.clipVisionModel = clipVisionModel; }, + ipaLayersDeleted: (state) => { + state.layers = state.layers.filter((l) => !isIPAdapterLayer(l)); + }, //#endregion //#region CA or IPA Layers @@ -435,6 +450,7 @@ export const controlLayersSlice = createSlice({ negativePrompt: null, ipAdapters: [], isSelected: true, + uploadedMaskImage: null, }; state.layers.push(layer); state.selectedLayerId = layer.id; @@ -484,6 +500,7 @@ export const controlLayersSlice = createSlice({ strokeWidth: state.brushSize, }); layer.bboxNeedsUpdate = true; + layer.uploadedMaskImage = null; if (!layer.needsPixelBbox && tool === 'eraser') { layer.needsPixelBbox = true; } @@ -503,6 +520,7 @@ export const controlLayersSlice = createSlice({ // TODO: Handle this in the event listener lastLine.points.push(point[0] - layer.x, point[1] - layer.y); layer.bboxNeedsUpdate = true; + layer.uploadedMaskImage = null; }, rgLayerRectAdded: { reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect; rectUuid: string }>) => { @@ -522,9 +540,15 @@ export const controlLayersSlice = createSlice({ height: rect.height, }); layer.bboxNeedsUpdate = true; + layer.uploadedMaskImage = null; }, prepare: (payload: { layerId: string; rect: IRect }) => ({ payload: { ...payload, rectUuid: uuidv4() } }), }, + rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => { + const { layerId, imageDTO } = action.payload; + const layer = selectRGLayerOrThrow(state, layerId); + layer.uploadedMaskImage = imageDTOToImageWithDims(imageDTO); + }, rgLayerAutoNegativeChanged: ( state, action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }> @@ -533,7 +557,7 @@ export const controlLayersSlice = createSlice({ const layer = selectRGLayerOrThrow(state, layerId); layer.autoNegative = autoNegative; }, - rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { + rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { const { layerId, ipAdapter } = action.payload; const layer = selectRGLayerOrThrow(state, layerId); layer.ipAdapters.push(ipAdapter); @@ -569,7 +593,7 @@ export const controlLayersSlice = createSlice({ }, rgLayerIPAdapterMethodChanged: ( state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethod }> + action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethodV2 }> ) => { const { layerId, ipAdapterId, method } = action.payload; const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); @@ -593,7 +617,7 @@ export const controlLayersSlice = createSlice({ }, rgLayerIPAdapterCLIPVisionModelChanged: ( state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModel }> + action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> ) => { const { layerId, ipAdapterId, clipVisionModel } = action.payload; const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); @@ -601,6 +625,49 @@ export const controlLayersSlice = createSlice({ }, //#endregion + //#region Initial Image Layer + iiLayerAdded: { + reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { + const { layerId, imageDTO } = action.payload; + // Highlander! There can be only one! + 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; @@ -617,20 +684,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; } @@ -728,7 +795,6 @@ export const { layerMovedToFront, layerMovedBackward, layerMovedToBack, - selectedLayerReset, selectedLayerDeleted, allLayersDeleted, // CA Layers @@ -741,12 +807,15 @@ export const { caLayerIsFilterEnabledChanged, caLayerOpacityChanged, caLayerIsProcessingImageChanged, + caLayerControlNetsDeleted, + caLayerT2IAdaptersDeleted, // IPA Layers ipaLayerAdded, ipaLayerImageChanged, ipaLayerMethodChanged, ipaLayerModelChanged, ipaLayerCLIPVisionModelChanged, + ipaLayersDeleted, // CA or IPA Layers caOrIPALayerWeightChanged, caOrIPALayerBeginEndStepPctChanged, @@ -758,6 +827,7 @@ export const { rgLayerLineAdded, rgLayerPointsAdded, rgLayerRectAdded, + rgLayerMaskImageUploaded, rgLayerAutoNegativeChanged, rgLayerIPAdapterAdded, rgLayerIPAdapterDeleted, @@ -767,6 +837,10 @@ export const { rgLayerIPAdapterMethodChanged, rgLayerIPAdapterModelChanged, rgLayerIPAdapterCLIPVisionModelChanged, + // II Layer + iiLayerAdded, + iiLayerImageChanged, + iiLayerOpacityChanged, // Globals positivePromptChanged, negativePromptChanged, @@ -789,11 +863,10 @@ 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); +export const $lastCursorPos = atom(null); // IDs for singleton Konva layers and objects export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer'; @@ -813,7 +886,10 @@ 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'; +export const COMPOSITING_RECT_NAME = 'compositing-rect'; // Getters for non-singleton layer and object IDs const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; @@ -823,6 +899,7 @@ export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${ export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`; const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`; export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; +export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`; export const controlLayersPersistConfig: PersistConfig = { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a4d88f3a0a..afb04aae37 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,4 +1,9 @@ -import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlLayers/util/controlAdapters'; +import type { + ControlNetConfigV2, + ImageWithDims, + IPAdapterConfigV2, + T2IAdapterConfigV2, +} from 'features/controlLayers/util/controlAdapters'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { ParameterAutoNegative, @@ -50,12 +55,12 @@ export type ControlAdapterLayer = RenderableLayerBase & { type: 'control_adapter_layer'; // technically, also t2i adapter layer opacity: number; isFilterEnabled: boolean; - controlAdapter: ControlNetConfig | T2IAdapterConfig; + controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; }; export type IPAdapterLayer = LayerBase & { type: 'ip_adapter_layer'; - ipAdapter: IPAdapterConfig; + ipAdapter: IPAdapterConfigV2; }; export type RegionalGuidanceLayer = RenderableLayerBase & { @@ -63,13 +68,20 @@ export type RegionalGuidanceLayer = RenderableLayerBase & { maskObjects: (VectorMaskLine | VectorMaskRect)[]; positivePrompt: ParameterPositivePrompt | null; negativePrompt: ParameterNegativePrompt | null; // Up to one text prompt per mask - ipAdapters: IPAdapterConfig[]; // Any number of image prompts + ipAdapters: IPAdapterConfigV2[]; // Any number of image prompts previewColor: RgbColor; autoNegative: ParameterAutoNegative; needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object + uploadedMaskImage: ImageWithDims | null; }; -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; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts index a4c7be6886..72aefe1eb4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts @@ -123,7 +123,7 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal return correctedLayerBbox; }; -export const getLayerBboxFast = (layer: KonvaLayerType): IRect | null => { +export const getLayerBboxFast = (layer: KonvaLayerType): IRect => { const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG); return { x: Math.floor(bbox.x), diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts index 656b759faa..880514bf7c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts @@ -4,20 +4,20 @@ import { assert } from 'tsafe'; import { describe, test } from 'vitest'; import type { - CLIPVisionModel, - ControlMode, + CLIPVisionModelV2, + ControlModeV2, DepthAnythingModelSize, - IPMethod, + IPMethodV2, ProcessorConfig, - ProcessorType, + ProcessorTypeV2, } from './controlAdapters'; describe('Control Adapter Types', () => { - test('ProcessorType', () => assert>()); - test('IP Adapter Method', () => assert, IPMethod>>()); + test('ProcessorType', () => assert>()); + test('IP Adapter Method', () => assert, IPMethodV2>>()); test('CLIP Vision Model', () => - assert, CLIPVisionModel>>()); - test('Control Mode', () => assert, ControlMode>>()); + assert, CLIPVisionModelV2>>()); + test('Control Mode', () => assert, ControlModeV2>>()); test('DepthAnything Model Size', () => assert, DepthAnythingModelSize>>()); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 6cedc81a0b..2964a2eb6c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -94,45 +94,45 @@ type ControlAdapterBase = { beginEndStepPct: [number, number]; }; -const zControlMode = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); -export type ControlMode = z.infer; -export const isControlMode = (v: unknown): v is ControlMode => zControlMode.safeParse(v).success; +const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); +export type ControlModeV2 = z.infer; +export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success; -export type ControlNetConfig = ControlAdapterBase & { +export type ControlNetConfigV2 = ControlAdapterBase & { type: 'controlnet'; model: ParameterControlNetModel | null; - controlMode: ControlMode; + controlMode: ControlModeV2; }; -export const isControlNetConfig = (ca: ControlNetConfig | T2IAdapterConfig): ca is ControlNetConfig => +export const isControlNetConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is ControlNetConfigV2 => ca.type === 'controlnet'; -export type T2IAdapterConfig = ControlAdapterBase & { +export type T2IAdapterConfigV2 = ControlAdapterBase & { type: 't2i_adapter'; model: ParameterT2IAdapterModel | null; }; -export const isT2IAdapterConfig = (ca: ControlNetConfig | T2IAdapterConfig): ca is T2IAdapterConfig => +export const isT2IAdapterConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is T2IAdapterConfigV2 => ca.type === 't2i_adapter'; -const zCLIPVisionModel = z.enum(['ViT-H', 'ViT-G']); -export type CLIPVisionModel = z.infer; -export const isCLIPVisionModel = (v: unknown): v is CLIPVisionModel => zCLIPVisionModel.safeParse(v).success; +const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); +export type CLIPVisionModelV2 = z.infer; +export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success; -const zIPMethod = z.enum(['full', 'style', 'composition']); -export type IPMethod = z.infer; -export const isIPMethod = (v: unknown): v is IPMethod => zIPMethod.safeParse(v).success; +const zIPMethodV2 = z.enum(['full', 'style', 'composition']); +export type IPMethodV2 = z.infer; +export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success; -export type IPAdapterConfig = { +export type IPAdapterConfigV2 = { id: string; type: 'ip_adapter'; weight: number; - method: IPMethod; + method: IPMethodV2; image: ImageWithDims | null; model: ParameterIPAdapterModel | null; - clipVisionModel: CLIPVisionModel; + clipVisionModel: CLIPVisionModelV2; beginEndStepPct: [number, number]; }; -const zProcessorType = z.enum([ +const zProcessorTypeV2 = z.enum([ 'canny_image_processor', 'color_map_image_processor', 'content_shuffle_image_processor', @@ -148,10 +148,10 @@ const zProcessorType = z.enum([ 'pidi_image_processor', 'zoe_depth_image_processor', ]); -export type ProcessorType = z.infer; -export const isProcessorType = (v: unknown): v is ProcessorType => zProcessorType.safeParse(v).success; +export type ProcessorTypeV2 = z.infer; +export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success; -type ProcessorData = { +type ProcessorData = { type: T; labelTKey: string; descriptionTKey: string; @@ -165,7 +165,7 @@ type ProcessorData = { const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); type CAProcessorsData = { - [key in ProcessorType]: ProcessorData; + [key in ProcessorTypeV2]: ProcessorData; }; /** * A dict of ControlNet processors, including: @@ -176,7 +176,7 @@ type CAProcessorsData = { * * TODO: Generate from the OpenAPI schema */ -export const CONTROLNET_PROCESSORS: CAProcessorsData = { +export const CA_PROCESSOR_DATA: CAProcessorsData = { canny_image_processor: { type: 'canny_image_processor', labelTKey: 'controlnet.canny', @@ -405,7 +405,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = { }, }; -const initialControlNet: Omit = { +export const initialControlNetV2: Omit = { type: 'controlnet', model: null, weight: 1, @@ -414,10 +414,10 @@ const initialControlNet: Omit = { image: null, processedImage: null, isProcessingImage: false, - processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), + processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -const initialT2IAdapter: Omit = { +export const initialT2IAdapterV2: Omit = { type: 't2i_adapter', model: null, weight: 1, @@ -425,10 +425,10 @@ const initialT2IAdapter: Omit = { image: null, processedImage: null, isProcessingImage: false, - processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), + processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), }; -const initialIPAdapter: Omit = { +export const initialIPAdapterV2: Omit = { type: 'ip_adapter', image: null, model: null, @@ -438,26 +438,26 @@ const initialIPAdapter: Omit = { weight: 1, }; -export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfig => { - return merge(deepClone(initialControlNet), { id, ...overrides }); +export const buildControlNet = (id: string, overrides?: Partial): ControlNetConfigV2 => { + return merge(deepClone(initialControlNetV2), { id, ...overrides }); }; -export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfig => { - return merge(deepClone(initialT2IAdapter), { id, ...overrides }); +export const buildT2IAdapter = (id: string, overrides?: Partial): T2IAdapterConfigV2 => { + return merge(deepClone(initialT2IAdapterV2), { id, ...overrides }); }; -export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfig => { - return merge(deepClone(initialIPAdapter), { id, ...overrides }); +export const buildIPAdapter = (id: string, overrides?: Partial): IPAdapterConfigV2 => { + return merge(deepClone(initialIPAdapterV2), { id, ...overrides }); }; -export const buildControlAdapterProcessor = ( +export const buildControlAdapterProcessorV2 = ( modelConfig: ControlNetModelConfig | T2IAdapterModelConfig ): ProcessorConfig | null => { const defaultPreprocessor = modelConfig.default_settings?.preprocessor; - if (!isProcessorType(defaultPreprocessor)) { + if (!isProcessorTypeV2(defaultPreprocessor)) { return null; } - const processorConfig = CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(modelConfig.base); + const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base); return processorConfig; }; @@ -467,15 +467,15 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO) height, }); -export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfig): ControlNetConfig => { +export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfigV2): ControlNetConfigV2 => { return { ...deepClone(t2iAdapter), type: 'controlnet', - controlMode: initialControlNet.controlMode, + controlMode: initialControlNetV2.controlMode, }; }; -export const controlNetToT2IAdapter = (controlNet: ControlNetConfig): T2IAdapterConfig => { +export const controlNetToT2IAdapter = (controlNet: ControlNetConfigV2): T2IAdapterConfigV2 => { return { ...omit(deepClone(controlNet), 'controlMode'), type: 't2i_adapter', diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index 85f48baa6e..f58b1e3b74 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -1,16 +1,21 @@ import { getStore } from 'app/store/nanostores/store'; import { rgbaColorToString, rgbColorToString } from 'features/canvas/util/colorToString'; -import { getScaledFlooredCursorPosition } from 'features/controlLayers/hooks/mouseEventHooks'; +import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/hooks/mouseEventHooks'; import { $tool, BACKGROUND_LAYER_ID, BACKGROUND_RECT_ID, CA_LAYER_IMAGE_NAME, CA_LAYER_NAME, + COMPOSITING_RECT_NAME, getCALayerImageId, + getIILayerImageId, getLayerBboxId, getRGLayerObjectGroupId, + INITIAL_IMAGE_LAYER_IMAGE_NAME, + INITIAL_IMAGE_LAYER_NAME, isControlAdapterLayer, + isInitialImageLayer, isRegionalGuidanceLayer, isRenderableLayer, LAYER_BBOX_NAME, @@ -28,6 +33,7 @@ import { } from 'features/controlLayers/store/controlLayersSlice'; import type { ControlAdapterLayer, + InitialImageLayer, Layer, RegionalGuidanceLayer, Tool, @@ -35,6 +41,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'; @@ -52,7 +59,8 @@ const STAGE_BG_DATAURL = const mapId = (object: { id: string }) => object.id; -const selectRenderableLayers = (n: Konva.Node) => n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME; +const selectRenderableLayers = (n: Konva.Node) => + n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME || n.name() === INITIAL_IMAGE_LAYER_NAME; const selectVectorMaskObjects = (node: Konva.Node) => { return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; @@ -137,10 +145,9 @@ const renderToolPreview = ( globalMaskLayerOpacity: number, cursorPos: Vector2d | null, lastMouseDownPos: Vector2d | null, - isMouseOver: boolean, brushSize: number ) => { - const layerCount = stage.find(`.${RG_LAYER_NAME}`).length; + const layerCount = stage.find(selectRenderableLayers).length; // Update the stage's pointer style if (layerCount === 0) { // We have no layers, so we should not render any tool @@ -161,7 +168,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; @@ -205,12 +212,13 @@ const renderToolPreview = ( } if (cursorPos && lastMouseDownPos && tool === 'rect') { + const snappedPos = snapPosToStage(cursorPos, stage); const rectPreview = toolPreviewLayer.findOne(`#${TOOL_PREVIEW_RECT_ID}`); rectPreview?.setAttrs({ - x: Math.min(cursorPos.x, lastMouseDownPos.x), - y: Math.min(cursorPos.y, lastMouseDownPos.y), - width: Math.abs(cursorPos.x - lastMouseDownPos.x), - height: Math.abs(cursorPos.y - lastMouseDownPos.y), + x: Math.min(snappedPos.x, lastMouseDownPos.x), + y: Math.min(snappedPos.y, lastMouseDownPos.y), + width: Math.abs(snappedPos.x - lastMouseDownPos.x), + height: Math.abs(snappedPos.y - lastMouseDownPos.y), }); rectPreview?.visible(true); } else { @@ -317,6 +325,12 @@ const createVectorMaskRect = (reduxObject: VectorMaskRect, konvaGroup: Konva.Gro return vectorMaskRect; }; +const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => { + const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false }); + konvaLayer.add(compositingRect); + return compositingRect; +}; + /** * Renders a vector mask layer. * @param stage The konva stage to render on. @@ -394,19 +408,157 @@ const renderRegionalGuidanceLayer = ( groupNeedsCache = true; } - if (konvaObjectGroup.children.length === 0) { + if (konvaObjectGroup.getChildren().length === 0) { // No objects - clear the cache to reset the previous pixel data konvaObjectGroup.clearCache(); - } else if (groupNeedsCache) { - konvaObjectGroup.cache(); + return; } - // Updating group opacity does not require re-caching - if (konvaObjectGroup.opacity() !== globalMaskLayerOpacity) { + const compositingRect = + konvaLayer.findOne(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer); + + /** + * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows + * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. + * + * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The + * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. + * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. + * + * Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to + * a single raster image, and _then_ applied the 50% opacity. + */ + if (reduxLayer.isSelected && tool !== 'move') { + // We must clear the cache first so Konva will re-draw the group with the new compositing rect + if (konvaObjectGroup.isCached()) { + konvaObjectGroup.clearCache(); + } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + konvaObjectGroup.opacity(1); + + compositingRect.setAttrs({ + // The rect should be the size of the layer - use the fast method bc it's OK if the rect is larger + ...getLayerBboxFast(konvaLayer), + fill: rgbColor, + opacity: globalMaskLayerOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: konvaObjectGroup.getChildren().length, + }); + } else { + // The compositing rect should only be shown when the layer is selected. + compositingRect.visible(false); + // Cache only if needed - or if we are on this code path and _don't_ have a cache + if (groupNeedsCache || !konvaObjectGroup.isCached()) { + konvaObjectGroup.cache(); + } + // Updating group opacity does not require re-caching konvaObjectGroup.opacity(globalMaskLayerOpacity); } }; +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, @@ -547,6 +699,9 @@ const renderLayers = ( if (isControlAdapterLayer(reduxLayer)) { renderControlNetLayer(stage, reduxLayer); } + if (isInitialImageLayer(reduxLayer)) { + renderInitialImageLayer(stage, reduxLayer); + } } }; @@ -711,7 +866,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 7d109473ed..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: { @@ -55,6 +51,13 @@ export type RGLayerIPAdapterImageDropData = BaseDropData & { }; }; +export type IILayerImageDropData = BaseDropData & { + actionType: 'SET_II_LAYER_IMAGE'; + context: { + layerId: string; + }; +}; + export type CanvasInitialImageDropData = BaseDropData & { actionType: 'SET_CANVAS_INITIAL_IMAGE'; }; @@ -78,7 +81,6 @@ export type RemoveFromBoardDropData = BaseDropData & { export type TypesafeDroppableData = | CurrentImageDropData - | InitialImageDropData | ControlAdapterDropData | CanvasInitialImageDropData | NodesImageDropData @@ -86,7 +88,8 @@ export type TypesafeDroppableData = | RemoveFromBoardDropData | CALayerImageDropData | IPALayerImageDropData - | RGLayerIPAdapterImageDropData; + | RGLayerIPAdapterImageDropData + | IILayerImageDropData; type BaseDragData = { id: string; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index c1da111087..b701c72947 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -15,8 +15,6 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: switch (actionType) { case 'SET_CURRENT_IMAGE': return payloadType === 'IMAGE_DTO'; - case 'SET_INITIAL_IMAGE': - return payloadType === 'IMAGE_DTO'; case 'SET_CONTROL_ADAPTER_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SET_CA_LAYER_IMAGE': @@ -25,6 +23,8 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: return payloadType === 'IMAGE_DTO'; case 'SET_RG_LAYER_IP_ADAPTER_IMAGE': return payloadType === 'IMAGE_DTO'; + case 'SET_II_LAYER_IMAGE': + return payloadType === 'IMAGE_DTO'; case 'SET_CANVAS_INITIAL_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SET_NODES_IMAGE': diff --git a/invokeai/frontend/web/src/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 72% 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..35abf07965 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,17 +51,12 @@ 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 onMouseOver = useCallback(() => { setShouldShowNextPrevButtons(true); window.clearTimeout(timeoutId.current); }, []); - - const handleMouseOut = useCallback(() => { + const onMouseOut = useCallback(() => { timeoutId.current = window.setTimeout(() => { setShouldShowNextPrevButtons(false); }, 500); @@ -68,8 +64,8 @@ const CurrentImagePreview = () => { return ( { dataTestId="image-preview" /> )} - {shouldShowImageDetails && imageDTO && ( - - - - )} - {!shouldShowImageDetails && imageDTO && shouldShowNextPrevButtons && ( - + {shouldShowImageDetails && imageDTO && ( + + + + )} + + + {shouldShowNextPrevButtons && imageDTO && ( + - + )} @@ -112,18 +132,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..cc2aa8c543 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/EditorButton.tsx @@ -0,0 +1,39 @@ +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 { PiArrowsDownUpBold } from 'react-icons/pi'; + +import { useImageViewer } from './useImageViewer'; + +const TAB_NAME_TO_TKEY_SHORT: Record = { + generation: 'controlLayers.controlLayers', + 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_SHORT[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..949e72fad1 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -0,0 +1,92 @@ +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]); + + // The AnimatePresence mode must be wait - else framer can get confused if you spam the toggle button + 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..edceb5099c --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx @@ -0,0 +1,25 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowsDownUpBold } from 'react-icons/pi'; + +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..5248977825 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import { setActiveTab } from 'features/ui/store/uiSlice'; import { uniqBy } from 'lodash-es'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; @@ -21,6 +22,7 @@ const initialGalleryState: GalleryState = { boardSearchText: '', limit: INITIAL_IMAGE_LIMIT, offset: 0, + isImageViewerOpen: false, }; export const gallerySlice = createSlice({ @@ -75,8 +77,14 @@ export const gallerySlice = createSlice({ alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction) => { state.alwaysShowImageSizeBadge = action.payload; }, + isImageViewerOpenChanged: (state, action: PayloadAction) => { + state.isImageViewerOpen = action.payload; + }, }, extraReducers: (builder) => { + builder.addCase(setActiveTab, (state) => { + state.isImageViewerOpen = false; + }); builder.addMatcher(isAnyBoardDeleted, (state, action) => { const deletedBoardId = action.meta.arg.originalArgs; if (deletedBoardId === state.selectedBoardId) { @@ -112,6 +120,7 @@ export const { boardSearchTextChanged, moreImagesLoaded, alwaysShowImageSizeBadgeChanged, + isImageViewerOpenChanged, } = gallerySlice.actions; const isAnyBoardDeleted = isAnyOf( @@ -133,5 +142,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/util/graph/addControlLayersToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts index 4581b51ee1..30c15fae10 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlLayersToGraph.ts @@ -4,15 +4,17 @@ import { isControlAdapterLayer, isIPAdapterLayer, isRegionalGuidanceLayer, + rgLayerMaskImageUploaded, } from 'features/controlLayers/store/controlLayersSlice'; +import type { RegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { - type ControlNetConfig, + type ControlNetConfigV2, type ImageWithDims, - type IPAdapterConfig, - isControlNetConfig, - isT2IAdapterConfig, + type IPAdapterConfigV2, + isControlNetConfigV2, + isT2IAdapterConfigV2, type ProcessorConfig, - type T2IAdapterConfig, + type T2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs'; import type { ImageField } from 'features/nodes/types/common'; @@ -32,12 +34,13 @@ import { } 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 { getImageDTO, imagesApi } from 'services/api/endpoints/images'; import type { CollectInvocation, ControlNetInvocation, CoreMetadataInvocation, Edge, + ImageDTO, IPAdapterInvocation, NonNullableGraph, S, @@ -64,7 +67,7 @@ const buildControlImage = ( assert(false, 'Attempted to add unprocessed control image'); }; -const buildControlNetMetadata = (controlNet: ControlNetConfig): S['ControlNetMetadataField'] => { +const buildControlNetMetadata = (controlNet: ControlNetConfigV2): S['ControlNetMetadataField'] => { const { beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet; assert(model, 'ControlNet model is required'); @@ -113,7 +116,7 @@ const addControlNetCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: stri }; const addGlobalControlNetsToGraph = async ( - controlNets: ControlNetConfig[], + controlNets: ControlNetConfigV2[], graph: NonNullableGraph, denoiseNodeId: string ) => { @@ -157,7 +160,7 @@ const addGlobalControlNetsToGraph = async ( upsertMetadata(graph, { controlnets: controlNetMetadata }); }; -const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfig): S['T2IAdapterMetadataField'] => { +const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfigV2): S['T2IAdapterMetadataField'] => { const { beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter; assert(model, 'T2I Adapter model is required'); @@ -205,7 +208,7 @@ const addT2IAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: stri }; const addGlobalT2IAdaptersToGraph = async ( - t2iAdapters: T2IAdapterConfig[], + t2iAdapters: T2IAdapterConfigV2[], graph: NonNullableGraph, denoiseNodeId: string ) => { @@ -249,7 +252,7 @@ const addGlobalT2IAdaptersToGraph = async ( upsertMetadata(graph, { t2iAdapters: t2iAdapterMetadata }); }; -const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfig): S['IPAdapterMetadataField'] => { +const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfigV2): S['IPAdapterMetadataField'] => { const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter; assert(model, 'IP Adapter model is required'); @@ -290,7 +293,7 @@ const addIPAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: strin }; const addGlobalIPAdaptersToGraph = async ( - ipAdapters: IPAdapterConfig[], + ipAdapters: IPAdapterConfigV2[], graph: NonNullableGraph, denoiseNodeId: string ) => { @@ -337,7 +340,6 @@ const addGlobalIPAdaptersToGraph = async ( }; export const addControlLayersToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => { - const { dispatch } = getStore(); const mainModel = state.generation.model; assert(mainModel, 'Missing main model when building graph'); const isSDXL = mainModel.base === 'sdxl'; @@ -351,7 +353,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab // We want the CAs themselves .map((l) => l.controlAdapter) // Must be a ControlNet - .filter(isControlNetConfig) + .filter(isControlNetConfigV2) .filter((ca) => { const hasModel = Boolean(ca.model); const modelMatchesBase = ca.model?.base === mainModel.base; @@ -368,7 +370,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab // We want the CAs themselves .map((l) => l.controlAdapter) // Must have a ControlNet CA - .filter(isT2IAdapterConfig) + .filter(isT2IAdapterConfigV2) .filter((ca) => { const hasModel = Boolean(ca.model); const modelMatchesBase = ca.model?.base === mainModel.base; @@ -404,10 +406,6 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab return hasTextPrompt || hasIPAdapter; }); - const layerIds = rgLayers.map((l) => l.id); - const blobs = await getRegionalPromptLayerBlobs(layerIds); - assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); - // TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing // the existing conditioning nodes. @@ -470,22 +468,15 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab }, }); - // 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). + const layerIds = rgLayers.map((l) => l.id); + const blobs = await getRegionalPromptLayerBlobs(layerIds); + assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs'); + for (const layer of rgLayers) { const blob = blobs[layer.id]; assert(blob, `Blob for layer ${layer.id} not found`); - - const file = new File([blob], `${layer.id}_mask.png`, { type: 'image/png' }); - const req = dispatch( - imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) - ); - req.reset(); - - // TODO: This will raise on network error - const { image_name } = await req.unwrap(); + // Upload the mask image, or get the cached image if it exists + const { image_name } = await getMaskImage(layer, blob); // The main mask-to-tensor node const maskToTensorNode: S['AlphaMaskToTensorInvocation'] = { @@ -633,7 +624,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab } // TODO(psyche): For some reason, I have to explicitly annotate regionalIPAdapters here. Not sure why. - const regionalIPAdapters: IPAdapterConfig[] = layer.ipAdapters.filter((ipAdapter) => { + const regionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipAdapter) => { const hasModel = Boolean(ipAdapter.model); const modelMatchesBase = ipAdapter.model?.base === mainModel.base; const hasControlImage = Boolean(ipAdapter.image); @@ -679,3 +670,23 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab } } }; + +const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise => { + if (layer.uploadedMaskImage) { + const imageDTO = await getImageDTO(layer.uploadedMaskImage.imageName); + if (imageDTO) { + return imageDTO; + } + } + const { dispatch } = getStore(); + // No cached mask, or the cached image no longer exists - we need to upload the mask image + const file = new File([blob], `${layer.id}_mask.png`, { type: 'image/png' }); + const req = dispatch( + imagesApi.endpoints.uploadImage.initiate({ file, image_category: 'mask', is_intermediate: true }) + ); + req.reset(); + + const imageDTO = await req.unwrap(); + dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO })); + return imageDTO; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts index 363d97badf..531c88335b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts @@ -31,9 +31,9 @@ export const addControlNetToLinearGraph = async ( } ); - // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper. + // The generation tab has special handling - its control adapters are set up in the Control Layers graph helper. const activeTabName = activeTabNameSelector(state); - assert(activeTabName !== 'txt2img', 'Tried to use addControlNetToLinearGraph on txt2img tab'); + assert(activeTabName !== 'generation', 'Tried to use addControlNetToLinearGraph on generation tab'); if (controlNets.length) { // Even though denoise_latents' control input is collection or scalar, keep it simple and always use a collect diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts index 5abf07740a..d6709f7058 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts @@ -106,7 +106,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = if (!state.hrf.hrfEnabled || state.config.disabledSDFeatures.includes('hrf')) { return; } - const log = logger('txt2img'); + const log = logger('generation'); const { vae, seamlessXAxis, seamlessYAxis } = state.generation; const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts index 12ba4e12a8..2cf93100eb 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts @@ -20,9 +20,9 @@ export const addIPAdapterToLinearGraph = async ( graph: NonNullableGraph, baseNodeId: string ): Promise => { - // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper. + // The generation tab has special handling - its control adapters are set up in the Control Layers graph helper. const activeTabName = activeTabNameSelector(state); - assert(activeTabName !== 'txt2img', 'Tried to use addT2IAdaptersToLinearGraph on txt2img tab'); + assert(activeTabName !== 'generation', 'Tried to use addT2IAdaptersToLinearGraph on generation tab'); const ipAdapters = selectValidIPAdapters(state.controlAdapters).filter(({ model, controlImage, isEnabled }) => { const hasModel = Boolean(model); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts new file mode 100644 index 0000000000..eae45acc5b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addInitialImageToLinearGraph.ts @@ -0,0 +1,130 @@ +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'; + +/** + * Returns true if an initial image was added, false if not. + */ +export const addInitialImageToLinearGraph = ( + state: RootState, + graph: NonNullableGraph, + denoiseNodeId: string +): boolean => { + // 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 false; + } + + 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, + }); + + return true; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts index 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 ddd87256f4..ee21bbff1b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts @@ -20,9 +20,9 @@ export const addT2IAdaptersToLinearGraph = async ( graph: NonNullableGraph, baseNodeId: string ): Promise => { - // The txt2img tab has special handling - its control adapters are set up in the Control Layers graph helper. + // The generation tab has special handling - its control adapters are set up in the Control Layers graph helper. const activeTabName = activeTabNameSelector(state); - assert(activeTabName !== 'txt2img', 'Tried to use addT2IAdaptersToLinearGraph on txt2img tab'); + assert(activeTabName !== 'generation', 'Tried to use addT2IAdaptersToLinearGraph on generation tab'); const t2iAdapters = selectValidT2IAdapters(state.controlAdapters).filter( ({ model, processedControlImage, processorType, controlImage, isEnabled }) => { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/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/buildAdHocUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts index 52c09b1db0..6c90dafd25 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; +import { getBoardField } from 'features/nodes/util/graph/graphBuilderUtils'; import type { ESRGANInvocation, Graph, NonNullableGraph } from 'services/api/types'; import { ESRGAN } from './constants'; @@ -18,7 +18,7 @@ export const buildAdHocUpscaleGraph = ({ image_name, state }: Arg): Graph => { type: 'esrgan', image: { image_name }, model_name: esrganModelName, - is_intermediate: getIsIntermediate(state), + is_intermediate: false, board: getBoardField(state), }; 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 94% 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 340a24bca4..41f9f4f748 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildGenerationTabGraph.ts @@ -2,6 +2,7 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers'; import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph'; +import { addInitialImageToLinearGraph } from 'features/nodes/util/graph/addInitialImageToLinearGraph'; import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types'; @@ -13,6 +14,7 @@ import { addVAEToGraph } from './addVAEToGraph'; import { addWatermarkerToGraph } from './addWatermarkerToGraph'; import { CLIP_SKIP, + CONTROL_LAYERS_GRAPH, DENOISE_LATENTS, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, @@ -20,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, @@ -66,7 +67,7 @@ export const buildLinearTextToImageGraph = async (state: RootState): Promise => { +export const buildGenerationTabSDXLGraph = async (state: RootState): Promise => { const log = logger('nodes'); const { model, @@ -70,7 +71,7 @@ export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise // copy-pasted graph from node editor, filled in with state values & friendly node ids const graph: NonNullableGraph = { - id: SDXL_TEXT_TO_IMAGE_GRAPH, + id: SDXL_CONTROL_LAYERS_GRAPH, nodes: { [modelLoaderNodeId]: { type: 'sdxl_model_loader', @@ -223,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, @@ -241,6 +242,8 @@ export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise LATENTS_TO_IMAGE ); + addInitialImageToLinearGraph(state, graph, SDXL_DENOISE_LATENTS); + // Add Seamless To Graph if (seamlessXAxis || seamlessYAxis) { addSeamlessToLinearGraph(state, graph, modelLoaderNodeId); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/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/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/ImageSizeLinear.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx index 0b28200ca2..ddf4997a16 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSizeLinear.tsx @@ -50,7 +50,7 @@ export const ImageSizeLinear = memo(() => { aspectRatioState={aspectRatioState} heightComponent={} widthComponent={} - previewComponent={tab === 'txt2img' ? : } + previewComponent={tab === 'generation' ? : } onChangeAspectRatioState={onChangeAspectRatioState} onChangeWidth={onChangeWidth} onChangeHeight={onChangeHeight} diff --git a/invokeai/frontend/web/src/features/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; @@ -260,8 +255,9 @@ 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/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 5b88170d41..a153780712 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -193,8 +193,9 @@ export type RGLayerIPAdapterImagePostUploadAction = { ipAdapterId: string; }; -type InitialImageAction = { - type: 'SET_INITIAL_IMAGE'; +export type IILayerImagePostUploadAction = { + type: 'SET_II_LAYER_IMAGE'; + layerId: string; }; type NodesAction = { @@ -218,11 +219,11 @@ type AddToBatchAction = { export type PostUploadAction = | ControlAdapterAction - | InitialImageAction | NodesAction | CanvasInitialImageAction | ToastAction | AddToBatchAction | CALayerImagePostUploadAction | IPALayerImagePostUploadAction - | RGLayerIPAdapterImagePostUploadAction; + | 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"