Merge branch 'main' into lstein/feat/simple-mm2-api

This commit is contained in:
Lincoln Stein 2024-05-04 17:01:15 -04:00 committed by GitHub
commit 8e5e9b53d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
163 changed files with 3124 additions and 3148 deletions

View File

@ -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. 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]
<div align="center"> <div align="center">

View File

@ -4,278 +4,6 @@ title: Training
# :material-file-document: Training # :material-file-document: Training
# Textual Inversion Training Invoke Training has moved to its own repository, with a dedicated UI for accessing common scripts like Textual Inversion and LoRA training.
## **Personalizing Text-to-Image Generation**
You may personalize the generated images to provide your own styles or objects You can find more by visiting the repo at https://github.com/invoke-ai/invoke-training
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
<https://huggingface.co/sd-concepts-library> 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:
<figure markdown>
![ti-frontend](../assets/textual-inversion/ti-frontend.png)
</figure>
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. <Tab> and <shift-TAB> 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 <space> 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 `<psychedelic>`.
### 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 <enter>. 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 <psychedelic> 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='<psychedelic>' \
--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 <trigger>. 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

View File

@ -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. Be sure to review the [installation requirements] and ensure your system has everything it needs to install Invoke.

View File

@ -1,4 +1,4 @@
# Installation Overview # Installation and Updating Overview
Before installing, review the [installation requirements] to ensure your system is set up properly. 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]. If you need more help, join our [discord] or [create an issue].
<h2>Automatic Install</h2> <h2>Automatic Install & Updates </h2>
✅ The automatic install is the best way to run InvokeAI. Check out the [installation guide] to get started. ✅ 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.
<h2>Manual Install</h2> <h2>Manual Install</h2>
If you are familiar with python and want more control over the packages that are installed, you can [install InvokeAI manually via PyPI]. 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.
<h2>Developer Install</h2> <h2>Developer Install</h2>
If you want to contribute to InvokeAI, consult the [developer install guide]. If you want to contribute to InvokeAI, consult the [developer install guide].

View File

@ -58,7 +58,7 @@
"@dnd-kit/sortable": "^8.0.0", "@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/inter": "^5.0.17", "@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", "@nanostores/react": "^0.7.2",
"@reduxjs/toolkit": "2.2.2", "@reduxjs/toolkit": "2.2.2",
"@roarr/browser-log-writer": "^1.3.0", "@roarr/browser-log-writer": "^1.3.0",

View File

@ -7,7 +7,7 @@ settings:
dependencies: dependencies:
'@chakra-ui/react': '@chakra-ui/react':
specifier: ^2.8.2 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': '@chakra-ui/react-use-size':
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0(react@18.2.0) version: 2.1.0(react@18.2.0)
@ -30,8 +30,8 @@ dependencies:
specifier: ^5.0.17 specifier: ^5.0.17
version: 5.0.17 version: 5.0.17
'@invoke-ai/ui-library': '@invoke-ai/ui-library':
specifier: ^0.0.21 specifier: ^0.0.25
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) 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': '@nanostores/react':
specifier: ^0.7.2 specifier: ^0.7.2
version: 0.7.2(nanostores@0.10.0)(react@18.2.0) version: 0.7.2(nanostores@0.10.0)(react@18.2.0)
@ -306,7 +306,7 @@ packages:
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
dev: true 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==} resolution: {integrity: sha512-1yG2MrzUlix6KthjQMCNiHnkXrWwEdFAX6D+HqGJaNu0XvaGul2J+wDNtjsdX+gxiWu1nXXEEOAWlFVYMUf65w==}
dependencies: dependencies:
'@zag-js/accordion': 0.32.1 '@zag-js/accordion': 0.32.1
@ -318,7 +318,7 @@ packages:
'@zag-js/color-utils': 0.32.1 '@zag-js/color-utils': 0.32.1
'@zag-js/combobox': 0.32.1 '@zag-js/combobox': 0.32.1
'@zag-js/date-picker': 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/dialog': 0.32.1
'@zag-js/editable': 0.32.1 '@zag-js/editable': 0.32.1
'@zag-js/file-upload': 0.32.1 '@zag-js/file-upload': 0.32.1
@ -345,13 +345,13 @@ packages:
- '@internationalized/date' - '@internationalized/date'
dev: false 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==} resolution: {integrity: sha512-JHjNoIX50+mUCTaEGMjfGQWGGi31pKsV646jZJlR/1xohpYJigzg8BvO97cTsVk8fwtur+cm11gz3Nf7f5QUnA==}
peerDependencies: peerDependencies:
react: '>=18.0.0' react: '>=18.0.0'
react-dom: '>=18.0.0' react-dom: '>=18.0.0'
dependencies: 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/accordion': 0.32.1
'@zag-js/avatar': 0.32.1 '@zag-js/avatar': 0.32.1
'@zag-js/carousel': 0.32.1 '@zag-js/carousel': 0.32.1
@ -361,7 +361,7 @@ packages:
'@zag-js/combobox': 0.32.1 '@zag-js/combobox': 0.32.1
'@zag-js/core': 0.32.1 '@zag-js/core': 0.32.1
'@zag-js/date-picker': 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/dialog': 0.32.1
'@zag-js/editable': 0.32.1 '@zag-js/editable': 0.32.1
'@zag-js/file-upload': 0.32.1 '@zag-js/file-upload': 0.32.1
@ -1681,7 +1681,7 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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==} resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==}
peerDependencies: peerDependencies:
'@chakra-ui/system': '>=2.0.0' '@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-controllable-state': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-merge-refs': 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/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/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.6)(react@18.2.0) '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0)
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: 18.2.0
dev: false dev: false
@ -1848,16 +1848,6 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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): /@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.4)(react@18.2.0):
resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==} resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==}
peerDependencies: peerDependencies:
@ -1905,18 +1895,6 @@ packages:
resolution: {integrity: sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==} resolution: {integrity: sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==}
dev: false 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): /@chakra-ui/focus-lock@2.1.0(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==} resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==}
peerDependencies: peerDependencies:
@ -1924,7 +1902,7 @@ packages:
dependencies: dependencies:
'@chakra-ui/dom-utils': 2.1.0 '@chakra-ui/dom-utils': 2.1.0
react: 18.2.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: transitivePeerDependencies:
- '@types/react' - '@types/react'
dev: false dev: false
@ -2100,59 +2078,6 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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): /@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==} resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==}
peerDependencies: peerDependencies:
@ -2170,11 +2095,37 @@ packages:
'@chakra-ui/shared-utils': 2.0.5 '@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/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) '@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) framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0 react: 18.2.0
react-dom: 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: transitivePeerDependencies:
- '@types/react' - '@types/react'
dev: false dev: false
@ -2248,7 +2199,7 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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==} resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==}
peerDependencies: peerDependencies:
'@chakra-ui/system': '>=2.0.0' '@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-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/react-use-merge-refs': 2.1.0(react@18.2.0)
'@chakra-ui/shared-utils': 2.0.5 '@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/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
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: 18.2.0
dev: false dev: false
@ -2305,25 +2256,6 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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): /@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==} resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==}
peerDependencies: peerDependencies:
@ -2554,77 +2486,6 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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): /@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==} resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==}
peerDependencies: peerDependencies:
@ -2696,6 +2557,77 @@ packages:
- '@types/react' - '@types/react'
dev: false 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): /@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0):
resolution: {integrity: sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==} resolution: {integrity: sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==}
peerDependencies: peerDependencies:
@ -2814,7 +2746,7 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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==} resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==}
peerDependencies: peerDependencies:
'@chakra-ui/system': '>=2.0.0' '@chakra-ui/system': '>=2.0.0'
@ -2823,30 +2755,11 @@ packages:
dependencies: dependencies:
'@chakra-ui/checkbox': 2.3.2(@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/shared-utils': 2.0.5 '@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/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
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: 18.2.0
dev: false 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): /@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==} resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==}
peerDependencies: peerDependencies:
@ -2975,7 +2888,7 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false 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==} resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==}
peerDependencies: peerDependencies:
'@chakra-ui/system': 2.6.2 '@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/react-use-update-effect': 2.1.0(react@18.2.0)
'@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/styled-system': 2.9.2 '@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) '@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: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
@ -3020,7 +2933,7 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false 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==} resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==}
peerDependencies: peerDependencies:
'@chakra-ui/system': '>=2.0.0' '@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-event-listener': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-merge-refs': 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/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/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
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: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
@ -3064,17 +2977,6 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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: /@chakra-ui/utils@2.0.15:
resolution: {integrity: sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==} resolution: {integrity: sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==}
dependencies: dependencies:
@ -3198,12 +3100,6 @@ packages:
dev: false dev: false
optional: true 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: /@emotion/is-prop-valid@1.2.2:
resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==}
dependencies: dependencies:
@ -3220,27 +3116,6 @@ packages:
resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==}
dev: false 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): /@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==} resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==}
peerDependencies: peerDependencies:
@ -3276,27 +3151,6 @@ packages:
resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==}
dev: false 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): /@emotion/styled@11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==}
peerDependencies: peerDependencies:
@ -3663,16 +3517,16 @@ packages:
resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==}
dev: true dev: true
/@internationalized/date@3.5.2: /@internationalized/date@3.5.3:
resolution: {integrity: sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==} resolution: {integrity: sha512-X9bi8NAEHAjD8yzmPYT2pdJsbe+tYSEBAfowtlxJVJdZR3aK8Vg7ZUT1Fm5M47KLzp/M1p1VwAaeSma3RT7biw==}
dependencies: dependencies:
'@swc/helpers': 0.5.7 '@swc/helpers': 0.5.11
dev: false dev: false
/@internationalized/number@3.5.1: /@internationalized/number@3.5.1:
resolution: {integrity: sha512-N0fPU/nz15SwR9IbfJ5xaS9Ss/O5h1sVXMZf43vc9mxEG48ovglvvzBjF53aHlq20uoR6c+88CrIXipU/LSzwg==} resolution: {integrity: sha512-N0fPU/nz15SwR9IbfJ5xaS9Ss/O5h1sVXMZf43vc9mxEG48ovglvvzBjF53aHlq20uoR6c+88CrIXipU/LSzwg==}
dependencies: dependencies:
'@swc/helpers': 0.5.7 '@swc/helpers': 0.5.11
dev: false dev: false
/@invoke-ai/eslint-config-react@0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.3): /@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 prettier: 3.2.5
dev: true 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): /@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-tCvgkBPDt0gNq+8IcR03e/Mw7R8Mb/SMXTqx3FEIxlTQEo93A/D38dKXeDCzTdx4sQ+sknfB+JLBbHs6sg5hhQ==} resolution: {integrity: sha512-Fmjdlu62NXHgairYXGjcuCrxPEAl1G6Q6ban8g3excF6pDDdBeS7CmSNCyEDMxnSIOZrQlI04OhaMB17Imi9Uw==}
peerDependencies: peerDependencies:
'@fontsource-variable/inter': ^5.0.16 '@fontsource-variable/inter': ^5.0.16
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
dependencies: 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/anatomy': 2.2.2
'@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) '@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) '@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==} resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
dev: true dev: true
/@swc/helpers@0.5.7: /@swc/helpers@0.5.11:
resolution: {integrity: sha512-BVvNZhx362+l2tSwSuyEUV4h7+jk9raNdoTSdLfwTshXJSaGmYKluGRJznziCI3KX02Z19DdsQrdfrpXAU3Hfg==} resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==}
dependencies: dependencies:
tslib: 2.6.2 tslib: 2.6.2
dev: false dev: false
@ -5844,10 +5698,6 @@ packages:
resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==} resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==}
dev: true dev: true
/@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
dev: false
/@types/prop-types@15.7.12: /@types/prop-types@15.7.12:
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
@ -5877,14 +5727,6 @@ packages:
'@types/react': 18.2.73 '@types/react': 18.2.73
dev: false 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: /@types/react@18.2.73:
resolution: {integrity: sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==} resolution: {integrity: sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==}
dependencies: dependencies:
@ -5895,10 +5737,6 @@ packages:
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==}
dev: true dev: true
/@types/scheduler@0.16.8:
resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==}
dev: false
/@types/semver@7.5.8: /@types/semver@7.5.8:
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
dev: true dev: true
@ -6405,10 +6243,10 @@ packages:
/@zag-js/date-picker@0.32.1: /@zag-js/date-picker@0.32.1:
resolution: {integrity: sha512-n/hYmF+/R4+NuyfPRzCgeuLT6LJihKSuKzK29STPWy3sC/tBBHiqhNv1/4UKbatHUJXdBW2XF+N8Rw08RffcFQ==} resolution: {integrity: sha512-n/hYmF+/R4+NuyfPRzCgeuLT6LJihKSuKzK29STPWy3sC/tBBHiqhNv1/4UKbatHUJXdBW2XF+N8Rw08RffcFQ==}
dependencies: dependencies:
'@internationalized/date': 3.5.2 '@internationalized/date': 3.5.3
'@zag-js/anatomy': 0.32.1 '@zag-js/anatomy': 0.32.1
'@zag-js/core': 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/dismissable': 0.32.1
'@zag-js/dom-event': 0.32.1 '@zag-js/dom-event': 0.32.1
'@zag-js/dom-query': 0.32.1 '@zag-js/dom-query': 0.32.1
@ -6420,12 +6258,12 @@ packages:
'@zag-js/utils': 0.32.1 '@zag-js/utils': 0.32.1
dev: false 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==} resolution: {integrity: sha512-dbBDRSVr5pRUw3rXndyGuSshZiWqQI5JQO4D2KIFGkXzorj6WzoOpcO910Z7AdM/9cCAMpCjUrka8d8o9BpJBg==}
peerDependencies: peerDependencies:
'@internationalized/date': '>=3.0.0' '@internationalized/date': '>=3.0.0'
dependencies: dependencies:
'@internationalized/date': 3.5.2 '@internationalized/date': 3.5.3
dev: false dev: false
/@zag-js/dialog@0.32.1: /@zag-js/dialog@0.32.1:
@ -6999,13 +6837,6 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: false 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: /aria-query@5.1.3:
resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
dependencies: dependencies:
@ -9026,13 +8857,6 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: false 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: /focus-trap@7.5.4:
resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==} resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==}
dependencies: dependencies:
@ -9095,24 +8919,6 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: false 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: /framesync@6.1.2:
resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==}
dependencies: dependencies:
@ -11485,7 +11291,7 @@ packages:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
dev: false 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==} resolution: {integrity: sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==}
peerDependencies: peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
@ -11495,31 +11301,12 @@ packages:
optional: true optional: true
dependencies: dependencies:
'@babel/runtime': 7.23.9 '@babel/runtime': 7.23.9
'@types/react': 18.2.59 '@types/react': 18.2.73
focus-lock: 1.3.3 focus-lock: 1.3.3
prop-types: 15.8.1 prop-types: 15.8.1
react: 18.2.0 react: 18.2.0
react-clientside-effect: 1.2.6(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-callback-ref: 1.3.1(@types/react@18.2.73)(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-sidecar: 1.1.2(@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 dev: false
@ -11634,25 +11421,9 @@ packages:
use-sync-external-store: 1.2.0(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0)
dev: false 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==} resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==}
engines: {node: '>=10'} 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: peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
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 tslib: 2.6.2
dev: false 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==} resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==}
engines: {node: '>=10'} 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: peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
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: dependencies:
'@types/react': 18.2.73 '@types/react': 18.2.73
react: 18.2.0 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) react-style-singleton: 2.2.1(@types/react@18.2.73)(react@18.2.0)
tslib: 2.6.2 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) use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0)
dev: false dev: false
@ -11756,23 +11508,6 @@ packages:
- '@types/react' - '@types/react'
dev: false 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): /react-style-singleton@2.2.1(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -13288,24 +13023,9 @@ packages:
punycode: 2.3.1 punycode: 2.3.1
dev: true 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==} resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==}
engines: {node: '>=10'} 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: peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
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 react: 18.2.0
dev: false 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): /use-sidecar@1.1.2(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'} engines: {node: '>=10'}

View File

@ -88,11 +88,13 @@
"negativePrompt": "Negative Prompt", "negativePrompt": "Negative Prompt",
"discordLabel": "Discord", "discordLabel": "Discord",
"dontAskMeAgain": "Don't ask me again", "dontAskMeAgain": "Don't ask me again",
"editor": "Editor",
"error": "Error", "error": "Error",
"file": "File", "file": "File",
"folder": "Folder", "folder": "Folder",
"format": "format", "format": "format",
"githubLabel": "Github", "githubLabel": "Github",
"goTo": "Go to",
"hotkeysLabel": "Hotkeys", "hotkeysLabel": "Hotkeys",
"imageFailedToLoad": "Unable to Load Image", "imageFailedToLoad": "Unable to Load Image",
"img2img": "Image To Image", "img2img": "Image To Image",
@ -140,7 +142,8 @@
"blue": "Blue", "blue": "Blue",
"alpha": "Alpha", "alpha": "Alpha",
"selected": "Selected", "selected": "Selected",
"viewer": "Viewer" "viewer": "Viewer",
"tab": "Tab"
}, },
"controlnet": { "controlnet": {
"controlAdapter_one": "Control Adapter", "controlAdapter_one": "Control Adapter",
@ -361,7 +364,8 @@
"bulkDownloadRequestFailed": "Problem Preparing Download", "bulkDownloadRequestFailed": "Problem Preparing Download",
"bulkDownloadFailed": "Download Failed", "bulkDownloadFailed": "Download Failed",
"problemDeletingImages": "Problem Deleting Images", "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": { "hotkeys": {
"searchHotkeys": "Search Hotkeys", "searchHotkeys": "Search Hotkeys",
@ -584,6 +588,14 @@
"upscale": { "upscale": {
"desc": "Upscale the current image", "desc": "Upscale the current image",
"title": "Upscale" "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": { "metadata": {
@ -1522,7 +1534,7 @@
"moveForward": "Move Forward", "moveForward": "Move Forward",
"moveBackward": "Move Backward", "moveBackward": "Move Backward",
"brushSize": "Brush Size", "brushSize": "Brush Size",
"controlLayers": "Control Layers (BETA)", "controlLayers": "Control Layers",
"globalMaskOpacity": "Global Mask Opacity", "globalMaskOpacity": "Global Mask Opacity",
"autoNegative": "Auto Negative", "autoNegative": "Auto Negative",
"toggleVisibility": "Toggle Layer Visibility", "toggleVisibility": "Toggle Layer Visibility",
@ -1543,8 +1555,25 @@
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
"globalIPAdapter": "Global $t(common.ipAdapter)", "globalIPAdapter": "Global $t(common.ipAdapter)",
"globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)", "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)",
"globalInitialImage": "Global Initial Image",
"globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)",
"opacityFilter": "Opacity Filter", "opacityFilter": "Opacity Filter",
"clearProcessor": "Clear Processor", "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)"
}
} }
} }

View File

@ -20,8 +20,7 @@ export type LoggerNamespace =
| 'models' | 'models'
| 'config' | 'config'
| 'canvas' | 'canvas'
| 'txt2img' | 'generation'
| 'img2img'
| 'nodes' | 'nodes'
| 'system' | 'system'
| 'socketio' | 'socketio'

View File

@ -32,7 +32,6 @@ import { addImagesStarredListener } from 'app/store/middleware/listenerMiddlewar
import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred'; import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred';
import { addImageToDeleteSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected'; import { addImageToDeleteSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected';
import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded'; 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 { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected';
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded'; import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged'; import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged';
@ -73,9 +72,6 @@ const startAppListening = listenerMiddleware.startListening as AppStartListening
// Image uploaded // Image uploaded
addImageUploadedFulfilledListener(startAppListening); addImageUploadedFulfilledListener(startAppListening);
// Image selected
addInitialImageSelectedListener(startAppListening);
// Image deleted // Image deleted
addRequestedSingleImageDeletionListener(startAppListening); addRequestedSingleImageDeletionListener(startAppListening);
addDeleteBoardAndImagesFulfilledListener(startAppListening); addDeleteBoardAndImagesFulfilledListener(startAppListening);

View File

@ -1,9 +1,9 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice'; import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice';
import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => { export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => {
@ -14,19 +14,14 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
// Remove all deleted images from the UI // Remove all deleted images from the UI
let wasInitialImageReset = false;
let wasCanvasReset = false; let wasCanvasReset = false;
let wasNodeEditorReset = false; let wasNodeEditorReset = false;
let wereControlAdaptersReset = false; let wereControlAdaptersReset = false;
let wereControlLayersReset = false;
const { generation, canvas, nodes, controlAdapters } = getState(); const { canvas, nodes, controlAdapters, controlLayers } = getState();
deleted_images.forEach((image_name) => { deleted_images.forEach((image_name) => {
const imageUsage = getImageUsage(generation, canvas, nodes, controlAdapters, image_name); const imageUsage = getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name);
if (imageUsage.isInitialImage && !wasInitialImageReset) {
dispatch(clearInitialImage());
wasInitialImageReset = true;
}
if (imageUsage.isCanvasImage && !wasCanvasReset) { if (imageUsage.isCanvasImage && !wasCanvasReset) {
dispatch(resetCanvas()); dispatch(resetCanvas());
@ -42,6 +37,11 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
dispatch(controlAdaptersReset()); dispatch(controlAdaptersReset());
wereControlAdaptersReset = true; wereControlAdaptersReset = true;
} }
if (imageUsage.isControlLayerImage && !wereControlLayersReset) {
dispatch(allLayersDeleted());
wereControlLayersReset = true;
}
}); });
}, },
}); });

