Compare commits

..

43 Commits

Author SHA1 Message Date
Ryan Dick
e2684b45af Add cProfile for profiling graph execution. 2024-01-12 10:58:03 -05:00
Riccardo Giovanetti
d4c36da3ee translationBot(ui): update translation (Italian)
Currently translated at 97.3% (1365 of 1402 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-01-12 22:52:38 +11:00
psychedelicious
dfe0b73890 fix(ui): fix usages of panel helpers
Upstream breaking change.
2024-01-12 09:31:07 +11:00
psychedelicious
c0c8fa9a89 fix(ui): use nodrag on invinput in workflow editor
Closes #5476
2024-01-12 09:31:07 +11:00
psychedelicious
ad7139829c fix(ui): fix canvas space hotkey
Need to do some checks to ensure we aren't taking over input elements, and are focused on the canvas.

Closes #5478
2024-01-12 09:31:07 +11:00
psychedelicious
a24e63d440 fix(ui): do not focus board search on load 2024-01-12 09:31:07 +11:00
psychedelicious
59437a02c3 feat(ui): restore resizable prompt boxes
The autosize proved to be unpopular. Changed back to resizable.
2024-01-12 09:31:07 +11:00
psychedelicious
98a44d7fa1 feat(ui): update assets
- Add various brand images, organise images
- Create favicon for docs pages (light blue version of key logo)
- Rename app title to `Invoke - Community Edition`
2024-01-12 08:02:59 +11:00
psychedelicious
07416753be feat(ui): more context in storage errors 2024-01-12 07:54:18 +11:00
Millun Atluri
630854ce26 3.6 Docs updates (#5412)
* Update UNIFIED_CANVAS.md

* Update index.md

* Update structure

* Docs updates
2024-01-11 16:52:22 +00:00
psychedelicious
b55c2b99a7 feat(ui): workflow library styling 2024-01-11 09:42:12 -05:00
psychedelicious
f81d36c95f fix(ui): do not string workflow id on rehydrate 2024-01-11 09:42:12 -05:00
psychedelicious
26b7aadd32 fix(db): fix workflows pagination math 2024-01-11 09:42:12 -05:00
Kent Keirsey
8e7e3c2b4a Initial Styling Commit 2024-01-11 09:42:12 -05:00
Lincoln Stein
f2e8b66be4 Fix "Cannot import name 'PagingArgumentParser' error when starting textual inversion
- Closes #5395
2024-01-11 13:57:06 +11:00
psychedelicious
ff09fd30dc feat(ui): if in dev mode, reset API on reconnect
This retains the current good developer experience when working on the server - the UI should fully reset when you restart the server.
2024-01-11 12:51:15 +11:00
psychedelicious
9fcc30c3d6 feat(ui): optimize reconnect queries
Add `FetchOnReconnect` tag, tagging relevant queries with it. This tag is invalidated in the socketConnected listener, when it is determined that the queue changed.
2024-01-11 12:51:15 +11:00
psychedelicious
b29a6522ef feat(ui): always check for change to queue status when reconnecting 2024-01-11 12:51:15 +11:00
psychedelicious
936d19cd60 feat(ui): improve comments on socketConnected listener 2024-01-11 12:51:15 +11:00
psychedelicious
f25b6ee5d1 chore(ui): lint 2024-01-11 12:51:15 +11:00
psychedelicious
7dea079220 fix(ui): reduce reconnect requests
- Add checks to the "recovery" logic for socket connect events to reduce the number of network requests.
- Remove the `isInitialized` state from `systemSlice` and make it a nanostore local to the socketConnected listener. It didn't need to be global state. It's also now more clearly named `isFirstConnection`.
- Export the queue status selector (minor improvement, memoizes it correctly).
2024-01-11 12:51:15 +11:00
Васянатор
7fc08962fb translationBot(ui): update translation (Russian)
Currently translated at 97.0% (1361 of 1402 strings)

Co-authored-by: Васянатор <ilabulanov339@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/
Translation: InvokeAI/Web UI
2024-01-11 12:48:23 +11:00
ItzAttila
71155d9e72 translationBot(ui): update translation (Hungarian)
Currently translated at 1.9% (28 of 1402 strings)

translationBot(ui): added translation (Hungarian)

Co-authored-by: ItzAttila <attila.gm.studio@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/hu/
Translation: InvokeAI/Web UI
2024-01-11 12:48:23 +11:00
Millun Atluri
6ccd72349d {release} v3.6.0rc6 (#5467)
## What type of PR is this? (check all applicable)

Release v3.6.0rc6

## Have you discussed this change with the InvokeAI team?
- [X] Yes
- [ ] No, because:

      
## Have you updated all relevant documentation?
- [X] Yes
- [ ] No


## Description
Release candidate $6 

## QA Instructions, Screenshots, Recordings

[InvokeAI-installer-v3.6.0rc6.zip](https://github.com/invoke-ai/InvokeAI/files/13890206/InvokeAI-installer-v3.6.0rc6.zip)



## Merge Plan
Merge when approved

## [optional] Are there any post deployment tasks we need to perform?
Release on PyPi & Github
2024-01-10 11:19:39 -05:00
Millun Atluri
30e12376d3 {release} v3.6.0rc6 2024-01-10 10:45:33 -05:00
psychedelicious
23c8a893e1 fix(ui): fix gallery display bug, major lag
- Fixed a bug where after you load more, changing boards doesn't work. The offset and limit for the list image query had some wonky logic, now resolved.
- Addressed major lag in gallery when selecting an image.

Both issues were related to the useMultiselect and useGalleryImages hooks, which caused every image in the gallery to re-render on whenever the selection changed. There's no way to memoize away this - we need to know when the selection changes. This is a longstanding issue.

The selection is only used in a callback, though - the onClick handler for an image to select it (or add it to the existing selection). We don't really need the reactivity for a callback, so we don't need to listen for changes to the selection.

The logic to handle multiple selection is moved to a new `galleryImageClicked` listener, which does all the selection right when it is needed.

The result is that gallery images no long need to do heavy re-renders on any selection change.

Besides the multiselect click handler, there was also inefficient use of DND payloads. Previously, the `IMAGE_DTOS` type had a payload of image DTO objects. This was only used to drag gallery selection into a board. There is no need to hold onto image DTOs when we have the selection state already in redux. We were recalculating this payload for every image, on every tick.

This payload is now just the board id (the only piece of information we need for this particular DND event).

- I also removed some unused DND types while making this change.
2024-01-10 08:22:46 -05:00
psychedelicious
7d93329401 feat(ui): de-jank context menu
There was a lot of convoluted, janky logic related to trying to not mount the context menu's portal until its needed. This was in the library where the component was originally copied from.

I've removed that and resolved the jank, at the cost of there being an extra portal for each instance of the context menu. Don't think this is going to be an issue. If it is, the whole context menu could be refactored to be a singleton.
2024-01-10 08:22:46 -05:00
Eugene Brodsky
968fb655a4 Report ci disk space + minor docker fixes (#5461)
* ci: add docker build timout; log free space on runner before and after build

* docker: bump frontend builder to node=20.x; skip linting on build

* chore: gitignore .pnpm-store

* update code owners for docker and CI

---------

Co-authored-by: Millun Atluri <Millu@users.noreply.github.com>
2024-01-10 05:20:26 +00:00
psychedelicious
80ec9f4131 chore(ui): lint 2024-01-10 00:11:05 -05:00
psychedelicious
f19def5f7b feat(ui): replace aspect ratio icon
closes #5448
2024-01-10 00:11:05 -05:00
psychedelicious
9e1dd8ac9c fix(ui): reset canvas coords/dims on reset 2024-01-10 00:11:05 -05:00
psychedelicious
ebd68b7a6c feat(ui): support reset canvas view when no image on canvas 2024-01-10 00:11:05 -05:00
psychedelicious
68a231afea feat(ui): move canvas stage and base layer to nanostores 2024-01-10 00:11:05 -05:00
psychedelicious
21ab650ac0 feat(ui): move canvas tool to nanostores
I was troubleshooting a hotkeys issue on canvas and thought I had broken the tool logic in a past change so I redid it moving it to nanostores. In the end, the issue was an upstream but with the hotkeys library, but I like having tool in nanostores so I'm leaving it.

It's ephemeral interaction state anyways, doesn't need to be in redux.
2024-01-10 00:11:05 -05:00
psychedelicious
b501bd709f fix(ui): canvas bbox number input wonky
It was rounding dimensions when it shouldn't.

Closes #5453
2024-01-10 00:11:05 -05:00
psychedelicious
4082f25062 feat(ui): do not optimize size when changing between models with same base model
There's a challenge to accomplish this due to our slice structure - the model is stored in `generationSlice`, but `canvasSlice` also needs to have awareness of it. For example, when the model changes, the canvas slice doesn't know what the previous model was, so it doesn't know whether or not to optimize the size.

This means we need to lift the "should we optimize size" information up. To do this, the `modelChanged` action creator accepts the previous model as an optional second arg.

Now the canvas has access to both the previous model and new model selection, and can decide whether or not it should optimize its size setting in the same way that the generation slice does.

Closes  #5452
2024-01-10 00:11:05 -05:00
psychedelicious
63d74b4ba6 feat(ui): remove unnecessary tabChanged listener
This was needed when we didn't support SDXL on canvas.
2024-01-10 00:11:05 -05:00
psychedelicious
da5907613b fix(ui): fix typing of usGalleryImages
For some reason `ReturnType<typeof useListImagesQuery>` isn't working correctly, and destructuring `queryResult` it results in `any`, when the hook is used.

I've removed the explicit return typing so that consumers of the hook get correct types.
2024-01-10 00:11:05 -05:00
psychedelicious
3a9201bd31 feat: pin deps
Organise deps into ~3 categories:
- Core generation dependencies, pinned for reproducible builds.
- Core application dependencies, pinned for reproducible builds.
- Auxiliary dependencies, pinned only if necessary.

I pinned / bumped these to latest:
- `controlnet_aux`
- `fastapi`
- `fastapi-events`
- `huggingface-hub`
- `numpy`
- `python-socketio`
- `torchmetrics`
- `transformers`
- `uvicorn`

I checked the release notes for these and didn't see any breaking changes that would affect us. There is a `fastapi` breaking change in v108 related to background tasks but it doesn't affect us.

I tested on a fresh venv. The app still works and I can generate on macOS.

Hopefully, enforcing explicit pinned versions will reduce the issues where people get CPU torch.

It also means we should periodically bump versions up to ensure we don't get too far behind on our dependencies and have to do painful upgrades.
2024-01-10 00:03:29 -05:00
psychedelicious
d6e2cb7cef fix(ui): use memoized selector for workflow watcher
Minor perf improvement.
2024-01-10 15:32:16 +11:00
psychedelicious
0809e832d4 fix(ui): use less brutally strict workflow validation
Workflow building would fail when a current image node was in the workflow due to the strict validation.

So we need to use the other workflow builder util first, which strips out extraneous data.

This bug was introduced during an attempt to optimize the workflow building logic, which was causing slowdowns on the workflow editor.
2024-01-10 14:31:14 +11:00
Lincoln Stein
7269c9f02e Enable correct probing of LoRA latent-consistency/lcm-lora-sdxl (#5449)
- Closes #5435

Co-authored-by: Lincoln Stein <lstein@gmail.com>
2024-01-08 17:18:26 -05:00
Mary Hipp Rogers
d86d7e5c33 do not show toast if 403 is triggered by forbidden image (#5447)
* do not show toast if 403 is triggered by lack of image access

* remove log

* lint

---------

Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local>
2024-01-08 12:15:46 -05:00
155 changed files with 1297 additions and 988 deletions

8
.github/CODEOWNERS vendored
View File

@@ -1,5 +1,5 @@
# continuous integration
/.github/workflows/ @lstein @blessedcoolant @hipsterusername
/.github/workflows/ @lstein @blessedcoolant @hipsterusername @ebr
# documentation
/docs/ @lstein @blessedcoolant @hipsterusername @Millu
@@ -10,7 +10,7 @@
# installation and configuration
/pyproject.toml @lstein @blessedcoolant @hipsterusername
/docker/ @lstein @blessedcoolant @hipsterusername
/docker/ @lstein @blessedcoolant @hipsterusername @ebr
/scripts/ @ebr @lstein @hipsterusername
/installer/ @lstein @ebr @hipsterusername
/invokeai/assets @lstein @ebr @hipsterusername
@@ -26,9 +26,7 @@
# front ends
/invokeai/frontend/CLI @lstein @hipsterusername
/invokeai/frontend/install @lstein @ebr @hipsterusername
/invokeai/frontend/install @lstein @ebr @hipsterusername
/invokeai/frontend/merge @lstein @blessedcoolant @hipsterusername
/invokeai/frontend/training @lstein @blessedcoolant @hipsterusername
/invokeai/frontend/web @psychedelicious @blessedcoolant @maryhipp @hipsterusername

View File

@@ -40,10 +40,14 @@ jobs:
- name: Free up more disk space on the runner
# https://github.com/actions/runner-images/issues/2840#issuecomment-1284059930
run: |
echo "----- Free space before cleanup"
df -h
sudo rm -rf /usr/share/dotnet
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo swapoff /mnt/swapfile
sudo rm -rf /mnt/swapfile
echo "----- Free space after cleanup"
df -h
- name: Checkout
uses: actions/checkout@v3
@@ -91,6 +95,7 @@ jobs:
# password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build container
timeout-minutes: 40
id: docker_build
uses: docker/build-push-action@v4
with:

View File

@@ -59,7 +59,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
# #### Build the Web UI ------------------------------------
FROM node:18-slim AS web-builder
FROM node:20-slim AS web-builder
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
@@ -68,7 +68,7 @@ WORKDIR /build
COPY invokeai/frontend/web/ ./
RUN --mount=type=cache,target=/pnpm/store \
pnpm install --frozen-lockfile
RUN pnpm run build
RUN npx vite build
#### Runtime stage ---------------------------------------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

53
docs/deprecated/2to3.md Normal file
View File

@@ -0,0 +1,53 @@
## :octicons-log-16: Important Changes Since Version 2.3
### Nodes
Behind the scenes, InvokeAI has been completely rewritten to support
"nodes," small unitary operations that can be combined into graphs to
form arbitrary workflows. For example, there is a prompt node that
processes the prompt string and feeds it to a text2latent node that
generates a latent image. The latents are then fed to a latent2image
node that translates the latent image into a PNG.
The WebGUI has a node editor that allows you to graphically design and
execute custom node graphs. The ability to save and load graphs is
still a work in progress, but coming soon.
### Command-Line Interface Retired
All "invokeai" command-line interfaces have been retired as of version
3.4.
To launch the Web GUI from the command-line, use the command
`invokeai-web` rather than the traditional `invokeai --web`.
### ControlNet
This version of InvokeAI features ControlNet, a system that allows you
to achieve exact poses for human and animal figures by providing a
model to follow. Full details are found in [ControlNet](features/CONTROLNET.md)
### New Schedulers
The list of schedulers has been completely revamped and brought up to date:
| **Short Name** | **Scheduler** | **Notes** |
|----------------|---------------------------------|-----------------------------|
| **ddim** | DDIMScheduler | |
| **ddpm** | DDPMScheduler | |
| **deis** | DEISMultistepScheduler | |
| **lms** | LMSDiscreteScheduler | |
| **pndm** | PNDMScheduler | |
| **heun** | HeunDiscreteScheduler | original noise schedule |
| **heun_k** | HeunDiscreteScheduler | using karras noise schedule |
| **euler** | EulerDiscreteScheduler | original noise schedule |
| **euler_k** | EulerDiscreteScheduler | using karras noise schedule |
| **kdpm_2** | KDPM2DiscreteScheduler | |
| **kdpm_2_a** | KDPM2AncestralDiscreteScheduler | |
| **dpmpp_2s** | DPMSolverSinglestepScheduler | |
| **dpmpp_2m** | DPMSolverMultistepScheduler | original noise scnedule |
| **dpmpp_2m_k** | DPMSolverMultistepScheduler | using karras noise schedule |
| **unipc** | UniPCMultistepScheduler | CPU only |
| **lcm** | LCMScheduler | |
Please see [3.0.0 Release Notes](https://github.com/invoke-ai/InvokeAI/releases/tag/v3.0.0) for further details.

View File

@@ -229,29 +229,28 @@ clarity on the intent and common use cases we expect for utilizing them.
currently being rendered by your browser into a merged copy of the image. This
lowers the resource requirements and should improve performance.
### Seam Correction
### Compositing / Seam Correction
When doing Inpainting or Outpainting, Invoke needs to merge the pixels generated
by Stable Diffusion into your existing image. To do this, the area around the
`seam` at the boundary between your image and the new generation is
by Stable Diffusion into your existing image. This is achieved through compositing - the area around the the boundary between your image and the new generation is
automatically blended to produce a seamless output. In a fully automatic
process, a mask is generated to cover the seam, and then the area of the seam is
process, a mask is generated to cover the boundary, and then the area of the boundary is
Inpainted.
Although the default options should work well most of the time, sometimes it can
help to alter the parameters that control the seam Inpainting. A wider seam and
a blur setting of about 1/3 of the seam have been noted as producing
consistently strong results (e.g. 96 wide and 16 blur - adds up to 32 blur with
both sides). Seam strength of 0.7 is best for reducing hard seams.
help to alter the parameters that control the Compositing. A larger blur and
a blur setting have been noted as producing
consistently strong results . Strength of 0.7 is best for reducing hard seams.
- **Mode** - What part of the image will have the the Compositing applied to it.
- **Mask edge** will apply Compositing to the edge of the masked area
- **Mask** will apply Compositing to the entire masked area
- **Unmasked** will apply Compositing to the entire image
- **Steps** - Number of generation steps that will occur during the Coherence Pass, similar to Denoising Steps. Higher step counts will generally have better results.
- **Strength** - How much noise is added for the Coherence Pass, similar to Denoising Strength. A strength of 0 will result in an unchanged image, while a strength of 1 will result in an image with a completely new area as defined by the Mode setting.
- **Blur** - Adjusts the pixel radius of the the mask. A larger blur radius will cause the mask to extend past the visibly masked area, while too small of a blur radius will result in a mask that is smaller than the visibly masked area.
- **Blur Method** - The method of blur applied to the masked area.
- **Seam Size** - The size of the seam masked area. Set higher to make a larger
mask around the seam.
- **Seam Blur** - The size of the blur that is applied on _each_ side of the
masked area.
- **Seam Strength** - The Image To Image Strength parameter used for the
Inpainting generation that is applied to the seam area.
- **Seam Steps** - The number of generation steps that should be used to Inpaint
the seam.
### Infill & Scaling

View File

@@ -18,7 +18,7 @@ title: Home
width: 100%;
max-width: 100%;
height: 50px;
background-color: #448AFF;
background-color: #35A4DB;
color: #fff;
font-size: 16px;
border: none;
@@ -43,7 +43,7 @@ title: Home
<div align="center" markdown>
[![project logo](assets/invoke_ai_banner.png)](https://github.com/invoke-ai/InvokeAI)
[![project logo](https://github.com/invoke-ai/InvokeAI/assets/31807370/6e3728c7-e90e-4711-905c-3b55844ff5be)](https://github.com/invoke-ai/InvokeAI)
[![discord badge]][discord link]
@@ -145,60 +145,6 @@ Mac and Linux machines, and runs on GPU cards with as little as 4 GB of RAM.
- [Guide to InvokeAI Runtime Settings](features/CONFIGURATION.md)
- [Database Maintenance and other Command Line Utilities](features/UTILITIES.md)
## :octicons-log-16: Important Changes Since Version 2.3
### Nodes
Behind the scenes, InvokeAI has been completely rewritten to support
"nodes," small unitary operations that can be combined into graphs to
form arbitrary workflows. For example, there is a prompt node that
processes the prompt string and feeds it to a text2latent node that
generates a latent image. The latents are then fed to a latent2image
node that translates the latent image into a PNG.
The WebGUI has a node editor that allows you to graphically design and
execute custom node graphs. The ability to save and load graphs is
still a work in progress, but coming soon.
### Command-Line Interface Retired
All "invokeai" command-line interfaces have been retired as of version
3.4.
To launch the Web GUI from the command-line, use the command
`invokeai-web` rather than the traditional `invokeai --web`.
### ControlNet
This version of InvokeAI features ControlNet, a system that allows you
to achieve exact poses for human and animal figures by providing a
model to follow. Full details are found in [ControlNet](features/CONTROLNET.md)
### New Schedulers
The list of schedulers has been completely revamped and brought up to date:
| **Short Name** | **Scheduler** | **Notes** |
|----------------|---------------------------------|-----------------------------|
| **ddim** | DDIMScheduler | |
| **ddpm** | DDPMScheduler | |
| **deis** | DEISMultistepScheduler | |
| **lms** | LMSDiscreteScheduler | |
| **pndm** | PNDMScheduler | |
| **heun** | HeunDiscreteScheduler | original noise schedule |
| **heun_k** | HeunDiscreteScheduler | using karras noise schedule |
| **euler** | EulerDiscreteScheduler | original noise schedule |
| **euler_k** | EulerDiscreteScheduler | using karras noise schedule |
| **kdpm_2** | KDPM2DiscreteScheduler | |
| **kdpm_2_a** | KDPM2AncestralDiscreteScheduler | |
| **dpmpp_2s** | DPMSolverSinglestepScheduler | |
| **dpmpp_2m** | DPMSolverMultistepScheduler | original noise scnedule |
| **dpmpp_2m_k** | DPMSolverMultistepScheduler | using karras noise schedule |
| **unipc** | UniPCMultistepScheduler | CPU only |
| **lcm** | LCMScheduler | |
Please see [3.0.0 Release Notes](https://github.com/invoke-ai/InvokeAI/releases/tag/v3.0.0) for further details.
## :material-target: Troubleshooting
Please check out our **[:material-frequently-asked-questions:

View File

@@ -6,10 +6,17 @@ If you're not familiar with Diffusion, take a look at our [Diffusion Overview.](
## Features
### Workflow Library
The Workflow Library enables you to save workflows to the Invoke database, allowing you to easily creating, modify and share workflows as needed.
A curated set of workflows are provided by default - these are designed to help explain important nodes' usage in the Workflow Editor.
![workflow_library](../assets/nodes/workflow_library.png)
### Linear View
The Workflow Editor allows you to create a UI for your workflow, to make it easier to iterate on your generations.
To add an input to the Linear UI, right click on the input label and select "Add to Linear View".
To add an input to the Linear UI, right click on the **input label** and select "Add to Linear View".
The Linear UI View will also be part of the saved workflow, allowing you share workflows and enable other to use them, regardless of complexity.
@@ -30,7 +37,7 @@ Any node or input field can be renamed in the workflow editor. If the input fiel
Nodes have a "Use Cache" option in their footer. This allows for performance improvements by using the previously cached values during the workflow processing.
## Important Concepts
## Important Nodes & Concepts
There are several node grouping concepts that can be examined with a narrow focus. These (and other) groupings can be pieced together to make up functional graph setups, and are important to understanding how groups of nodes work together as part of a whole. Note that the screenshots below aren't examples of complete functioning node graphs (see Examples).
@@ -56,7 +63,7 @@ The ImageToLatents node takes in a pixel image and a VAE and outputs a latents.
It is common to want to use both the same seed (for continuity) and random seeds (for variety). To define a seed, simply enter it into the 'Seed' field on a noise node. Conversely, the RandomInt node generates a random integer between 'Low' and 'High', and can be used as input to the 'Seed' edge point on a noise node to randomize your seed.
![groupsrandseed](../assets/nodes/groupsrandseed.png)
![groupsrandseed](../assets/nodes/groupsnoise.png)
### ControlNet

View File

@@ -1,6 +1,6 @@
# Example Workflows
We've curated some example workflows for you to get started with Workflows in InvokeAI
We've curated some example workflows for you to get started with Workflows in InvokeAI! These can also be found in the Workflow Library, located in the Workflow Editor of Invoke.
To use them, right click on your desired workflow, follow the link to GitHub and click the "⬇" button to download the raw file. You can then use the "Load Workflow" functionality in InvokeAI to load the workflow and start generating images!

View File

@@ -215,6 +215,7 @@ We thank them for all of their time and hard work.
- Robert Bolender
- Robin Rombach
- Rohan Barar
- rohinish404
- rpagliuca
- rromb
- Rupesh Sreeraman

View File

@@ -0,0 +1,5 @@
:root {
--md-primary-fg-color: #35A4DB;
--md-primary-fg-color--light: #35A4DB;
--md-primary-fg-color--dark: #35A4DB;
}

View File

@@ -241,12 +241,12 @@ class InvokeAiInstance:
pip[
"install",
"--require-virtualenv",
"numpy~=1.24.0", # choose versions that won't be uninstalled during phase 2
"numpy==1.26.3", # choose versions that won't be uninstalled during phase 2
"urllib3~=1.26.0",
"requests~=2.28.0",
"torch==2.1.2",
"torchmetrics==0.11.4",
"torchvision>=0.16.2",
"torchvision==0.16.2",
"--force-reinstall",
"--find-links" if find_links is not None else None,
find_links,

View File

@@ -76,7 +76,7 @@ mimetypes.add_type("text/css", ".css")
# Create the app
# TODO: create this all in a method so configuration/etc. can be passed in?
app = FastAPI(title="Invoke AI", docs_url=None, redoc_url=None, separate_input_output_schemas=False)
app = FastAPI(title="Invoke - Community Edition", docs_url=None, redoc_url=None, separate_input_output_schemas=False)
# Add event handler
event_handler_id: int = id(app)
@@ -205,8 +205,8 @@ app.openapi = custom_openapi # type: ignore [method-assign] # this is a valid a
def overridden_swagger() -> HTMLResponse:
return get_swagger_ui_html(
openapi_url=app.openapi_url, # type: ignore [arg-type] # this is always a string
title=app.title,
swagger_favicon_url="/static/docs/favicon.ico",
title=f"{app.title} - Swagger UI",
swagger_favicon_url="static/docs/invoke-favicon-docs.svg",
)
@@ -214,8 +214,8 @@ def overridden_swagger() -> HTMLResponse:
def overridden_redoc() -> HTMLResponse:
return get_redoc_html(
openapi_url=app.openapi_url, # type: ignore [arg-type] # this is always a string
title=app.title,
redoc_favicon_url="/static/docs/favicon.ico",
title=f"{app.title} - Redoc",
redoc_favicon_url="static/docs/invoke-favicon-docs.svg",
)
@@ -229,7 +229,7 @@ if (web_root_path / "dist").exists():
def get_index() -> FileResponse:
return FileResponse(Path(web_root_path, "dist/index.html"), headers={"Cache-Control": "no-store"})
# # Must mount *after* the other routes else it borks em
# Must mount *after* the other routes else it borks em
app.mount("/assets", StaticFiles(directory=Path(web_root_path, "dist/assets/")), name="assets")
app.mount("/locales", StaticFiles(directory=Path(web_root_path, "dist/locales/")), name="locales")

View File

@@ -1,5 +1,7 @@
"""Init file for InvokeAI configure package."""
from invokeai.app.services.config.config_common import PagingArgumentParser
from .config_default import InvokeAIAppConfig, get_invokeai_config
__all__ = ["InvokeAIAppConfig", "get_invokeai_config"]
__all__ = ["InvokeAIAppConfig", "get_invokeai_config", "PagingArgumentParser"]

View File

@@ -1,3 +1,4 @@
import cProfile
import time
import traceback
from threading import BoundedSemaphore, Event, Thread
@@ -39,6 +40,9 @@ class DefaultInvocationProcessor(InvocationProcessorABC):
self.__threadLimit.acquire()
queue_item: Optional[InvocationQueueItem] = None
profiler = None
last_gesid = None
while not stop_event.is_set():
try:
queue_item = self.__invoker.services.queue.get()
@@ -49,6 +53,21 @@ class DefaultInvocationProcessor(InvocationProcessorABC):
# do not hammer the queue
time.sleep(0.5)
continue
if last_gesid != queue_item.graph_execution_state_id:
if profiler is not None:
# I'm not sure what would cause us to get here, but if we do, we should restart the profiler for
# the new graph_execution_state_id.
profiler.disable()
logger.info(f"Stopped profiler for {last_gesid}.")
profiler = None
last_gesid = None
profiler = cProfile.Profile()
profiler.enable()
last_gesid = queue_item.graph_execution_state_id
logger.info(f"Started profiling {last_gesid}.")
try:
graph_execution_state = self.__invoker.services.graph_execution_manager.get(
queue_item.graph_execution_state_id
@@ -201,6 +220,13 @@ class DefaultInvocationProcessor(InvocationProcessorABC):
queue_id=queue_item.session_queue_id,
graph_execution_state_id=graph_execution_state.id,
)
if profiler is not None:
profiler.disable()
dump_path = f"{last_gesid}.prof"
profiler.dump_stats(dump_path)
logger.info(f"Saved profile to {dump_path}.")
profiler = None
last_gesid = None
except KeyboardInterrupt:
pass # Log something? KeyboardInterrupt is probably not going to be seen by the processor

View File

@@ -169,7 +169,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
self._cursor.execute(count_query, count_params)
total = self._cursor.fetchone()[0]
pages = int(total / per_page) + 1
pages = total // per_page + (total % per_page > 0)
return PaginatedResults(
items=workflows,

View File

@@ -370,6 +370,8 @@ class LoRACheckpointProbe(CheckpointProbeBase):
return BaseModelType.StableDiffusion1
elif token_vector_length == 1024:
return BaseModelType.StableDiffusion2
elif token_vector_length == 1280:
return BaseModelType.StableDiffusionXL # recognizes format at https://civitai.com/models/224641
elif token_vector_length == 2048:
return BaseModelType.StableDiffusionXL
else:

View File

@@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
.pnpm-store
# We want to distribute the repo
dist
dist/**

View File

@@ -1,4 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="#E6FD13"/>
<path d="M19.2378 10.9H25V7H7V10.9H12.7622L19.2378 21.1H25V25H7V21.1H12.7622" stroke="#181818" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 272 B

View File

@@ -7,9 +7,8 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="mask-icon" type="icon" href="favicon-outline.svg" color="#E6FD13" sizes="any" />
<link rel="icon" type="icon" href="favicon-key.svg" />
<title>Invoke - Community Edition</title>
<link rel="icon" type="icon" href="assets/images/invoke-favicon.svg" />
<style>
html,
body {

View File

@@ -96,7 +96,7 @@
"react-icons": "^4.12.0",
"react-konva": "^18.2.10",
"react-redux": "9.0.4",
"react-resizable-panels": "^1.0.7",
"react-resizable-panels": "^1.0.8",
"react-select": "5.8.0",
"react-textarea-autosize": "^8.5.3",
"react-use": "^17.4.2",

View File

@@ -143,8 +143,8 @@ dependencies:
specifier: 9.0.4
version: 9.0.4(@types/react@18.2.47)(react@18.2.0)(redux@5.0.1)
react-resizable-panels:
specifier: ^1.0.7
version: 1.0.7(react-dom@18.2.0)(react@18.2.0)
specifier: ^1.0.8
version: 1.0.8(react-dom@18.2.0)(react@18.2.0)
react-select:
specifier: 5.8.0
version: 5.8.0(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
@@ -11638,8 +11638,8 @@ packages:
use-sidecar: 1.1.2(@types/react@18.2.47)(react@18.2.0)
dev: false
/react-resizable-panels@1.0.7(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CluJkHQheeNqIJly2FYDfri3ME+2h2nCXpf0Y+hTO1K1eVtNxXFA5hVp5cUD6NS70iiufswOmnku9QZiLr1hYg==}
/react-resizable-panels@1.0.8(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-IuivK06FWN115VSN8TDGYuIoAzplC4oPUCDZ5d+VWJj0p6N3SMfwjggpjMUGSpQJLvMi0FXPSLLn4rGVmESjmA==}
peerDependencies:
react: ^16.14.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0

View File

@@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="50" fill="#E6FD13"/>
<path d="M55.8887 40.7241H66.369V33.6309H33.6309V40.7241H44.1111L55.8887 59.2757H66.369V66.369H33.6309V59.2757H44.1111" stroke="#181818" stroke-width="2.5"/>
</svg>

After

Width:  |  Height:  |  Size: 321 B

View File

@@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" fill="#E6FD13"/>
<path d="M55.8887 40.7241H66.369V33.6309H33.6309V40.7241H44.1111L55.8887 59.2757H66.369V66.369H33.6309V59.2757H44.1111" stroke="#181818" stroke-width="2.5"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="2" fill="#E6FD13"/>
<path d="M9.61889 5.45H12.5V3.5H3.5V5.45H6.38111L9.61889 10.55H12.5V12.5H3.5V10.55H6.38111" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 265 B

View File

@@ -0,0 +1,4 @@
<svg width="106" height="106" viewBox="0 0 106 106" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="100" height="100" rx="6.66667" stroke="#181818" stroke-width="5"/>
<path d="M63.9137 36H83.1211V23H23.1211V36H42.3285L63.9137 70H83.1211V83H23.1211V70H42.3285" stroke="#181818" stroke-width="5"/>
</svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@@ -0,0 +1,4 @@
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="40" height="40" rx="4" stroke="#181818" stroke-width="2"/>
<path d="M25.3659 14.2H33.0488V9H9.04883V14.2H16.7318L25.3659 27.8H33.0488V33H9.04883V27.8H16.7318" stroke="#181818" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View File

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 324 B

View File

@@ -0,0 +1,4 @@
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="40" height="40" rx="4" stroke="white" stroke-width="2"/>
<path d="M25.3659 14.2H33.0488V9H9.04883V14.2H16.7318L25.3659 27.8H33.0488V33H9.04883V27.8H16.7318" stroke="white" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@@ -0,0 +1,3 @@
<svg width="66" height="66" viewBox="0 0 66 66" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M43.9137 16H63.1211V3H3.12109V16H22.3285L43.9137 50H63.1211V63H3.12109V50H22.3285" stroke="#181818" stroke-width="5"/>
</svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5975 5.33333H21V1H1V5.33333H7.40246L14.5975 16.6667H21V21H1V16.6667H7.40246" stroke="#181818" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@@ -0,0 +1,3 @@
<svg width="66" height="66" viewBox="0 0 66 66" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M43.9137 16H63.1211V3H3.12109V16H22.3285L43.9137 50H63.1211V63H3.12109V50H22.3285" stroke="white" stroke-width="5"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5975 5.33333H21V1H1V5.33333H7.40246L14.5975 16.6667H21V21H1V16.6667H7.40246" stroke="white" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

View File

Before

Width:  |  Height:  |  Size: 231 B

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -0,0 +1,13 @@
<svg width="231" height="100" viewBox="0 0 231 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_659_3591)">
<rect width="100" height="100" fill="#181818"/>
<path d="M57.1951 38.6667H70V30H30V38.6667H42.8049L57.1951 61.3333H70V70H30V61.3333H42.8049" stroke="white" stroke-width="2.8"/>
<rect width="131" height="100" transform="translate(100)" fill="#181818"/>
<path d="M111.37 43.34H107.89V39.65H111.37V43.34ZM116.38 60.5H102.76V58.16H108.22V47.84H103.36V45.5H111.07V58.16H116.38V60.5ZM120.538 60.5V45.5H123.388V47.6C124.288 46.25 125.788 45.2 128.188 45.2C131.638 45.2 133.588 47.3 133.588 50.45V60.5H130.738V51.05C130.738 48.95 129.838 47.6 127.708 47.6H127.468C125.338 47.6 123.388 49.25 123.388 52.1V60.5H120.538ZM141.786 60.5L136.236 45.5H139.176L143.526 57.71L147.876 45.5H150.786L145.236 60.5H141.786ZM159.723 60.8C155.673 60.8 152.523 57.65 152.523 53C152.523 48.35 155.673 45.2 159.723 45.2C163.773 45.2 166.923 48.35 166.923 53C166.923 57.65 163.773 60.8 159.723 60.8ZM155.373 53C155.373 56.45 157.173 58.4 159.603 58.4H159.843C162.273 58.4 164.073 56.45 164.073 53C164.073 49.55 162.273 47.6 159.843 47.6H159.603C157.173 47.6 155.373 49.55 155.373 53ZM170.804 60.5V39.5H173.654V51.98L180.254 45.5H184.004L177.764 51.47L184.304 60.5H180.794L175.724 53.42L173.654 55.43V60.5H170.804ZM192.459 60.8C188.739 60.8 185.379 58.22 185.379 52.97C185.379 47.78 188.829 45.2 192.369 45.2C196.269 45.2 198.729 47.9 198.729 51.8V53.45H188.229C188.409 56.87 190.599 58.4 192.429 58.4H192.669C194.139 58.4 195.549 57.65 195.849 56.06H198.699C198.159 59.09 195.549 60.8 192.459 60.8ZM188.409 51.2H195.879C195.759 48.59 194.199 47.6 192.489 47.6H192.249C190.689 47.6 188.949 48.53 188.409 51.2Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_659_3591">
<rect width="231" height="100" rx="5" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,13 @@
<svg width="116" height="50" viewBox="0 0 116 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_659_3586)">
<rect width="50" height="50" fill="#181818"/>
<path d="M28.5975 19.3333H35V15H15V19.3333H21.4025L28.5975 30.6667H35V35H15V30.6667H21.4025" stroke="white" stroke-width="1.2"/>
<rect width="66" height="50" transform="translate(50)" fill="#181818"/>
<path d="M55.685 21.92H53.945V20.075H55.685V21.92ZM58.19 30.5H51.38V29.33H54.11V24.17H51.68V23H55.535V29.33H58.19V30.5ZM60.2691 30.5V23H61.6941V24.05C62.1441 23.375 62.8941 22.85 64.0941 22.85C65.8191 22.85 66.7941 23.9 66.7941 25.475V30.5H65.3691V25.775C65.3691 24.725 64.9191 24.05 63.8541 24.05H63.7341C62.6691 24.05 61.6941 24.875 61.6941 26.3V30.5H60.2691ZM70.8931 30.5L68.1181 23H69.5881L71.7631 29.105L73.9381 23H75.3931L72.6181 30.5H70.8931ZM79.8614 30.65C77.8364 30.65 76.2614 29.075 76.2614 26.75C76.2614 24.425 77.8364 22.85 79.8614 22.85C81.8864 22.85 83.4614 24.425 83.4614 26.75C83.4614 29.075 81.8864 30.65 79.8614 30.65ZM77.6864 26.75C77.6864 28.475 78.5864 29.45 79.8014 29.45H79.9214C81.1364 29.45 82.0364 28.475 82.0364 26.75C82.0364 25.025 81.1364 24.05 79.9214 24.05H79.8014C78.5864 24.05 77.6864 25.025 77.6864 26.75ZM85.4018 30.5V20H86.8268V26.24L90.1268 23H92.0018L88.8818 25.985L92.1518 30.5H90.3968L87.8618 26.96L86.8268 27.965V30.5H85.4018ZM96.2296 30.65C94.3696 30.65 92.6896 29.36 92.6896 26.735C92.6896 24.14 94.4146 22.85 96.1846 22.85C98.1346 22.85 99.3646 24.2 99.3646 26.15V26.975H94.1146C94.2046 28.685 95.2996 29.45 96.2146 29.45H96.3346C97.0696 29.45 97.7746 29.075 97.9246 28.28H99.3496C99.0796 29.795 97.7746 30.65 96.2296 30.65ZM94.2046 25.85H97.9396C97.8796 24.545 97.0996 24.05 96.2446 24.05H96.1246C95.3446 24.05 94.4746 24.515 94.2046 25.85Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_659_3586">
<rect width="116" height="50" rx="4" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,13 @@
<svg width="231" height="100" viewBox="0 0 231 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_659_3578)">
<rect width="100" height="100" fill="#E6FD13"/>
<path d="M57.1951 38.6667H70V30H30V38.6667H42.8049L57.1951 61.3333H70V70H30V61.3333H42.8049" stroke="#181818" stroke-width="2.8"/>
<rect width="131" height="100" transform="translate(100)" fill="#E6FD13"/>
<path d="M111.37 43.34H107.89V39.65H111.37V43.34ZM116.38 60.5H102.76V58.16H108.22V47.84H103.36V45.5H111.07V58.16H116.38V60.5ZM120.538 60.5V45.5H123.388V47.6C124.288 46.25 125.788 45.2 128.188 45.2C131.638 45.2 133.588 47.3 133.588 50.45V60.5H130.738V51.05C130.738 48.95 129.838 47.6 127.708 47.6H127.468C125.338 47.6 123.388 49.25 123.388 52.1V60.5H120.538ZM141.786 60.5L136.236 45.5H139.176L143.526 57.71L147.876 45.5H150.786L145.236 60.5H141.786ZM159.723 60.8C155.673 60.8 152.523 57.65 152.523 53C152.523 48.35 155.673 45.2 159.723 45.2C163.773 45.2 166.923 48.35 166.923 53C166.923 57.65 163.773 60.8 159.723 60.8ZM155.373 53C155.373 56.45 157.173 58.4 159.603 58.4H159.843C162.273 58.4 164.073 56.45 164.073 53C164.073 49.55 162.273 47.6 159.843 47.6H159.603C157.173 47.6 155.373 49.55 155.373 53ZM170.804 60.5V39.5H173.654V51.98L180.254 45.5H184.004L177.764 51.47L184.304 60.5H180.794L175.724 53.42L173.654 55.43V60.5H170.804ZM192.459 60.8C188.739 60.8 185.379 58.22 185.379 52.97C185.379 47.78 188.829 45.2 192.369 45.2C196.269 45.2 198.729 47.9 198.729 51.8V53.45H188.229C188.409 56.87 190.599 58.4 192.429 58.4H192.669C194.139 58.4 195.549 57.65 195.849 56.06H198.699C198.159 59.09 195.549 60.8 192.459 60.8ZM188.409 51.2H195.879C195.759 48.59 194.199 47.6 192.489 47.6H192.249C190.689 47.6 188.949 48.53 188.409 51.2Z" fill="#181818"/>
</g>
<defs>
<clipPath id="clip0_659_3578">
<rect width="231" height="100" rx="5" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,13 @@
<svg width="116" height="50" viewBox="0 0 116 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_659_3573)">
<rect width="50" height="50" fill="#E6FD13"/>
<path d="M28.5975 19.3333H35V15H15V19.3333H21.4025L28.5975 30.6667H35V35H15V30.6667H21.4025" stroke="#181818" stroke-width="1.2"/>
<rect width="66" height="50" transform="translate(50)" fill="#E6FD13"/>
<path d="M55.685 21.92H53.945V20.075H55.685V21.92ZM58.19 30.5H51.38V29.33H54.11V24.17H51.68V23H55.535V29.33H58.19V30.5ZM60.2691 30.5V23H61.6941V24.05C62.1441 23.375 62.8941 22.85 64.0941 22.85C65.8191 22.85 66.7941 23.9 66.7941 25.475V30.5H65.3691V25.775C65.3691 24.725 64.9191 24.05 63.8541 24.05H63.7341C62.6691 24.05 61.6941 24.875 61.6941 26.3V30.5H60.2691ZM70.8931 30.5L68.1181 23H69.5881L71.7631 29.105L73.9381 23H75.3931L72.6181 30.5H70.8931ZM79.8614 30.65C77.8364 30.65 76.2614 29.075 76.2614 26.75C76.2614 24.425 77.8364 22.85 79.8614 22.85C81.8864 22.85 83.4614 24.425 83.4614 26.75C83.4614 29.075 81.8864 30.65 79.8614 30.65ZM77.6864 26.75C77.6864 28.475 78.5864 29.45 79.8014 29.45H79.9214C81.1364 29.45 82.0364 28.475 82.0364 26.75C82.0364 25.025 81.1364 24.05 79.9214 24.05H79.8014C78.5864 24.05 77.6864 25.025 77.6864 26.75ZM85.4018 30.5V20H86.8268V26.24L90.1268 23H92.0018L88.8818 25.985L92.1518 30.5H90.3968L87.8618 26.96L86.8268 27.965V30.5H85.4018ZM96.2296 30.65C94.3696 30.65 92.6896 29.36 92.6896 26.735C92.6896 24.14 94.4146 22.85 96.1846 22.85C98.1346 22.85 99.3646 24.2 99.3646 26.15V26.975H94.1146C94.2046 28.685 95.2996 29.45 96.2146 29.45H96.3346C97.0696 29.45 97.7746 29.075 97.9246 28.28H99.3496C99.0796 29.795 97.7746 30.65 96.2296 30.65ZM94.2046 25.85H97.9396C97.8796 24.545 97.0996 24.05 96.2446 24.05H96.1246C95.3446 24.05 94.4746 24.515 94.2046 25.85Z" fill="#181818"/>
</g>
<defs>
<clipPath id="clip0_659_3573">
<rect width="116" height="50" rx="4" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,8 @@
<svg width="293" height="65" viewBox="0 0 293 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M273.51 65.0002C269.604 65.0002 266.003 64.1152 262.707 62.3453C259.472 60.5143 256.848 57.7983 254.834 54.1974C252.881 50.5964 251.904 46.2326 251.904 41.1058C251.904 36.0401 252.881 31.7373 254.834 28.1974C256.848 24.5964 259.472 21.911 262.707 20.141C266.003 18.31 269.512 17.3945 273.235 17.3945C277.142 17.3945 280.559 18.249 283.489 19.9579C286.419 21.6058 288.677 23.9556 290.264 27.0072C291.851 30.0589 292.644 33.5683 292.644 37.5354V42.5706H260.602C260.785 45.8664 261.517 48.6434 262.799 50.9016C264.08 53.1598 265.667 54.8688 267.559 56.0284C269.512 57.127 271.465 57.6763 273.419 57.6763H274.151C276.531 57.6763 278.637 57.0659 280.468 55.8453C282.299 54.6246 283.428 52.8547 283.855 50.5354H292.553C291.759 55.0518 289.592 58.5918 286.052 61.1551C282.513 63.7185 278.332 65.0002 273.51 65.0002ZM283.947 35.7044C283.764 31.9814 282.696 29.2349 280.743 27.465C278.851 25.634 276.47 24.7185 273.602 24.7185H272.869C270.184 24.7185 267.742 25.6035 265.545 27.3734C263.409 29.1434 261.944 31.9204 261.151 35.7044H283.947Z" fill="#181818"/>
<path d="M205.254 0H213.951V38.0846L234.092 18.3099H245.536L226.494 36.5282L246.451 64.0846H235.74L220.268 42.4789L213.951 48.6127V64.0846H205.254V0Z" fill="#181818"/>
<path d="M173.507 65.0002C169.418 65.0002 165.695 63.9932 162.338 61.9791C158.981 59.965 156.326 57.1575 154.373 53.5565C152.481 49.9556 151.535 45.8359 151.535 41.1974C151.535 36.5589 152.481 32.4391 154.373 28.8382C156.326 25.2373 158.981 22.4297 162.338 20.4157C165.695 18.4016 169.418 17.3945 173.507 17.3945C177.596 17.3945 181.319 18.4016 184.676 20.4157C188.033 22.4297 190.658 25.2373 192.55 28.8382C194.503 32.4391 195.479 36.5589 195.479 41.1974C195.479 45.8359 194.503 49.9556 192.55 53.5565C190.658 57.1575 188.033 59.965 184.676 61.9791C181.319 63.9932 177.596 65.0002 173.507 65.0002ZM173.873 57.6763C177.657 57.6763 180.74 56.2115 183.12 53.2819C185.561 50.3523 186.782 46.3241 186.782 41.1974C186.782 36.0706 185.561 32.0424 183.12 29.1129C180.74 26.1833 177.657 24.7185 173.873 24.7185H173.141C169.357 24.7185 166.244 26.1833 163.803 29.1129C161.423 32.0424 160.232 36.0706 160.232 41.1974C160.232 46.3241 161.423 50.3523 163.803 53.2819C166.244 56.2115 169.357 57.6763 173.141 57.6763H173.873Z" fill="#181818"/>
<path d="M146.078 18.3096L129.141 64.0843H118.613L101.676 18.3096H110.648L123.922 55.5702L137.197 18.3096H146.078Z" fill="#181818"/>
<path d="M53.9727 18.31H62.6699V24.7185C65.9047 19.8358 70.7873 17.3945 77.3179 17.3945C82.5057 17.3945 86.5339 18.8593 89.4025 21.7889C92.3321 24.6575 93.7969 28.533 93.7969 33.4157V64.0847H85.0997V35.2467C85.0997 31.8899 84.3368 29.296 82.8109 27.465C81.3461 25.634 79.0268 24.7185 75.8531 24.7185H75.1207C72.9235 24.7185 70.8789 25.2678 68.9869 26.3664C67.0948 27.465 65.569 29.0518 64.4094 31.1269C63.2497 33.2021 62.6699 35.6434 62.6699 38.4509V64.0847H53.9727V18.31Z" fill="#181818"/>
<path d="M15.732 0.144531H26.4041V11.4605H15.732V0.144531ZM0 56.9086H16.744V25.2606H1.84V18.0846H25.4841V56.9086H41.7681V64.0846H0V56.9086Z" fill="#181818"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,8 @@
<svg width="293" height="65" viewBox="0 0 293 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M273.51 65.0002C269.604 65.0002 266.003 64.1152 262.707 62.3453C259.472 60.5143 256.848 57.7983 254.834 54.1974C252.881 50.5964 251.904 46.2326 251.904 41.1058C251.904 36.0401 252.881 31.7373 254.834 28.1974C256.848 24.5964 259.472 21.911 262.707 20.141C266.003 18.31 269.512 17.3945 273.235 17.3945C277.142 17.3945 280.559 18.249 283.489 19.9579C286.419 21.6058 288.677 23.9556 290.264 27.0072C291.851 30.0589 292.644 33.5683 292.644 37.5354V42.5706H260.602C260.785 45.8664 261.517 48.6434 262.799 50.9016C264.08 53.1598 265.667 54.8688 267.559 56.0284C269.512 57.127 271.465 57.6763 273.419 57.6763H274.151C276.531 57.6763 278.637 57.0659 280.468 55.8453C282.299 54.6246 283.428 52.8547 283.855 50.5354H292.553C291.759 55.0518 289.592 58.5918 286.052 61.1551C282.513 63.7185 278.332 65.0002 273.51 65.0002ZM283.947 35.7044C283.764 31.9814 282.696 29.2349 280.743 27.465C278.851 25.634 276.47 24.7185 273.602 24.7185H272.869C270.184 24.7185 267.742 25.6035 265.545 27.3734C263.409 29.1434 261.944 31.9204 261.151 35.7044H283.947Z" fill="white"/>
<path d="M205.254 0H213.951V38.0846L234.092 18.3099H245.536L226.494 36.5282L246.451 64.0846H235.74L220.268 42.4789L213.951 48.6127V64.0846H205.254V0Z" fill="white"/>
<path d="M173.507 65.0002C169.418 65.0002 165.695 63.9932 162.338 61.9791C158.981 59.965 156.326 57.1575 154.373 53.5565C152.481 49.9556 151.535 45.8359 151.535 41.1974C151.535 36.5589 152.481 32.4391 154.373 28.8382C156.326 25.2373 158.981 22.4297 162.338 20.4157C165.695 18.4016 169.418 17.3945 173.507 17.3945C177.596 17.3945 181.319 18.4016 184.676 20.4157C188.033 22.4297 190.658 25.2373 192.55 28.8382C194.503 32.4391 195.479 36.5589 195.479 41.1974C195.479 45.8359 194.503 49.9556 192.55 53.5565C190.658 57.1575 188.033 59.965 184.676 61.9791C181.319 63.9932 177.596 65.0002 173.507 65.0002ZM173.873 57.6763C177.657 57.6763 180.74 56.2115 183.12 53.2819C185.561 50.3523 186.782 46.3241 186.782 41.1974C186.782 36.0706 185.561 32.0424 183.12 29.1129C180.74 26.1833 177.657 24.7185 173.873 24.7185H173.141C169.357 24.7185 166.244 26.1833 163.803 29.1129C161.423 32.0424 160.232 36.0706 160.232 41.1974C160.232 46.3241 161.423 50.3523 163.803 53.2819C166.244 56.2115 169.357 57.6763 173.141 57.6763H173.873Z" fill="white"/>
<path d="M146.078 18.3096L129.141 64.0843H118.613L101.676 18.3096H110.648L123.922 55.5702L137.197 18.3096H146.078Z" fill="white"/>
<path d="M53.9727 18.31H62.6699V24.7185C65.9047 19.8358 70.7873 17.3945 77.3179 17.3945C82.5057 17.3945 86.5339 18.8593 89.4025 21.7889C92.3321 24.6575 93.7969 28.533 93.7969 33.4157V64.0847H85.0997V35.2467C85.0997 31.8899 84.3368 29.296 82.8109 27.465C81.3461 25.634 79.0268 24.7185 75.8531 24.7185H75.1207C72.9235 24.7185 70.8789 25.2678 68.9869 26.3664C67.0948 27.465 65.569 29.0518 64.4094 31.1269C63.2497 33.2021 62.6699 35.6434 62.6699 38.4509V64.0847H53.9727V18.31Z" fill="white"/>
<path d="M15.732 0.144531H26.4041V11.4605H15.732V0.144531ZM0 56.9086H16.744V25.2606H1.84V18.0846H25.4841V56.9086H41.7681V64.0846H0V56.9086Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,40 @@
{
"accessibility": {
"mode": "Mód",
"uploadImage": "Fénykép feltöltése",
"zoomIn": "Nagyítás",
"nextImage": "Következő kép",
"previousImage": "Előző kép",
"menu": "Menü",
"zoomOut": "Kicsinyítés",
"loadMore": "Több betöltése"
},
"boards": {
"cancel": "Mégsem",
"loading": "Betöltés..."
},
"accordions": {
"image": {
"title": "Kép"
}
},
"common": {
"accept": "Elfogad",
"ai": "ai",
"back": "Vissza",
"cancel": "Mégsem",
"close": "Bezár",
"or": "vagy",
"details": "Részletek",
"darkMode": "Sötét Mód",
"error": "Hiba",
"file": "Fájl",
"githubLabel": "Github",
"hotkeysLabel": "Gyorsbillentyűk",
"delete": "Törlés",
"data": "Adat",
"discordLabel": "Discord",
"folder": "Mappa",
"langEnglish": "Angol"
}
}

View File

@@ -116,7 +116,9 @@
"unsaved": "Non salvato",
"direction": "Direzione",
"advancedOptions": "Opzioni avanzate",
"free": "Libero"
"free": "Libero",
"or": "o",
"preferencesLabel": "Preferenze"
},
"gallery": {
"generations": "Generazioni",

View File

@@ -634,7 +634,8 @@
"missingNodeTemplate": "Отсутствует шаблон узла",
"readyToInvoke": "Готово к вызову",
"missingFieldTemplate": "Отсутствует шаблон поля",
"addingImagesTo": "Добавление изображений в"
"addingImagesTo": "Добавление изображений в",
"invoke": "Создать"
},
"seamlessX&Y": "Бесшовный X & Y",
"isAllowedToUpscale": {
@@ -662,7 +663,14 @@
"useSize": "Использовать размер",
"unmasked": "Без маски",
"enableNoiseSettings": "Включить настройки шума",
"coherenceMode": "Режим"
"coherenceMode": "Режим",
"aspect": "Соотношение",
"imageSize": "Размер изображения",
"swapDimensions": "Поменять местами",
"setToOptimalSize": "Установить оптимальный для модели размер",
"setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (может быть слишком маленьким)",
"setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (может быть слишком большим)",
"lockAspectRatio": "Заблокировать соотношение"
},
"settings": {
"models": "Модели",
@@ -1250,7 +1258,9 @@
"promptsWithCount_one": "{{count}} Запрос",
"promptsWithCount_few": "{{count}} Запроса",
"promptsWithCount_many": "{{count}} Запросов",
"dynamicPrompts": "Динамические запросы"
"dynamicPrompts": "Динамические запросы",
"loading": "Создание динамических запросов...",
"showDynamicPrompts": "Показать динамические запросы"
},
"popovers": {
"noiseUseCPU": {
@@ -1513,7 +1523,7 @@
"queueBack": "Добавить в очередь",
"batchValues": "Пакетные значения",
"cancelFailed": "Проблема с отменой элемента",
"queueCountPrediction": "Добавить {{predicted}} в очередь",
"queueCountPrediction": "{{promptsCount}} запросов × {{iterations}} изображений -> {{count}} генераций",
"batchQueued": "Пакетная очередь",
"pauseFailed": "Проблема с приостановкой рендеринга",
"clearFailed": "Проблема с очисткой очереди",
@@ -1623,7 +1633,8 @@
"workflows": "Рабочие процессы",
"noDescription": "Без описания",
"uploadWorkflow": "Загрузить рабочий процесс",
"userWorkflows": "Мои рабочие процессы"
"userWorkflows": "Мои рабочие процессы",
"newWorkflowCreated": "Создан новый рабочий процесс"
},
"embedding": {
"noEmbeddingsLoaded": "встраивания не загружены",

View File

@@ -1,4 +1,5 @@
import { StorageError } from 'app/store/enhancers/reduxRemember/errors';
import { $projectId } from 'app/store/nanostores/projectId';
import type { UseStore } from 'idb-keyval';
import {
clear,
@@ -24,14 +25,23 @@ export const idbKeyValDriver: Driver = {
try {
return get(key, $idbKeyValStore.get());
} catch (originalError) {
throw new StorageError({ key, originalError });
throw new StorageError({
key,
projectId: $projectId.get(),
originalError,
});
}
},
setItem: (key, value) => {
try {
return set(key, value, $idbKeyValStore.get());
} catch (originalError) {
throw new StorageError({ key, value, originalError });
throw new StorageError({
key,
value,
projectId: $projectId.get(),
originalError,
});
}
},
};

View File

@@ -8,6 +8,7 @@ export type StorageErrorArgs = {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ // any is correct
value?: any;
originalError?: unknown;
projectId?: string;
};
export class StorageError extends Error {
@@ -15,14 +16,18 @@ export class StorageError extends Error {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ // any is correct
value?: any;
originalError?: Error;
projectId?: string;
constructor({ key, value, originalError }: StorageErrorArgs) {
constructor({ key, value, originalError, projectId }: StorageErrorArgs) {
super(`Error setting ${key}`);
this.name = 'StorageSetError';
this.key = key;
if (value !== undefined) {
this.value = value;
}
if (projectId !== undefined) {
this.projectId = projectId;
}
if (originalError instanceof Error) {
this.originalError = originalError;
}

View File

@@ -5,6 +5,7 @@ import type {
UnknownAction,
} from '@reduxjs/toolkit';
import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
import type { AppDispatch, RootState } from 'app/store/store';
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
@@ -69,7 +70,6 @@ import { addSessionRetrievalErrorEventListener } from './listeners/socketio/sock
import { addSocketSubscribedEventListener as addSocketSubscribedListener } from './listeners/socketio/socketSubscribed';
import { addSocketUnsubscribedEventListener as addSocketUnsubscribedListener } from './listeners/socketio/socketUnsubscribed';
import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSaved';
import { addTabChangedListener } from './listeners/tabChanged';
import { addUpdateAllNodesRequestedListener } from './listeners/updateAllNodesRequested';
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
import { addWorkflowLoadRequestedListener } from './listeners/workflowLoadRequested';
@@ -118,6 +118,9 @@ addImageToDeleteSelectedListener();
addImagesStarredListener();
addImagesUnstarredListener();
// Gallery
addGalleryImageClickedListener();
// User Invoked
addEnqueueRequestedCanvasListener();
addEnqueueRequestedNodes();
@@ -136,19 +139,7 @@ addCanvasMergedListener();
addStagingAreaImageSavedListener();
addCommitStagingAreaImageListener();
/**
* Socket.IO Events - these handle SIO events directly and pass on internal application actions.
* We don't handle SIO events in slices via `extraReducers` because some of these events shouldn't
* actually be handled at all.
*
* For example, we don't want to respond to progress events for canceled sessions. To avoid
* duplicating the logic to determine if an event should be responded to, we handle all of that
* "is this session canceled?" logic in these listeners.
*
* The `socketGeneratorProgress` listener will then only dispatch the `appSocketGeneratorProgress`
* action if it should be handled by the rest of the application. It is this `appSocketGeneratorProgress`
* action that is handled by reducers in slices.
*/
// Socket.IO
addGeneratorProgressListener();
addGraphExecutionStateCompleteListener();
addInvocationCompleteListener();
@@ -196,8 +187,5 @@ addFirstListImagesListener();
// Ad-hoc upscale workflwo
addUpscaleRequestedListener();
// Tab Change
addTabChangedListener();
// Dynamic prompts
addDynamicPromptsListener();

View File

@@ -1,4 +1,4 @@
import { queueApi } from 'services/api/endpoints/queue';
import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
import { startAppListening } from '..';
@@ -6,7 +6,7 @@ export const addAnyEnqueuedListener = () => {
startAppListening({
matcher: queueApi.endpoints.enqueueBatch.matchFulfilled,
effect: async (_, { dispatch, getState }) => {
const { data } = queueApi.endpoints.getQueueStatus.select()(getState());
const { data } = selectQueueStatus(getState());
if (!data || data.processor.is_started) {
return;

View File

@@ -1,8 +1,8 @@
import { $logger } from 'app/logging/logger';
import { canvasMerged } from 'features/canvas/store/actions';
import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore';
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images';
@@ -30,7 +30,7 @@ export const addCanvasMergedListener = () => {
return;
}
const canvasBaseLayer = getCanvasBaseLayer();
const canvasBaseLayer = $canvasBaseLayer.get();
if (!canvasBaseLayer) {
moduleLog.error('Problem getting canvas base layer');

View File

@@ -1,6 +1,6 @@
import { enqueueRequested } from 'app/store/actions';
import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
import { buildWorkflowRight } from 'features/nodes/util/workflow/buildWorkflow';
import { buildWorkflowWithValidation } from 'features/nodes/util/workflow/buildWorkflow';
import { queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig } from 'services/api/types';
@@ -15,7 +15,7 @@ export const addEnqueueRequestedNodes = () => {
const { nodes, edges } = state.nodes;
const workflow = state.workflow;
const graph = buildNodesGraph(state.nodes);
const builtWorkflow = buildWorkflowRight({
const builtWorkflow = buildWorkflowWithValidation({
nodes,
edges,
workflow,

View File

@@ -0,0 +1,80 @@
import { createAction } from '@reduxjs/toolkit';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { selectionChanged } from 'features/gallery/store/gallerySlice';
import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import { imagesSelectors } from 'services/api/util';
import { startAppListening } from '..';
export const galleryImageClicked = createAction<{
imageDTO: ImageDTO;
shiftKey: boolean;
ctrlKey: boolean;
metaKey: boolean;
}>('gallery/imageClicked');
/**
* This listener handles the logic for selecting images in the gallery.
*
* Previously, this logic was in a `useCallback` with the whole gallery selection as a dependency. Every time
* the selection changed, the callback got recreated and all images rerendered. This could easily block for
* hundreds of ms, more for lower end devices.
*
* Moving this logic into a listener means we don't need to recalculate anything dynamically and the gallery
* is much more responsive.
*/
export const addGalleryImageClickedListener = () => {
startAppListening({
actionCreator: galleryImageClicked,
effect: async (action, { dispatch, getState }) => {
const { imageDTO, shiftKey, ctrlKey, metaKey } = action.payload;
const state = getState();
const queryArgs = selectListImagesQueryArgs(state);
const { data: listImagesData } =
imagesApi.endpoints.listImages.select(queryArgs)(state);
if (!listImagesData) {
// Should never happen if we have clicked a gallery image
return;
}
const imageDTOs = imagesSelectors.selectAll(listImagesData);
const selection = state.gallery.selection;
if (shiftKey) {
const rangeEndImageName = imageDTO.image_name;
const lastSelectedImage = selection[selection.length - 1]?.image_name;
const lastClickedIndex = imageDTOs.findIndex(
(n) => n.image_name === lastSelectedImage
);
const currentClickedIndex = imageDTOs.findIndex(
(n) => n.image_name === rangeEndImageName
);
if (lastClickedIndex > -1 && currentClickedIndex > -1) {
// We have a valid range!
const start = Math.min(lastClickedIndex, currentClickedIndex);
const end = Math.max(lastClickedIndex, currentClickedIndex);
const imagesToSelect = imageDTOs.slice(start, end + 1);
dispatch(selectionChanged(selection.concat(imagesToSelect)));
}
} else if (ctrlKey || metaKey) {
if (
selection.some((i) => i.image_name === imageDTO.image_name) &&
selection.length > 1
) {
dispatch(
selectionChanged(
selection.filter((n) => n.image_name !== imageDTO.image_name)
)
);
} else {
dispatch(selectionChanged(selection.concat(imageDTO)));
}
} else {
dispatch(selectionChanged([imageDTO]));
}
},
});
};

View File

@@ -8,7 +8,7 @@ import {
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { isImageFieldInputInstance } from 'features/nodes/types/field';
@@ -49,7 +49,7 @@ export const addRequestedSingleImageDeletionListener = () => {
if (imageDTO && imageDTO?.image_name === lastSelectedImage) {
const { image_name } = imageDTO;
const baseQueryArgs = selectListImagesBaseQueryArgs(state);
const baseQueryArgs = selectListImagesQueryArgs(state);
const { data } =
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
@@ -180,9 +180,9 @@ export const addRequestedMultipleImageDeletionListener = () => {
imagesApi.endpoints.deleteImages.initiate({ imageDTOs })
).unwrap();
const state = getState();
const baseQueryArgs = selectListImagesBaseQueryArgs(state);
const queryArgs = selectListImagesQueryArgs(state);
const { data } =
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
imagesApi.endpoints.listImages.select(queryArgs)(state);
const newSelectedImageDTO = data
? imagesSelectors.selectAll(data)[0]

View File

@@ -12,7 +12,6 @@ import type {
} from 'features/dnd/types';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { workflowExposedFieldAdded } from 'features/nodes/store/workflowSlice';
import {
initialImageChanged,
selectOptimalDimension,
@@ -35,10 +34,10 @@ export const addImageDroppedListener = () => {
if (activeData.payloadType === 'IMAGE_DTO') {
log.debug({ activeData, overData }, 'Image dropped');
} else if (activeData.payloadType === 'IMAGE_DTOS') {
} else if (activeData.payloadType === 'GALLERY_SELECTION') {
log.debug(
{ activeData, overData },
`Images (${activeData.payload.imageDTOs.length}) dropped`
`Images (${getState().gallery.selection.length}) dropped`
);
} else if (activeData.payloadType === 'NODE_FIELD') {
log.debug(
@@ -49,19 +48,6 @@ export const addImageDroppedListener = () => {
log.debug({ activeData, overData }, `Unknown payload dropped`);
}
if (
overData.actionType === 'ADD_FIELD_TO_LINEAR' &&
activeData.payloadType === 'NODE_FIELD'
) {
const { nodeId, field } = activeData.payload;
dispatch(
workflowExposedFieldAdded({
nodeId,
fieldName: field.name,
})
);
}
/**
* Image dropped on current image
*/
@@ -207,10 +193,9 @@ export const addImageDroppedListener = () => {
*/
if (
overData.actionType === 'ADD_TO_BOARD' &&
activeData.payloadType === 'IMAGE_DTOS' &&
activeData.payload.imageDTOs
activeData.payloadType === 'GALLERY_SELECTION'
) {
const { imageDTOs } = activeData.payload;
const imageDTOs = getState().gallery.selection;
const { boardId } = overData.context;
dispatch(
imagesApi.endpoints.addImagesToBoard.initiate({
@@ -226,10 +211,9 @@ export const addImageDroppedListener = () => {
*/
if (
overData.actionType === 'REMOVE_FROM_BOARD' &&
activeData.payloadType === 'IMAGE_DTOS' &&
activeData.payload.imageDTOs
activeData.payloadType === 'GALLERY_SELECTION'
) {
const { imageDTOs } = activeData.payload;
const imageDTOs = getState().gallery.selection;
dispatch(
imagesApi.endpoints.removeImagesFromBoard.initiate({
imageDTOs,

View File

@@ -37,8 +37,10 @@ export const addModelSelectedListener = () => {
const newModel = result.data;
const { base_model } = newModel;
const didBaseModelChange =
state.generation.model?.base_model !== base_model;
if (state.generation.model?.base_model !== base_model) {
if (didBaseModelChange) {
// we may need to reset some incompatible submodels
let modelsCleared = 0;
@@ -81,7 +83,7 @@ export const addModelSelectedListener = () => {
}
}
dispatch(modelChanged(newModel));
dispatch(modelChanged(newModel, state.generation.model));
},
});
};

View File

@@ -74,7 +74,7 @@ export const addModelsLoadedListener = () => {
return;
}
dispatch(modelChanged(result.data));
dispatch(modelChanged(result.data, currentModel));
},
});
startAppListening({
@@ -149,7 +149,7 @@ export const addModelsLoadedListener = () => {
if (!firstModel) {
// No custom VAEs loaded at all; use the default
dispatch(modelChanged(null));
dispatch(vaeSelected(null));
return;
}
@@ -227,7 +227,7 @@ export const addModelsLoadedListener = () => {
const log = logger('models');
log.info(
{ models: action.payload.entities },
`ControlNet models loaded (${action.payload.ids.length})`
`T2I Adapter models loaded (${action.payload.ids.length})`
);
selectAllT2IAdapters(getState().controlAdapters).forEach((ca) => {

View File

@@ -1,7 +1,9 @@
import { logger } from 'app/logging/logger';
import { isInitializedChanged } from 'features/system/store/systemSlice';
import { size } from 'lodash-es';
import { $baseUrl } from 'app/store/nanostores/baseUrl';
import { isEqual, size } from 'lodash-es';
import { atom } from 'nanostores';
import { api } from 'services/api';
import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import { socketConnected } from 'services/events/actions';
@@ -9,25 +11,88 @@ import { startAppListening } from '../..';
const log = logger('socketio');
const $isFirstConnection = atom(true);
export const addSocketConnectedEventListener = () => {
startAppListening({
actionCreator: socketConnected,
effect: (action, { dispatch, getState }) => {
effect: async (
action,
{ dispatch, getState, cancelActiveListeners, delay }
) => {
log.debug('Connected');
const { nodeTemplates, config, system } = getState();
/**
* The rest of this listener has recovery logic for when the socket disconnects and reconnects.
*
* We need to re-fetch if something has changed while we were disconnected. In practice, the only
* thing that could change while disconnected is a queue item finishes processing.
*
* The queue status is a proxy for this - if the queue status has changed, we need to re-fetch
* the queries that may have changed while we were disconnected.
*/
const { disabledTabs } = config;
if (!size(nodeTemplates.templates) && !disabledTabs.includes('nodes')) {
dispatch(receivedOpenAPISchema());
// Bail on the recovery logic if this is the first connection - we don't need to recover anything
if ($isFirstConnection.get()) {
$isFirstConnection.set(false);
return;
}
if (system.isInitialized) {
// only reset the query caches if this connect event is a *reconnect* event
// If we are in development mode, reset the whole API state. In this scenario, reconnects will
// typically be caused by reloading the server, in which case we do want to reset the whole API.
if (import.meta.env.MODE === 'development') {
dispatch(api.util.resetApiState());
} else {
dispatch(isInitializedChanged(true));
}
// Else, we need to compare the last-known queue status with the current queue status, re-fetching
// everything if it has changed.
if ($baseUrl.get()) {
// If we have a baseUrl (e.g. not localhost), we need to debounce the re-fetch to not hammer server
cancelActiveListeners();
// Add artificial jitter to the debounce
await delay(1000 + Math.random() * 1000);
}
const prevQueueStatusData = selectQueueStatus(getState()).data;
try {
// Fetch the queue status again
const queueStatusRequest = dispatch(
await queueApi.endpoints.getQueueStatus.initiate(undefined, {
forceRefetch: true,
})
);
const nextQueueStatusData = await queueStatusRequest.unwrap();
queueStatusRequest.unsubscribe();
// If the queue hasn't changed, we don't need to do anything.
if (isEqual(prevQueueStatusData?.queue, nextQueueStatusData.queue)) {
return;
}
//The queue has changed. We need to re-fetch everything that may have changed while we were
// disconnected.
dispatch(api.util.invalidateTags(['FetchOnReconnect']));
} catch {
// no-op
log.debug('Unable to get current queue status on reconnect');
}
},
});
startAppListening({
actionCreator: socketConnected,
effect: async (action, { dispatch, getState }) => {
const { nodeTemplates, config } = getState();
// We only want to re-fetch the schema if we don't have any node templates
if (
!size(nodeTemplates.templates) &&
!config.disabledTabs.includes('nodes')
) {
// This request is a createAsyncThunk - resetting API state as in the above listener
// will not trigger this request, so we need to manually do it.
dispatch(receivedOpenAPISchema());
}
},
});

View File

@@ -1,67 +0,0 @@
import { modelChanged } from 'features/parameters/store/generationSlice';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { NON_REFINER_BASE_MODELS } from 'services/api/constants';
import {
mainModelsAdapterSelectors,
modelsApi,
} from 'services/api/endpoints/models';
import { startAppListening } from '..';
export const addTabChangedListener = () => {
startAppListening({
actionCreator: setActiveTab,
effect: async (action, { getState, dispatch }) => {
const activeTabName = action.payload;
if (activeTabName === 'unifiedCanvas') {
const currentBaseModel = getState().generation.model?.base_model;
if (
currentBaseModel &&
['sd-1', 'sd-2', 'sdxl'].includes(currentBaseModel)
) {
// if we're already on a valid model, no change needed
return;
}
try {
// just grab fresh models
const modelsRequest = dispatch(
modelsApi.endpoints.getMainModels.initiate(NON_REFINER_BASE_MODELS)
);
const models = await modelsRequest.unwrap();
// cancel this cache subscription
modelsRequest.unsubscribe();
if (!models.ids.length) {
// no valid canvas models
dispatch(modelChanged(null));
return;
}
// need to filter out all the invalid canvas models (currently refiner & any)
const validCanvasModels = mainModelsAdapterSelectors
.selectAll(models)
.filter((model) =>
['sd-1', 'sd-2', 'sdxl'].includes(model.base_model)
);
const firstValidCanvasModel = validCanvasModels[0];
if (!firstValidCanvasModel) {
// no valid canvas models
dispatch(modelChanged(null));
return;
}
const { base_model, model_name, model_type } = firstValidCanvasModel;
dispatch(modelChanged({ base_model, model_name, model_type }));
} catch {
// network request failed, bail
dispatch(modelChanged(null));
}
}
},
});
};

View File

@@ -1,3 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29.1951 10.6667H42V2H2V10.6667H14.8049L29.1951 33.3333H42V42H2V33.3333H14.8049" stroke="#E6FD13" stroke-width="2.8"/>
</svg>

Before

Width:  |  Height:  |  Size: 231 B

View File

@@ -24,8 +24,8 @@ export const InvAutosizeTextarea = memo(
ref={ref}
overflow="scroll"
w="100%"
resize="none"
minRows={3}
minH={20}
onPaste={stopPastePropagation}
onKeyUp={onKeyUpDown}
onKeyDown={onKeyUpDown}

View File

@@ -1,16 +1,5 @@
/**
* This is a copy-paste of https://github.com/lukasbach/chakra-ui-contextmenu with a small change.
*
* The reactflow background element somehow prevents the chakra `useOutsideClick()` hook from working.
* With a menu open, clicking on the reactflow background element doesn't close the menu.
*
* Reactflow does provide an `onPaneClick` to handle clicks on the background element, but it is not
* straightforward to programatically close the menu.
*
* As a (hopefully temporary) workaround, we will use a dirty hack:
* - create `globalContextMenuCloseTrigger: number` in `ui` slice
* - increment it in `onPaneClick` (and wherever else we want to close the menu)
* - `useEffect()` to close the menu when `globalContextMenuCloseTrigger` changes
* Adapted from https://github.com/lukasbach/chakra-ui-contextmenu
*/
import type {
ChakraProps,
@@ -18,9 +7,9 @@ import type {
MenuProps,
PortalProps,
} from '@chakra-ui/react';
import { Portal, useEventListener } from '@chakra-ui/react';
import { Portal, useDisclosure, useEventListener } from '@chakra-ui/react';
import { InvMenu, InvMenuButton } from 'common/components/InvMenu/wrapper';
import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
import { useGlobalMenuClose } from 'common/hooks/useGlobalMenuClose';
import { typedMemo } from 'common/util/typedMemo';
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -34,94 +23,89 @@ export interface InvContextMenuProps<T extends HTMLElement = HTMLDivElement> {
export const InvContextMenu = typedMemo(
<T extends HTMLElement = HTMLElement>(props: InvContextMenuProps<T>) => {
const [isOpen, setIsOpen] = useState(false);
const [isRendered, setIsRendered] = useState(false);
const [isDeferredOpen, setIsDeferredOpen] = useState(false);
const [position, setPosition] = useState<[number, number]>([0, 0]);
const { isOpen, onOpen, onClose } = useDisclosure();
const [position, setPosition] = useState([-1, -1]);
const targetRef = useRef<T>(null);
const lastPositionRef = useRef([-1, -1]);
const timeoutRef = useRef(0);
useEffect(() => {
if (isOpen) {
setTimeout(() => {
setIsRendered(true);
setTimeout(() => {
setIsDeferredOpen(true);
});
});
} else {
setIsDeferredOpen(false);
const timeout = setTimeout(() => {
setIsRendered(isOpen);
}, 1000);
return () => clearTimeout(timeout);
}
}, [isOpen]);
useGlobalMenuClose(onClose);
const onClose = useCallback(() => {
setIsOpen(false);
setIsDeferredOpen(false);
setIsRendered(false);
}, []);
const onContextMenu = useCallback(
(e: MouseEvent) => {
if (e.shiftKey) {
onClose();
return;
}
if (
targetRef.current?.contains(e.target as HTMLElement) ||
e.target === targetRef.current
) {
// clear pending delayed open
window.clearTimeout(timeoutRef.current);
e.preventDefault();
if (
lastPositionRef.current[0] !== e.pageX ||
lastPositionRef.current[1] !== e.pageY
) {
// if the mouse moved, we need to close, wait for animation and reopen the menu at the new position
onClose();
timeoutRef.current = window.setTimeout(() => {
onOpen();
setPosition([e.pageX, e.pageY]);
}, 100);
} else {
// else we can just open the menu at the current position
onOpen();
setPosition([e.pageX, e.pageY]);
}
}
lastPositionRef.current = [e.pageX, e.pageY];
},
[onClose, onOpen]
);
// This is the change from the original chakra-ui-contextmenu
// Close all menus when the globalContextMenuCloseTrigger changes
useGlobalMenuCloseTrigger(onClose);
useEffect(
() => () => {
window.clearTimeout(timeoutRef.current);
},
[]
);
useEventListener('contextmenu', (e) => {
if (
targetRef.current?.contains(e.target as HTMLElement) ||
e.target === targetRef.current
) {
e.preventDefault();
setIsOpen(true);
setPosition([e.pageX, e.pageY]);
} else {
setIsOpen(false);
}
});
const onCloseHandler = useCallback(() => {
props.menuProps?.onClose?.();
setIsOpen(false);
}, [props.menuProps]);
useEventListener('contextmenu', onContextMenu);
return (
<>
{props.children(targetRef)}
{isRendered && (
<Portal {...props.portalProps}>
<InvMenu
isLazy
isOpen={isDeferredOpen}
gutter={0}
onClose={onCloseHandler}
placement="auto-end"
{...props.menuProps}
>
<InvMenuButton
aria-hidden={true}
w={1}
h={1}
position="absolute"
left={position[0]}
top={position[1]}
cursor="default"
bg="transparent"
size="sm"
_hover={_hover}
{...props.menuButtonProps}
/>
{props.renderMenu()}
</InvMenu>
</Portal>
)}
<Portal {...props.portalProps}>
<InvMenu
isLazy
isOpen={isOpen}
gutter={0}
placement="auto-end"
onClose={onClose}
{...props.menuProps}
>
<InvMenuButton
aria-hidden={true}
w={1}
h={1}
position="absolute"
left={position[0]}
top={position[1]}
cursor="default"
bg="transparent"
size="sm"
_hover={_hover}
pointerEvents="none"
{...props.menuButtonProps}
/>
{props.renderMenu()}
</InvMenu>
</Portal>
</>
);
}
);
const _hover: ChakraProps['_hover'] = { bg: 'transparent' };
Object.assign(InvContextMenu, {
displayName: 'InvContextMenu',
});

View File

@@ -3,6 +3,7 @@ import {
MenuList as ChakraMenuList,
Portal,
} from '@chakra-ui/react';
import { skipMouseEvent } from 'common/util/skipMouseEvent';
import { memo } from 'react';
import { menuListMotionProps } from './constants';
@@ -16,6 +17,7 @@ export const InvMenuList = memo(
<ChakraMenuList
ref={ref}
motionProps={menuListMotionProps}
onContextMenu={skipMouseEvent}
{...props}
/>
</Portal>

View File

@@ -23,6 +23,7 @@ export const InvTextarea = memo(
onPaste={stopPastePropagation}
onKeyUp={onKeyUpDown}
onKeyDown={onKeyUpDown}
minH={20}
{...rest}
/>
);

View File

@@ -1,5 +1,5 @@
import { Flex, Image, Spinner } from '@chakra-ui/react';
import InvokeLogoWhite from 'assets/images/invoke-key-wht-lrg.svg';
import InvokeLogoWhite from 'public/assets/images/invoke-symbol-wht-lrg.svg';
import { memo } from 'react';
// This component loads before the theme so we cannot use theme tokens here

View File

@@ -15,7 +15,7 @@ const $onCloseCallbacks = atom<CB[]>([]);
* This hook provides a way to close all menus by calling `onCloseGlobal()`. Menus that want to be closed
* in this way should register themselves by passing a callback to `useGlobalMenuCloseTrigger()`.
*/
export const useGlobalMenuCloseTrigger = (onClose?: CB) => {
export const useGlobalMenuClose = (onClose?: CB) => {
useEffect(() => {
if (!onClose) {
return;

View File

@@ -1,27 +1,33 @@
// https://stackoverflow.com/a/73731908
import { useCallback, useEffect, useState } from 'react';
export function useSingleAndDoubleClick(
handleSingleClick: () => void,
handleDoubleClick: () => void,
delay = 250
) {
export type UseSingleAndDoubleClickOptions = {
onSingleClick: () => void;
onDoubleClick: () => void;
latency?: number;
};
export function useSingleAndDoubleClick({
onSingleClick,
onDoubleClick,
latency = 250,
}: UseSingleAndDoubleClickOptions): () => void {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
if (click === 1) {
handleSingleClick();
onSingleClick();
}
setClick(0);
}, delay);
}, latency);
if (click === 2) {
handleDoubleClick();
onDoubleClick();
}
return () => clearTimeout(timer);
}, [click, handleSingleClick, handleDoubleClick, delay]);
}, [click, onDoubleClick, latency, onSingleClick]);
const onClick = useCallback(() => setClick((prev) => prev + 1), []);

View File

@@ -0,0 +1,7 @@
export const isInputElement = (el: HTMLElement) => {
return (
el.tagName.toLowerCase() === 'input' ||
el.tagName.toLowerCase() === 'textarea' ||
el.tagName.toLowerCase() === 'select'
);
};

View File

@@ -0,0 +1,8 @@
import type { MouseEvent } from 'react';
/**
* Prevents the default behavior of the event.
*/
export const skipMouseEvent = (e: MouseEvent) => {
e.preventDefault();
};

View File

@@ -10,20 +10,19 @@ import useCanvasMouseOut from 'features/canvas/hooks/useCanvasMouseOut';
import useCanvasMouseUp from 'features/canvas/hooks/useCanvasMouseUp';
import useCanvasWheel from 'features/canvas/hooks/useCanvasZoom';
import {
$canvasBaseLayer,
$canvasStage,
$isModifyingBoundingBox,
$isMouseOverBoundingBox,
$isMovingStage,
$isTransformingBoundingBox,
$tool,
} from 'features/canvas/store/canvasNanostore';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import {
canvasResized,
selectCanvasSlice,
} from 'features/canvas/store/canvasSlice';
import {
setCanvasBaseLayer,
setCanvasStage,
} from 'features/canvas/util/konvaInstanceProvider';
import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import type { Vector2d } from 'konva/lib/types';
@@ -61,7 +60,6 @@ const IAICanvas = () => {
);
const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
const tool = useAppSelector((s) => s.canvas.tool);
const shouldShowIntermediates = useAppSelector(
(s) => s.canvas.shouldShowIntermediates
);
@@ -78,10 +76,11 @@ const IAICanvas = () => {
const isMovingStage = useStore($isMovingStage);
const isTransformingBoundingBox = useStore($isTransformingBoundingBox);
const isMouseOverBoundingBox = useStore($isMouseOverBoundingBox);
const tool = useStore($tool);
useCanvasHotkeys();
const canvasStageRefCallback = useCallback((el: Konva.Stage) => {
setCanvasStage(el as Konva.Stage);
stageRef.current = el;
const canvasStageRefCallback = useCallback((stageElement: Konva.Stage) => {
$canvasStage.set(stageElement);
stageRef.current = stageElement;
}, []);
const stageCursor = useMemo(() => {
if (tool === 'move' || isStaging) {
@@ -104,10 +103,14 @@ const IAICanvas = () => {
shouldRestrictStrokesToBox,
tool,
]);
const canvasBaseLayerRefCallback = useCallback((el: Konva.Layer) => {
setCanvasBaseLayer(el as Konva.Layer);
canvasBaseLayerRef.current = el;
}, []);
const canvasBaseLayerRefCallback = useCallback(
(layerElement: Konva.Layer) => {
$canvasBaseLayer.set(layerElement);
canvasBaseLayerRef.current = layerElement;
},
[]
);
const lastCursorPositionRef = useRef<Vector2d>({ x: 0, y: 0 });

View File

@@ -5,6 +5,7 @@ import {
$cursorPosition,
$isMovingBoundingBox,
$isTransformingBoundingBox,
$tool,
} from 'features/canvas/store/canvasNanostore';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
@@ -89,7 +90,7 @@ const IAICanvasToolPreview = (props: GroupConfig) => {
const maskColorString = useAppSelector((s) =>
rgbaColorToString({ ...s.canvas.maskColor, a: 0.5 })
);
const tool = useAppSelector((s) => s.canvas.tool);
const tool = useStore($tool);
const layer = useAppSelector((s) => s.canvas.layer);
const dotRadius = useAppSelector((s) => 1.5 / s.canvas.stageScale);
const strokeWidth = useAppSelector((s) => 1.5 / s.canvas.stageScale);

View File

@@ -8,11 +8,11 @@ import {
} from 'common/util/roundDownToMultiple';
import {
$isDrawing,
$isMouseOverBoundingBox,
$isMouseOverBoundingBoxOutline,
$isMovingBoundingBox,
$isTransformingBoundingBox,
setIsMouseOverBoundingBox,
setIsMovingBoundingBox,
setIsTransformingBoundingBox,
$tool,
} from 'features/canvas/store/canvasNanostore';
import {
aspectRatioChanged,
@@ -30,7 +30,7 @@ import type Konva from 'konva';
import type { GroupConfig } from 'konva/lib/Group';
import type { KonvaEventObject } from 'konva/lib/Node';
import type { Vector2d } from 'konva/lib/types';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Group, Rect, Transformer } from 'react-konva';
@@ -49,18 +49,19 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid);
const tool = useAppSelector((s) => s.canvas.tool);
const hitStrokeWidth = useAppSelector((s) => 20 / s.canvas.stageScale);
const aspectRatio = useAppSelector((s) => s.canvas.aspectRatio);
const optimalDimension = useAppSelector(selectOptimalDimension);
const transformerRef = useRef<Konva.Transformer>(null);
const shapeRef = useRef<Konva.Rect>(null);
const shift = useStore($shift);
const tool = useStore($tool);
const isDrawing = useStore($isDrawing);
const isMovingBoundingBox = useStore($isMovingBoundingBox);
const isTransformingBoundingBox = useStore($isTransformingBoundingBox);
const [isMouseOverBoundingBoxOutline, setIsMouseOverBoundingBoxOutline] =
useState(false);
const isMouseOverBoundingBoxOutline = useStore(
$isMouseOverBoundingBoxOutline
);
useEffect(() => {
if (!transformerRef.current || !shapeRef.current) {
@@ -228,43 +229,43 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
);
const handleStartedTransforming = useCallback(() => {
setIsTransformingBoundingBox(true);
$isTransformingBoundingBox.set(true);
}, []);
const handleEndedTransforming = useCallback(() => {
setIsTransformingBoundingBox(false);
setIsMovingBoundingBox(false);
setIsMouseOverBoundingBox(false);
setIsMouseOverBoundingBoxOutline(false);
$isTransformingBoundingBox.set(false);
$isMovingBoundingBox.set(false);
$isMouseOverBoundingBox.set(false);
$isMouseOverBoundingBoxOutline.set(false);
}, []);
const handleStartedMoving = useCallback(() => {
setIsMovingBoundingBox(true);
$isMovingBoundingBox.set(true);
}, []);
const handleEndedModifying = useCallback(() => {
setIsTransformingBoundingBox(false);
setIsMovingBoundingBox(false);
setIsMouseOverBoundingBox(false);
setIsMouseOverBoundingBoxOutline(false);
$isTransformingBoundingBox.set(false);
$isMovingBoundingBox.set(false);
$isMouseOverBoundingBox.set(false);
$isMouseOverBoundingBoxOutline.set(false);
}, []);
const handleMouseOver = useCallback(() => {
setIsMouseOverBoundingBoxOutline(true);
$isMouseOverBoundingBoxOutline.set(true);
}, []);
const handleMouseOut = useCallback(() => {
!isTransformingBoundingBox &&
!isMovingBoundingBox &&
setIsMouseOverBoundingBoxOutline(false);
$isMouseOverBoundingBoxOutline.set(false);
}, [isMovingBoundingBox, isTransformingBoundingBox]);
const handleMouseEnterBoundingBox = useCallback(() => {
setIsMouseOverBoundingBox(true);
$isMouseOverBoundingBox.set(true);
}, []);
const handleMouseLeaveBoundingBox = useCallback(() => {
setIsMouseOverBoundingBox(false);
$isMouseOverBoundingBox.set(false);
}, []);
const stroke = useMemo(() => {

View File

@@ -1,4 +1,5 @@
import { Box, Flex } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
@@ -10,14 +11,16 @@ import {
InvPopoverTrigger,
} from 'common/components/InvPopover/wrapper';
import { InvSlider } from 'common/components/InvSlider/InvSlider';
import { resetToolInteractionState } from 'features/canvas/store/canvasNanostore';
import {
$tool,
resetToolInteractionState,
} from 'features/canvas/store/canvasNanostore';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import {
addEraseRect,
addFillRect,
setBrushColor,
setBrushSize,
setTool,
} from 'features/canvas/store/canvasSlice';
import { InvIconButton, InvPopover } from 'index';
import { clamp } from 'lodash-es';
@@ -34,9 +37,11 @@ import {
PiXBold,
} from 'react-icons/pi';
const marks = [1, 25, 50, 75, 100];
const IAICanvasToolChooserOptions = () => {
const dispatch = useAppDispatch();
const tool = useAppSelector((s) => s.canvas.tool);
const tool = useStore($tool);
const brushColor = useAppSelector((s) => s.canvas.brushColor);
const brushSize = useAppSelector((s) => s.canvas.brushSize);
const isStaging = useAppSelector(isStagingSelector);
@@ -163,17 +168,17 @@ const IAICanvasToolChooserOptions = () => {
);
const handleSelectBrushTool = useCallback(() => {
dispatch(setTool('brush'));
$tool.set('brush');
resetToolInteractionState();
}, [dispatch]);
}, []);
const handleSelectEraserTool = useCallback(() => {
dispatch(setTool('eraser'));
$tool.set('eraser');
resetToolInteractionState();
}, [dispatch]);
}, []);
const handleSelectColorPickerTool = useCallback(() => {
dispatch(setTool('colorPicker'));
$tool.set('colorPicker');
resetToolInteractionState();
}, [dispatch]);
}, []);
const handleFillRect = useCallback(() => {
dispatch(addFillRect());
}, [dispatch]);
@@ -281,5 +286,3 @@ const IAICanvasToolChooserOptions = () => {
};
export default memo(IAICanvasToolChooserOptions);
const marks = [1, 25, 50, 75, 100];

View File

@@ -1,4 +1,5 @@
import { Flex } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
import { InvControl } from 'common/components/InvControl/InvControl';
@@ -14,17 +15,16 @@ import {
canvasMerged,
canvasSavedToGallery,
} from 'features/canvas/store/actions';
import { $canvasBaseLayer, $tool } from 'features/canvas/store/canvasNanostore';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import {
resetCanvas,
resetCanvasView,
setIsMaskEnabled,
setLayer,
setTool,
} from 'features/canvas/store/canvasSlice';
import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import { InvIconButton } from 'index';
import { memo, useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
@@ -50,9 +50,8 @@ const IAICanvasToolbar = () => {
const dispatch = useAppDispatch();
const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
const layer = useAppSelector((s) => s.canvas.layer);
const tool = useAppSelector((s) => s.canvas.tool);
const tool = useStore($tool);
const isStaging = useAppSelector(isStagingSelector);
const canvasBaseLayer = getCanvasBaseLayer();
const { t } = useTranslation();
const { isClipboardAPIAvailable } = useCopyImageToClipboard();
@@ -81,7 +80,7 @@ const IAICanvasToolbar = () => {
enabled: () => true,
preventDefault: true,
},
[canvasBaseLayer]
[]
);
useHotkeys(
@@ -93,7 +92,7 @@ const IAICanvasToolbar = () => {
enabled: () => !isStaging,
preventDefault: true,
},
[canvasBaseLayer]
[]
);
useHotkeys(
@@ -105,7 +104,7 @@ const IAICanvasToolbar = () => {
enabled: () => !isStaging,
preventDefault: true,
},
[canvasBaseLayer]
[]
);
useHotkeys(
@@ -117,7 +116,7 @@ const IAICanvasToolbar = () => {
enabled: () => !isStaging && isClipboardAPIAvailable,
preventDefault: true,
},
[canvasBaseLayer, isClipboardAPIAvailable]
[isClipboardAPIAvailable]
);
useHotkeys(
@@ -129,33 +128,42 @@ const IAICanvasToolbar = () => {
enabled: () => !isStaging,
preventDefault: true,
},
[canvasBaseLayer]
[]
);
const handleSelectMoveTool = useCallback(() => {
dispatch(setTool('move'));
}, [dispatch]);
$tool.set('move');
}, []);
const handleClickResetCanvasView = useSingleAndDoubleClick(
() => handleResetCanvasView(false),
() => handleResetCanvasView(true)
const handleResetCanvasView = useCallback(
(shouldScaleTo1 = false) => {
const canvasBaseLayer = $canvasBaseLayer.get();
if (!canvasBaseLayer) {
return;
}
const clientRect = canvasBaseLayer.getClientRect({
skipTransform: true,
});
dispatch(
resetCanvasView({
contentRect: clientRect,
shouldScaleTo1,
})
);
},
[dispatch]
);
const onSingleClick = useCallback(() => {
handleResetCanvasView(false);
}, [handleResetCanvasView]);
const onDoubleClick = useCallback(() => {
handleResetCanvasView(true);
}, [handleResetCanvasView]);
const handleResetCanvasView = (shouldScaleTo1 = false) => {
const canvasBaseLayer = getCanvasBaseLayer();
if (!canvasBaseLayer) {
return;
}
const clientRect = canvasBaseLayer.getClientRect({
skipTransform: true,
});
dispatch(
resetCanvasView({
contentRect: clientRect,
shouldScaleTo1,
})
);
};
const handleClickResetCanvasView = useSingleAndDoubleClick({
onSingleClick,
onDoubleClick,
});
const handleResetCanvas = useCallback(() => {
dispatch(resetCanvas());

View File

@@ -1,8 +1,8 @@
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
$isMovingBoundingBox,
setIsMovingStage,
$isMovingStage,
$tool,
} from 'features/canvas/store/canvasNanostore';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { setStageCoordinates } from 'features/canvas/store/canvasSlice';
@@ -12,18 +12,19 @@ import { useCallback } from 'react';
const useCanvasDrag = () => {
const dispatch = useAppDispatch();
const isStaging = useAppSelector(isStagingSelector);
const tool = useAppSelector((s) => s.canvas.tool);
const isMovingBoundingBox = useStore($isMovingBoundingBox);
const handleDragStart = useCallback(() => {
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
if (
!(($tool.get() === 'move' || isStaging) && !$isMovingBoundingBox.get())
) {
return;
}
setIsMovingStage(true);
}, [isMovingBoundingBox, isStaging, tool]);
$isMovingStage.set(true);
}, [isStaging]);
const handleDragMove = useCallback(
(e: KonvaEventObject<MouseEvent>) => {
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
const tool = $tool.get();
if (!((tool === 'move' || isStaging) && !$isMovingBoundingBox.get())) {
return;
}
@@ -31,15 +32,17 @@ const useCanvasDrag = () => {
dispatch(setStageCoordinates(newCoordinates));
},
[dispatch, isMovingBoundingBox, isStaging, tool]
[dispatch, isStaging]
);
const handleDragEnd = useCallback(() => {
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {
if (
!(($tool.get() === 'move' || isStaging) && !$isMovingBoundingBox.get())
) {
return;
}
setIsMovingStage(false);
}, [isMovingBoundingBox, isStaging, tool]);
$isMovingStage.set(false);
}, [isStaging]);
return {
handleDragStart,

View File

@@ -1,5 +1,9 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isInputElement } from 'common/util/isInputElement';
import {
$canvasStage,
$tool,
$toolStash,
resetCanvasInteractionState,
resetToolInteractionState,
} from 'features/canvas/store/canvasNanostore';
@@ -9,12 +13,10 @@ import {
setIsMaskEnabled,
setShouldShowBoundingBox,
setShouldSnapToGrid,
setTool,
} from 'features/canvas/store/canvasSlice';
import type { CanvasTool } from 'features/canvas/store/canvasTypes';
import { getCanvasStage } from 'features/canvas/util/konvaInstanceProvider';
import { isElChildOfCanvasTab } from 'features/canvas/util/isElChildOfCanvasTab';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback, useRef } from 'react';
import { useCallback, useEffect } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
const useInpaintingCanvasHotkeys = () => {
@@ -23,12 +25,9 @@ const useInpaintingCanvasHotkeys = () => {
const shouldShowBoundingBox = useAppSelector(
(s) => s.canvas.shouldShowBoundingBox
);
const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid);
const previousToolRef = useRef<CanvasTool | null>(null);
const canvasStage = getCanvasStage();
// Beta Keys
const handleClearMask = useCallback(() => dispatch(clearMask()), [dispatch]);
@@ -96,37 +95,49 @@ const useInpaintingCanvasHotkeys = () => {
[activeTabName, shouldShowBoundingBox]
);
useHotkeys(
['space'],
(e: KeyboardEvent) => {
if (e.repeat) {
return;
}
const onKeyDown = useCallback((e: KeyboardEvent) => {
if (
e.repeat ||
e.key !== ' ' ||
isInputElement(e.target as HTMLElement) ||
!isElChildOfCanvasTab(e.target as HTMLElement)
) {
return;
}
if ($toolStash.get() || $tool.get() === 'move') {
return;
}
$canvasStage.get()?.container().focus();
$toolStash.set($tool.get());
$tool.set('move');
resetToolInteractionState();
}, []);
const onKeyUp = useCallback((e: KeyboardEvent) => {
if (
e.repeat ||
e.key !== ' ' ||
isInputElement(e.target as HTMLElement) ||
!isElChildOfCanvasTab(e.target as HTMLElement)
) {
return;
}
if (!$toolStash.get() || $tool.get() !== 'move') {
return;
}
$canvasStage.get()?.container().focus();
$tool.set($toolStash.get() ?? 'move');
$toolStash.set(null);
}, []);
canvasStage?.container().focus();
useEffect(() => {
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
if (tool !== 'move') {
previousToolRef.current = tool;
dispatch(setTool('move'));
resetToolInteractionState();
}
if (
tool === 'move' &&
previousToolRef.current &&
previousToolRef.current !== 'move'
) {
dispatch(setTool(previousToolRef.current));
previousToolRef.current = 'move';
}
},
{
keyup: true,
keydown: true,
preventDefault: true,
},
[tool, previousToolRef]
);
return () => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
};
}, [onKeyDown, onKeyUp]);
};
export default useInpaintingCanvasHotkeys;

View File

@@ -1,7 +1,8 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
setIsDrawing,
setIsMovingStage,
$isDrawing,
$isMovingStage,
$tool,
} from 'features/canvas/store/canvasNanostore';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { addLine } from 'features/canvas/store/canvasSlice';
@@ -15,7 +16,6 @@ import useColorPicker from './useColorUnderCursor';
const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
const dispatch = useAppDispatch();
const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
const { commitColorUnderCursor } = useColorPicker();
@@ -26,9 +26,10 @@ const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
}
stageRef.current.container().focus();
const tool = $tool.get();
if (tool === 'move' || isStaging) {
setIsMovingStage(true);
$isMovingStage.set(true);
return;
}
@@ -45,12 +46,17 @@ const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
e.evt.preventDefault();
setIsDrawing(true);
$isDrawing.set(true);
// Add a new line starting from the current cursor position.
dispatch(addLine([scaledCursorPosition.x, scaledCursorPosition.y]));
dispatch(
addLine({
points: [scaledCursorPosition.x, scaledCursorPosition.y],
tool,
})
);
},
[stageRef, tool, isStaging, dispatch, commitColorUnderCursor]
[stageRef, isStaging, dispatch, commitColorUnderCursor]
);
};

View File

@@ -1,8 +1,8 @@
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
$cursorPosition,
$isDrawing,
setCursorPosition,
$tool,
} from 'features/canvas/store/canvasNanostore';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice';
@@ -20,8 +20,6 @@ const useCanvasMouseMove = (
lastCursorPositionRef: MutableRefObject<Vector2d>
) => {
const dispatch = useAppDispatch();
const isDrawing = useStore($isDrawing);
const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
const { updateColorUnderCursor } = useColorPicker();
@@ -36,16 +34,17 @@ const useCanvasMouseMove = (
return;
}
setCursorPosition(scaledCursorPosition);
$cursorPosition.set(scaledCursorPosition);
lastCursorPositionRef.current = scaledCursorPosition;
const tool = $tool.get();
if (tool === 'colorPicker') {
updateColorUnderCursor();
return;
}
if (!isDrawing || tool === 'move' || isStaging) {
if (!$isDrawing.get() || tool === 'move' || isStaging) {
return;
}
@@ -56,11 +55,9 @@ const useCanvasMouseMove = (
}, [
didMouseMoveRef,
dispatch,
isDrawing,
isStaging,
lastCursorPositionRef,
stageRef,
tool,
updateColorUnderCursor,
]);
};

View File

@@ -2,8 +2,8 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
$isDrawing,
setIsDrawing,
setIsMovingStage,
$isMovingStage,
$tool,
} from 'features/canvas/store/canvasNanostore';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice';
@@ -18,12 +18,11 @@ const useCanvasMouseUp = (
) => {
const dispatch = useAppDispatch();
const isDrawing = useStore($isDrawing);
const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
return useCallback(() => {
if (tool === 'move' || isStaging) {
setIsMovingStage(false);
if ($tool.get() === 'move' || isStaging) {
$isMovingStage.set(false);
return;
}
@@ -46,8 +45,8 @@ const useCanvasMouseUp = (
} else {
didMouseMoveRef.current = false;
}
setIsDrawing(false);
}, [didMouseMoveRef, dispatch, isDrawing, isStaging, stageRef, tool]);
$isDrawing.set(false);
}, [didMouseMoveRef, dispatch, isDrawing, isStaging, stageRef]);
};
export default useCanvasMouseUp;

View File

@@ -1,21 +1,22 @@
import { useAppDispatch } from 'app/store/storeHooks';
import {
$canvasBaseLayer,
$canvasStage,
$tool,
} from 'features/canvas/store/canvasNanostore';
import {
commitColorPickerColor,
setColorPickerColor,
} from 'features/canvas/store/canvasSlice';
import {
getCanvasBaseLayer,
getCanvasStage,
} from 'features/canvas/util/konvaInstanceProvider';
import Konva from 'konva';
import { useCallback } from 'react';
const useColorPicker = () => {
const dispatch = useAppDispatch();
const canvasBaseLayer = getCanvasBaseLayer();
const stage = getCanvasStage();
const updateColorUnderCursor = useCallback(() => {
const stage = $canvasStage.get();
const canvasBaseLayer = $canvasBaseLayer.get();
if (!stage || !canvasBaseLayer) {
return;
}
@@ -47,10 +48,11 @@ const useColorPicker = () => {
}
dispatch(setColorPickerColor({ r, g, b, a }));
}, [canvasBaseLayer, dispatch, stage]);
}, [dispatch]);
const commitColorUnderCursor = useCallback(() => {
dispatch(commitColorPickerColor());
$tool.set('brush');
}, [dispatch]);
return { updateColorUnderCursor, commitColorUnderCursor };

View File

@@ -1,7 +1,11 @@
import type { CanvasTool } from 'features/canvas/store/canvasTypes';
import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types';
import { atom, computed } from 'nanostores';
export const $cursorPosition = atom<Vector2d | null>(null);
export const $tool = atom<CanvasTool>('move');
export const $toolStash = atom<CanvasTool | null>(null);
export const $isDrawing = atom<boolean>(false);
export const $isMouseOverBoundingBox = atom<boolean>(false);
export const $isMoveBoundingBoxKeyHeld = atom<boolean>(false);
@@ -9,6 +13,7 @@ export const $isMoveStageKeyHeld = atom<boolean>(false);
export const $isMovingBoundingBox = atom<boolean>(false);
export const $isMovingStage = atom<boolean>(false);
export const $isTransformingBoundingBox = atom<boolean>(false);
export const $isMouseOverBoundingBoxOutline = atom<boolean>(false);
export const $isModifyingBoundingBox = computed(
[$isTransformingBoundingBox, $isMovingBoundingBox],
(isTransformingBoundingBox, isMovingBoundingBox) =>
@@ -25,49 +30,15 @@ export const resetCanvasInteractionState = () => {
$isMovingStage.set(false);
};
export const setCursorPosition = (cursorPosition: Vector2d | null) => {
$cursorPosition.set(cursorPosition);
};
export const setIsDrawing = (isDrawing: boolean) => {
$isDrawing.set(isDrawing);
};
export const setIsMouseOverBoundingBox = (isMouseOverBoundingBox: boolean) => {
$isMouseOverBoundingBox.set(isMouseOverBoundingBox);
};
export const setIsMoveBoundingBoxKeyHeld = (
isMoveBoundingBoxKeyHeld: boolean
) => {
$isMoveBoundingBoxKeyHeld.set(isMoveBoundingBoxKeyHeld);
};
export const setIsMoveStageKeyHeld = (isMoveStageKeyHeld: boolean) => {
$isMoveStageKeyHeld.set(isMoveStageKeyHeld);
};
export const setIsMovingBoundingBox = (isMovingBoundingBox: boolean) => {
$isMovingBoundingBox.set(isMovingBoundingBox);
};
export const setIsMovingStage = (isMovingStage: boolean) => {
$isMovingStage.set(isMovingStage);
};
export const setIsTransformingBoundingBox = (
isTransformingBoundingBox: boolean
) => {
$isTransformingBoundingBox.set(isTransformingBoundingBox);
};
export const resetToolInteractionState = () => {
setIsTransformingBoundingBox(false);
setIsMouseOverBoundingBox(false);
setIsMovingBoundingBox(false);
setIsMovingStage(false);
$isTransformingBoundingBox.set(false);
$isMouseOverBoundingBox.set(false);
$isMovingBoundingBox.set(false);
$isMovingStage.set(false);
};
export const setCanvasInteractionStateMouseOut = () => {
setCursorPosition(null);
$cursorPosition.set(null);
};
export const $canvasBaseLayer = atom<Konva.Layer | null>(null);
export const $canvasStage = atom<Konva.Stage | null>(null);

View File

@@ -10,7 +10,6 @@ import calculateScale from 'features/canvas/util/calculateScale';
import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants';
import floorCoordinates from 'features/canvas/util/floorCoordinates';
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
import roundDimensionsToMultiple from 'features/canvas/util/roundDimensionsToMultiple';
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import { modelChanged } from 'features/parameters/store/generationSlice';
@@ -86,7 +85,6 @@ export const initialCanvasState: CanvasState = {
stageCoordinates: { x: 0, y: 0 },
stageDimensions: { width: 0, height: 0 },
stageScale: 1,
tool: 'brush',
batchIds: [],
aspectRatio: {
id: '1:1',
@@ -101,13 +99,10 @@ const setBoundingBoxDimensionsReducer = (
optimalDimension: number
) => {
const boundingBoxDimensions = payload;
const newDimensions = roundDimensionsToMultiple(
{
...state.boundingBoxDimensions,
...boundingBoxDimensions,
},
CANVAS_GRID_SIZE_FINE
);
const newDimensions = {
...state.boundingBoxDimensions,
...boundingBoxDimensions,
};
state.boundingBoxDimensions = newDimensions;
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
@@ -122,18 +117,9 @@ export const canvasSlice = createSlice({
name: 'canvas',
initialState: initialCanvasState,
reducers: {
setTool: (state, action: PayloadAction<CanvasTool>) => {
state.tool = action.payload;
},
setLayer: (state, action: PayloadAction<CanvasLayer>) => {
state.layer = action.payload;
},
toggleTool: (state) => {
const currentTool = state.tool;
if (currentTool !== 'move') {
state.tool = currentTool === 'brush' ? 'eraser' : 'brush';
}
},
setMaskColor: (state, action: PayloadAction<RgbaColor>) => {
state.maskColor = action.payload;
},
@@ -379,9 +365,13 @@ export const canvasSlice = createSlice({
state.futureLayerStates = [];
},
addLine: (state, action: PayloadAction<number[]>) => {
const { tool, layer, brushColor, brushSize, shouldRestrictStrokesToBox } =
addLine: (
state,
action: PayloadAction<{ points: number[]; tool: CanvasTool }>
) => {
const { layer, brushColor, brushSize, shouldRestrictStrokesToBox } =
state;
const { points, tool } = action.payload;
if (tool === 'move' || tool === 'colorPicker') {
return;
@@ -404,7 +394,7 @@ export const canvasSlice = createSlice({
layer,
tool,
strokeWidth: newStrokeWidth,
points: action.payload,
points,
...newColor,
};
@@ -472,10 +462,31 @@ export const canvasSlice = createSlice({
},
resetCanvas: (state) => {
state.pastLayerStates.push(cloneDeep(state.layerState));
state.layerState = cloneDeep(initialLayerState);
state.futureLayerStates = [];
state.batchIds = [];
state.boundingBoxCoordinates = {
...initialCanvasState.boundingBoxCoordinates,
};
state.boundingBoxDimensions = {
...initialCanvasState.boundingBoxDimensions,
};
state.stageScale = calculateScale(
state.stageDimensions.width,
state.stageDimensions.height,
state.boundingBoxDimensions.width,
state.boundingBoxDimensions.height,
STAGE_PADDING_PERCENTAGE
);
state.stageCoordinates = calculateCoordinates(
state.stageDimensions.width,
state.stageDimensions.height,
0,
0,
state.boundingBoxDimensions.width,
state.boundingBoxDimensions.height,
1
);
},
canvasResized: (
state,
@@ -498,32 +509,28 @@ export const canvasSlice = createSlice({
stageDimensions: { width: stageWidth, height: stageHeight },
} = state;
const { x, y, width, height } = contentRect;
const newScale = shouldScaleTo1
? 1
: calculateScale(
stageWidth,
stageHeight,
contentRect.width || state.boundingBoxDimensions.width,
contentRect.height || state.boundingBoxDimensions.height,
STAGE_PADDING_PERCENTAGE
);
if (width !== 0 && height !== 0) {
const newScale = shouldScaleTo1
? 1
: calculateScale(
stageWidth,
stageHeight,
width,
height,
STAGE_PADDING_PERCENTAGE
);
const newCoordinates = calculateCoordinates(
stageWidth,
stageHeight,
contentRect.x || state.boundingBoxCoordinates.x,
contentRect.y || state.boundingBoxCoordinates.y,
contentRect.width || state.boundingBoxDimensions.width,
contentRect.height || state.boundingBoxDimensions.height,
newScale
);
const newCoordinates = calculateCoordinates(
stageWidth,
stageHeight,
x,
y,
width,
height,
newScale
);
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
}
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
},
nextStagingAreaImage: (state) => {
if (!state.layerState.stagingArea.images.length) {
@@ -667,7 +674,6 @@ export const canvasSlice = createSlice({
...state.colorPickerColor,
a: state.brushColor.a,
};
state.tool = 'brush';
},
setMergedCanvas: (state, action: PayloadAction<CanvasImage>) => {
state.pastLayerStates.push(cloneDeep(state.layerState));
@@ -682,6 +688,12 @@ export const canvasSlice = createSlice({
},
extraReducers: (builder) => {
builder.addCase(modelChanged, (state, action) => {
if (
action.meta.previousModel?.base_model === action.payload?.base_model
) {
// The base model hasn't changed, we don't need to optimize the size
return;
}
const optimalDimension = getOptimalDimension(action.payload);
const { width, height } = state.boundingBoxDimensions;
if (getIsSizeOptimal(width, height, optimalDimension)) {
@@ -768,9 +780,7 @@ export const {
setShouldSnapToGrid,
setStageCoordinates,
setStageScale,
setTool,
toggleShouldLockBoundingBox,
toggleTool,
undo,
setScaledBoundingBoxDimensions,
setShouldRestrictStrokesToBox,

View File

@@ -149,7 +149,6 @@ export interface CanvasState {
stageCoordinates: Vector2d;
stageDimensions: Dimensions;
stageScale: number;
tool: CanvasTool;
generationMode?: GenerationMode;
batchIds: string[];
aspectRatio: AspectRatioState;

View File

@@ -1,2 +1,3 @@
export const CANVAS_GRID_SIZE_COARSE = 64;
export const CANVAS_GRID_SIZE_FINE = 8;
export const CANVAS_TAB_TESTID = 'unified-canvas-tab';

View File

@@ -1,6 +1,6 @@
import type { RootState } from 'app/store/store';
import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore';
import { getCanvasBaseLayer } from './konvaInstanceProvider';
import { konvaNodeToBlob } from './konvaNodeToBlob';
/**
@@ -10,7 +10,7 @@ export const getBaseLayerBlob = async (
state: RootState,
alwaysUseBoundingBox: boolean = false
) => {
const canvasBaseLayer = getCanvasBaseLayer();
const canvasBaseLayer = $canvasBaseLayer.get();
if (!canvasBaseLayer) {
throw new Error('Problem getting base layer blob');

View File

@@ -1,15 +1,18 @@
import { logger } from 'app/logging/logger';
import {
$canvasBaseLayer,
$canvasStage,
} from 'features/canvas/store/canvasNanostore';
import type {
CanvasLayerState,
Dimensions,
} from 'features/canvas/store/canvasTypes';
import { isCanvasMaskLine } from 'features/canvas/store/canvasTypes';
import { konvaNodeToImageData } from 'features/canvas/util/konvaNodeToImageData';
import type { Vector2d } from 'konva/lib/types';
import createMaskStage from './createMaskStage';
import { getCanvasBaseLayer, getCanvasStage } from './konvaInstanceProvider';
import { konvaNodeToBlob } from './konvaNodeToBlob';
import { konvaNodeToImageData } from './konvaNodeToImageData';
/**
* Gets Blob and ImageData objects for the base and mask layers
@@ -23,8 +26,8 @@ export const getCanvasData = async (
) => {
const log = logger('canvas');
const canvasBaseLayer = getCanvasBaseLayer();
const canvasStage = getCanvasStage();
const canvasBaseLayer = $canvasBaseLayer.get();
const canvasStage = $canvasStage.get();
if (!canvasBaseLayer || !canvasStage) {
log.error('Unable to find canvas / stage');

View File

@@ -1,11 +1,12 @@
import { getCanvasBaseLayer } from './konvaInstanceProvider';
import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore';
import { konvaNodeToBlob } from './konvaNodeToBlob';
/**
* Gets the canvas base layer blob, without bounding box
*/
export const getFullBaseLayerBlob = async () => {
const canvasBaseLayer = getCanvasBaseLayer();
const canvasBaseLayer = $canvasBaseLayer.get();
if (!canvasBaseLayer) {
return;

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