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 (
+ }
+ >
+ {t(TAB_NAME_TO_TKEY_SHORT[activeTabName])}
+
+ );
+};
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 (
+ }
+ >
+ {t('common.viewer')}
+
+ );
+};
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')}{' '}
- {t('modelManager.modelManager')}.
+ {t('ui.tabs.modelsTab')}.
);
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"