View File

@ -48,10 +48,12 @@ export const addCanvasImageToControlNetListener = (startAppListening: AppStartLi
}) })
).unwrap(); ).unwrap();
const { image_name } = imageDTO;
dispatch( dispatch(
controlAdapterImageChanged({ controlAdapterImageChanged({
id, id,
controlImage: imageDTO, controlImage: image_name,
}) })
); );
}, },

View File

@ -58,10 +58,12 @@ export const addCanvasMaskToControlNetListener = (startAppListening: AppStartLis
}) })
).unwrap(); ).unwrap();
const { image_name } = imageDTO;
dispatch( dispatch(
controlAdapterImageChanged({ controlAdapterImageChanged({
id, id,
controlImage: imageDTO, controlImage: image_name,
}) })
); );
}, },

View File

@ -10,7 +10,7 @@ import {
caLayerProcessorConfigChanged, caLayerProcessorConfigChanged,
isControlAdapterLayer, isControlAdapterLayer,
} from 'features/controlLayers/store/controlLayersSlice'; } 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 { isImageOutput } from 'features/nodes/types/common';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; 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... // @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 = { const enqueueBatchArg: BatchConfig = {
prepend: true, prepend: true,
batch: { batch: {

View File

@ -12,7 +12,6 @@ import {
selectControlAdapterById, selectControlAdapterById,
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { isEqual } from 'lodash-es';
type AnyControlAdapterParamChangeAction = type AnyControlAdapterParamChangeAction =
| ReturnType<typeof controlAdapterProcessorParamsChanged> | ReturnType<typeof controlAdapterProcessorParamsChanged>
@ -53,11 +52,6 @@ const predicate: AnyListenerPredicate<RootState> = (action, state, prevState) =>
return false; 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 isProcessorSelected = processorType !== 'none';
const hasControlImage = Boolean(controlImage); const hasControlImage = Boolean(controlImage);

View File

@ -91,7 +91,7 @@ export const addControlNetImageProcessedListener = (startAppListening: AppStartL
dispatch( dispatch(
controlAdapterProcessedImageChanged({ controlAdapterProcessedImageChanged({
id, id,
processedControlImage, processedControlImage: processedControlImage.image_name,
}) })
); );
} }

View File

@ -30,7 +30,7 @@ import type { ImageDTO } from 'services/api/types';
export const addEnqueueRequestedCanvasListener = (startAppListening: AppStartListening) => { export const addEnqueueRequestedCanvasListener = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> => predicate: (action): action is ReturnType<typeof enqueueRequested> =>
enqueueRequested.match(action) && action.payload.tabName === 'unifiedCanvas', enqueueRequested.match(action) && action.payload.tabName === 'canvas',
effect: async (action, { getState, dispatch }) => { effect: async (action, { getState, dispatch }) => {
const log = logger('queue'); const log = logger('queue');
const { prepend } = action.payload; const { prepend } = action.payload;

View File

@ -1,16 +1,14 @@
import { enqueueRequested } from 'app/store/actions'; import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; 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 { 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'; import { queueApi } from 'services/api/endpoints/queue';
export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> => predicate: (action): action is ReturnType<typeof enqueueRequested> =>
enqueueRequested.match(action) && (action.payload.tabName === 'txt2img' || action.payload.tabName === 'img2img'), enqueueRequested.match(action) && action.payload.tabName === 'generation',
effect: async (action, { getState, dispatch }) => { effect: async (action, { getState, dispatch }) => {
const state = getState(); const state = getState();
const model = state.generation.model; const model = state.generation.model;
@ -19,17 +17,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
let graph; let graph;
if (model && model.base === 'sdxl') { if (model && model.base === 'sdxl') {
if (action.payload.tabName === 'txt2img') { graph = await buildGenerationTabSDXLGraph(state);
graph = await buildLinearSDXLTextToImageGraph(state);
} else {
graph = await buildLinearSDXLImageToImageGraph(state);
}
} else { } else {
if (action.payload.tabName === 'txt2img') { graph = await buildGenerationTabGraph(state);
graph = await buildLinearTextToImageGraph(state);
} else {
graph = await buildLinearImageToImageGraph(state);
}
} }
const batchConfig = prepareLinearUIBatch(state, graph, prepend); const batchConfig = prepareLinearUIBatch(state, graph, prepend);

View File

@ -8,7 +8,7 @@ import type { BatchConfig } from 'services/api/types';
export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) => { export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> => predicate: (action): action is ReturnType<typeof enqueueRequested> =>
enqueueRequested.match(action) && action.payload.tabName === 'nodes', enqueueRequested.match(action) && action.payload.tabName === 'workflows',
effect: async (action, { getState, dispatch }) => { effect: async (action, { getState, dispatch }) => {
const state = getState(); const state = getState();
const { nodes, edges } = state.nodes; const { nodes, edges } = state.nodes;

View File

@ -1,7 +1,7 @@
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; 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 { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import { imagesSelectors } from 'services/api/util'; import { imagesSelectors } from 'services/api/util';
@ -62,6 +62,7 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
} else { } else {
dispatch(selectionChanged([imageDTO])); dispatch(selectionChanged([imageDTO]));
} }
dispatch(isImageViewerOpenChanged(true));
}, },
}); });
}; };

View File

@ -1,5 +1,6 @@
import { logger } from 'app/logging/logger'; import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
import { resetCanvas } from 'features/canvas/store/canvasSlice'; import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { import {
controlAdapterImageChanged, controlAdapterImageChanged,
@ -7,6 +8,13 @@ import {
selectControlAdapterAll, selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; 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 { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; 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 { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { isImageFieldInputInstance } from 'features/nodes/types/field'; import { isImageFieldInputInstance } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { clamp, forEach } from 'lodash-es'; import { clamp, forEach } from 'lodash-es';
import { api } from 'services/api'; import { api } from 'services/api';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import { imagesSelectors } from 'services/api/util'; 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) => { export const addRequestedSingleImageDeletionListener = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({
actionCreator: imageDeletionConfirmed, actionCreator: imageDeletionConfirmed,
@ -73,50 +151,9 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
} }
imageDTOs.forEach((imageDTO) => { imageDTOs.forEach((imageDTO) => {
// reset init image if we deleted it deleteControlAdapterImages(state, dispatch, imageDTO);
if (getState().generation.initialImage?.imageName === imageDTO.image_name) { deleteNodesImages(state, dispatch, imageDTO);
dispatch(clearInitialImage()); deleteControlLayerImages(state, dispatch, imageDTO);
}
// reset control adapters that use the deleted images
forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => {
if (
ca.controlImage === imageDTO.image_name ||
(isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
) {
dispatch(
controlAdapterImageChanged({
id: ca.id,
controlImage: null,
})
);
dispatch(
controlAdapterProcessedImageChanged({
id: ca.id,
processedControlImage: null,
})
);
}
});
// reset nodes that use the deleted images
getState().nodes.nodes.forEach((node) => {
if (!isInvocationNode(node)) {
return;
}
forEach(node.data.inputs, (input) => {
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
dispatch(
fieldImageValueChanged({
nodeId: node.data.id,
fieldName: input.name,
value: undefined,
})
);
}
});
});
}); });
// Delete from server // Delete from server
@ -168,50 +205,9 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
} }
imageDTOs.forEach((imageDTO) => { imageDTOs.forEach((imageDTO) => {
// reset init image if we deleted it deleteControlAdapterImages(state, dispatch, imageDTO);
if (getState().generation.initialImage?.imageName === imageDTO.image_name) { deleteNodesImages(state, dispatch, imageDTO);
dispatch(clearInitialImage()); deleteControlLayerImages(state, dispatch, imageDTO);
}
// reset control adapters that use the deleted images
forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => {
if (
ca.controlImage === imageDTO.image_name ||
(isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
) {
dispatch(
controlAdapterImageChanged({
id: ca.id,
controlImage: null,
})
);
dispatch(
controlAdapterProcessedImageChanged({
id: ca.id,
processedControlImage: null,
})
);
}
});
// reset nodes that use the deleted images
getState().nodes.nodes.forEach((node) => {
if (!isInvocationNode(node)) {
return;
}
forEach(node.data.inputs, (input) => {
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
dispatch(
fieldImageValueChanged({
nodeId: node.data.id,
fieldName: input.name,
value: undefined,
})
);
}
});
});
}); });
} catch { } catch {
// no-op // no-op

View File

@ -9,13 +9,14 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import { import {
caLayerImageChanged, caLayerImageChanged,
iiLayerImageChanged,
ipaLayerImageChanged, ipaLayerImageChanged,
rgLayerIPAdapterImageChanged, rgLayerIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { imageSelected } from 'features/gallery/store/gallerySlice'; import { imageSelected } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; 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'; import { imagesApi } from 'services/api/endpoints/images';
export const dndDropped = createAction<{ export const dndDropped = createAction<{
@ -52,18 +53,6 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
return; 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 * Image dropped on ControlNet
*/ */
@ -76,7 +65,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
dispatch( dispatch(
controlAdapterImageChanged({ controlAdapterImageChanged({
id, id,
controlImage: activeData.payload.imageDTO, controlImage: activeData.payload.imageDTO.image_name,
}) })
); );
dispatch( dispatch(
@ -143,6 +132,24 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
return; 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 * Image dropped on Canvas
*/ */

View File

@ -14,7 +14,6 @@ export const addImageToDeleteSelectedListener = (startAppListening: AppStartList
const isImageInUse = const isImageInUse =
imagesUsage.some((i) => i.isCanvasImage) || imagesUsage.some((i) => i.isCanvasImage) ||
imagesUsage.some((i) => i.isInitialImage) ||
imagesUsage.some((i) => i.isControlImage) || imagesUsage.some((i) => i.isControlImage) ||
imagesUsage.some((i) => i.isNodesImage); imagesUsage.some((i) => i.isNodesImage);

View File

@ -8,11 +8,12 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import { import {
caLayerImageChanged, caLayerImageChanged,
iiLayerImageChanged,
ipaLayerImageChanged, ipaLayerImageChanged,
rgLayerIPAdapterImageChanged, rgLayerIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; 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 { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { omit } from 'lodash-es'; import { omit } from 'lodash-es';
@ -101,7 +102,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
dispatch( dispatch(
controlAdapterImageChanged({ controlAdapterImageChanged({
id, id,
controlImage: imageDTO, controlImage: imageDTO.image_name,
}) })
); );
dispatch( dispatch(
@ -146,15 +147,15 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
); );
} }
if (postUploadAction?.type === 'SET_INITIAL_IMAGE') { if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') {
dispatch(initialImageChanged(imageDTO)); const { layerId } = postUploadAction;
dispatch(iiLayerImageChanged({ layerId, imageDTO }));
dispatch( dispatch(
addToast({ addToast({
...DEFAULT_UPLOADED_TOAST, ...DEFAULT_UPLOADED_TOAST,
description: t('toast.setInitialImage'), description: t('toast.setControlImage'),
}) })
); );
return;
} }
if (postUploadAction?.type === 'SET_NODES_IMAGE') { if (postUploadAction?.type === 'SET_NODES_IMAGE') {

View File

@ -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'))));
},
});
};

View File

@ -1,7 +1,7 @@
import { logger } from 'app/logging/logger'; import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { import {
controlAdapterModelChanged, controlAdapterIsEnabledChanged,
selectControlAdapterAll, selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import { loraRemoved } from 'features/lora/store/loraSlice'; import { loraRemoved } from 'features/lora/store/loraSlice';
@ -54,7 +54,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
// handle incompatible controlnets // handle incompatible controlnets
selectControlAdapterAll(state.controlAdapters).forEach((ca) => { selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
if (ca.model?.base !== newBaseModel) { if (ca.model?.base !== newBaseModel) {
dispatch(controlAdapterModelChanged({ id: ca.id, modelConfig: null })); dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false }));
modelsCleared += 1; modelsCleared += 1;
} }
}); });

View File

@ -96,16 +96,16 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
dispatch(setScheduler(scheduler)); dispatch(setScheduler(scheduler));
} }
} }
const setSizeOptions = { updateAspectRatio: true, clamp: true };
if (width) { if (width) {
if (isParameterWidth(width)) { if (isParameterWidth(width)) {
dispatch(widthChanged({ width, updateAspectRatio: true })); dispatch(widthChanged({ width, ...setSizeOptions }));
} }
} }
if (height) { if (height) {
if (isParameterHeight(height)) { if (isParameterHeight(height)) {
dispatch(heightChanged({ height, updateAspectRatio: true })); dispatch(heightChanged({ height, ...setSizeOptions }));
} }
} }

View File

@ -17,14 +17,10 @@ const accept: Accept = {
const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => { const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => {
let postUploadAction: PostUploadAction = { type: 'TOAST' }; let postUploadAction: PostUploadAction = { type: 'TOAST' };
if (activeTabName === 'unifiedCanvas') { if (activeTabName === 'canvas') {
postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' }; postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' };
} }
if (activeTabName === 'img2img') {
postUploadAction = { type: 'SET_INITIAL_IMAGE' };
}
return postUploadAction; return postUploadAction;
}); });

View File

@ -67,7 +67,7 @@ export const useGlobalHotkeys = () => {
useHotkeys( useHotkeys(
'1', '1',
() => { () => {
dispatch(setActiveTab('txt2img')); dispatch(setActiveTab('generation'));
}, },
[dispatch] [dispatch]
); );
@ -75,7 +75,7 @@ export const useGlobalHotkeys = () => {
useHotkeys( useHotkeys(
'2', '2',
() => { () => {
dispatch(setActiveTab('img2img')); dispatch(setActiveTab('canvas'));
}, },
[dispatch] [dispatch]
); );
@ -83,31 +83,23 @@ export const useGlobalHotkeys = () => {
useHotkeys( useHotkeys(
'3', '3',
() => { () => {
dispatch(setActiveTab('unifiedCanvas')); dispatch(setActiveTab('workflows'));
}, },
[dispatch] [dispatch]
); );
useHotkeys( useHotkeys(
'4', '4',
() => {
dispatch(setActiveTab('nodes'));
},
[dispatch]
);
useHotkeys(
'5',
() => { () => {
if (isModelManagerEnabled) { if (isModelManagerEnabled) {
dispatch(setActiveTab('modelManager')); dispatch(setActiveTab('models'));
} }
}, },
[dispatch, isModelManagerEnabled] [dispatch, isModelManagerEnabled]
); );
useHotkeys( useHotkeys(
isModelManagerEnabled ? '6' : '5', isModelManagerEnabled ? '5' : '4',
() => { () => {
dispatch(setActiveTab('queue')); dispatch(setActiveTab('queue'));
}, },

View File

@ -16,7 +16,6 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import i18n from 'i18next'; import i18n from 'i18next';
import { forEach } from 'lodash-es'; import { forEach } from 'lodash-es';
import { getConnectedEdges } from 'reactflow'; import { getConnectedEdges } from 'reactflow';
import { assert } from 'tsafe';
const selector = createMemoizedSelector( const selector = createMemoizedSelector(
[ [
@ -29,7 +28,7 @@ const selector = createMemoizedSelector(
activeTabNameSelector, activeTabNameSelector,
], ],
(controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => { (controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => {
const { initialImage, model } = generation; const { model } = generation;
const { positivePrompt } = controlLayers.present; const { positivePrompt } = controlLayers.present;
const { isConnected } = system; const { isConnected } = system;
@ -41,11 +40,7 @@ const selector = createMemoizedSelector(
reasons.push(i18n.t('parameters.invoke.systemDisconnected')); reasons.push(i18n.t('parameters.invoke.systemDisconnected'));
} }
if (activeTabName === 'img2img' && !initialImage) { if (activeTabName === 'workflows') {
reasons.push(i18n.t('parameters.invoke.noInitialImageSelected'));
}
if (activeTabName === 'nodes') {
if (nodes.shouldValidateGraph) { if (nodes.shouldValidateGraph) {
if (!nodes.nodes.length) { if (!nodes.nodes.length) {
reasons.push(i18n.t('parameters.invoke.noNodesInGraph')); reasons.push(i18n.t('parameters.invoke.noNodesInGraph'));
@ -98,8 +93,8 @@ const selector = createMemoizedSelector(
reasons.push(i18n.t('parameters.invoke.noModelSelected')); reasons.push(i18n.t('parameters.invoke.noModelSelected'));
} }
if (activeTabName === 'txt2img') { if (activeTabName === 'generation') {
// Handling for Control Layers - only exists on txt2img tab now // Handling for generation tab
controlLayers.present.layers controlLayers.present.layers
.filter((l) => l.isEnabled) .filter((l) => l.isEnabled)
.flatMap((l) => { .flatMap((l) => {
@ -110,7 +105,7 @@ const selector = createMemoizedSelector(
} else if (l.type === 'regional_guidance_layer') { } else if (l.type === 'regional_guidance_layer') {
return l.ipAdapters; return l.ipAdapters;
} }
assert(false); return [];
}) })
.forEach((ca, i) => { .forEach((ca, i) => {
const hasNoModel = !ca.model; const hasNoModel = !ca.model;

View File

@ -22,6 +22,7 @@ import {
} from 'features/canvas/store/canvasSlice'; } from 'features/canvas/store/canvasSlice';
import type { CanvasLayer } from 'features/canvas/store/canvasTypes'; import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
import { LAYER_NAMES_DICT } 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 { memo, useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -219,97 +220,107 @@ const IAICanvasToolbar = () => {
const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]); const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]);
return ( return (
<Flex alignItems="center" gap={2} flexWrap="wrap"> <Flex w="full" gap={2} alignItems="center">
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}> <Flex flex={1} justifyContent="center">
<FormControl isDisabled={isStaging} w="5rem"> <Flex gap={2} marginInlineEnd="auto" />
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} /> </Flex>
</FormControl> <Flex flex={1} gap={2} justifyContent="center">
</Tooltip> <Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
<FormControl isDisabled={isStaging} w="5rem">
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} />
</FormControl>
</Tooltip>
<IAICanvasMaskOptions /> <IAICanvasMaskOptions />
<IAICanvasToolChooserOptions /> <IAICanvasToolChooserOptions />
<ButtonGroup> <ButtonGroup>
<IconButton
aria-label={`${t('unifiedCanvas.move')} (V)`}
tooltip={`${t('unifiedCanvas.move')} (V)`}
icon={<PiHandGrabbingBold />}
isChecked={tool === 'move' || isStaging}
onClick={handleSelectMoveTool}
/>
<IconButton
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
onClick={handleSetShouldShowBoundingBox}
isDisabled={isStaging}
/>
<IconButton
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
icon={<PiCrosshairSimpleBold />}
onClick={handleClickResetCanvasView}
/>
</ButtonGroup>
<ButtonGroup>
<IconButton
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
icon={<PiStackBold />}
onClick={handleMergeVisible}
isDisabled={isStaging}
/>
<IconButton
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
icon={<PiFloppyDiskBold />}
onClick={handleSaveToGallery}
isDisabled={isStaging}
/>
{isClipboardAPIAvailable && (
<IconButton <IconButton
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`} aria-label={`${t('unifiedCanvas.move')} (V)`}
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`} tooltip={`${t('unifiedCanvas.move')} (V)`}
icon={<PiCopyBold />} icon={<PiHandGrabbingBold />}
onClick={handleCopyImageToClipboard} isChecked={tool === 'move' || isStaging}
onClick={handleSelectMoveTool}
/>
<IconButton
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
onClick={handleSetShouldShowBoundingBox}
isDisabled={isStaging} isDisabled={isStaging}
/> />
)} <IconButton
<IconButton aria-label={`${t('unifiedCanvas.resetView')} (R)`}
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`} tooltip={`${t('unifiedCanvas.resetView')} (R)`}
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`} icon={<PiCrosshairSimpleBold />}
icon={<PiDownloadSimpleBold />} onClick={handleClickResetCanvasView}
onClick={handleDownloadAsImage} />
isDisabled={isStaging} </ButtonGroup>
/>
</ButtonGroup>
<ButtonGroup>
<IAICanvasUndoButton />
<IAICanvasRedoButton />
</ButtonGroup>
<ButtonGroup> <ButtonGroup>
<IconButton <IconButton
aria-label={`${t('common.upload')}`} aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
tooltip={`${t('common.upload')}`} tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
icon={<PiUploadSimpleBold />} icon={<PiStackBold />}
isDisabled={isStaging} onClick={handleMergeVisible}
{...getUploadButtonProps()} isDisabled={isStaging}
/> />
<input {...getUploadInputProps()} /> <IconButton
<IconButton aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
aria-label={`${t('unifiedCanvas.clearCanvas')}`} tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
tooltip={`${t('unifiedCanvas.clearCanvas')}`} icon={<PiFloppyDiskBold />}
icon={<PiTrashSimpleBold />} onClick={handleSaveToGallery}
onClick={handleResetCanvas} isDisabled={isStaging}
colorScheme="error" />
isDisabled={isStaging} {isClipboardAPIAvailable && (
/> <IconButton
</ButtonGroup> aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
<ButtonGroup> tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
<IAICanvasSettingsButtonPopover /> icon={<PiCopyBold />}
</ButtonGroup> onClick={handleCopyImageToClipboard}
isDisabled={isStaging}
/>
)}
<IconButton
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
icon={<PiDownloadSimpleBold />}
onClick={handleDownloadAsImage}
isDisabled={isStaging}
/>
</ButtonGroup>
<ButtonGroup>
<IAICanvasUndoButton />
<IAICanvasRedoButton />
</ButtonGroup>
<ButtonGroup>
<IconButton
aria-label={`${t('common.upload')}`}
tooltip={`${t('common.upload')}`}
icon={<PiUploadSimpleBold />}
isDisabled={isStaging}
{...getUploadButtonProps()}
/>
<input {...getUploadInputProps()} />
<IconButton
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
icon={<PiTrashSimpleBold />}
onClick={handleResetCanvas}
colorScheme="error"
isDisabled={isStaging}
/>
</ButtonGroup>
<ButtonGroup>
<IAICanvasSettingsButtonPopover />
</ButtonGroup>
</Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
<ViewerButton />
</Flex>
</Flex>
</Flex> </Flex>
); );
}; };

View File

@ -75,7 +75,7 @@ const useInpaintingCanvasHotkeys = () => {
const onKeyDown = useCallback( const onKeyDown = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'unifiedCanvas') { if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') {
return; return;
} }
if ($toolStash.get() || $tool.get() === 'move') { if ($toolStash.get() || $tool.get() === 'move') {
@ -90,7 +90,7 @@ const useInpaintingCanvasHotkeys = () => {
); );
const onKeyUp = useCallback( const onKeyUp = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'unifiedCanvas') { if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') {
return; return;
} }
if (!$toolStash.get() || $tool.get() !== 'move') { if (!$toolStash.get() || $tool.get() !== 'move') {

View File

@ -76,7 +76,7 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => {
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s"> <Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<ParamControlAdapterModel id={id} /> <ParamControlAdapterModel id={id} />
</Box> </Box>
{activeTabName === 'unifiedCanvas' && <ControlNetCanvasImageImports id={id} />} {activeTabName === 'canvas' && <ControlNetCanvasImageImports id={id} />}
<IconButton <IconButton
size="sm" size="sm"
tooltip={t('controlnet.duplicate')} tooltip={t('controlnet.duplicate')}
@ -113,7 +113,7 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => {
<Flex w="full" flexDir="column" gap={4}> <Flex w="full" flexDir="column" gap={4}>
<Flex gap={8} w="full" alignItems="center"> <Flex gap={8} w="full" alignItems="center">
<Flex flexDir="column" gap={4} h={controlAdapterType === 'ip_adapter' ? 40 : 32} w="full"> <Flex flexDir="column" gap={4} h={controlAdapterType === 'ip_adapter' ? 40 : 32} w="full">
{controlAdapterType === 'ip_adapter' && <ParamControlAdapterIPMethod id={id} />} <ParamControlAdapterIPMethod id={id} />
<ParamControlAdapterWeight id={id} /> <ParamControlAdapterWeight id={id} />
<ParamControlAdapterBeginEnd id={id} /> <ParamControlAdapterBeginEnd id={id} />
</Flex> </Flex>

View File

@ -93,15 +93,16 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => {
return; return;
} }
if (activeTabName === 'unifiedCanvas') { if (activeTabName === 'canvas') {
dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)); dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension));
} else { } else {
const options = { updateAspectRatio: true, clamp: true };
const { width, height } = calculateNewSize( const { width, height } = calculateNewSize(
controlImage.width / controlImage.height, controlImage.width / controlImage.height,
optimalDimension * optimalDimension optimalDimension * optimalDimension
); );
dispatch(widthChanged({ width, updateAspectRatio: true })); dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, updateAspectRatio: true })); dispatch(heightChanged({ height, ...options }));
} }
}, [controlImage, activeTabName, dispatch, optimalDimension]); }, [controlImage, activeTabName, dispatch, optimalDimension]);

View File

@ -46,9 +46,13 @@ const ParamControlAdapterIPMethod = ({ id }: Props) => {
const value = useMemo(() => options.find((o) => o.value === method), [options, method]); const value = useMemo(() => options.find((o) => o.value === method), [options, method]);
if (!method) {
return null;
}
return ( return (
<FormControl> <FormControl>
<InformationalPopover feature="ipAdapterMethod"> <InformationalPopover feature="controlNetResizeMode">
<FormLabel>{t('controlnet.ipAdapterMethod')}</FormLabel> <FormLabel>{t('controlnet.ipAdapterMethod')}</FormLabel>
</InformationalPopover> </InformationalPopover>
<Combobox value={value} options={options} isDisabled={!isEnabled} onChange={handleIPMethodChanged} /> <Combobox value={value} options={options} isDisabled={!isEnabled} onChange={handleIPMethodChanged} />

View File

@ -102,9 +102,13 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => {
); );
return ( return (
<Flex gap={4}> <Flex sx={{ gap: 2 }}>
<Tooltip label={selectedModel?.description}> <Tooltip label={selectedModel?.description}>
<FormControl isDisabled={!isEnabled} isInvalid={!value || mainModel?.base !== modelConfig?.base} w="full"> <FormControl
isDisabled={!isEnabled}
isInvalid={!value || mainModel?.base !== modelConfig?.base}
sx={{ width: '100%' }}
>
<Combobox <Combobox
options={options} options={options}
placeholder={t('controlnet.selectModel')} placeholder={t('controlnet.selectModel')}
@ -118,8 +122,7 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => {
<FormControl <FormControl
isDisabled={!isEnabled} isDisabled={!isEnabled}
isInvalid={!value || mainModel?.base !== modelConfig?.base} isInvalid={!value || mainModel?.base !== modelConfig?.base}
width="max-content" sx={{ width: 'max-content', minWidth: 28 }}
minWidth={28}
> >
<Combobox <Combobox
options={clipVisionOptions} options={clipVisionOptions}

View File

@ -5,15 +5,15 @@ import {
selectControlAdaptersSlice, selectControlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useControlAdapterIPMethod = (id: string) => { export const useControlAdapterIPMethod = (id: string) => {
const selector = useMemo( const selector = useMemo(
() => () =>
createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => {
const ca = selectControlAdapterById(controlAdapters, id); const cn = selectControlAdapterById(controlAdapters, id);
assert(ca?.type === 'ip_adapter'); if (cn && cn?.type === 'ip_adapter') {
return ca.method; return cn.method;
}
}), }),
[id] [id]
); );

View File

@ -7,7 +7,7 @@ import { buildControlAdapter } from 'features/controlAdapters/util/buildControlA
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common';
import { merge, uniq } from 'lodash-es'; 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 { socketInvocationError } from 'services/events/actions';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -134,46 +134,23 @@ export const controlAdaptersSlice = createSlice({
const { id, isEnabled } = action.payload; const { id, isEnabled } = action.payload;
caAdapter.updateOne(state, { id, changes: { isEnabled } }); 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 { id, controlImage } = action.payload;
const ca = selectControlAdapterById(state, id); const ca = selectControlAdapterById(state, id);
if (!ca) { if (!ca) {
return; return;
} }
if (isControlNetOrT2IAdapter(ca)) { caAdapter.updateOne(state, {
if (controlImage) { id,
const { image_name, width, height } = controlImage; changes: { controlImage, processedControlImage: null },
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 } });
}
if (controlImage !== null && isControlNetOrT2IAdapter(ca) && ca.processorType !== 'none') { if (controlImage !== null && isControlNetOrT2IAdapter(ca) && ca.processorType !== 'none') {
state.pendingControlImages.push(id); state.pendingControlImages.push(id);
@ -183,7 +160,7 @@ export const controlAdaptersSlice = createSlice({
state, state,
action: PayloadAction<{ action: PayloadAction<{
id: string; id: string;
processedControlImage: ImageDTO | null; processedControlImage: string | null;
}> }>
) => { ) => {
const { id, processedControlImage } = action.payload; const { id, processedControlImage } = action.payload;
@ -196,24 +173,12 @@ export const controlAdaptersSlice = createSlice({
return; return;
} }
if (processedControlImage) { caAdapter.updateOne(state, {
const { image_name, width, height } = processedControlImage; id,
caAdapter.updateOne(state, { changes: {
id, processedControlImage,
changes: { },
processedControlImage: image_name, });
processedControlImageDimensions: { width, height },
},
});
} else {
caAdapter.updateOne(state, {
id,
changes: {
processedControlImage: null,
processedControlImageDimensions: null,
},
});
}
state.pendingControlImages = state.pendingControlImages.filter((pendingId) => pendingId !== id); state.pendingControlImages = state.pendingControlImages.filter((pendingId) => pendingId !== id);
}, },
@ -227,7 +192,7 @@ export const controlAdaptersSlice = createSlice({
state, state,
action: PayloadAction<{ action: PayloadAction<{
id: string; id: string;
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig | null; modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig;
}> }>
) => { ) => {
const { id, modelConfig } = action.payload; const { id, modelConfig } = action.payload;
@ -236,11 +201,6 @@ export const controlAdaptersSlice = createSlice({
return; return;
} }
if (modelConfig === null) {
caAdapter.updateOne(state, { id, changes: { model: null } });
return;
}
const model = zModelIdentifierField.parse(modelConfig); const model = zModelIdentifierField.parse(modelConfig);
if (!isControlNetOrT2IAdapter(cn)) { if (!isControlNetOrT2IAdapter(cn)) {
@ -248,36 +208,22 @@ export const controlAdaptersSlice = createSlice({
return; return;
} }
const update: Update<ControlNetConfig | T2IAdapterConfig, string> = {
id,
changes: { model, shouldAutoConfig: true },
};
update.changes.processedControlImage = null;
if (modelConfig.type === 'ip_adapter') { if (modelConfig.type === 'ip_adapter') {
// should never happen... // should never happen...
return; return;
} }
// We always update the model
const update: Update<ControlNetConfig | T2IAdapterConfig, string> = { id, changes: { model } };
// Build the default processor for this model
const processor = buildControlAdapterProcessor(modelConfig); const processor = buildControlAdapterProcessor(modelConfig);
if (processor.processorType !== cn.processorNode.type) { update.changes.processorType = processor.processorType;
// If the processor type has changed, update the processor node update.changes.processorNode = processor.processorNode;
update.changes.shouldAutoConfig = true;
update.changes.processedControlImage = null;
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); caAdapter.updateOne(state, update);
}, },
controlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { controlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
@ -394,23 +340,8 @@ export const controlAdaptersSlice = createSlice({
if (update.changes.shouldAutoConfig && modelConfig) { if (update.changes.shouldAutoConfig && modelConfig) {
const processor = buildControlAdapterProcessor(modelConfig); const processor = buildControlAdapterProcessor(modelConfig);
if (processor.processorType !== cn.processorNode.type) { update.changes.processorType = processor.processorType;
update.changes.processorType = processor.processorType; update.changes.processorNode = processor.processorNode;
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;
}
}
}
} }
caAdapter.updateOne(state, update); caAdapter.updateOne(state, update);

View File

@ -225,9 +225,7 @@ export type ControlNetConfig = {
controlMode: ControlMode; controlMode: ControlMode;
resizeMode: ResizeMode; resizeMode: ResizeMode;
controlImage: string | null; controlImage: string | null;
controlImageDimensions: { width: number; height: number } | null;
processedControlImage: string | null; processedControlImage: string | null;
processedControlImageDimensions: { width: number; height: number } | null;
processorType: ControlAdapterProcessorType; processorType: ControlAdapterProcessorType;
processorNode: RequiredControlAdapterProcessorNode; processorNode: RequiredControlAdapterProcessorNode;
shouldAutoConfig: boolean; shouldAutoConfig: boolean;
@ -243,9 +241,7 @@ export type T2IAdapterConfig = {
endStepPct: number; endStepPct: number;
resizeMode: ResizeMode; resizeMode: ResizeMode;
controlImage: string | null; controlImage: string | null;
controlImageDimensions: { width: number; height: number } | null;
processedControlImage: string | null; processedControlImage: string | null;
processedControlImageDimensions: { width: number; height: number } | null;
processorType: ControlAdapterProcessorType; processorType: ControlAdapterProcessorType;
processorNode: RequiredControlAdapterProcessorNode; processorNode: RequiredControlAdapterProcessorNode;
shouldAutoConfig: boolean; shouldAutoConfig: boolean;

View File

@ -20,9 +20,7 @@ export const initialControlNet: Omit<ControlNetConfig, 'id'> = {
controlMode: 'balanced', controlMode: 'balanced',
resizeMode: 'just_resize', resizeMode: 'just_resize',
controlImage: null, controlImage: null,
controlImageDimensions: null,
processedControlImage: null, processedControlImage: null,
processedControlImageDimensions: null,
processorType: 'canny_image_processor', processorType: 'canny_image_processor',
processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation, processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation,
shouldAutoConfig: true, shouldAutoConfig: true,
@ -37,9 +35,7 @@ export const initialT2IAdapter: Omit<T2IAdapterConfig, 'id'> = {
endStepPct: 1, endStepPct: 1,
resizeMode: 'just_resize', resizeMode: 'just_resize',
controlImage: null, controlImage: null,
controlImageDimensions: null,
processedControlImage: null, processedControlImage: null,
processedControlImageDimensions: null,
processorType: 'canny_image_processor', processorType: 'canny_image_processor',
processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation, processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation,
shouldAutoConfig: true, shouldAutoConfig: true,

View File

@ -1,6 +1,6 @@
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; 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 { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -11,6 +11,7 @@ export const AddLayerButton = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [addCALayer, isAddCALayerDisabled] = useAddCALayer(); const [addCALayer, isAddCALayerDisabled] = useAddCALayer();
const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer();
const [addIILayer, isAddIILayerDisabled] = useAddIILayer();
const addRGLayer = useCallback(() => { const addRGLayer = useCallback(() => {
dispatch(rgLayerAdded()); dispatch(rgLayerAdded());
}, [dispatch]); }, [dispatch]);
@ -30,6 +31,9 @@ export const AddLayerButton = memo(() => {
<MenuItem icon={<PiPlusBold />} onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> <MenuItem icon={<PiPlusBold />} onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}>
{t('controlLayers.globalIPAdapterLayer')} {t('controlLayers.globalIPAdapterLayer')}
</MenuItem> </MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIILayer} isDisabled={isAddIILayerDisabled}>
{t('controlLayers.globalInitialImageLayer')}
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
); );

View File

@ -5,6 +5,7 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; 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 { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
@ -17,37 +18,28 @@ type Props = {
export const CALayer = memo(({ layerId }: Props) => { export const CALayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected); 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 // Must be capture so that the layer is selected before deleting/resetting/etc
dispatch(layerSelected(layerId)); dispatch(layerSelected(layerId));
}, [dispatch, layerId]); }, [dispatch, layerId]);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
return ( return (
<Flex <LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
gap={2} <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
onClickCapture={onClickCapture} <LayerVisibilityToggle layerId={layerId} />
bg={isSelected ? 'base.400' : 'base.800'} <LayerTitle type="control_adapter_layer" />
px={2} <Spacer />
borderRadius="base" <CALayerOpacity layerId={layerId} />
py="1px" <LayerMenu layerId={layerId} />
> <LayerDeleteButton layerId={layerId} />
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="control_adapter_layer" />
<Spacer />
<CALayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<CALayerControlAdapterWrapper layerId={layerId} />
</Flex>
)}
</Flex> </Flex>
</Flex> {isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<CALayerControlAdapterWrapper layerId={layerId} />
</Flex>
)}
</LayerWrapper>
); );
}); });

View File

@ -9,7 +9,7 @@ import {
caOrIPALayerWeightChanged, caOrIPALayerWeightChanged,
selectCALayerOrThrow, selectCALayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice'; } 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 type { CALayerImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import type { import type {
@ -40,7 +40,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
); );
const onChangeControlMode = useCallback( const onChangeControlMode = useCallback(
(controlMode: ControlMode) => { (controlMode: ControlModeV2) => {
dispatch( dispatch(
caLayerControlModeChanged({ caLayerControlModeChanged({
layerId, layerId,

View File

@ -55,7 +55,7 @@ const CALayerOpacity = ({ layerId }: Props) => {
onDoubleClick={stopPropagation} onDoubleClick={stopPropagation}
/> />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent> <PopoverContent onDoubleClick={stopPropagation}>
<PopoverArrow /> <PopoverArrow />
<PopoverBody> <PopoverBody>
<Flex direction="column" gap={2}> <Flex direction="column" gap={2}>

View File

@ -1,10 +1,10 @@
import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox'; import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox';
import type { import type {
ControlMode, ControlModeV2,
ControlNetConfig, ControlNetConfigV2,
ProcessorConfig, ProcessorConfig,
T2IAdapterConfig, T2IAdapterConfigV2,
} from 'features/controlLayers/util/controlAdapters'; } from 'features/controlLayers/util/controlAdapters';
import type { TypesafeDroppableData } from 'features/dnd/types'; import type { TypesafeDroppableData } from 'features/dnd/types';
import { memo } from 'react'; import { memo } from 'react';
@ -21,9 +21,9 @@ import { ControlAdapterProcessorTypeSelect } from './ControlAdapterProcessorType
import { ControlAdapterWeight } from './ControlAdapterWeight'; import { ControlAdapterWeight } from './ControlAdapterWeight';
type Props = { type Props = {
controlAdapter: ControlNetConfig | T2IAdapterConfig; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2;
onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void;
onChangeControlMode: (controlMode: ControlMode) => void; onChangeControlMode: (controlMode: ControlModeV2) => void;
onChangeWeight: (weight: number) => void; onChangeWeight: (weight: number) => void;
onChangeProcessorConfig: (processorConfig: ProcessorConfig | null) => void; onChangeProcessorConfig: (processorConfig: ProcessorConfig | null) => void;
onChangeModel: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void; onChangeModel: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void;

View File

@ -1,15 +1,15 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { ControlMode } from 'features/controlLayers/util/controlAdapters'; import type { ControlModeV2 } from 'features/controlLayers/util/controlAdapters';
import { isControlMode } from 'features/controlLayers/util/controlAdapters'; import { isControlModeV2 } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
type Props = { type Props = {
controlMode: ControlMode; controlMode: ControlModeV2;
onChange: (controlMode: ControlMode) => void; onChange: (controlMode: ControlModeV2) => void;
}; };
export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => { export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => {
@ -26,7 +26,7 @@ export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }:
const handleControlModeChange = useCallback<ComboboxOnChange>( const handleControlModeChange = useCallback<ComboboxOnChange>(
(v) => { (v) => {
assert(isControlMode(v?.value)); assert(isControlModeV2(v?.value));
onChange(v.value); onChange(v.value);
}, },
[onChange] [onChange]

View File

@ -6,7 +6,7 @@ import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; 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 type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
@ -23,7 +23,7 @@ import {
import type { ImageDTO, PostUploadAction } from 'services/api/types'; import type { ImageDTO, PostUploadAction } from 'services/api/types';
type Props = { type Props = {
controlAdapter: ControlNetConfig | T2IAdapterConfig; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2;
onChangeImage: (imageDTO: ImageDTO | null) => void; onChangeImage: (imageDTO: ImageDTO | null) => void;
droppableData: TypesafeDroppableData; droppableData: TypesafeDroppableData;
postUploadAction: PostUploadAction; postUploadAction: PostUploadAction;
@ -80,22 +80,24 @@ export const ControlAdapterImagePreview = memo(
return; return;
} }
if (activeTabName === 'unifiedCanvas') { if (activeTabName === 'canvas') {
dispatch( dispatch(
setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)
); );
} else { } else {
const options = { updateAspectRatio: true, clamp: true };
if (shift) { if (shift) {
const { width, height } = controlImage; const { width, height } = controlImage;
dispatch(widthChanged({ width, updateAspectRatio: true })); dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, updateAspectRatio: true })); dispatch(heightChanged({ height, ...options }));
} else { } else {
const { width, height } = calculateNewSize( const { width, height } = calculateNewSize(
controlImage.width / controlImage.height, controlImage.width / controlImage.height,
optimalDimension * optimalDimension optimalDimension * optimalDimension
); );
dispatch(widthChanged({ width, updateAspectRatio: true })); dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, updateAspectRatio: true })); dispatch(heightChanged({ height, ...options }));
} }
} }
}, [controlImage, activeTabName, dispatch, optimalDimension, shift]); }, [controlImage, activeTabName, dispatch, optimalDimension, shift]);

View File

@ -4,7 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; 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 { configSelector } from 'features/system/store/configSelectors';
import { includes, map } from 'lodash-es'; import { includes, map } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
@ -26,7 +26,7 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro
const { t } = useTranslation(); const { t } = useTranslation();
const disabledProcessors = useAppSelector(selectDisabledProcessors); const disabledProcessors = useAppSelector(selectDisabledProcessors);
const options = useMemo(() => { 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) (o) => !includes(disabledProcessors, o.value)
); );
}, [disabledProcessors, t]); }, [disabledProcessors, t]);
@ -36,8 +36,8 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro
if (!v) { if (!v) {
onChange(null); onChange(null);
} else { } else {
assert(isProcessorType(v.value)); assert(isProcessorTypeV2(v.value));
onChange(CONTROLNET_PROCESSORS[v.value].buildDefaults()); onChange(CA_PROCESSOR_DATA[v.value].buildDefaults());
} }
}, },
[onChange] [onChange]

View File

@ -4,18 +4,18 @@ import { ControlAdapterWeight } from 'features/controlLayers/components/ControlA
import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview'; import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview';
import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod'; import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod';
import { IPAdapterModelSelect } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect'; 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 type { TypesafeDroppableData } from 'features/dnd/types';
import { memo } from 'react'; import { memo } from 'react';
import type { ImageDTO, IPAdapterModelConfig, PostUploadAction } from 'services/api/types'; import type { ImageDTO, IPAdapterModelConfig, PostUploadAction } from 'services/api/types';
type Props = { type Props = {
ipAdapter: IPAdapterConfig; ipAdapter: IPAdapterConfigV2;
onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void; onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void;
onChangeWeight: (weight: number) => void; onChangeWeight: (weight: number) => void;
onChangeIPMethod: (method: IPMethod) => void; onChangeIPMethod: (method: IPMethodV2) => void;
onChangeModel: (modelConfig: IPAdapterModelConfig) => void; onChangeModel: (modelConfig: IPAdapterModelConfig) => void;
onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void; onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void;
onChangeImage: (imageDTO: ImageDTO | null) => void; onChangeImage: (imageDTO: ImageDTO | null) => void;
droppableData: TypesafeDroppableData; droppableData: TypesafeDroppableData;
postUploadAction: PostUploadAction; postUploadAction: PostUploadAction;

View File

@ -46,22 +46,23 @@ export const IPAdapterImagePreview = memo(
return; return;
} }
if (activeTabName === 'unifiedCanvas') { if (activeTabName === 'canvas') {
dispatch( dispatch(
setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension) setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)
); );
} else { } else {
const options = { updateAspectRatio: true, clamp: true };
if (shift) { if (shift) {
const { width, height } = controlImage; const { width, height } = controlImage;
dispatch(widthChanged({ width, updateAspectRatio: true })); dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, updateAspectRatio: true })); dispatch(heightChanged({ height, ...options }));
} else { } else {
const { width, height } = calculateNewSize( const { width, height } = calculateNewSize(
controlImage.width / controlImage.height, controlImage.width / controlImage.height,
optimalDimension * optimalDimension optimalDimension * optimalDimension
); );
dispatch(widthChanged({ width, updateAspectRatio: true })); dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, updateAspectRatio: true })); dispatch(heightChanged({ height, ...options }));
} }
} }
}, [controlImage, activeTabName, dispatch, optimalDimension, shift]); }, [controlImage, activeTabName, dispatch, optimalDimension, shift]);

View File

@ -1,20 +1,20 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { IPMethod } from 'features/controlLayers/util/controlAdapters'; import type { IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import { isIPMethod } from 'features/controlLayers/util/controlAdapters'; import { isIPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
type Props = { type Props = {
method: IPMethod; method: IPMethodV2;
onChange: (method: IPMethod) => void; onChange: (method: IPMethodV2) => void;
}; };
export const IPAdapterMethod = memo(({ method, onChange }: Props) => { export const IPAdapterMethod = memo(({ method, onChange }: Props) => {
const { t } = useTranslation(); 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.full'), value: 'full' },
{ label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' }, { label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' },
@ -24,7 +24,7 @@ export const IPAdapterMethod = memo(({ method, onChange }: Props) => {
); );
const _onChange = useCallback<ComboboxOnChange>( const _onChange = useCallback<ComboboxOnChange>(
(v) => { (v) => {
assert(isIPMethod(v?.value)); assert(isIPMethodV2(v?.value));
onChange(v.value); onChange(v.value);
}, },
[onChange] [onChange]

View File

@ -2,8 +2,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import type { CLIPVisionModel } from 'features/controlLayers/util/controlAdapters'; import type { CLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters';
import { isCLIPVisionModel } from 'features/controlLayers/util/controlAdapters'; import { isCLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useIPAdapterModels } from 'services/api/hooks/modelsByType'; import { useIPAdapterModels } from 'services/api/hooks/modelsByType';
@ -18,8 +18,8 @@ const CLIP_VISION_OPTIONS = [
type Props = { type Props = {
modelKey: string | null; modelKey: string | null;
onChangeModel: (modelConfig: IPAdapterModelConfig) => void; onChangeModel: (modelConfig: IPAdapterModelConfig) => void;
clipVisionModel: CLIPVisionModel; clipVisionModel: CLIPVisionModelV2;
onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void; onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void;
}; };
export const IPAdapterModelSelect = memo( export const IPAdapterModelSelect = memo(
@ -41,7 +41,7 @@ export const IPAdapterModelSelect = memo(
const _onChangeCLIPVisionModel = useCallback<ComboboxOnChange>( const _onChangeCLIPVisionModel = useCallback<ComboboxOnChange>(
(v) => { (v) => {
assert(isCLIPVisionModel(v?.value)); assert(isCLIPVisionModelV2(v?.value));
onChangeCLIPVisionModel(v.value); onChangeCLIPVisionModel(v.value);
}, },
[onChangeCLIPVisionModel] [onChangeCLIPVisionModel]

View File

@ -1,13 +1,13 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; 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 { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper'; import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<CannyProcessorConfig>; type Props = ProcessorComponentProps<CannyProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['canny_image_processor'].buildDefaults(); const DEFAULTS = CA_PROCESSOR_DATA['canny_image_processor'].buildDefaults();
export const CannyProcessor = ({ onChange, config }: Props) => { export const CannyProcessor = ({ onChange, config }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,13 +1,13 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; 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 { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper'; import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<ColorMapProcessorConfig>; type Props = ProcessorComponentProps<ColorMapProcessorConfig>;
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) => { export const ColorMapProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,14 +1,14 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { ContentShuffleProcessorConfig } from 'features/controlLayers/util/controlAdapters'; 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 { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper'; import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<ContentShuffleProcessorConfig>; type Props = ProcessorComponentProps<ContentShuffleProcessorConfig>;
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) => { export const ContentShuffleProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,7 +1,7 @@
import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { DWOpenposeProcessorConfig } from 'features/controlLayers/util/controlAdapters'; 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 type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper'; import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<DWOpenposeProcessorConfig>; type Props = ProcessorComponentProps<DWOpenposeProcessorConfig>;
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) => { export const DWOpenposeProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -2,14 +2,14 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } 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 { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters'; 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 { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper'; import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<DepthAnythingProcessorConfig>; type Props = ProcessorComponentProps<DepthAnythingProcessorConfig>;
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) => { export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,13 +1,13 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; 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 { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper'; import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<MediapipeFaceProcessorConfig>; type Props = ProcessorComponentProps<MediapipeFaceProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['mediapipe_face_processor'].buildDefaults(); const DEFAULTS = CA_PROCESSOR_DATA['mediapipe_face_processor'].buildDefaults();
export const MediapipeFaceProcessor = memo(({ onChange, config }: Props) => { export const MediapipeFaceProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,14 +1,14 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { MidasDepthProcessorConfig } from 'features/controlLayers/util/controlAdapters'; 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 { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper'; import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<MidasDepthProcessorConfig>; type Props = ProcessorComponentProps<MidasDepthProcessorConfig>;
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) => { export const MidasDepthProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,14 +1,14 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { MlsdProcessorConfig } from 'features/controlLayers/util/controlAdapters'; 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 { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper'; import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<MlsdProcessorConfig>; type Props = ProcessorComponentProps<MlsdProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['mlsd_image_processor'].buildDefaults(); const DEFAULTS = CA_PROCESSOR_DATA['mlsd_image_processor'].buildDefaults();
export const MlsdImageProcessor = memo(({ onChange, config }: Props) => { export const MlsdImageProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -2,16 +2,19 @@
import { Flex } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton'; import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton';
import { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; import { CALayer } from 'features/controlLayers/components/CALayer/CALayer';
import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton';
import { IILayer } from 'features/controlLayers/components/IILayer/IILayer';
import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer'; import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer';
import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer';
import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types'; import type { Layer } from 'features/controlLayers/store/types';
import { partition } from 'lodash-es'; import { partition } from 'lodash-es';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer); const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer);
@ -19,20 +22,24 @@ const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice,
}); });
export const ControlLayersPanelContent = memo(() => { export const ControlLayersPanelContent = memo(() => {
const { t } = useTranslation();
const layerIdTypePairs = useAppSelector(selectLayerIdTypePairs); const layerIdTypePairs = useAppSelector(selectLayerIdTypePairs);
return ( return (
<Flex flexDir="column" gap={4} w="full" h="full"> <Flex flexDir="column" gap={2} w="full" h="full">
<Flex justifyContent="space-around"> <Flex justifyContent="space-around">
<AddLayerButton /> <AddLayerButton />
<DeleteAllLayersButton /> <DeleteAllLayersButton />
</Flex> </Flex>
<ScrollableContent> {layerIdTypePairs.length > 0 && (
<Flex flexDir="column" gap={4}> <ScrollableContent>
{layerIdTypePairs.map(({ id, type }) => ( <Flex flexDir="column" gap={2}>
<LayerWrapper key={id} id={id} type={type} /> {layerIdTypePairs.map(({ id, type }) => (
))} <LayerWrapper key={id} id={id} type={type} />
</Flex> ))}
</ScrollableContent> </Flex>
</ScrollableContent>
)}
{layerIdTypePairs.length === 0 && <IAINoContentFallback icon={null} label={t('controlLayers.noLayersAdded')} />}
</Flex> </Flex>
); );
}); });
@ -54,6 +61,9 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => {
if (type === 'ip_adapter_layer') { if (type === 'ip_adapter_layer') {
return <IPALayer key={id} layerId={id} />; return <IPALayer key={id} layerId={id} />;
} }
if (type === 'initial_image_layer') {
return <IILayer key={id} layerId={id} />;
}
}); });
LayerWrapper.displayName = 'LayerWrapper'; LayerWrapper.displayName = 'LayerWrapper';

View File

@ -4,15 +4,26 @@ import { BrushSize } from 'features/controlLayers/components/BrushSize';
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
import { memo } from 'react'; import { memo } from 'react';
export const ControlLayersToolbar = memo(() => { export const ControlLayersToolbar = memo(() => {
return ( return (
<Flex gap={4}> <Flex w="full" gap={2}>
<BrushSize /> <Flex flex={1} justifyContent="center">
<ToolChooser /> <Flex gap={2} marginInlineEnd="auto" />
<UndoRedoButtonGroup /> </Flex>
<ControlLayersSettingsPopover /> <Flex flex={1} gap={2} justifyContent="center">
<BrushSize />
<ToolChooser />
<UndoRedoButtonGroup />
<ControlLayersSettingsPopover />
</Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
<ViewerButton />
</Flex>
</Flex>
</Flex> </Flex>
); );
}); });

View File

@ -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<IILayerImageDropData>(
() => ({
actionType: 'SET_II_LAYER_IMAGE',
context: {
layerId,
},
id: layerId,
}),
[layerId]
);
const postUploadAction = useMemo<IILayerImagePostUploadAction>(
() => ({
layerId,
type: 'SET_II_LAYER_IMAGE',
}),
[layerId]
);
return (
<LayerWrapper onClick={onClick} borderColor={layer.isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="initial_image_layer" />
<Spacer />
<IILayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<ImageToImageStrength />
<InitialImagePreview
image={layer.image}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>
</Flex>
)}
</LayerWrapper>
);
});
IILayer.displayName = 'IILayer';

View File

@ -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 (
<Popover isLazy>
<PopoverTrigger>
<IconButton
aria-label={t('controlLayers.opacity')}
size="sm"
icon={<PiDropHalfFill size={16} />}
variant="ghost"
onDoubleClick={stopPropagation}
/>
</PopoverTrigger>
<PopoverContent onDoubleClick={stopPropagation}>
<PopoverArrow />
<PopoverBody>
<Flex direction="column" gap={2}>
<FormControl orientation="horizontal">
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
<CompositeSlider
min={0}
max={100}
step={1}
value={opacity}
defaultValue={100}
onChange={onChangeOpacity}
marks={marks}
w={48}
/>
<CompositeNumberInput
min={0}
max={100}
step={1}
value={opacity}
defaultValue={100}
onChange={onChangeOpacity}
w={24}
format={formatPct}
/>
</FormControl>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default memo(IILayerOpacity);

View File

@ -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<ImageDraggableData | undefined>(() => {
if (imageDTO) {
return {
id: 'initial_image_layer',
payloadType: 'IMAGE_DTO',
payload: { imageDTO: imageDTO },
};
}
}, [imageDTO]);
useEffect(() => {
if (isConnected && isErrorControlImage) {
onReset();
}
}, [onReset, isConnected, isErrorControlImage]);
return (
<Flex position="relative" w="full" h={36} alignItems="center" justifyContent="center">
<IAIDndImage
draggableData={draggableData}
droppableData={droppableData}
imageDTO={imageDTO}
postUploadAction={postUploadAction}
/>
<>
<IAIDndImageIcon
onClick={onReset}
icon={imageDTO ? <PiArrowCounterClockwiseBold size={16} /> : undefined}
tooltip={t('controlnet.resetControlImage')}
/>
<IAIDndImageIcon
onClick={onUseSize}
icon={imageDTO ? <PiRulerBold size={16} /> : undefined}
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
styleOverrides={useSizeStyleOverrides}
/>
</>
</Flex>
);
});
InitialImagePreview.displayName = 'InitialImagePreview';
const useSizeStyleOverrides: SystemStyleObject = { mt: 6 };

View File

@ -3,6 +3,7 @@ import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPAL
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { memo } from 'react'; import { memo } from 'react';
type Props = { type Props = {
@ -12,21 +13,19 @@ type Props = {
export const IPALayer = memo(({ layerId }: Props) => { export const IPALayer = memo(({ layerId }: Props) => {
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
return ( return (
<Flex gap={2} bg="base.800" borderRadius="base" p="1px" px={2}> <LayerWrapper borderColor="base.800">
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base"> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <LayerVisibilityToggle layerId={layerId} />
<LayerVisibilityToggle layerId={layerId} /> <LayerTitle type="ip_adapter_layer" />
<LayerTitle type="ip_adapter_layer" /> <Spacer />
<Spacer /> <LayerDeleteButton layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<IPALayerIPAdapterWrapper layerId={layerId} />
</Flex>
)}
</Flex> </Flex>
</Flex> {isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<IPALayerIPAdapterWrapper layerId={layerId} />
</Flex>
)}
</LayerWrapper>
); );
}); });

View File

@ -9,7 +9,7 @@ import {
ipaLayerModelChanged, ipaLayerModelChanged,
selectIPALayerOrThrow, selectIPALayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice'; } 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 type { IPALayerImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types'; import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types';
@ -42,7 +42,7 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => {
); );
const onChangeIPMethod = useCallback( const onChangeIPMethod = useCallback(
(method: IPMethod) => { (method: IPMethodV2) => {
dispatch(ipaLayerMethodChanged({ layerId, method })); dispatch(ipaLayerMethodChanged({ layerId, method }));
}, },
[dispatch, layerId] [dispatch, layerId]
@ -56,7 +56,7 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => {
); );
const onChangeCLIPVisionModel = useCallback( const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModel) => { (clipVisionModel: CLIPVisionModelV2) => {
dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel })); dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel }));
}, },
[dispatch, layerId] [dispatch, layerId]

View File

@ -37,7 +37,9 @@ export const LayerMenu = memo(({ layerId }: Props) => {
<MenuDivider /> <MenuDivider />
</> </>
)} )}
{(layerType === 'regional_guidance_layer' || layerType === 'control_adapter_layer') && ( {(layerType === 'regional_guidance_layer' ||
layerType === 'control_adapter_layer' ||
layerType === 'initial_image_layer') && (
<> <>
<LayerMenuArrangeActions layerId={layerId} /> <LayerMenuArrangeActions layerId={layerId} />
<MenuDivider /> <MenuDivider />

View File

@ -16,6 +16,8 @@ export const LayerTitle = memo(({ type }: Props) => {
return t('controlLayers.globalControlAdapter'); return t('controlLayers.globalControlAdapter');
} else if (type === 'ip_adapter_layer') { } else if (type === 'ip_adapter_layer') {
return t('controlLayers.globalIPAdapter'); return t('controlLayers.globalIPAdapter');
} else if (type === 'initial_image_layer') {
return t('controlLayers.globalInitialImage');
} }
}, [t, type]); }, [t, type]);

View File

@ -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 (
<Flex gap={2} onClick={onClick} bg={borderColor} px={2} borderRadius="base" py="1px">
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
{children}
</Flex>
</Flex>
);
});
LayerWrapper.displayName = 'LayerWrapper';

View File

@ -7,6 +7,7 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { import {
isRegionalGuidanceLayer, isRegionalGuidanceLayer,
layerSelected, layerSelected,
@ -52,32 +53,30 @@ export const RGLayer = memo(({ layerId }: Props) => {
dispatch(layerSelected(layerId)); dispatch(layerSelected(layerId));
}, [dispatch, layerId]); }, [dispatch, layerId]);
return ( return (
<Flex gap={2} onClick={onClick} bg={isSelected ? color : 'base.800'} px={2} borderRadius="base" py="1px"> <LayerWrapper onClick={onClick} borderColor={isSelected ? color : 'base.800'}>
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base"> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <LayerVisibilityToggle layerId={layerId} />
<LayerVisibilityToggle layerId={layerId} /> <LayerTitle type="regional_guidance_layer" />
<LayerTitle type="regional_guidance_layer" /> <Spacer />
<Spacer /> {autoNegative === 'invert' && (
{autoNegative === 'invert' && ( <Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none"> {t('controlLayers.autoNegative')}
{t('controlLayers.autoNegative')} </Badge>
</Badge>
)}
<RGLayerColorPicker layerId={layerId} />
<RGLayerSettingsPopover layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons layerId={layerId} />}
{hasPositivePrompt && <RGLayerPositivePrompt layerId={layerId} />}
{hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />}
{hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />}
</Flex>
)} )}
<RGLayerColorPicker layerId={layerId} />
<RGLayerSettingsPopover layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex> </Flex>
</Flex> {isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons layerId={layerId} />}
{hasPositivePrompt && <RGLayerPositivePrompt layerId={layerId} />}
{hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />}
{hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />}
</Flex>
)}
</LayerWrapper>
); );
}); });

View File

@ -11,7 +11,7 @@ import {
rgLayerIPAdapterWeightChanged, rgLayerIPAdapterWeightChanged,
selectRGLayerIPAdapterOrThrow, selectRGLayerIPAdapterOrThrow,
} from 'features/controlLayers/store/controlLayersSlice'; } 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 type { RGLayerIPAdapterImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { PiTrashSimpleBold } from 'react-icons/pi'; import { PiTrashSimpleBold } from 'react-icons/pi';
@ -51,7 +51,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
); );
const onChangeIPMethod = useCallback( const onChangeIPMethod = useCallback(
(method: IPMethod) => { (method: IPMethodV2) => {
dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method })); dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method }));
}, },
[dispatch, ipAdapterId, layerId] [dispatch, ipAdapterId, layerId]
@ -65,7 +65,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
); );
const onChangeCLIPVisionModel = useCallback( const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModel) => { (clipVisionModel: CLIPVisionModelV2) => {
dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel })); dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel }));
}, },
[dispatch, ipAdapterId, layerId] [dispatch, ipAdapterId, layerId]

View File

@ -6,8 +6,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useMouseEvents } from 'features/controlLayers/hooks/mouseEventHooks'; import { useMouseEvents } from 'features/controlLayers/hooks/mouseEventHooks';
import { import {
$cursorPosition, $lastCursorPos,
$isMouseOver,
$lastMouseDownPos, $lastMouseDownPos,
$tool, $tool,
isRegionalGuidanceLayer, isRegionalGuidanceLayer,
@ -48,10 +47,9 @@ const useStageRenderer = (
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const state = useAppSelector((s) => s.controlLayers.present); const state = useAppSelector((s) => s.controlLayers.present);
const tool = useStore($tool); const tool = useStore($tool);
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel } = useMouseEvents(); const mouseEventHandlers = useMouseEvents();
const cursorPosition = useStore($cursorPosition); const lastCursorPos = useStore($lastCursorPos);
const lastMouseDownPos = useStore($lastMouseDownPos); const lastMouseDownPos = useStore($lastMouseDownPos);
const isMouseOver = useStore($isMouseOver);
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor); const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
const selectedLayerType = useAppSelector(selectSelectedLayerType); const selectedLayerType = useAppSelector(selectSelectedLayerType);
const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]); const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]);
@ -90,23 +88,21 @@ const useStageRenderer = (
if (asPreview) { if (asPreview) {
return; return;
} }
stage.on('mousedown', onMouseDown); stage.on('mousedown', mouseEventHandlers.onMouseDown);
stage.on('mouseup', onMouseUp); stage.on('mouseup', mouseEventHandlers.onMouseUp);
stage.on('mousemove', onMouseMove); stage.on('mousemove', mouseEventHandlers.onMouseMove);
stage.on('mouseenter', onMouseEnter); stage.on('mouseleave', mouseEventHandlers.onMouseLeave);
stage.on('mouseleave', onMouseLeave); stage.on('wheel', mouseEventHandlers.onMouseWheel);
stage.on('wheel', onMouseWheel);
return () => { return () => {
log.trace('Cleaning up stage listeners'); log.trace('Cleaning up stage listeners');
stage.off('mousedown', onMouseDown); stage.off('mousedown', mouseEventHandlers.onMouseDown);
stage.off('mouseup', onMouseUp); stage.off('mouseup', mouseEventHandlers.onMouseUp);
stage.off('mousemove', onMouseMove); stage.off('mousemove', mouseEventHandlers.onMouseMove);
stage.off('mouseenter', onMouseEnter); stage.off('mouseleave', mouseEventHandlers.onMouseLeave);
stage.off('mouseleave', onMouseLeave); stage.off('wheel', mouseEventHandlers.onMouseWheel);
stage.off('wheel', onMouseWheel);
}; };
}, [stage, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel]); }, [stage, asPreview, mouseEventHandlers]);
useLayoutEffect(() => { useLayoutEffect(() => {
log.trace('Updating stage dimensions'); log.trace('Updating stage dimensions');
@ -145,9 +141,8 @@ const useStageRenderer = (
selectedLayerIdColor, selectedLayerIdColor,
selectedLayerType, selectedLayerType,
state.globalMaskLayerOpacity, state.globalMaskLayerOpacity,
cursorPosition, lastCursorPos,
lastMouseDownPos, lastMouseDownPos,
isMouseOver,
state.brushSize state.brushSize
); );
}, [ }, [
@ -157,9 +152,8 @@ const useStageRenderer = (
selectedLayerIdColor, selectedLayerIdColor,
selectedLayerType, selectedLayerType,
state.globalMaskLayerOpacity, state.globalMaskLayerOpacity,
cursorPosition, lastCursorPos,
lastMouseDownPos, lastMouseDownPos,
isMouseOver,
state.brushSize, state.brushSize,
renderers, renderers,
]); ]);

View File

@ -4,9 +4,9 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { import {
$tool, $tool,
layerReset,
selectControlLayersSlice, selectControlLayersSlice,
selectedLayerDeleted, selectedLayerDeleted,
selectedLayerReset,
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -22,6 +22,7 @@ export const ToolChooser: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isDisabled = useAppSelector(selectIsDisabled); const isDisabled = useAppSelector(selectIsDisabled);
const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId);
const tool = useStore($tool); const tool = useStore($tool);
const setToolToBrush = useCallback(() => { const setToolToBrush = useCallback(() => {
@ -42,8 +43,11 @@ export const ToolChooser: React.FC = () => {
useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]); useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]);
const resetSelectedLayer = useCallback(() => { const resetSelectedLayer = useCallback(() => {
dispatch(selectedLayerReset()); if (selectedLayerId === null) {
}, [dispatch]); return;
}
dispatch(layerReset(selectedLayerId));
}, [dispatch, selectedLayerId]);
useHotkeys('shift+c', resetSelectedLayer); useHotkeys('shift+c', resetSelectedLayer);
const deleteSelectedLayer = useCallback(() => { const deleteSelectedLayer = useCallback(() => {

View File

@ -1,11 +1,17 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; 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 { import {
buildControlNet, buildControlNet,
buildIPAdapter, buildIPAdapter,
buildT2IAdapter, buildT2IAdapter,
CONTROLNET_PROCESSORS, CA_PROCESSOR_DATA,
isProcessorType, isProcessorTypeV2,
} from 'features/controlLayers/util/controlAdapters'; } from 'features/controlLayers/util/controlAdapters';
import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
@ -30,8 +36,8 @@ export const useAddCALayer = () => {
const id = uuidv4(); const id = uuidv4();
const defaultPreprocessor = model.default_settings?.preprocessor; const defaultPreprocessor = model.default_settings?.preprocessor;
const processorConfig = isProcessorType(defaultPreprocessor) const processorConfig = isProcessorTypeV2(defaultPreprocessor)
? CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(baseModel) ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(baseModel)
: null; : null;
const builder = model.type === 'controlnet' ? buildControlNet : buildT2IAdapter; const builder = model.type === 'controlnet' ? buildControlNet : buildT2IAdapter;
@ -93,3 +99,13 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => {
return [addIPAdapter, isDisabled] as const; 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;
};

View File

@ -1,4 +1,5 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { import {
isControlAdapterLayer, isControlAdapterLayer,
@ -69,7 +70,7 @@ export const useLayerType = (layerId: string) => {
export const useLayerOpacity = (layerId: string) => { export const useLayerOpacity = (layerId: string) => {
const selectLayer = useMemo( const selectLayer = useMemo(
() => () =>
createSelector(selectControlLayersSlice, (controlLayers) => { createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`); assert(layer, `Layer ${layerId} not found`);
return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled }; return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled };

View File

@ -3,9 +3,8 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom';
import { import {
$cursorPosition, $isDrawing,
$isMouseDown, $lastCursorPos,
$isMouseOver,
$lastMouseDownPos, $lastMouseDownPos,
$tool, $tool,
brushSizeChanged, brushSizeChanged,
@ -16,16 +15,41 @@ import {
import type Konva from 'konva'; import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node'; import type { KonvaEventObject } from 'konva/lib/Node';
import type { Vector2d } from 'konva/lib/types'; 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) => { const getIsFocused = (stage: Konva.Stage) => {
return stage.container().contains(document.activeElement); return stage.container().contains(document.activeElement);
}; };
const getIsMouseDown = (e: KonvaEventObject<MouseEvent>) => 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) => { export const getScaledFlooredCursorPosition = (stage: Konva.Stage) => {
const pointerPosition = stage.getPointerPosition(); const pointerPosition = stage.getPointerPosition();
const stageTransform = stage.getAbsoluteTransform().copy(); const stageTransform = stage.getAbsoluteTransform().copy();
if (!pointerPosition || !stageTransform) { if (!pointerPosition) {
return; return;
} }
const scaledCursorPosition = stageTransform.invert().point(pointerPosition); const scaledCursorPosition = stageTransform.invert().point(pointerPosition);
@ -40,33 +64,41 @@ const syncCursorPos = (stage: Konva.Stage): Vector2d | null => {
if (!pos) { if (!pos) {
return null; return null;
} }
$cursorPosition.set(pos); $lastCursorPos.set(pos);
return 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 = () => { export const useMouseEvents = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId); 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 tool = useStore($tool);
const lastCursorPosRef = useRef<[number, number] | null>(null); const lastCursorPosRef = useRef<[number, number] | null>(null);
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection);
const brushSize = useAppSelector((s) => s.controlLayers.present.brushSize); 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( const onMouseDown = useCallback(
(e: KonvaEventObject<MouseEvent | TouchEvent>) => { (e: KonvaEventObject<MouseEvent>) => {
const stage = e.target.getStage(); const stage = e.target.getStage();
if (!stage) { if (!stage) {
return; return;
} }
const pos = syncCursorPos(stage); const pos = syncCursorPos(stage);
if (!pos) { if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
return;
}
$isMouseDown.set(true);
$lastMouseDownPos.set(pos);
if (!selectedLayerId) {
return; return;
} }
if (tool === 'brush' || tool === 'eraser') { if (tool === 'brush' || tool === 'eraser') {
@ -77,126 +109,105 @@ export const useMouseEvents = () => {
tool, 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( const onMouseUp = useCallback(
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
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<MouseEvent | TouchEvent>) => {
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<MouseEvent | TouchEvent>) => {
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<MouseEvent>) => { (e: KonvaEventObject<MouseEvent>) => {
const stage = e.target.getStage(); const stage = e.target.getStage();
if (!stage) { if (!stage) {
return; 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<MouseEvent>) => {
const stage = e.target.getStage();
if (!stage) {
return;
}
const pos = syncCursorPos(stage); const pos = syncCursorPos(stage);
if (!pos) { if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
return; return;
} }
if (!getIsFocused(stage)) { if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) {
return; if ($isDrawing.get()) {
} // Continue the last line
if (e.evt.buttons !== 1) { if (lastCursorPosRef.current) {
$isMouseDown.set(false); // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number
} else { if (Math.hypot(lastCursorPosRef.current[0] - pos.x, lastCursorPosRef.current[1] - pos.y) < brushSpacingPx) {
$isMouseDown.set(true); return;
if (!selectedLayerId) { }
return; }
} lastCursorPosRef.current = [pos.x, pos.y];
if (tool === 'brush' || tool === 'eraser') { dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: lastCursorPosRef.current }));
dispatch( } else {
rgLayerLineAdded({ // Start a new line
layerId: selectedLayerId, dispatch(rgLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool }));
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<MouseEvent>) => {
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( const onMouseWheel = useCallback(
(e: KonvaEventObject<WheelEvent>) => { (e: KonvaEventObject<WheelEvent>) => {
e.evt.preventDefault(); e.evt.preventDefault();
if (selectedLayerType !== 'regional_guidance_layer' || (tool !== 'brush' && tool !== 'eraser')) {
return;
}
// checking for ctrl key is pressed or not, // checking for ctrl key is pressed or not,
// so that brush size can be controlled using ctrl + scroll up/down // so that brush size can be controlled using ctrl + scroll up/down
@ -210,8 +221,8 @@ export const useMouseEvents = () => {
dispatch(brushSizeChanged(calculateNewBrushSize(brushSize, delta))); 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 };
}; };

View File

@ -1,20 +1,43 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks'; 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 { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => { const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => {
const validLayers = controlLayers.present.layers let count = 0;
.filter(isRegionalGuidanceLayer) controlLayers.present.layers.forEach((l) => {
.filter((l) => l.isEnabled) if (isRegionalGuidanceLayer(l)) {
.filter((l) => {
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
const hasAtLeastOneImagePrompt = l.ipAdapters.length > 0; const hasAtLeastOneImagePrompt = l.ipAdapters.filter((ipa) => Boolean(ipa.image)).length > 0;
return hasTextPrompt || hasAtLeastOneImagePrompt; 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 = () => { export const useControlLayersTitle = () => {

View File

@ -3,17 +3,18 @@ import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store'; import type { PersistConfig, RootState } from 'app/store/store';
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
import { deepClone } from 'common/util/deepClone'; import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
import type { import type {
CLIPVisionModel, CLIPVisionModelV2,
ControlMode, ControlModeV2,
ControlNetConfig, ControlNetConfigV2,
IPAdapterConfig, IPAdapterConfigV2,
IPMethod, IPMethodV2,
ProcessorConfig, ProcessorConfig,
T2IAdapterConfig, T2IAdapterConfigV2,
} from 'features/controlLayers/util/controlAdapters'; } from 'features/controlLayers/util/controlAdapters';
import { import {
buildControlAdapterProcessor, buildControlAdapterProcessorV2,
controlNetToT2IAdapter, controlNetToT2IAdapter,
imageDTOToImageWithDims, imageDTOToImageWithDims,
t2iAdapterToControlNet, t2iAdapterToControlNet,
@ -38,6 +39,7 @@ import type {
ControlAdapterLayer, ControlAdapterLayer,
ControlLayersState, ControlLayersState,
DrawingTool, DrawingTool,
InitialImageLayer,
IPAdapterLayer, IPAdapterLayer,
Layer, Layer,
RegionalGuidanceLayer, RegionalGuidanceLayer,
@ -70,18 +72,13 @@ export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanc
export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer =>
layer?.type === 'control_adapter_layer'; layer?.type === 'control_adapter_layer';
export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => layer?.type === 'ip_adapter_layer'; export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => layer?.type === 'ip_adapter_layer';
export const isRenderableLayer = (layer?: Layer): layer is RegionalGuidanceLayer | ControlAdapterLayer => export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => layer?.type === 'initial_image_layer';
layer?.type === 'regional_guidance_layer' || layer?.type === 'control_adapter_layer'; export const isRenderableLayer = (
const resetLayer = (layer: Layer) => { layer?: Layer
if (layer.type === 'regional_guidance_layer') { ): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer =>
layer.maskObjects = []; layer?.type === 'regional_guidance_layer' ||
layer.bbox = null; layer?.type === 'control_adapter_layer' ||
layer.isEnabled = true; layer?.type === 'initial_image_layer';
layer.needsPixelBbox = false;
layer.bboxNeedsUpdate = false;
return;
}
};
export const selectCALayerOrThrow = (state: ControlLayersState, layerId: string): ControlAdapterLayer => { export const selectCALayerOrThrow = (state: ControlLayersState, layerId: string): ControlAdapterLayer => {
const layer = state.layers.find((l) => l.id === layerId); const layer = state.layers.find((l) => l.id === layerId);
@ -93,6 +90,11 @@ export const selectIPALayerOrThrow = (state: ControlLayersState, layerId: string
assert(isIPAdapterLayer(layer)); assert(isIPAdapterLayer(layer));
return 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 = ( const selectCAOrIPALayerOrThrow = (
state: ControlLayersState, state: ControlLayersState,
layerId: string layerId: string
@ -110,7 +112,7 @@ export const selectRGLayerIPAdapterOrThrow = (
state: ControlLayersState, state: ControlLayersState,
layerId: string, layerId: string,
ipAdapterId: string ipAdapterId: string
): IPAdapterConfig => { ): IPAdapterConfigV2 => {
const layer = state.layers.find((l) => l.id === layerId); const layer = state.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer)); assert(isRegionalGuidanceLayer(layer));
const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId);
@ -151,6 +153,9 @@ export const controlLayersSlice = createSlice({
layer.x = x; layer.x = x;
layer.y = y; layer.y = y;
} }
if (isRegionalGuidanceLayer(layer)) {
layer.uploadedMaskImage = null;
}
}, },
layerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => { layerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => {
const { layerId, bbox } = action.payload; const { layerId, bbox } = action.payload;
@ -161,14 +166,21 @@ export const controlLayersSlice = createSlice({
if (bbox === null && layer.type === 'regional_guidance_layer') { if (bbox === null && layer.type === 'regional_guidance_layer') {
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects // The layer was fully erased, empty its objects to prevent accumulation of invisible objects
layer.maskObjects = []; layer.maskObjects = [];
layer.uploadedMaskImage = null;
layer.needsPixelBbox = false; layer.needsPixelBbox = false;
} }
} }
}, },
layerReset: (state, action: PayloadAction<string>) => { layerReset: (state, action: PayloadAction<string>) => {
const layer = state.layers.find((l) => l.id === action.payload); const layer = state.layers.find((l) => l.id === action.payload);
if (layer) { // TODO(psyche): Should other layer types also have reset functionality?
resetLayer(layer); if (isRegionalGuidanceLayer(layer)) {
layer.maskObjects = [];
layer.bbox = null;
layer.isEnabled = true;
layer.needsPixelBbox = false;
layer.bboxNeedsUpdate = false;
layer.uploadedMaskImage = null;
} }
}, },
layerDeleted: (state, action: PayloadAction<string>) => { layerDeleted: (state, action: PayloadAction<string>) => {
@ -201,12 +213,6 @@ export const controlLayersSlice = createSlice({
moveToFront(renderableLayers, cb); moveToFront(renderableLayers, cb);
state.layers = [...ipAdapterLayers, ...renderableLayers]; state.layers = [...ipAdapterLayers, ...renderableLayers];
}, },
selectedLayerReset: (state) => {
const layer = state.layers.find((l) => l.id === state.selectedLayerId);
if (layer) {
resetLayer(layer);
}
},
selectedLayerDeleted: (state) => { selectedLayerDeleted: (state) => {
state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId); state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId);
state.selectedLayerId = state.layers[0]?.id ?? null; state.selectedLayerId = state.layers[0]?.id ?? null;
@ -221,7 +227,7 @@ export const controlLayersSlice = createSlice({
caLayerAdded: { caLayerAdded: {
reducer: ( reducer: (
state, state,
action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }> action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2 }>
) => { ) => {
const { layerId, controlAdapter } = action.payload; const { layerId, controlAdapter } = action.payload;
const layer: ControlAdapterLayer = { const layer: ControlAdapterLayer = {
@ -245,7 +251,7 @@ export const controlLayersSlice = createSlice({
} }
} }
}, },
prepare: (controlAdapter: ControlNetConfig | T2IAdapterConfig) => ({ prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({
payload: { layerId: uuidv4(), controlAdapter }, payload: { layerId: uuidv4(), controlAdapter },
}), }),
}, },
@ -297,7 +303,7 @@ export const controlLayersSlice = createSlice({
layer.controlAdapter = controlNetToT2IAdapter(layer.controlAdapter); layer.controlAdapter = controlNetToT2IAdapter(layer.controlAdapter);
} }
const candidateProcessorConfig = buildControlAdapterProcessor(modelConfig); const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig);
if (candidateProcessorConfig?.type !== layer.controlAdapter.processorConfig?.type) { 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 // 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. // model. We need to use the new processor.
@ -305,7 +311,7 @@ export const controlLayersSlice = createSlice({
layer.controlAdapter.processorConfig = candidateProcessorConfig; 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 { layerId, controlMode } = action.payload;
const layer = selectCALayerOrThrow(state, layerId); const layer = selectCALayerOrThrow(state, layerId);
assert(layer.controlAdapter.type === 'controlnet'); assert(layer.controlAdapter.type === 'controlnet');
@ -340,11 +346,17 @@ export const controlLayersSlice = createSlice({
const layer = selectCALayerOrThrow(state, layerId); const layer = selectCALayerOrThrow(state, layerId);
layer.controlAdapter.isProcessingImage = isProcessingImage; 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 //#endregion
//#region IP Adapter Layers //#region IP Adapter Layers
ipaLayerAdded: { ipaLayerAdded: {
reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => { reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => {
const { layerId, ipAdapter } = action.payload; const { layerId, ipAdapter } = action.payload;
const layer: IPAdapterLayer = { const layer: IPAdapterLayer = {
id: getIPALayerId(layerId), id: getIPALayerId(layerId),
@ -354,14 +366,14 @@ export const controlLayersSlice = createSlice({
}; };
state.layers.push(layer); 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 }>) => { ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
const { layerId, imageDTO } = action.payload; const { layerId, imageDTO } = action.payload;
const layer = selectIPALayerOrThrow(state, layerId); const layer = selectIPALayerOrThrow(state, layerId);
layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; 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 { layerId, method } = action.payload;
const layer = selectIPALayerOrThrow(state, layerId); const layer = selectIPALayerOrThrow(state, layerId);
layer.ipAdapter.method = method; layer.ipAdapter.method = method;
@ -383,12 +395,15 @@ export const controlLayersSlice = createSlice({
}, },
ipaLayerCLIPVisionModelChanged: ( ipaLayerCLIPVisionModelChanged: (
state, state,
action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }> action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModelV2 }>
) => { ) => {
const { layerId, clipVisionModel } = action.payload; const { layerId, clipVisionModel } = action.payload;
const layer = selectIPALayerOrThrow(state, layerId); const layer = selectIPALayerOrThrow(state, layerId);
layer.ipAdapter.clipVisionModel = clipVisionModel; layer.ipAdapter.clipVisionModel = clipVisionModel;
}, },
ipaLayersDeleted: (state) => {
state.layers = state.layers.filter((l) => !isIPAdapterLayer(l));
},
//#endregion //#endregion
//#region CA or IPA Layers //#region CA or IPA Layers
@ -435,6 +450,7 @@ export const controlLayersSlice = createSlice({
negativePrompt: null, negativePrompt: null,
ipAdapters: [], ipAdapters: [],
isSelected: true, isSelected: true,
uploadedMaskImage: null,
}; };
state.layers.push(layer); state.layers.push(layer);
state.selectedLayerId = layer.id; state.selectedLayerId = layer.id;
@ -484,6 +500,7 @@ export const controlLayersSlice = createSlice({
strokeWidth: state.brushSize, strokeWidth: state.brushSize,
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
layer.uploadedMaskImage = null;
if (!layer.needsPixelBbox && tool === 'eraser') { if (!layer.needsPixelBbox && tool === 'eraser') {
layer.needsPixelBbox = true; layer.needsPixelBbox = true;
} }
@ -503,6 +520,7 @@ export const controlLayersSlice = createSlice({
// TODO: Handle this in the event listener // TODO: Handle this in the event listener
lastLine.points.push(point[0] - layer.x, point[1] - layer.y); lastLine.points.push(point[0] - layer.x, point[1] - layer.y);
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
layer.uploadedMaskImage = null;
}, },
rgLayerRectAdded: { rgLayerRectAdded: {
reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect; rectUuid: string }>) => { reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect; rectUuid: string }>) => {
@ -522,9 +540,15 @@ export const controlLayersSlice = createSlice({
height: rect.height, height: rect.height,
}); });
layer.bboxNeedsUpdate = true; layer.bboxNeedsUpdate = true;
layer.uploadedMaskImage = null;
}, },
prepare: (payload: { layerId: string; rect: IRect }) => ({ payload: { ...payload, rectUuid: uuidv4() } }), 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: ( rgLayerAutoNegativeChanged: (
state, state,
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }> action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
@ -533,7 +557,7 @@ export const controlLayersSlice = createSlice({
const layer = selectRGLayerOrThrow(state, layerId); const layer = selectRGLayerOrThrow(state, layerId);
layer.autoNegative = autoNegative; 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 { layerId, ipAdapter } = action.payload;
const layer = selectRGLayerOrThrow(state, layerId); const layer = selectRGLayerOrThrow(state, layerId);
layer.ipAdapters.push(ipAdapter); layer.ipAdapters.push(ipAdapter);
@ -569,7 +593,7 @@ export const controlLayersSlice = createSlice({
}, },
rgLayerIPAdapterMethodChanged: ( rgLayerIPAdapterMethodChanged: (
state, state,
action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethod }> action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethodV2 }>
) => { ) => {
const { layerId, ipAdapterId, method } = action.payload; const { layerId, ipAdapterId, method } = action.payload;
const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId);
@ -593,7 +617,7 @@ export const controlLayersSlice = createSlice({
}, },
rgLayerIPAdapterCLIPVisionModelChanged: ( rgLayerIPAdapterCLIPVisionModelChanged: (
state, state,
action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModel }> action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }>
) => { ) => {
const { layerId, ipAdapterId, clipVisionModel } = action.payload; const { layerId, ipAdapterId, clipVisionModel } = action.payload;
const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId);
@ -601,6 +625,49 @@ export const controlLayersSlice = createSlice({
}, },
//#endregion //#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 //#region Globals
positivePromptChanged: (state, action: PayloadAction<string>) => { positivePromptChanged: (state, action: PayloadAction<string>) => {
state.positivePrompt = action.payload; state.positivePrompt = action.payload;
@ -617,20 +684,20 @@ export const controlLayersSlice = createSlice({
shouldConcatPromptsChanged: (state, action: PayloadAction<boolean>) => { shouldConcatPromptsChanged: (state, action: PayloadAction<boolean>) => {
state.shouldConcatPrompts = action.payload; state.shouldConcatPrompts = action.payload;
}, },
widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean }>) => { widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => {
const { width, updateAspectRatio } = action.payload; const { width, updateAspectRatio, clamp } = action.payload;
state.size.width = width; state.size.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width;
if (updateAspectRatio) { 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.id = 'Free';
state.size.aspectRatio.isLocked = false; state.size.aspectRatio.isLocked = false;
} }
}, },
heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean }>) => { heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>) => {
const { height, updateAspectRatio } = action.payload; const { height, updateAspectRatio, clamp } = action.payload;
state.size.height = height; state.size.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height;
if (updateAspectRatio) { 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.id = 'Free';
state.size.aspectRatio.isLocked = false; state.size.aspectRatio.isLocked = false;
} }
@ -728,7 +795,6 @@ export const {
layerMovedToFront, layerMovedToFront,
layerMovedBackward, layerMovedBackward,
layerMovedToBack, layerMovedToBack,
selectedLayerReset,
selectedLayerDeleted, selectedLayerDeleted,
allLayersDeleted, allLayersDeleted,
// CA Layers // CA Layers
@ -741,12 +807,15 @@ export const {
caLayerIsFilterEnabledChanged, caLayerIsFilterEnabledChanged,
caLayerOpacityChanged, caLayerOpacityChanged,
caLayerIsProcessingImageChanged, caLayerIsProcessingImageChanged,
caLayerControlNetsDeleted,
caLayerT2IAdaptersDeleted,
// IPA Layers // IPA Layers
ipaLayerAdded, ipaLayerAdded,
ipaLayerImageChanged, ipaLayerImageChanged,
ipaLayerMethodChanged, ipaLayerMethodChanged,
ipaLayerModelChanged, ipaLayerModelChanged,
ipaLayerCLIPVisionModelChanged, ipaLayerCLIPVisionModelChanged,
ipaLayersDeleted,
// CA or IPA Layers // CA or IPA Layers
caOrIPALayerWeightChanged, caOrIPALayerWeightChanged,
caOrIPALayerBeginEndStepPctChanged, caOrIPALayerBeginEndStepPctChanged,
@ -758,6 +827,7 @@ export const {
rgLayerLineAdded, rgLayerLineAdded,
rgLayerPointsAdded, rgLayerPointsAdded,
rgLayerRectAdded, rgLayerRectAdded,
rgLayerMaskImageUploaded,
rgLayerAutoNegativeChanged, rgLayerAutoNegativeChanged,
rgLayerIPAdapterAdded, rgLayerIPAdapterAdded,
rgLayerIPAdapterDeleted, rgLayerIPAdapterDeleted,
@ -767,6 +837,10 @@ export const {
rgLayerIPAdapterMethodChanged, rgLayerIPAdapterMethodChanged,
rgLayerIPAdapterModelChanged, rgLayerIPAdapterModelChanged,
rgLayerIPAdapterCLIPVisionModelChanged, rgLayerIPAdapterCLIPVisionModelChanged,
// II Layer
iiLayerAdded,
iiLayerImageChanged,
iiLayerOpacityChanged,
// Globals // Globals
positivePromptChanged, positivePromptChanged,
negativePromptChanged, negativePromptChanged,
@ -789,11 +863,10 @@ const migrateControlLayersState = (state: any): any => {
return state; return state;
}; };
export const $isMouseDown = atom(false); export const $isDrawing = atom(false);
export const $isMouseOver = atom(false);
export const $lastMouseDownPos = atom<Vector2d | null>(null); export const $lastMouseDownPos = atom<Vector2d | null>(null);
export const $tool = atom<Tool>('brush'); export const $tool = atom<Tool>('brush');
export const $cursorPosition = atom<Vector2d | null>(null); export const $lastCursorPos = atom<Vector2d | null>(null);
// IDs for singleton Konva layers and objects // IDs for singleton Konva layers and objects
export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer'; 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_LINE_NAME = 'regional_guidance_layer.line';
export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group'; export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group';
export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect'; 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 LAYER_BBOX_NAME = 'layer.bbox';
export const COMPOSITING_RECT_NAME = 'compositing-rect';
// Getters for non-singleton layer and object IDs // Getters for non-singleton layer and object IDs
const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; 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`; export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`; const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`;
export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; 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}`; const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`;
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = { export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {

View File

@ -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 { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type { import type {
ParameterAutoNegative, ParameterAutoNegative,
@ -50,12 +55,12 @@ export type ControlAdapterLayer = RenderableLayerBase & {
type: 'control_adapter_layer'; // technically, also t2i adapter layer type: 'control_adapter_layer'; // technically, also t2i adapter layer
opacity: number; opacity: number;
isFilterEnabled: boolean; isFilterEnabled: boolean;
controlAdapter: ControlNetConfig | T2IAdapterConfig; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2;
}; };
export type IPAdapterLayer = LayerBase & { export type IPAdapterLayer = LayerBase & {
type: 'ip_adapter_layer'; type: 'ip_adapter_layer';
ipAdapter: IPAdapterConfig; ipAdapter: IPAdapterConfigV2;
}; };
export type RegionalGuidanceLayer = RenderableLayerBase & { export type RegionalGuidanceLayer = RenderableLayerBase & {
@ -63,13 +68,20 @@ export type RegionalGuidanceLayer = RenderableLayerBase & {
maskObjects: (VectorMaskLine | VectorMaskRect)[]; maskObjects: (VectorMaskLine | VectorMaskRect)[];
positivePrompt: ParameterPositivePrompt | null; positivePrompt: ParameterPositivePrompt | null;
negativePrompt: ParameterNegativePrompt | null; // Up to one text prompt per mask 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; previewColor: RgbColor;
autoNegative: ParameterAutoNegative; autoNegative: ParameterAutoNegative;
needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object 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 = { export type ControlLayersState = {
_version: 1; _version: 1;

View File

@ -123,7 +123,7 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal
return correctedLayerBbox; return correctedLayerBbox;
}; };
export const getLayerBboxFast = (layer: KonvaLayerType): IRect | null => { export const getLayerBboxFast = (layer: KonvaLayerType): IRect => {
const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG); const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG);
return { return {
x: Math.floor(bbox.x), x: Math.floor(bbox.x),

View File

@ -4,20 +4,20 @@ import { assert } from 'tsafe';
import { describe, test } from 'vitest'; import { describe, test } from 'vitest';
import type { import type {
CLIPVisionModel, CLIPVisionModelV2,
ControlMode, ControlModeV2,
DepthAnythingModelSize, DepthAnythingModelSize,
IPMethod, IPMethodV2,
ProcessorConfig, ProcessorConfig,
ProcessorType, ProcessorTypeV2,
} from './controlAdapters'; } from './controlAdapters';
describe('Control Adapter Types', () => { describe('Control Adapter Types', () => {
test('ProcessorType', () => assert<Equals<ProcessorConfig['type'], ProcessorType>>()); test('ProcessorType', () => assert<Equals<ProcessorConfig['type'], ProcessorTypeV2>>());
test('IP Adapter Method', () => assert<Equals<NonNullable<S['IPAdapterInvocation']['method']>, IPMethod>>()); test('IP Adapter Method', () => assert<Equals<NonNullable<S['IPAdapterInvocation']['method']>, IPMethodV2>>());
test('CLIP Vision Model', () => test('CLIP Vision Model', () =>
assert<Equals<NonNullable<S['IPAdapterInvocation']['clip_vision_model']>, CLIPVisionModel>>()); assert<Equals<NonNullable<S['IPAdapterInvocation']['clip_vision_model']>, CLIPVisionModelV2>>());
test('Control Mode', () => assert<Equals<NonNullable<S['ControlNetInvocation']['control_mode']>, ControlMode>>()); test('Control Mode', () => assert<Equals<NonNullable<S['ControlNetInvocation']['control_mode']>, ControlModeV2>>());
test('DepthAnything Model Size', () => test('DepthAnything Model Size', () =>
assert<Equals<NonNullable<S['DepthAnythingImageProcessorInvocation']['model_size']>, DepthAnythingModelSize>>()); assert<Equals<NonNullable<S['DepthAnythingImageProcessorInvocation']['model_size']>, DepthAnythingModelSize>>());
}); });

View File

@ -94,45 +94,45 @@ type ControlAdapterBase = {
beginEndStepPct: [number, number]; beginEndStepPct: [number, number];
}; };
const zControlMode = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
export type ControlMode = z.infer<typeof zControlMode>; export type ControlModeV2 = z.infer<typeof zControlModeV2>;
export const isControlMode = (v: unknown): v is ControlMode => zControlMode.safeParse(v).success; export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success;
export type ControlNetConfig = ControlAdapterBase & { export type ControlNetConfigV2 = ControlAdapterBase & {
type: 'controlnet'; type: 'controlnet';
model: ParameterControlNetModel | null; 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'; ca.type === 'controlnet';
export type T2IAdapterConfig = ControlAdapterBase & { export type T2IAdapterConfigV2 = ControlAdapterBase & {
type: 't2i_adapter'; type: 't2i_adapter';
model: ParameterT2IAdapterModel | null; 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'; ca.type === 't2i_adapter';
const zCLIPVisionModel = z.enum(['ViT-H', 'ViT-G']); const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']);
export type CLIPVisionModel = z.infer<typeof zCLIPVisionModel>; export type CLIPVisionModelV2 = z.infer<typeof zCLIPVisionModelV2>;
export const isCLIPVisionModel = (v: unknown): v is CLIPVisionModel => zCLIPVisionModel.safeParse(v).success; export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success;
const zIPMethod = z.enum(['full', 'style', 'composition']); const zIPMethodV2 = z.enum(['full', 'style', 'composition']);
export type IPMethod = z.infer<typeof zIPMethod>; export type IPMethodV2 = z.infer<typeof zIPMethodV2>;
export const isIPMethod = (v: unknown): v is IPMethod => zIPMethod.safeParse(v).success; export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success;
export type IPAdapterConfig = { export type IPAdapterConfigV2 = {
id: string; id: string;
type: 'ip_adapter'; type: 'ip_adapter';
weight: number; weight: number;
method: IPMethod; method: IPMethodV2;
image: ImageWithDims | null; image: ImageWithDims | null;
model: ParameterIPAdapterModel | null; model: ParameterIPAdapterModel | null;
clipVisionModel: CLIPVisionModel; clipVisionModel: CLIPVisionModelV2;
beginEndStepPct: [number, number]; beginEndStepPct: [number, number];
}; };
const zProcessorType = z.enum([ const zProcessorTypeV2 = z.enum([
'canny_image_processor', 'canny_image_processor',
'color_map_image_processor', 'color_map_image_processor',
'content_shuffle_image_processor', 'content_shuffle_image_processor',
@ -148,10 +148,10 @@ const zProcessorType = z.enum([
'pidi_image_processor', 'pidi_image_processor',
'zoe_depth_image_processor', 'zoe_depth_image_processor',
]); ]);
export type ProcessorType = z.infer<typeof zProcessorType>; export type ProcessorTypeV2 = z.infer<typeof zProcessorTypeV2>;
export const isProcessorType = (v: unknown): v is ProcessorType => zProcessorType.safeParse(v).success; export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success;
type ProcessorData<T extends ProcessorType> = { type ProcessorData<T extends ProcessorTypeV2> = {
type: T; type: T;
labelTKey: string; labelTKey: string;
descriptionTKey: string; descriptionTKey: string;
@ -165,7 +165,7 @@ type ProcessorData<T extends ProcessorType> = {
const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height);
type CAProcessorsData = { type CAProcessorsData = {
[key in ProcessorType]: ProcessorData<key>; [key in ProcessorTypeV2]: ProcessorData<key>;
}; };
/** /**
* A dict of ControlNet processors, including: * A dict of ControlNet processors, including:
@ -176,7 +176,7 @@ type CAProcessorsData = {
* *
* TODO: Generate from the OpenAPI schema * TODO: Generate from the OpenAPI schema
*/ */
export const CONTROLNET_PROCESSORS: CAProcessorsData = { export const CA_PROCESSOR_DATA: CAProcessorsData = {
canny_image_processor: { canny_image_processor: {
type: 'canny_image_processor', type: 'canny_image_processor',
labelTKey: 'controlnet.canny', labelTKey: 'controlnet.canny',
@ -405,7 +405,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = {
}, },
}; };
const initialControlNet: Omit<ControlNetConfig, 'id'> = { export const initialControlNetV2: Omit<ControlNetConfigV2, 'id'> = {
type: 'controlnet', type: 'controlnet',
model: null, model: null,
weight: 1, weight: 1,
@ -414,10 +414,10 @@ const initialControlNet: Omit<ControlNetConfig, 'id'> = {
image: null, image: null,
processedImage: null, processedImage: null,
isProcessingImage: false, isProcessingImage: false,
processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
}; };
const initialT2IAdapter: Omit<T2IAdapterConfig, 'id'> = { export const initialT2IAdapterV2: Omit<T2IAdapterConfigV2, 'id'> = {
type: 't2i_adapter', type: 't2i_adapter',
model: null, model: null,
weight: 1, weight: 1,
@ -425,10 +425,10 @@ const initialT2IAdapter: Omit<T2IAdapterConfig, 'id'> = {
image: null, image: null,
processedImage: null, processedImage: null,
isProcessingImage: false, isProcessingImage: false,
processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(), processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
}; };
const initialIPAdapter: Omit<IPAdapterConfig, 'id'> = { export const initialIPAdapterV2: Omit<IPAdapterConfigV2, 'id'> = {
type: 'ip_adapter', type: 'ip_adapter',
image: null, image: null,
model: null, model: null,
@ -438,26 +438,26 @@ const initialIPAdapter: Omit<IPAdapterConfig, 'id'> = {
weight: 1, weight: 1,
}; };
export const buildControlNet = (id: string, overrides?: Partial<ControlNetConfig>): ControlNetConfig => { export const buildControlNet = (id: string, overrides?: Partial<ControlNetConfigV2>): ControlNetConfigV2 => {
return merge(deepClone(initialControlNet), { id, ...overrides }); return merge(deepClone(initialControlNetV2), { id, ...overrides });
}; };
export const buildT2IAdapter = (id: string, overrides?: Partial<T2IAdapterConfig>): T2IAdapterConfig => { export const buildT2IAdapter = (id: string, overrides?: Partial<T2IAdapterConfigV2>): T2IAdapterConfigV2 => {
return merge(deepClone(initialT2IAdapter), { id, ...overrides }); return merge(deepClone(initialT2IAdapterV2), { id, ...overrides });
}; };
export const buildIPAdapter = (id: string, overrides?: Partial<IPAdapterConfig>): IPAdapterConfig => { export const buildIPAdapter = (id: string, overrides?: Partial<IPAdapterConfigV2>): IPAdapterConfigV2 => {
return merge(deepClone(initialIPAdapter), { id, ...overrides }); return merge(deepClone(initialIPAdapterV2), { id, ...overrides });
}; };
export const buildControlAdapterProcessor = ( export const buildControlAdapterProcessorV2 = (
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig modelConfig: ControlNetModelConfig | T2IAdapterModelConfig
): ProcessorConfig | null => { ): ProcessorConfig | null => {
const defaultPreprocessor = modelConfig.default_settings?.preprocessor; const defaultPreprocessor = modelConfig.default_settings?.preprocessor;
if (!isProcessorType(defaultPreprocessor)) { if (!isProcessorTypeV2(defaultPreprocessor)) {
return null; return null;
} }
const processorConfig = CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(modelConfig.base); const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base);
return processorConfig; return processorConfig;
}; };
@ -467,15 +467,15 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO)
height, height,
}); });
export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfig): ControlNetConfig => { export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfigV2): ControlNetConfigV2 => {
return { return {
...deepClone(t2iAdapter), ...deepClone(t2iAdapter),
type: 'controlnet', type: 'controlnet',
controlMode: initialControlNet.controlMode, controlMode: initialControlNetV2.controlMode,
}; };
}; };
export const controlNetToT2IAdapter = (controlNet: ControlNetConfig): T2IAdapterConfig => { export const controlNetToT2IAdapter = (controlNet: ControlNetConfigV2): T2IAdapterConfigV2 => {
return { return {
...omit(deepClone(controlNet), 'controlMode'), ...omit(deepClone(controlNet), 'controlMode'),
type: 't2i_adapter', type: 't2i_adapter',

View File

@ -1,16 +1,21 @@
import { getStore } from 'app/store/nanostores/store'; import { getStore } from 'app/store/nanostores/store';
import { rgbaColorToString, rgbColorToString } from 'features/canvas/util/colorToString'; import { rgbaColorToString, rgbColorToString } from 'features/canvas/util/colorToString';
import { getScaledFlooredCursorPosition } from 'features/controlLayers/hooks/mouseEventHooks'; import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/hooks/mouseEventHooks';
import { import {
$tool, $tool,
BACKGROUND_LAYER_ID, BACKGROUND_LAYER_ID,
BACKGROUND_RECT_ID, BACKGROUND_RECT_ID,
CA_LAYER_IMAGE_NAME, CA_LAYER_IMAGE_NAME,
CA_LAYER_NAME, CA_LAYER_NAME,
COMPOSITING_RECT_NAME,
getCALayerImageId, getCALayerImageId,
getIILayerImageId,
getLayerBboxId, getLayerBboxId,
getRGLayerObjectGroupId, getRGLayerObjectGroupId,
INITIAL_IMAGE_LAYER_IMAGE_NAME,
INITIAL_IMAGE_LAYER_NAME,
isControlAdapterLayer, isControlAdapterLayer,
isInitialImageLayer,
isRegionalGuidanceLayer, isRegionalGuidanceLayer,
isRenderableLayer, isRenderableLayer,
LAYER_BBOX_NAME, LAYER_BBOX_NAME,
@ -28,6 +33,7 @@ import {
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import type { import type {
ControlAdapterLayer, ControlAdapterLayer,
InitialImageLayer,
Layer, Layer,
RegionalGuidanceLayer, RegionalGuidanceLayer,
Tool, Tool,
@ -35,6 +41,7 @@ import type {
VectorMaskRect, VectorMaskRect,
} from 'features/controlLayers/store/types'; } from 'features/controlLayers/store/types';
import { getLayerBboxFast, getLayerBboxPixels } from 'features/controlLayers/util/bbox'; import { getLayerBboxFast, getLayerBboxPixels } from 'features/controlLayers/util/bbox';
import { t } from 'i18next';
import Konva from 'konva'; import Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types'; import type { IRect, Vector2d } from 'konva/lib/types';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
@ -52,7 +59,8 @@ const STAGE_BG_DATAURL =
const mapId = (object: { id: string }) => object.id; 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) => { const selectVectorMaskObjects = (node: Konva.Node) => {
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME; return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME;
@ -137,10 +145,9 @@ const renderToolPreview = (
globalMaskLayerOpacity: number, globalMaskLayerOpacity: number,
cursorPos: Vector2d | null, cursorPos: Vector2d | null,
lastMouseDownPos: Vector2d | null, lastMouseDownPos: Vector2d | null,
isMouseOver: boolean,
brushSize: number brushSize: number
) => { ) => {
const layerCount = stage.find(`.${RG_LAYER_NAME}`).length; const layerCount = stage.find(selectRenderableLayers).length;
// Update the stage's pointer style // Update the stage's pointer style
if (layerCount === 0) { if (layerCount === 0) {
// We have no layers, so we should not render any tool // We have no layers, so we should not render any tool
@ -161,7 +168,7 @@ const renderToolPreview = (
const toolPreviewLayer = stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`) ?? createToolPreviewLayer(stage); const toolPreviewLayer = stage.findOne<Konva.Layer>(`#${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 // We can bail early if the mouse isn't over the stage or there are no layers
toolPreviewLayer.visible(false); toolPreviewLayer.visible(false);
return; return;
@ -205,12 +212,13 @@ const renderToolPreview = (
} }
if (cursorPos && lastMouseDownPos && tool === 'rect') { if (cursorPos && lastMouseDownPos && tool === 'rect') {
const snappedPos = snapPosToStage(cursorPos, stage);
const rectPreview = toolPreviewLayer.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`); const rectPreview = toolPreviewLayer.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`);
rectPreview?.setAttrs({ rectPreview?.setAttrs({
x: Math.min(cursorPos.x, lastMouseDownPos.x), x: Math.min(snappedPos.x, lastMouseDownPos.x),
y: Math.min(cursorPos.y, lastMouseDownPos.y), y: Math.min(snappedPos.y, lastMouseDownPos.y),
width: Math.abs(cursorPos.x - lastMouseDownPos.x), width: Math.abs(snappedPos.x - lastMouseDownPos.x),
height: Math.abs(cursorPos.y - lastMouseDownPos.y), height: Math.abs(snappedPos.y - lastMouseDownPos.y),
}); });
rectPreview?.visible(true); rectPreview?.visible(true);
} else { } else {
@ -317,6 +325,12 @@ const createVectorMaskRect = (reduxObject: VectorMaskRect, konvaGroup: Konva.Gro
return vectorMaskRect; 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. * Renders a vector mask layer.
* @param stage The konva stage to render on. * @param stage The konva stage to render on.
@ -394,19 +408,157 @@ const renderRegionalGuidanceLayer = (
groupNeedsCache = true; groupNeedsCache = true;
} }
if (konvaObjectGroup.children.length === 0) { if (konvaObjectGroup.getChildren().length === 0) {
// No objects - clear the cache to reset the previous pixel data // No objects - clear the cache to reset the previous pixel data
konvaObjectGroup.clearCache(); konvaObjectGroup.clearCache();
} else if (groupNeedsCache) { return;
konvaObjectGroup.cache();
} }
// Updating group opacity does not require re-caching const compositingRect =
if (konvaObjectGroup.opacity() !== globalMaskLayerOpacity) { konvaLayer.findOne<Konva.Rect>(`.${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); 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<Konva.Image>(`.${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<Konva.Layer>(`#${reduxLayer.id}`) ?? createInitialImageLayer(stage, reduxLayer);
const konvaImage = konvaLayer.findOne<Konva.Image>(`.${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 createControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLayer): Konva.Layer => {
const konvaLayer = new Konva.Layer({ const konvaLayer = new Konva.Layer({
id: reduxLayer.id, id: reduxLayer.id,
@ -547,6 +699,9 @@ const renderLayers = (
if (isControlAdapterLayer(reduxLayer)) { if (isControlAdapterLayer(reduxLayer)) {
renderControlNetLayer(stage, reduxLayer); renderControlNetLayer(stage, reduxLayer);
} }
if (isInitialImageLayer(reduxLayer)) {
renderInitialImageLayer(stage, reduxLayer);
}
} }
}; };
@ -711,7 +866,7 @@ const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => {
y: 0, y: 0,
align: 'center', align: 'center',
verticalAlign: 'middle', verticalAlign: 'middle',
text: 'No Layers Added', text: t('controlLayers.noLayersAdded'),
fontFamily: '"Inter Variable", sans-serif', fontFamily: '"Inter Variable", sans-serif',
fontStyle: '600', fontStyle: '600',
fill: 'white', fill: 'white',

View File

@ -3,6 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors'; import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors';
import { import {
@ -12,7 +13,6 @@ import {
} from 'features/deleteImageModal/store/slice'; } from 'features/deleteImageModal/store/slice';
import type { ImageUsage } from 'features/deleteImageModal/store/types'; import type { ImageUsage } from 'features/deleteImageModal/store/types';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice'; import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
import { some } from 'lodash-es'; import { some } from 'lodash-es';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
@ -24,24 +24,24 @@ import ImageUsageMessage from './ImageUsageMessage';
const selectImageUsages = createMemoizedSelector( const selectImageUsages = createMemoizedSelector(
[ [
selectDeleteImageModalSlice, selectDeleteImageModalSlice,
selectGenerationSlice,
selectCanvasSlice, selectCanvasSlice,
selectNodesSlice, selectNodesSlice,
selectControlAdaptersSlice, selectControlAdaptersSlice,
selectControlLayersSlice,
selectImageUsage, selectImageUsage,
], ],
(deleteImageModal, generation, canvas, nodes, controlAdapters, imagesUsage) => { (deleteImageModal, canvas, nodes, controlAdapters, controlLayers, imagesUsage) => {
const { imagesToDelete } = deleteImageModal; const { imagesToDelete } = deleteImageModal;
const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) => const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) =>
getImageUsage(generation, canvas, nodes, controlAdapters, image_name) getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name)
); );
const imageUsageSummary: ImageUsage = { const imageUsageSummary: ImageUsage = {
isInitialImage: some(allImageUsage, (i) => i.isInitialImage),
isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage), isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage),
isNodesImage: some(allImageUsage, (i) => i.isNodesImage), isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
isControlImage: some(allImageUsage, (i) => i.isControlImage), isControlImage: some(allImageUsage, (i) => i.isControlImage),
isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
}; };
return { return {

View File

@ -29,10 +29,10 @@ const ImageUsageMessage = (props: Props) => {
<> <>
<Text>{topMessage}</Text> <Text>{topMessage}</Text>
<UnorderedList paddingInlineStart={6}> <UnorderedList paddingInlineStart={6}>
{imageUsage.isInitialImage && <ListItem>{t('common.img2img')}</ListItem>} {imageUsage.isCanvasImage && <ListItem>{t('ui.tabs.canvasTab')}</ListItem>}
{imageUsage.isCanvasImage && <ListItem>{t('common.unifiedCanvas')}</ListItem>}
{imageUsage.isControlImage && <ListItem>{t('common.controlNet')}</ListItem>} {imageUsage.isControlImage && <ListItem>{t('common.controlNet')}</ListItem>}
{imageUsage.isNodesImage && <ListItem>{t('common.nodeEditor')}</ListItem>} {imageUsage.isNodesImage && <ListItem>{t('ui.tabs.workflowsTab')}</ListItem>}
{imageUsage.isControlLayerImage && <ListItem>{t('ui.tabs.generationTab')}</ListItem>}
</UnorderedList> </UnorderedList>
<Text>{bottomMessage}</Text> <Text>{bottomMessage}</Text>
</> </>

View File

@ -7,26 +7,30 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import type { ControlAdaptersState } from 'features/controlAdapters/store/types'; import type { ControlAdaptersState } from 'features/controlAdapters/store/types';
import { isControlNetOrT2IAdapter } 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 { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import type { NodesState } from 'features/nodes/store/types'; import type { NodesState } from 'features/nodes/store/types';
import { isImageFieldInputInstance } from 'features/nodes/types/field'; import { isImageFieldInputInstance } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation'; 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 { some } from 'lodash-es';
import type { ImageUsage } from './types'; import type { ImageUsage } from './types';
export const getImageUsage = ( export const getImageUsage = (
generation: GenerationState,
canvas: CanvasState, canvas: CanvasState,
nodes: NodesState, nodes: NodesState,
controlAdapters: ControlAdaptersState, controlAdapters: ControlAdaptersState,
controlLayers: ControlLayersState,
image_name: string 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 isCanvasImage = canvas.layerState.objects.some((obj) => obj.kind === 'image' && obj.imageName === image_name);
const isNodesImage = nodes.nodes.filter(isInvocationNode).some((node) => { 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) (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 = { const imageUsage: ImageUsage = {
isInitialImage,
isCanvasImage, isCanvasImage,
isNodesImage, isNodesImage,
isControlImage, isControlImage,
isControlLayerImage,
}; };
return imageUsage; return imageUsage;
@ -52,11 +74,11 @@ export const getImageUsage = (
export const selectImageUsage = createMemoizedSelector( export const selectImageUsage = createMemoizedSelector(
selectDeleteImageModalSlice, selectDeleteImageModalSlice,
selectGenerationSlice,
selectCanvasSlice, selectCanvasSlice,
selectNodesSlice, selectNodesSlice,
selectControlAdaptersSlice, selectControlAdaptersSlice,
(deleteImageModal, generation, canvas, nodes, controlAdapters) => { selectControlLayersSlice,
(deleteImageModal, canvas, nodes, controlAdapters, controlLayers) => {
const { imagesToDelete } = deleteImageModal; const { imagesToDelete } = deleteImageModal;
if (!imagesToDelete.length) { if (!imagesToDelete.length) {
@ -64,7 +86,7 @@ export const selectImageUsage = createMemoizedSelector(
} }
const imagesUsage = imagesToDelete.map((i) => const imagesUsage = imagesToDelete.map((i) =>
getImageUsage(generation, canvas, nodes, controlAdapters, i.image_name) getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, i.image_name)
); );
return imagesUsage; return imagesUsage;

View File

@ -6,8 +6,8 @@ export type DeleteImageState = {
}; };
export type ImageUsage = { export type ImageUsage = {
isInitialImage: boolean;
isCanvasImage: boolean; isCanvasImage: boolean;
isNodesImage: boolean; isNodesImage: boolean;
isControlImage: boolean; isControlImage: boolean;
isControlLayerImage: boolean;
}; };

View File

@ -7,7 +7,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback } from 'react'; import { useCallback } from 'react';
const selectZoom = createSelector([selectNodesSlice, activeTabNameSelector], (nodes, activeTabName) => const selectZoom = createSelector([selectNodesSlice, activeTabNameSelector], (nodes, activeTabName) =>
activeTabName === 'nodes' ? nodes.viewport.zoom : 1 activeTabName === 'workflows' ? nodes.viewport.zoom : 1
); );
/** /**

View File

@ -22,10 +22,6 @@ type CurrentImageDropData = BaseDropData & {
actionType: 'SET_CURRENT_IMAGE'; actionType: 'SET_CURRENT_IMAGE';
}; };
type InitialImageDropData = BaseDropData & {
actionType: 'SET_INITIAL_IMAGE';
};
type ControlAdapterDropData = BaseDropData & { type ControlAdapterDropData = BaseDropData & {
actionType: 'SET_CONTROL_ADAPTER_IMAGE'; actionType: 'SET_CONTROL_ADAPTER_IMAGE';
context: { 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 & { export type CanvasInitialImageDropData = BaseDropData & {
actionType: 'SET_CANVAS_INITIAL_IMAGE'; actionType: 'SET_CANVAS_INITIAL_IMAGE';
}; };
@ -78,7 +81,6 @@ export type RemoveFromBoardDropData = BaseDropData & {
export type TypesafeDroppableData = export type TypesafeDroppableData =
| CurrentImageDropData | CurrentImageDropData
| InitialImageDropData
| ControlAdapterDropData | ControlAdapterDropData
| CanvasInitialImageDropData | CanvasInitialImageDropData
| NodesImageDropData | NodesImageDropData
@ -86,7 +88,8 @@ export type TypesafeDroppableData =
| RemoveFromBoardDropData | RemoveFromBoardDropData
| CALayerImageDropData | CALayerImageDropData
| IPALayerImageDropData | IPALayerImageDropData
| RGLayerIPAdapterImageDropData; | RGLayerIPAdapterImageDropData
| IILayerImageDropData;
type BaseDragData = { type BaseDragData = {
id: string; id: string;

View File

@ -15,8 +15,6 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active:
switch (actionType) { switch (actionType) {
case 'SET_CURRENT_IMAGE': case 'SET_CURRENT_IMAGE':
return payloadType === 'IMAGE_DTO'; return payloadType === 'IMAGE_DTO';
case 'SET_INITIAL_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_CONTROL_ADAPTER_IMAGE': case 'SET_CONTROL_ADAPTER_IMAGE':
return payloadType === 'IMAGE_DTO'; return payloadType === 'IMAGE_DTO';
case 'SET_CA_LAYER_IMAGE': case 'SET_CA_LAYER_IMAGE':
@ -25,6 +23,8 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active:
return payloadType === 'IMAGE_DTO'; return payloadType === 'IMAGE_DTO';
case 'SET_RG_LAYER_IP_ADAPTER_IMAGE': case 'SET_RG_LAYER_IP_ADAPTER_IMAGE':
return payloadType === 'IMAGE_DTO'; return payloadType === 'IMAGE_DTO';
case 'SET_II_LAYER_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_CANVAS_INITIAL_IMAGE': case 'SET_CANVAS_INITIAL_IMAGE':
return payloadType === 'IMAGE_DTO'; return payloadType === 'IMAGE_DTO';
case 'SET_NODES_IMAGE': case 'SET_NODES_IMAGE':

View File

@ -15,11 +15,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import type { ImageUsage } from 'features/deleteImageModal/store/types'; import type { ImageUsage } from 'features/deleteImageModal/store/types';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { some } from 'lodash-es'; import { some } from 'lodash-es';
import { memo, useCallback, useMemo, useRef } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -43,17 +43,17 @@ const DeleteBoardModal = (props: Props) => {
const selectImageUsageSummary = useMemo( const selectImageUsageSummary = useMemo(
() => () =>
createMemoizedSelector( createMemoizedSelector(
[selectGenerationSlice, selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice], [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectControlLayersSlice],
(generation, canvas, nodes, controlAdapters) => { (canvas, nodes, controlAdapters, controlLayers) => {
const allImageUsage = (boardImageNames ?? []).map((imageName) => const allImageUsage = (boardImageNames ?? []).map((imageName) =>
getImageUsage(generation, canvas, nodes, controlAdapters, imageName) getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName)
); );
const imageUsageSummary: ImageUsage = { const imageUsageSummary: ImageUsage = {
isInitialImage: some(allImageUsage, (i) => i.isInitialImage),
isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage), isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage),
isNodesImage: some(allImageUsage, (i) => i.isNodesImage), isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
isControlImage: some(allImageUsage, (i) => i.isControlImage), isControlImage: some(allImageUsage, (i) => i.isControlImage),
isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
}; };
return imageUsageSummary; return imageUsageSummary;

View File

@ -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 (
<>
<Flex flexWrap="wrap" justifyContent="center" alignItems="center" gap={2}>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<Menu isLazy>
<MenuButton
as={IconButton}
aria-label={t('parameters.imageActions')}
tooltip={t('parameters.imageActions')}
isDisabled={!imageDTO}
icon={<PiDotsThreeOutlineFill />}
/>
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
</Menu>
</ButtonGroup>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<IconButton
icon={<PiFlowArrowBold />}
tooltip={`${t('nodes.loadWorkflow')} (W)`}
aria-label={`${t('nodes.loadWorkflow')} (W)`}
isDisabled={!imageDTO?.has_workflow}
onClick={handleLoadWorkflow}
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiArrowsCounterClockwiseBold />}
tooltip={`${t('parameters.remixImage')} (R)`}
aria-label={`${t('parameters.remixImage')} (R)`}
isDisabled={!hasMetadata}
onClick={remix}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiQuotesBold />}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!hasPrompts}
onClick={recallPrompts}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiPlantBold />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!hasSeed}
onClick={recallSeed}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiRulerBold />}
tooltip={`${t('parameters.useSize')} (D)`}
aria-label={`${t('parameters.useSize')} (D)`}
onClick={handleUseSize}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiAsteriskBold />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={!hasMetadata}
onClick={recallAll}
/>
</ButtonGroup>
{isUpscalingEnabled && (
<ButtonGroup isDisabled={isQueueMutationInProgress}>
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
</ButtonGroup>
)}
<ButtonGroup>
<IconButton
icon={<PiInfoBold />}
tooltip={`${t('parameters.info')} (I)`}
aria-label={`${t('parameters.info')} (I)`}
isChecked={shouldShowImageDetails}
onClick={handleClickShowImageDetails}
/>
</ButtonGroup>
<ButtonGroup>
<IconButton
aria-label={t('settings.displayInProgress')}
tooltip={t('settings.displayInProgress')}
icon={<PiHourglassHighBold />}
isChecked={shouldShowProgressInViewer}
onClick={handleClickProgressImagesToggle}
/>
</ButtonGroup>
<ButtonGroup>
<DeleteImageButton onClick={handleDelete} />
</ButtonGroup>
</Flex>
</>
);
};
export default memo(CurrentImageButtons);

View File

@ -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 (
<Flex
position="relative"
flexDirection="column"
height="100%"
width="100%"
rowGap={4}
alignItems="center"
justifyContent="center"
>
<CurrentImageButtons />
<CurrentImagePreview />
</Flex>
);
};
export default memo(CurrentImageDisplay);

View File

@ -7,10 +7,10 @@ import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
import { useDownloadImage } from 'common/hooks/useDownloadImage'; import { useDownloadImage } from 'common/hooks/useDownloadImage';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice'; import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions'; import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions';
import { initialImageSelected } from 'features/parameters/store/actions';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { setActiveTab } from 'features/ui/store/uiSlice'; import { setActiveTab } from 'features/ui/store/uiSlice';
@ -45,7 +45,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const toaster = useAppToaster(); const toaster = useAppToaster();
const isCanvasEnabled = useFeatureStatus('unifiedCanvas'); const isCanvasEnabled = useFeatureStatus('canvas');
const customStarUi = useStore($customStarUI); const customStarUi = useStore($customStarUI);
const { downloadImage } = useDownloadImage(); const { downloadImage } = useDownloadImage();
@ -72,13 +72,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const handleSendToImageToImage = useCallback(() => { const handleSendToImageToImage = useCallback(() => {
dispatch(sentImageToImg2Img()); dispatch(sentImageToImg2Img());
dispatch(initialImageSelected(imageDTO)); dispatch(iiLayerAdded(imageDTO));
}, [dispatch, imageDTO]); }, [dispatch, imageDTO]);
const handleSendToCanvas = useCallback(() => { const handleSendToCanvas = useCallback(() => {
dispatch(sentImageToCanvas()); dispatch(sentImageToCanvas());
flushSync(() => { flushSync(() => {
dispatch(setActiveTab('unifiedCanvas')); dispatch(setActiveTab('canvas'));
}); });
dispatch(setInitialCanvasImage(imageDTO, optimalDimension)); dispatch(setInitialCanvasImage(imageDTO, optimalDimension));

View File

@ -1,9 +1,14 @@
import { useAppSelector } from 'app/store/storeHooks';
import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets'; import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets';
import { MetadataControlNetsV2 } from 'features/metadata/components/MetadataControlNetsV2';
import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters'; import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters';
import { MetadataIPAdaptersV2 } from 'features/metadata/components/MetadataIPAdaptersV2';
import { MetadataItem } from 'features/metadata/components/MetadataItem'; import { MetadataItem } from 'features/metadata/components/MetadataItem';
import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs'; import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs';
import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters'; import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters';
import { MetadataT2IAdaptersV2 } from 'features/metadata/components/MetadataT2IAdaptersV2';
import { handlers } from 'features/metadata/util/handlers'; import { handlers } from 'features/metadata/util/handlers';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react'; import { memo } from 'react';
type Props = { type Props = {
@ -11,6 +16,7 @@ type Props = {
}; };
const ImageMetadataActions = (props: Props) => { const ImageMetadataActions = (props: Props) => {
const activeTabName = useAppSelector(activeTabNameSelector);
const { metadata } = props; const { metadata } = props;
if (!metadata || Object.keys(metadata).length === 0) { if (!metadata || Object.keys(metadata).length === 0) {
@ -46,9 +52,12 @@ const ImageMetadataActions = (props: Props) => {
<MetadataItem metadata={metadata} handlers={handlers.refinerStart} /> <MetadataItem metadata={metadata} handlers={handlers.refinerStart} />
<MetadataItem metadata={metadata} handlers={handlers.refinerSteps} /> <MetadataItem metadata={metadata} handlers={handlers.refinerSteps} />
<MetadataLoRAs metadata={metadata} /> <MetadataLoRAs metadata={metadata} />
<MetadataControlNets metadata={metadata} /> {activeTabName !== 'generation' && <MetadataControlNets metadata={metadata} />}
<MetadataT2IAdapters metadata={metadata} /> {activeTabName !== 'generation' && <MetadataT2IAdapters metadata={metadata} />}
<MetadataIPAdapters metadata={metadata} /> {activeTabName !== 'generation' && <MetadataIPAdapters metadata={metadata} />}
{activeTabName === 'generation' && <MetadataControlNetsV2 metadata={metadata} />}
{activeTabName === 'generation' && <MetadataT2IAdaptersV2 metadata={metadata} />}
{activeTabName === 'generation' && <MetadataIPAdaptersV2 metadata={metadata} />}
</> </>
); );
}; };

View File

@ -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 (
<>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<Menu isLazy>
<MenuButton
as={IconButton}
aria-label={t('parameters.imageActions')}
tooltip={t('parameters.imageActions')}
isDisabled={!imageDTO}
icon={<PiDotsThreeOutlineFill />}
/>
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
</Menu>
</ButtonGroup>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<IconButton
icon={<PiFlowArrowBold />}
tooltip={`${t('nodes.loadWorkflow')} (W)`}
aria-label={`${t('nodes.loadWorkflow')} (W)`}
isDisabled={!imageDTO?.has_workflow}
onClick={handleLoadWorkflow}
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiArrowsCounterClockwiseBold />}
tooltip={`${t('parameters.remixImage')} (R)`}
aria-label={`${t('parameters.remixImage')} (R)`}
isDisabled={!hasMetadata}
onClick={remix}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiQuotesBold />}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!hasPrompts}
onClick={recallPrompts}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiPlantBold />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!hasSeed}
onClick={recallSeed}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiRulerBold />}
tooltip={`${t('parameters.useSize')} (D)`}
aria-label={`${t('parameters.useSize')} (D)`}
onClick={handleUseSize}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiAsteriskBold />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={!hasMetadata}
onClick={recallAll}
/>
</ButtonGroup>
{isUpscalingEnabled && (
<ButtonGroup isDisabled={isQueueMutationInProgress}>
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
</ButtonGroup>
)}
<ButtonGroup>
<DeleteImageButton onClick={handleDelete} />
</ButtonGroup>
</>
);
};
export default memo(CurrentImageButtons);

View File

@ -5,24 +5,25 @@ import { useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; 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 ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons'; import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import type { AnimationProps } from 'framer-motion'; import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo, useRef, useState } from 'react'; import { memo, useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiImageBold } from 'react-icons/pi'; import { PiImageBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import ProgressImage from './ProgressImage';
const selectLastSelectedImageName = createSelector( const selectLastSelectedImageName = createSelector(
selectLastSelectedImage, selectLastSelectedImage,
(lastSelectedImage) => lastSelectedImage?.image_name (lastSelectedImage) => lastSelectedImage?.image_name
); );
const CurrentImagePreview = () => { const CurrentImagePreview = () => {
const { t } = useTranslation();
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails); const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
const imageName = useAppSelector(selectLastSelectedImageName); const imageName = useAppSelector(selectLastSelectedImageName);
const hasDenoiseProgress = useAppSelector((s) => Boolean(s.system.denoiseProgress)); const hasDenoiseProgress = useAppSelector((s) => Boolean(s.system.denoiseProgress));
@ -50,17 +51,12 @@ const CurrentImagePreview = () => {
// Show and hide the next/prev buttons on mouse move // Show and hide the next/prev buttons on mouse move
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false); const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
const timeoutId = useRef(0); const timeoutId = useRef(0);
const onMouseOver = useCallback(() => {
const { t } = useTranslation();
const handleMouseOver = useCallback(() => {
setShouldShowNextPrevButtons(true); setShouldShowNextPrevButtons(true);
window.clearTimeout(timeoutId.current); window.clearTimeout(timeoutId.current);
}, []); }, []);
const onMouseOut = useCallback(() => {
const handleMouseOut = useCallback(() => {
timeoutId.current = window.setTimeout(() => { timeoutId.current = window.setTimeout(() => {
setShouldShowNextPrevButtons(false); setShouldShowNextPrevButtons(false);
}, 500); }, 500);
@ -68,8 +64,8 @@ const CurrentImagePreview = () => {
return ( return (
<Flex <Flex
onMouseOver={handleMouseOver} onMouseOver={onMouseOver}
onMouseOut={handleMouseOut} onMouseOut={onMouseOut}
width="full" width="full"
height="full" height="full"
alignItems="center" alignItems="center"
@ -91,16 +87,40 @@ const CurrentImagePreview = () => {
dataTestId="image-preview" dataTestId="image-preview"
/> />
)} )}
{shouldShowImageDetails && imageDTO && (
<Box position="absolute" top="0" width="full" height="full" borderRadius="base">
<ImageMetadataViewer image={imageDTO} />
</Box>
)}
<AnimatePresence> <AnimatePresence>
{!shouldShowImageDetails && imageDTO && shouldShowNextPrevButtons && ( {shouldShowImageDetails && imageDTO && (
<motion.div key="nextPrevButtons" initial={initial} animate={animate} exit={exit} style={motionStyles}> <Box
as={motion.div}
key="metadataViewer"
initial={initial}
animate={animateMetadata}
exit={exit}
position="absolute"
top={0}
width="full"
height="full"
borderRadius="base"
>
<ImageMetadataViewer image={imageDTO} />
</Box>
)}
</AnimatePresence>
<AnimatePresence>
{shouldShowNextPrevButtons && imageDTO && (
<Box
as={motion.div}
key="nextPrevButtons"
initial={initial}
animate={animateArrows}
exit={exit}
position="absolute"
top={0}
width="full"
height="full"
pointerEvents="none"
>
<NextPrevImageButtons /> <NextPrevImageButtons />
</motion.div> </Box>
)} )}
</AnimatePresence> </AnimatePresence>
</Flex> </Flex>
@ -112,18 +132,15 @@ export default memo(CurrentImagePreview);
const initial: AnimationProps['initial'] = { const initial: AnimationProps['initial'] = {
opacity: 0, opacity: 0,
}; };
const animate: AnimationProps['animate'] = { const animateArrows: AnimationProps['animate'] = {
opacity: 1, 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'] = { const exit: AnimationProps['exit'] = {
opacity: 0, opacity: 0,
transition: { duration: 0.1 }, transition: { duration: 0.07 },
};
const motionStyles: CSSProperties = {
position: 'absolute',
top: '0',
width: '100%',
height: '100%',
pointerEvents: 'none',
}; };

View File

@ -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<InvokeTabName, string> = {
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 (
<Button
aria-label={tooltip}
tooltip={tooltip}
onClick={onClose}
variant="outline"
leftIcon={<PiArrowsDownUpBold />}
>
{t(TAB_NAME_TO_TKEY_SHORT[activeTabName])}
</Button>
);
};

View File

@ -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 (
<AnimatePresence mode="wait">
{shouldShowViewer && (
<Flex
key="imageViewer"
as={motion.div}
initial={initial}
animate={animate}
exit={exit}
layerStyle="first"
borderRadius="base"
position="absolute"
flexDirection="column"
top={0}
right={0}
bottom={0}
left={0}
p={2}
rowGap={4}
alignItems="center"
justifyContent="center"
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
>
<Flex w="full" gap={2}>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineEnd="auto">
<ToggleProgressButton />
<ToggleMetadataViewerButton />
</Flex>
</Flex>
<Flex flex={1} gap={2} justifyContent="center">
<CurrentImageButtons />
</Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
<EditorButton />
</Flex>
</Flex>
</Flex>
<CurrentImagePreview />
</Flex>
)}
</AnimatePresence>
);
});
ImageViewer.displayName = 'ImageViewer';

Some files were not shown because too many files have changed in this diff Show More