Compare commits

...

73 Commits

Author SHA1 Message Date
psychedelicious
8cf94d602f chore: bump version to v5.3.1 2024-11-01 13:31:51 +11:00
psychedelicious
016a6f182f Make T2I Adapters work with any resolution supported by the models (#7215)
## Summary

This change mimics the unet padding strategy to align T2I featuremaps
with the latents during denoising. It also slightly adjusts the crop and
scale logic so that the control will match the input image without
shifting when it needs to pad.

## Related Issues / Discussions

<!--WHEN APPLICABLE: List any related issues or discussions on github or
discord. If this PR closes an issue, please use the "Closes #1234"
format, so that the issue will be automatically closed when the PR
merges.-->

## QA Instructions

Image generated at 1032x1024

![image](https://github.com/user-attachments/assets/7ea579e4-61dc-4b6b-aa84-33d676d160c6)

Image generated at 1080x1040 to prove feature alignment.

![image](https://github.com/user-attachments/assets/ee6e5b6a-d0d5-474d-9fc4-f65c104964bd)

Edge artifacts on the bottom and right are a result of SDXL's unet
padding, and t2i influence will be cut off in those regions.

## Merge Plan

Contingent on #7205 
Currently the Canvas UI prevents users from generating non-64
resolutions while t2i adapter layers are active. Will leave this as a
draft until fixing that.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Documentation added / updated (if applicable)_
2024-11-01 13:22:00 +11:00
Kent Keirsey
6fbc019142 Merge branch 'main' into t2i_resolution_hack 2024-10-31 22:08:38 -04:00
psychedelicious
26f95d6a97 fix(ui): disable move tool when staging 2024-10-31 22:08:16 -04:00
psychedelicious
40f7b0d171 fix(ui): cursor disappearing on empty layers 2024-10-31 22:08:16 -04:00
psychedelicious
4904700751 feat(ui): more info in state module repr 2024-10-31 22:08:16 -04:00
psychedelicious
83538c4b2b fix(ui): flash of canvas state between last progress image and generation result 2024-10-31 22:08:16 -04:00
psychedelicious
eb7b559529 fix(ui): sync canvas layer visibility when staging state changes 2024-10-31 22:08:16 -04:00
Kent Keirsey
4945465cf0 Merge branch 'main' into t2i_resolution_hack 2024-10-31 21:17:06 -04:00
Will
7eed7282a9 removing periods from update link to prevent page not found error 2024-11-01 07:42:31 +11:00
psychedelicious
47f0781822 fix(ui): add missing translations
Closes #7229
2024-11-01 07:40:52 +11:00
Eugene Brodsky
88b8e3e3d5 chore(deps): adjust pins for torch, numpy, other dependencies, to satisfy stricted dependency resolution 2024-10-31 16:26:53 -04:00
dunkeroni
47c3ab9214 Remove UI restrictions for T2I resolutions 2024-10-31 16:07:46 -04:00
dunkeroni
d6d436b59c Merge branch 'invoke-ai:main' into t2i_resolution_hack 2024-10-31 15:52:24 -04:00
Hippalectryon
6ff7057967 fix broken link in installer 2024-10-31 09:50:08 -04:00
psychedelicious
e032ab1179 fix(ui): ensure compositing rect is rendered correctly
This fixes an issue uncovered by the previous commit in which we do not exit filter/select-object on save-as.
2024-10-31 08:57:10 -04:00
psychedelicious
65bddfcd93 feat(ui): filter/select-object do not exit on save-as 2024-10-31 08:57:10 -04:00
aidawanglion
2d3ce418dd translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 73.7% (1160 of 1573 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
2024-10-31 17:18:35 +11:00
Hosted Weblate
548d72f7b9 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
2024-10-31 17:18:35 +11:00
aidawanglion
19837a0f29 translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 73.3% (1146 of 1563 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
2024-10-31 17:18:35 +11:00
aidawanglion
483b65a1dc translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 69.4% (1086 of 1563 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
2024-10-31 17:18:35 +11:00
Riccardo Giovanetti
b85931c7ab translationBot(ui): update translation (Italian)
Currently translated at 99.4% (1554 of 1563 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
2024-10-31 17:18:35 +11:00
Hosted Weblate
9225f47338 translationBot(ui): update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
2024-10-31 17:18:35 +11:00
Riccardo Giovanetti
bccac5e4a6 translationBot(ui): update translation (Italian)
Currently translated at 99.4% (1553 of 1562 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
2024-10-31 17:18:35 +11:00
Hosted Weblate
7cb07fdc04 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
2024-10-31 17:18:35 +11:00
dakota2472
b137450026 translationBot(ui): update translation (Italian)
Currently translated at 100.0% (1562 of 1562 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
2024-10-31 17:18:35 +11:00
Hosted Weblate
dc5090469a translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
2024-10-31 17:18:35 +11:00
Thomas Bolteau
e0ae2ace89 translationBot(ui): update translation (French)
Currently translated at 100.0% (1561 of 1561 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fr/
2024-10-31 17:18:35 +11:00
Riku
269faae04b translationBot(ui): update translation (German)
Currently translated at 71.4% (1115 of 1561 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
2024-10-31 17:18:35 +11:00
Riccardo Giovanetti
e282acd41c translationBot(ui): update translation (Italian)
Currently translated at 98.8% (1543 of 1561 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
2024-10-31 17:18:35 +11:00
Ettore Atalan
a266668348 translationBot(ui): update translation (German)
Currently translated at 69.3% (1083 of 1561 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
2024-10-31 17:18:35 +11:00
Riccardo Giovanetti
3bb3e142fc translationBot(ui): update translation (Italian)
Currently translated at 98.8% (1543 of 1561 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
2024-10-31 17:18:35 +11:00
Hosted Weblate
6ac6d70a22 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-10-31 17:18:35 +11:00
Riccardo Giovanetti
b0acf33ba5 translationBot(ui): update translation (Italian)
Currently translated at 98.5% (1496 of 1518 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-10-31 17:18:35 +11:00
qyouqme
b3eb64b64c translationBot(ui): update translation (Chinese (Simplified Han script))
Currently translated at 66.0% (1003 of 1518 strings)

Co-authored-by: qyouqme <camtasiacn@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
Translation: InvokeAI/Web UI
2024-10-31 17:18:35 +11:00
Riku
95f8ab1a29 translationBot(ui): update translation (German)
Currently translated at 71.3% (1083 of 1518 strings)

Co-authored-by: Riku <riku.block@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
Translation: InvokeAI/Web UI
2024-10-31 17:18:35 +11:00
Hosted Weblate
4e043384db translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-10-31 17:18:35 +11:00
psychedelicious
0f5df8ba17 chore(ui): lint 2024-10-31 16:54:31 +11:00
psychedelicious
2826ab48a2 refactor(ui): layer interaction locking
Previously we maintained an `isInteractable` flag, which was derived from these layer flags:
- Locked/unlocked
- Enabled/disabled
- Layer's type visible/hidden

When a layer was not interactable, we blocked all layer actions.

After comparing to the behaviour in Affinity and considering user feedback, I've loosened these restrictions while maintaining safety. First, some definitions.

There two kinds of layer actions - mutating actions and non-mutating actions.
- Mutating actions are drawing on the layer, cropping it, filtering it, converting it, etc. Anything that changes the layer.
- Non-mutating actions are copying the layer, saving the layer to gallery, etc. Anything that _uses_ the layer.

Then, there are two broad canvas states - busy and not busy. "Busy" means the canvas is actively filtering, staging, compositing layers together, etc - something that is "single-threaded" by nature.

And here are the revised restrictions:
- When canvas is busy, you cannot initiate any layer actions.
- When the canvas is not busy, and the layer is locked, you initiate any mutating actions.
- When the canvas is not busy and the layer is not locked, you can initiate any layer action.

Besides safely giving users more freedom, it also fixes an issue where the context menu for a layer was disabled if it was not the selected layer.
2024-10-31 16:54:31 +11:00
psychedelicious
7ff1b635c8 docs: clarify comments for invoke method return annotation validation 2024-10-31 16:21:07 +11:00
psychedelicious
dfb5e8b5e5 tests: add invoke method & output annotation tests 2024-10-31 16:21:07 +11:00
psychedelicious
7259da799c feat(nodes): attempt to look up invoke return types by name 2024-10-31 16:21:07 +11:00
psychedelicious
965069fce1 tests: fix nodes tests
they now require a valid output
2024-10-31 16:21:07 +11:00
psychedelicious
90232806d9 feat(nodes): add validation for invoke method return types 2024-10-31 16:21:07 +11:00
Hippalectryon
81bc153399 Fix link in dev docs 2024-10-31 16:06:44 +11:00
Jonathan
c63e526f99 Update FAQ.md
Fixed typo
2024-10-31 16:04:23 +11:00
nirmal0001
2b74263007 Update patchmatch.md
add required Install dependencies for arch linux
2024-10-31 16:01:57 +11:00
psychedelicious
d3a82f7119 feat(ui): do not show hftoken error until user attempts to set it 2024-10-31 15:47:14 +11:00
Mary Hipp
291c5a0341 lint 2024-10-31 15:47:14 +11:00
Mary Hipp
bcb41399ca feat(ui,api): support for HF tokens in UI, handle Unauthorized and Forbidden errors 2024-10-31 15:47:14 +11:00
psychedelicious
6f0f53849b tests: reset config changes in test_deny_nodes when finished testing 2024-10-31 15:22:14 +11:00
psychedelicious
4e7d63761a fix(nodes): nodes denylist handling
- Add method to force a rebuild of the pydantic type adapter for the union of invocations, which is used to validate graphs.
- Update the xfail'd test.
2024-10-31 15:22:14 +11:00
psychedelicious
198c84105d fix(ui): compositor not setting processing flag when cleaning up 2024-10-30 16:27:36 +11:00
psychedelicious
2453b9f443 chore: bump version to v5.3.0rc1 2024-10-30 13:11:41 +11:00
psychedelicious
b091aca986 chore(ui): lint 2024-10-30 11:05:46 +11:00
psychedelicious
8f02ce54a0 perf(ui): cache image data & transparency mode during generation mode calculation
Perf boost and reduces the number of images we create on the backend.
2024-10-30 11:05:46 +11:00
psychedelicious
f4b7c63002 feat(ui): omit non-render-impacting keys when hashing entities
Had missed several of these, which means we were invalidating caches far too often. For example, when you changed a RG prompt, we were invalidating the cached canvas for that entity, even though changing the prompt doesn't affect the canvas at all.
2024-10-30 11:05:46 +11:00
psychedelicious
a4629280b5 feat(ui): use typeguard instead of string comparison 2024-10-30 11:05:46 +11:00
psychedelicious
855fb007da tidy(ui): minor type fix 2024-10-30 11:05:46 +11:00
psychedelicious
d805b52c1f feat(ui): merge down deletes merged entities 2024-10-30 11:05:46 +11:00
psychedelicious
2ea55685bb feat(ui): add save to assets for inpaint & rg 2024-10-30 11:05:46 +11:00
psychedelicious
bd6ff3deaa feat(ui): add merge down for all entity types 2024-10-30 11:05:46 +11:00
psychedelicious
82dd53ec88 tidy(ui): clean up merge visible logic 2024-10-30 11:05:46 +11:00
psychedelicious
71d749541d feat(ui): control layers supports merge visible
The "lighter" GlobalCompositeOperation is used. This seems to be the best one when merging control layers, as it retains edge maps.
2024-10-30 11:05:46 +11:00
psychedelicious
48a57fc4b9 feat(ui): support globalCompositeOperation when compositing canvas 2024-10-30 11:05:46 +11:00
psychedelicious
530e0910fc feat(ui): regional guidance supports merge visible 2024-10-30 11:05:46 +11:00
psychedelicious
2fdf8fc0a2 feat(ui): merge visible creates new layer
Previously, merge visible deleted all other visible layers. This is not how affinity works, I should have confirmed before making it work like this in the first place.Ï
2024-10-30 11:05:46 +11:00
psychedelicious
91db9c9300 refactor(ui): generalize compositor methods
`CanvasCompositorModule` had a fairly inflexible API, only supporting compositing all raster layers or inpaint masks.

The API has been generalized work with a list of canvas entities. This enables `Merge Down` and `Merge Selected` functionality (though `Merge Selected` is not part of this set of changes).
2024-10-30 11:05:46 +11:00
dunkeroni
34569a2410 Make T2I Adapters compatible with x8 resolutions 2024-10-27 15:38:22 -04:00
dunkeroni
acfa9c87ef Merge branch 'main' into sdxl_t2i_bgr 2024-10-25 23:44:13 -04:00
dunkeroni
f245d8e429 chore: make ruff 2024-10-25 23:43:33 -04:00
dunkeroni
62cf0f54e0 fix preview progress bar pre-denoise 2024-10-25 23:22:06 -04:00
dunkeroni
5f015e76ba convert to bgr on sdxl t2i 2024-10-25 23:04:17 -04:00
95 changed files with 2290 additions and 985 deletions

View File

@@ -5,7 +5,7 @@ If you're a new contributor to InvokeAI or Open Source Projects, this is the gui
## New Contributor Checklist
- [x] Set up your local development environment & fork of InvokAI by following [the steps outlined here](../dev-environment.md)
- [x] Set up your local tooling with [this guide](InvokeAI/contributing/LOCAL_DEVELOPMENT/#developing-invokeai-in-vscode). Feel free to skip this step if you already have tooling you're comfortable with.
- [x] Set up your local tooling with [this guide](../LOCAL_DEVELOPMENT.md). Feel free to skip this step if you already have tooling you're comfortable with.
- [x] Familiarize yourself with [Git](https://www.atlassian.com/git) & our project structure by reading through the [development documentation](development.md)
- [x] Join the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord
- [x] Choose an issue to work on! This can be achieved by asking in the #dev-chat channel, tackling a [good first issue](https://github.com/invoke-ai/InvokeAI/contribute) or finding an item on the [roadmap](https://github.com/orgs/invoke-ai/projects/7). If nothing in any of those places catches your eye, feel free to work on something of interest to you!

View File

@@ -209,7 +209,7 @@ checkpoint models.
To solve this, go to the Model Manager tab (the cube), select the
checkpoint model that's giving you trouble, and press the "Convert"
button in the upper right of your browser window. This will conver the
button in the upper right of your browser window. This will convert the
checkpoint into a diffusers model, after which loading should be
faster and less memory-intensive.

View File

@@ -97,16 +97,16 @@ Prior to installing PyPatchMatch, you need to take the following steps:
sudo pacman -S --needed base-devel
```
2. Install `opencv` and `blas`:
2. Install `opencv`, `blas`, and required dependencies:
```sh
sudo pacman -S opencv blas
sudo pacman -S opencv blas fmt glew vtk hdf5
```
or for CUDA support
```sh
sudo pacman -S opencv-cuda blas
sudo pacman -S opencv-cuda blas fmt glew vtk hdf5
```
3. Fix the naming of the `opencv` package configuration file:

View File

@@ -259,7 +259,7 @@ def select_gpu() -> GpuType:
[
f"Detected the [gold1]{OS}-{ARCH}[/] platform",
"",
"See [deep_sky_blue1]https://invoke-ai.github.io/InvokeAI/#system[/] to ensure your system meets the minimum requirements.",
"See [deep_sky_blue1]https://invoke-ai.github.io/InvokeAI/installation/requirements/[/] to ensure your system meets the minimum requirements.",
"",
"[red3]🠶[/] [b]Your GPU drivers must be correctly installed before using InvokeAI![/] [red3]🠴[/]",
]

View File

@@ -68,7 +68,7 @@ do_line_input() {
printf "2: Open the developer console\n"
printf "3: Command-line help\n"
printf "Q: Quit\n\n"
printf "To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest.\n\n"
printf "To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest\n\n"
read -p "Please enter 1-4, Q: [1] " yn
choice=${yn:='1'}
do_choice $choice

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2023 Lincoln D. Stein
"""FastAPI route for model configuration records."""
import contextlib
import io
import pathlib
import shutil
@@ -10,6 +11,7 @@ from enum import Enum
from tempfile import TemporaryDirectory
from typing import List, Optional, Type
import huggingface_hub
from fastapi import Body, Path, Query, Response, UploadFile
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.routing import APIRouter
@@ -27,6 +29,7 @@ from invokeai.app.services.model_records import (
ModelRecordChanges,
UnknownModelException,
)
from invokeai.app.util.suppress_output import SuppressOutput
from invokeai.backend.model_manager.config import (
AnyModelConfig,
BaseModelType,
@@ -923,3 +926,51 @@ async def get_stats() -> Optional[CacheStats]:
"""Return performance statistics on the model manager's RAM cache. Will return null if no models have been loaded."""
return ApiDependencies.invoker.services.model_manager.load.ram_cache.stats
class HFTokenStatus(str, Enum):
VALID = "valid"
INVALID = "invalid"
UNKNOWN = "unknown"
class HFTokenHelper:
@classmethod
def get_status(cls) -> HFTokenStatus:
try:
if huggingface_hub.get_token_permission(huggingface_hub.get_token()):
# Valid token!
return HFTokenStatus.VALID
# No token set
return HFTokenStatus.INVALID
except Exception:
return HFTokenStatus.UNKNOWN
@classmethod
def set_token(cls, token: str) -> HFTokenStatus:
with SuppressOutput(), contextlib.suppress(Exception):
huggingface_hub.login(token=token, add_to_git_credential=False)
return cls.get_status()
@model_manager_router.get("/hf_login", operation_id="get_hf_login_status", response_model=HFTokenStatus)
async def get_hf_login_status() -> HFTokenStatus:
token_status = HFTokenHelper.get_status()
if token_status is HFTokenStatus.UNKNOWN:
ApiDependencies.invoker.services.logger.warning("Unable to verify HF token")
return token_status
@model_manager_router.post("/hf_login", operation_id="do_hf_login", response_model=HFTokenStatus)
async def do_hf_login(
token: str = Body(description="Hugging Face token to use for login", embed=True),
) -> HFTokenStatus:
HFTokenHelper.set_token(token)
token_status = HFTokenHelper.get_status()
if token_status is HFTokenStatus.UNKNOWN:
ApiDependencies.invoker.services.logger.warning("Unable to verify HF token")
return token_status

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import inspect
import re
import sys
import warnings
from abc import ABC, abstractmethod
from enum import Enum
@@ -192,12 +193,19 @@ class BaseInvocation(ABC, BaseModel):
"""Gets a pydantc TypeAdapter for the union of all invocation types."""
if not cls._typeadapter or cls._typeadapter_needs_update:
AnyInvocation = TypeAliasType(
"AnyInvocation", Annotated[Union[tuple(cls._invocation_classes)], Field(discriminator="type")]
"AnyInvocation", Annotated[Union[tuple(cls.get_invocations())], Field(discriminator="type")]
)
cls._typeadapter = TypeAdapter(AnyInvocation)
cls._typeadapter_needs_update = False
return cls._typeadapter
@classmethod
def invalidate_typeadapter(cls) -> None:
"""Invalidates the typeadapter, forcing it to be rebuilt on next access. If the invocation allowlist or
denylist is changed, this should be called to ensure the typeadapter is updated and validation respects
the updated allowlist and denylist."""
cls._typeadapter_needs_update = True
@classmethod
def get_invocations(cls) -> Iterable[BaseInvocation]:
"""Gets all invocations, respecting the allowlist and denylist."""
@@ -479,6 +487,26 @@ def invocation(
title="type", default=invocation_type, json_schema_extra={"field_kind": FieldKind.NodeAttribute}
)
# Validate the `invoke()` method is implemented
if "invoke" in cls.__abstractmethods__:
raise ValueError(f'Invocation "{invocation_type}" must implement the "invoke" method')
# And validate that `invoke()` returns a subclass of `BaseInvocationOutput
invoke_return_annotation = signature(cls.invoke).return_annotation
try:
# TODO(psyche): If `invoke()` is not defined, `return_annotation` ends up as the string "BaseInvocationOutput"
# instead of the class `BaseInvocationOutput`. This may be a pydantic bug: https://github.com/pydantic/pydantic/issues/7978
if isinstance(invoke_return_annotation, str):
invoke_return_annotation = getattr(sys.modules[cls.__module__], invoke_return_annotation)
assert invoke_return_annotation is not BaseInvocationOutput
assert issubclass(invoke_return_annotation, BaseInvocationOutput)
except Exception:
raise ValueError(
f'Invocation "{invocation_type}" must have a return annotation of a subclass of BaseInvocationOutput (got "{invoke_return_annotation}")'
)
docstring = cls.__doc__
cls = create_model(
cls.__qualname__,

View File

@@ -622,7 +622,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
for t2i_adapter_field in t2i_adapter:
t2i_adapter_model_config = context.models.get_config(t2i_adapter_field.t2i_adapter_model.key)
t2i_adapter_loaded_model = context.models.load(t2i_adapter_field.t2i_adapter_model)
image = context.images.get_pil(t2i_adapter_field.image.image_name)
image = context.images.get_pil(t2i_adapter_field.image.image_name, mode="RGB")
# The max_unet_downscale is the maximum amount that the UNet model downscales the latent image internally.
if t2i_adapter_model_config.base == BaseModelType.StableDiffusion1:
@@ -640,29 +640,39 @@ class DenoiseLatentsInvocation(BaseInvocation):
with t2i_adapter_loaded_model as t2i_adapter_model:
total_downscale_factor = t2i_adapter_model.total_downscale_factor
# Resize the T2I-Adapter input image.
# We select the resize dimensions so that after the T2I-Adapter's total_downscale_factor is applied, the
# result will match the latent image's dimensions after max_unet_downscale is applied.
t2i_input_height = latents_shape[2] // max_unet_downscale * total_downscale_factor
t2i_input_width = latents_shape[3] // max_unet_downscale * total_downscale_factor
# Note: We have hard-coded `do_classifier_free_guidance=False`. This is because we only want to prepare
# a single image. If CFG is enabled, we will duplicate the resultant tensor after applying the
# T2I-Adapter model.
#
# Note: We re-use the `prepare_control_image(...)` from ControlNet for T2I-Adapter, because it has many
# of the same requirements (e.g. preserving binary masks during resize).
# Assuming fixed dimensional scaling of LATENT_SCALE_FACTOR.
_, _, latent_height, latent_width = latents_shape
control_height_resize = latent_height * LATENT_SCALE_FACTOR
control_width_resize = latent_width * LATENT_SCALE_FACTOR
t2i_image = prepare_control_image(
image=image,
do_classifier_free_guidance=False,
width=t2i_input_width,
height=t2i_input_height,
width=control_width_resize,
height=control_height_resize,
num_channels=t2i_adapter_model.config["in_channels"], # mypy treats this as a FrozenDict
device=t2i_adapter_model.device,
dtype=t2i_adapter_model.dtype,
resize_mode=t2i_adapter_field.resize_mode,
)
# Resize the T2I-Adapter input image.
# We select the resize dimensions so that after the T2I-Adapter's total_downscale_factor is applied, the
# result will match the latent image's dimensions after max_unet_downscale is applied.
# We crop the image to this size so that the positions match the input image on non-standard resolutions
t2i_input_height = latents_shape[2] // max_unet_downscale * total_downscale_factor
t2i_input_width = latents_shape[3] // max_unet_downscale * total_downscale_factor
if t2i_image.shape[2] > t2i_input_height or t2i_image.shape[3] > t2i_input_width:
t2i_image = t2i_image[
:, :, : min(t2i_image.shape[2], t2i_input_height), : min(t2i_image.shape[3], t2i_input_width)
]
adapter_state = t2i_adapter_model(t2i_image)
if do_classifier_free_guidance:

View File

@@ -499,6 +499,22 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
for idx, value in enumerate(single_t2i_adapter_data.adapter_state):
accum_adapter_state[idx] += value * t2i_adapter_weight
# Hack: force compatibility with irregular resolutions by padding the feature map with zeros
for idx, tensor in enumerate(accum_adapter_state):
# The tensor size is supposed to be some integer downscale factor of the latents size.
# Internally, the unet will pad the latents before downscaling between levels when it is no longer divisible by its downscale factor.
# If the latent size does not scale down evenly, we need to pad the tensor so that it matches the the downscaled padded latents later on.
scale_factor = latents.size()[-1] // tensor.size()[-1]
required_padding_width = math.ceil(latents.size()[-1] / scale_factor) - tensor.size()[-1]
required_padding_height = math.ceil(latents.size()[-2] / scale_factor) - tensor.size()[-2]
tensor = torch.nn.functional.pad(
tensor,
(0, required_padding_width, 0, required_padding_height, 0, 0, 0, 0),
mode="constant",
value=0,
)
accum_adapter_state[idx] = tensor
down_intrablock_additional_residuals = accum_adapter_state
# Handle inpainting models.

View File

@@ -95,7 +95,8 @@
"none": "Keine",
"new": "Neu",
"ok": "OK",
"close": "Schließen"
"close": "Schließen",
"clipboard": "Zwischenablage"
},
"gallery": {
"galleryImageSize": "Bildgröße",
@@ -535,14 +536,12 @@
"addModels": "Model hinzufügen",
"deleteModelImage": "Lösche Model Bild",
"huggingFaceRepoID": "HuggingFace Repo ID",
"hfToken": "HuggingFace Schlüssel",
"huggingFacePlaceholder": "besitzer/model-name",
"modelSettings": "Modelleinstellungen",
"typePhraseHere": "Phrase hier eingeben",
"spandrelImageToImage": "Bild zu Bild (Spandrel)",
"starterModels": "Einstiegsmodelle",
"t5Encoder": "T5-Kodierer",
"useDefaultSettings": "Standardeinstellungen verwenden",
"uploadImage": "Bild hochladen",
"urlOrLocalPath": "URL oder lokaler Pfad",
"install": "Installieren",
@@ -678,10 +677,41 @@
"toast": {
"uploadFailed": "Hochladen fehlgeschlagen",
"imageCopied": "Bild kopiert",
"parametersNotSet": "Parameter nicht festgelegt",
"parametersNotSet": "Parameter nicht zurückgerufen",
"addedToBoard": "Dem Board hinzugefügt",
"loadedWithWarnings": "Workflow mit Warnungen geladen",
"imageSaved": "Bild gespeichert"
"imageSaved": "Bild gespeichert",
"linkCopied": "Link kopiert",
"problemCopyingLayer": "Ebene kann nicht kopiert werden",
"problemSavingLayer": "Ebene kann nicht gespeichert werden",
"parameterSetDesc": "{{parameter}} zurückgerufen",
"imageUploaded": "Bild hochgeladen",
"problemCopyingImage": "Bild kann nicht kopiert werden",
"parameterNotSetDesc": "{{parameter}} kann nicht zurückgerufen werden",
"prunedQueue": "Warteschlange bereinigt",
"modelAddedSimple": "Modell zur Warteschlange hinzugefügt",
"parametersSet": "Parameter zurückgerufen",
"imageNotLoadedDesc": "Bild konnte nicht gefunden werden",
"setControlImage": "Als Kontrollbild festlegen",
"sentToUpscale": "An Vergrößerung gesendet",
"parameterNotSetDescWithMessage": "{{parameter}} kann nicht zurückgerufen werden: {{message}}",
"unableToLoadImageMetadata": "Bildmetadaten können nicht geladen werden",
"unableToLoadImage": "Bild kann nicht geladen werden",
"serverError": "Serverfehler",
"parameterNotSet": "Parameter nicht zurückgerufen",
"sessionRef": "Sitzung: {{sessionId}}",
"problemDownloadingImage": "Bild kann nicht heruntergeladen werden",
"parameters": "Parameter",
"parameterSet": "Parameter zurückgerufen",
"importFailed": "Import fehlgeschlagen",
"importSuccessful": "Import erfolgreich",
"setNodeField": "Als Knotenfeld festlegen",
"somethingWentWrong": "Etwas ist schief gelaufen",
"workflowLoaded": "Arbeitsablauf geladen",
"workflowDeleted": "Arbeitsablauf gelöscht",
"errorCopied": "Fehler kopiert",
"layerCopiedToClipboard": "Ebene in die Zwischenablage kopiert",
"sentToCanvas": "An Leinwand gesendet"
},
"accessibility": {
"uploadImage": "Bild hochladen",
@@ -825,7 +855,6 @@
"width": "Breite",
"createdBy": "Erstellt von",
"steps": "Schritte",
"seamless": "Nahtlos",
"positivePrompt": "Positiver Prompt",
"generationMode": "Generierungsmodus",
"Threshold": "Rauschen-Schwelle",
@@ -1170,7 +1199,19 @@
"workflowVersion": "Version",
"saveToGallery": "In Galerie speichern",
"noWorkflows": "Keine Arbeitsabläufe",
"noMatchingWorkflows": "Keine passenden Arbeitsabläufe"
"noMatchingWorkflows": "Keine passenden Arbeitsabläufe",
"unknownErrorValidatingWorkflow": "Unbekannter Fehler beim Validieren des Arbeitsablaufes",
"inputFieldTypeParseError": "Typ des Eingabefelds {{node}}.{{field}} kann nicht analysiert werden ({{message}})",
"workflowSettings": "Arbeitsablauf Editor Einstellungen",
"unableToLoadWorkflow": "Arbeitsablauf kann nicht geladen werden",
"viewMode": "In linearen Ansicht verwenden",
"unableToValidateWorkflow": "Arbeitsablauf kann nicht validiert werden",
"outputFieldTypeParseError": "Typ des Ausgabefelds {{node}}.{{field}} kann nicht analysiert werden ({{message}})",
"unableToGetWorkflowVersion": "Version des Arbeitsablaufschemas kann nicht bestimmt werden",
"unknownFieldType": "$t(nodes.unknownField) Typ: {{type}}",
"unknownField": "Unbekanntes Feld",
"unableToUpdateNodes_one": "{{count}} Knoten kann nicht aktualisiert werden",
"unableToUpdateNodes_other": "{{count}} Knoten können nicht aktualisiert werden"
},
"hrf": {
"enableHrf": "Korrektur für hohe Auflösungen",
@@ -1300,15 +1341,7 @@
"enableLogging": "Protokollierung aktivieren"
},
"whatsNew": {
"whatsNewInInvoke": "Was gibt's Neues",
"canvasV2Announcement": {
"fluxSupport": "Unterstützung für Flux-Modelle",
"newCanvas": "Eine leistungsstarke neue Kontrollfläche",
"newLayerTypes": "Neue Ebenentypen für noch mehr Kontrolle",
"readReleaseNotes": "Anmerkungen zu dieser Version lesen",
"watchReleaseVideo": "Video über diese Version anzeigen",
"watchUiUpdatesOverview": "Interface-Updates Übersicht"
}
"whatsNewInInvoke": "Was gibt's Neues"
},
"stylePresets": {
"name": "Name",

View File

@@ -733,7 +733,17 @@
"huggingFacePlaceholder": "owner/model-name",
"huggingFaceRepoID": "HuggingFace Repo ID",
"huggingFaceHelper": "If multiple models are found in this repo, you will be prompted to select one to install.",
"hfToken": "HuggingFace Token",
"hfTokenLabel": "HuggingFace Token (Required for some models)",
"hfTokenHelperText": "A HF token is required to use some models. Click here to create or get your token.",
"hfTokenInvalid": "Invalid or Missing HF Token",
"hfForbidden": "You do not have access to this HF model",
"hfForbiddenErrorMessage": "We recommend visiting the repo page on HuggingFace.com. The owner may require acceptance of terms in order to download.",
"hfTokenInvalidErrorMessage": "Invalid or missing HuggingFace token.",
"hfTokenRequired": "You are trying to download a model that requires a valid HuggingFace Token.",
"hfTokenInvalidErrorMessage2": "Update it in the ",
"hfTokenUnableToVerify": "Unable to Verify HF Token",
"hfTokenUnableToVerifyErrorMessage": "Unable to verify HuggingFace token. This is likely due to a network error. Please try again later.",
"hfTokenSaved": "HF Token Saved",
"imageEncoderModelId": "Image Encoder Model ID",
"includesNModels": "Includes {{n}} models and their dependencies",
"installQueue": "Install Queue",
@@ -1037,6 +1047,7 @@
"patchmatchDownScaleSize": "Downscale",
"perlinNoise": "Perlin Noise",
"positivePromptPlaceholder": "Positive Prompt",
"recallMetadata": "Recall Metadata",
"iterations": "Iterations",
"scale": "Scale",
"scaleBeforeProcessing": "Scale Before Processing",
@@ -1641,14 +1652,16 @@
"newControlLayerError": "Problem Creating Control Layer",
"newRasterLayerOk": "Created Raster Layer",
"newRasterLayerError": "Problem Creating Raster Layer",
"newFromImage": "New from Image",
"pullBboxIntoLayerOk": "Bbox Pulled Into Layer",
"pullBboxIntoLayerError": "Problem Pulling BBox Into Layer",
"pullBboxIntoReferenceImageOk": "Bbox Pulled Into ReferenceImage",
"pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage",
"regionIsEmpty": "Selected region is empty",
"mergeVisible": "Merge Visible",
"mergeVisibleOk": "Merged visible layers",
"mergeVisibleError": "Error merging visible layers",
"mergeDown": "Merge Down",
"mergeVisibleOk": "Merged layers",
"mergeVisibleError": "Error merging layers",
"clearHistory": "Clear History",
"bboxOverlay": "Show Bbox Overlay",
"resetCanvas": "Reset Canvas",

View File

@@ -5,7 +5,7 @@
"reportBugLabel": "Signaler un bug",
"settingsLabel": "Paramètres",
"img2img": "Image vers Image",
"nodes": "Processus",
"nodes": "Workflows",
"upload": "Importer",
"load": "Charger",
"back": "Retour",
@@ -95,7 +95,8 @@
"positivePrompt": "Prompt Positif",
"negativePrompt": "Prompt Négatif",
"ok": "Ok",
"close": "Fermer"
"close": "Fermer",
"clipboard": "Presse-papier"
},
"gallery": {
"galleryImageSize": "Taille de l'image",
@@ -161,7 +162,7 @@
"unstarImage": "Retirer le marquage de l'Image",
"viewerImage": "Visualisation de l'Image",
"imagesSettings": "Paramètres des images de la galerie",
"assetsTab": "Fichiers que vous avez importé pour vos projets.",
"assetsTab": "Fichiers que vous avez importés pour vos projets.",
"imagesTab": "Images que vous avez créées et enregistrées dans Invoke.",
"boardsSettings": "Paramètres des planches"
},
@@ -219,7 +220,6 @@
"typePhraseHere": "Écrire une phrase ici",
"cancel": "Annuler",
"defaultSettingsSaved": "Paramètres par défaut enregistrés",
"hfToken": "Token HuggingFace",
"imageEncoderModelId": "ID du modèle d'encodeur d'image",
"path": "Chemin sur le disque",
"repoVariant": "Variante de dépôt",
@@ -254,7 +254,6 @@
"loraModels": "LoRAs",
"main": "Principal",
"urlOrLocalPathHelper": "Les URL doivent pointer vers un seul fichier. Les chemins locaux peuvent pointer vers un seul fichier ou un dossier pour un seul modèle de diffuseurs.",
"useDefaultSettings": "Utiliser les paramètres par défaut",
"modelImageUpdateFailed": "Mise à jour de l'image du modèle échouée",
"loraTriggerPhrases": "Phrases de déclenchement LoRA",
"mainModelTriggerPhrases": "Phrases de déclenchement du modèle principal",
@@ -284,24 +283,28 @@
"skippingXDuplicates_many": ", en ignorant {{count}} doublons",
"skippingXDuplicates_other": ", en ignorant {{count}} doublons",
"installingModel": "Modèle en cours d'installation",
"installingBundle": "Pack en cours d'installation"
"installingBundle": "Pack en cours d'installation",
"noDefaultSettings": "Aucun paramètre par défaut configuré pour ce modèle. Visitez le Gestionnaire de Modèles pour ajouter des paramètres par défaut.",
"usingDefaultSettings": "Utilisation des paramètres par défaut du modèle",
"defaultSettingsOutOfSync": "Certain paramètres ne correspondent pas aux valeurs par défaut du modèle :",
"restoreDefaultSettings": "Cliquez pour utiliser les paramètres par défaut du modèle."
},
"parameters": {
"images": "Images",
"steps": "Etapes",
"cfgScale": "CFG Echelle",
"steps": "Étapes",
"cfgScale": "Échelle CFG",
"width": "Largeur",
"height": "Hauteur",
"seed": "Graine",
"shuffle": "Mélanger la graine",
"shuffle": "Nouvelle graine",
"noiseThreshold": "Seuil de Bruit",
"perlinNoise": "Bruit de Perlin",
"type": "Type",
"strength": "Force",
"upscaling": "Agrandissement",
"scale": "Echelle",
"scale": "Échelle",
"imageFit": "Ajuster Image Initiale à la Taille de Sortie",
"scaleBeforeProcessing": "Echelle Avant Traitement",
"scaleBeforeProcessing": "Échelle Avant Traitement",
"scaledWidth": "Larg. Échelle",
"scaledHeight": "Haut. Échelle",
"infillMethod": "Méthode de Remplissage",
@@ -422,7 +425,10 @@
"clearIntermediatesWithCount_other": "Effacé {{count}} Intermédiaires",
"informationalPopoversDisabled": "Pop-ups d'information désactivés",
"informationalPopoversDisabledDesc": "Les pop-ups d'information ont été désactivés. Activez-les dans les paramètres.",
"confirmOnNewSession": "Confirmer lors d'une nouvelle session"
"confirmOnNewSession": "Confirmer lors d'une nouvelle session",
"modelDescriptionsDisabledDesc": "Les descriptions des modèles dans les menus déroulants ont été désactivées. Activez-les dans les paramètres.",
"enableModelDescriptions": "Activer les descriptions de modèle dans les menus déroulants",
"modelDescriptionsDisabled": "Descriptions de modèle dans les menus déroulants désactivés"
},
"toast": {
"uploadFailed": "Importation échouée",
@@ -435,22 +441,22 @@
"parameterNotSet": "Paramètre non Rappelé",
"canceled": "Traitement annulé",
"addedToBoard": "Ajouté aux ressources de la planche {{name}}",
"workflowLoaded": "Processus chargé",
"workflowLoaded": "Workflow chargé",
"connected": "Connecté au serveur",
"setNodeField": "Définir comme champ de nœud",
"imageUploadFailed": "Échec de l'importation de l'image",
"loadedWithWarnings": "Processus chargé avec des avertissements",
"loadedWithWarnings": "Workflow chargé avec des avertissements",
"imageUploaded": "Image importée",
"modelAddedSimple": "Modèle ajouté à la file d'attente",
"setControlImage": "Définir comme image de contrôle",
"workflowDeleted": "Processus supprimé",
"workflowDeleted": "Workflow supprimé",
"baseModelChangedCleared_one": "Effacé ou désactivé {{count}} sous-modèle incompatible",
"baseModelChangedCleared_many": "Effacé ou désactivé {{count}} sous-modèles incompatibles",
"baseModelChangedCleared_other": "Effacé ou désactivé {{count}} sous-modèles incompatibles",
"invalidUpload": "Importation invalide",
"problemDownloadingImage": "Impossible de télécharger l'image",
"problemRetrievingWorkflow": "Problème de récupération du processus",
"problemDeletingWorkflow": "Problème de suppression du processus",
"problemRetrievingWorkflow": "Problème de récupération du Workflow",
"problemDeletingWorkflow": "Problème de suppression du Workflow",
"prunedQueue": "File d'attente vidée",
"parameters": "Paramètres",
"modelImportCanceled": "Importation du modèle annulée",
@@ -550,7 +556,7 @@
"accordions": {
"advanced": {
"title": "Avancé",
"options": "$t(accordions.advanced.title) Options"
"options": "Options $t(accordions.advanced.title)"
},
"image": {
"title": "Image"
@@ -631,7 +637,7 @@
"graphQueued": "Graph ajouté à la file d'attente",
"other": "Autre",
"generation": "Génération",
"workflows": "Processus",
"workflows": "Workflows",
"batchFailedToQueue": "Impossible d'ajouter le Lot dans à la file d'attente",
"graphFailedToQueue": "Impossible d'ajouter le graph à la file d'attente",
"item": "Élément",
@@ -704,8 +710,8 @@
"desc": "Rappelle toutes les métadonnées pour l'image actuelle."
},
"loadWorkflow": {
"title": "Charger le processus",
"desc": "Charge le processus enregistré de l'image actuelle (s'il en a un)."
"title": "Ouvrir un Workflow",
"desc": "Charge le workflow enregistré lié à l'image actuelle (s'il en a un)."
},
"recallSeed": {
"desc": "Rappelle la graine pour l'image actuelle.",
@@ -756,8 +762,8 @@
"desc": "Séléctionne l'onglet Agrandissement."
},
"selectWorkflowsTab": {
"desc": "Sélectionne l'onglet Processus.",
"title": "Sélectionner l'onglet Processus"
"desc": "Sélectionne l'onglet Workflows.",
"title": "Sélectionner l'onglet Workflows"
},
"togglePanels": {
"desc": "Affiche ou masque les panneaux gauche et droit en même temps.",
@@ -963,11 +969,11 @@
},
"undo": {
"title": "Annuler",
"desc": "Annule la dernière action de processus."
"desc": "Annule la dernière action de workflow."
},
"redo": {
"title": "Rétablir",
"desc": "Rétablit la dernière action de processus."
"desc": "Rétablit la dernière action de workflow."
},
"addNode": {
"desc": "Ouvre le menu d'ajout de nœud.",
@@ -985,7 +991,7 @@
"desc": "Colle les nœuds et les connections copiés.",
"title": "Coller"
},
"title": "Processus"
"title": "Workflows"
}
},
"popovers": {
@@ -1372,6 +1378,43 @@
"Des valeurs de guidage élevées peuvent entraîner une saturation excessive, et un guidage élevé ou faible peut entraîner des résultats de génération déformés. Le guidage ne s'applique qu'aux modèles FLUX DEV."
],
"heading": "Guidage"
},
"globalReferenceImage": {
"heading": "Image de Référence Globale",
"paragraphs": [
"Applique une image de référence pour influencer l'ensemble de la génération."
]
},
"regionalReferenceImage": {
"heading": "Image de Référence Régionale",
"paragraphs": [
"Pinceau pour appliquer une image de référence à des zones spécifiques."
]
},
"inpainting": {
"heading": "Inpainting",
"paragraphs": [
"Contrôle la zone qui est modifiée, guidé par la force de débruitage."
]
},
"regionalGuidance": {
"heading": "Guide Régional",
"paragraphs": [
"Pinceau pour guider l'emplacement des éléments provenant des prompts globaux."
]
},
"regionalGuidanceAndReferenceImage": {
"heading": "Guide régional et image de référence régionale",
"paragraphs": [
"Pour le Guide Régional, utilisez le pinceau pour indiquer où les éléments des prompts globaux doivent apparaître.",
"Pour l'image de référence régionale, pinceau pour appliquer une image de référence à des zones spécifiques."
]
},
"rasterLayer": {
"heading": "Couche Rastérisation",
"paragraphs": [
"Contenu basé sur les pixels de votre toile, utilisé lors de la génération d'images."
]
}
},
"dynamicPrompts": {
@@ -1392,12 +1435,11 @@
"positivePrompt": "Prompt Positif",
"allPrompts": "Tous les Prompts",
"negativePrompt": "Prompt Négatif",
"seamless": "Sans jointure",
"metadata": "Métadonné",
"scheduler": "Planificateur",
"imageDetails": "Détails de l'Image",
"seed": "Graine",
"workflow": "Processus",
"workflow": "Workflow",
"width": "Largeur",
"Threshold": "Seuil de bruit",
"noMetaData": "Aucune métadonnée trouvée",
@@ -1446,8 +1488,8 @@
"hideMinimapnodes": "Masquer MiniCarte",
"zoomOutNodes": "Dézoomer",
"zoomInNodes": "Zoomer",
"downloadWorkflow": "Télécharger processus en JSON",
"loadWorkflow": "Charger le processus",
"downloadWorkflow": "Exporter le Workflow au format JSON",
"loadWorkflow": "Charger un Workflow",
"reloadNodeTemplates": "Recharger les modèles de nœuds",
"animatedEdges": "Connexions animées",
"cannotConnectToSelf": "Impossible de se connecter à soi-même",
@@ -1470,16 +1512,16 @@
"float": "Flottant",
"mismatchedVersion": "Nœud invalide : le nœud {{node}} de type {{type}} a une version incompatible (essayez de mettre à jour?)",
"missingTemplate": "Nœud invalide : le nœud {{node}} de type {{type}} modèle manquant (non installé?)",
"noWorkflow": "Pas de processus",
"noWorkflow": "Pas de Workflow",
"validateConnectionsHelp": "Prévenir la création de connexions invalides et l'invocation de graphes invalides",
"workflowSettings": "Paramètres de l'Éditeur de Processus",
"workflowValidation": "Erreur de validation du processus",
"workflowSettings": "Paramètres de l'Éditeur de Workflow",
"workflowValidation": "Erreur de validation du Workflow",
"executionStateInProgress": "En cours",
"node": "Noeud",
"scheduler": "Planificateur",
"notes": "Notes",
"notesDescription": "Ajouter des notes sur votre processus",
"unableToLoadWorkflow": "Impossible de charger le processus",
"notesDescription": "Ajouter des notes sur votre workflow",
"unableToLoadWorkflow": "Impossible de charger le Workflow",
"addNode": "Ajouter un nœud",
"problemSettingTitle": "Problème lors de définition du Titre",
"connectionWouldCreateCycle": "La connexion créerait un cycle",
@@ -1502,7 +1544,7 @@
"noOutputRecorded": "Aucun résultat enregistré",
"removeLinearView": "Retirer de la vue linéaire",
"snapToGrid": "Aligner sur la grille",
"workflow": "Processus",
"workflow": "Workflow",
"updateApp": "Mettre à jour l'application",
"updateNode": "Mettre à jour le nœud",
"nodeOutputs": "Sorties de nœud",
@@ -1515,7 +1557,7 @@
"string": "Chaîne de caractères",
"workflowName": "Nom",
"snapToGridHelp": "Aligner les nœuds sur la grille lors du déplacement",
"unableToValidateWorkflow": "Impossible de valider le processus",
"unableToValidateWorkflow": "Impossible de valider le Workflow",
"validateConnections": "Valider les connexions et le graphique",
"unableToUpdateNodes_one": "Impossible de mettre à jour {{count}} nœud",
"unableToUpdateNodes_many": "Impossible de mettre à jour {{count}} nœuds",
@@ -1528,15 +1570,15 @@
"nodePack": "Paquet de nœuds",
"sourceNodeDoesNotExist": "Connexion invalide : le nœud source/de sortie {{node}} n'existe pas",
"sourceNodeFieldDoesNotExist": "Connexion invalide : {{node}}.{{field}} n'existe pas",
"unableToGetWorkflowVersion": "Impossible d'obtenir la version du schéma de processus",
"newWorkflowDesc2": "Votre processus actuel comporte des modifications non enregistrées.",
"unableToGetWorkflowVersion": "Impossible d'obtenir la version du schéma du Workflow",
"newWorkflowDesc2": "Votre workflow actuel comporte des modifications non enregistrées.",
"deletedInvalidEdge": "Connexion invalide supprimé {{source}} -> {{target}}",
"targetNodeDoesNotExist": "Connexion invalide : le nœud cible/entrée {{node}} n'existe pas",
"targetNodeFieldDoesNotExist": "Connexion invalide : le champ {{node}}.{{field}} n'existe pas",
"nodeVersion": "Version du noeud",
"clearWorkflowDesc2": "Votre processus actuel comporte des modifications non enregistrées.",
"clearWorkflow": "Effacer le Processus",
"clearWorkflowDesc": "Effacer ce processus et en commencer un nouveau?",
"clearWorkflowDesc2": "Votre workflow actuel comporte des modifications non enregistrées.",
"clearWorkflow": "Effacer le Workflow",
"clearWorkflowDesc": "Effacer ce workflow et en commencer un nouveau?",
"unsupportedArrayItemType": "type d'élément de tableau non pris en charge \"{{type}}\"",
"addLinearView": "Ajouter à la vue linéaire",
"collectionOrScalarFieldType": "{{name}} (Unique ou Collection)",
@@ -1545,7 +1587,7 @@
"ipAdapter": "IP-Adapter",
"viewMode": "Utiliser en vue linéaire",
"collectionFieldType": "{{name}} (Collection)",
"newWorkflow": "Nouveau processus",
"newWorkflow": "Nouveau Workflow",
"reorderLinearView": "Réorganiser la vue linéaire",
"unknownOutput": "Sortie inconnue : {{name}}",
"outputFieldTypeParseError": "Impossible d'analyser le type du champ de sortie {{node}}.{{field}} ({{message}})",
@@ -1555,13 +1597,13 @@
"unknownFieldType": "$t(nodes.unknownField) type : {{type}}",
"inputFieldTypeParseError": "Impossible d'analyser le type du champ d'entrée {{node}}.{{field}} ({{message}})",
"unableToExtractSchemaNameFromRef": "impossible d'extraire le nom du schéma à partir de la référence",
"editMode": "Modifier dans l'éditeur de processus",
"unknownErrorValidatingWorkflow": "Erreur inconnue lors de la validation du processus",
"editMode": "Modifier dans l'éditeur de Workflow",
"unknownErrorValidatingWorkflow": "Erreur inconnue lors de la validation du Workflow",
"updateAllNodes": "Mettre à jour les nœuds",
"allNodesUpdated": "Tous les nœuds mis à jour",
"newWorkflowDesc": "Créer un nouveau processus?",
"newWorkflowDesc": "Créer un nouveau workflow?",
"edit": "Modifier",
"noFieldsViewMode": "Ce processus n'a aucun champ sélectionné à afficher. Consultez le processus complet pour configurer les valeurs.",
"noFieldsViewMode": "Ce workflow n'a aucun champ sélectionné à afficher. Consultez le workflow complet pour configurer les valeurs.",
"graph": "Graph",
"modelAccessError": "Impossible de trouver le modèle {{key}}, réinitialisation aux paramètres par défaut",
"showEdgeLabelsHelp": "Afficher le nom sur les connections, indiquant les nœuds connectés",
@@ -1575,9 +1617,9 @@
"missingInvocationTemplate": "Modèle d'invocation manquant",
"imageAccessError": "Impossible de trouver l'image {{image_name}}, réinitialisation à la valeur par défaut",
"boardAccessError": "Impossible de trouver la planche {{board_id}}, réinitialisation à la valeur par défaut",
"workflowHelpText": "Besoin d'aide? Consultez notre guide sur <LinkComponent>Comment commencer avec les Processus</LinkComponent>.",
"noWorkflows": "Aucun Processus",
"noMatchingWorkflows": "Aucun processus correspondant"
"workflowHelpText": "Besoin d'aide? Consultez notre guide sur <LinkComponent>Comment commencer avec les Workflows</LinkComponent>.",
"noWorkflows": "Aucun Workflows",
"noMatchingWorkflows": "Aucun Workflows correspondant"
},
"models": {
"noMatchingModels": "Aucun modèle correspondant",
@@ -1594,59 +1636,51 @@
},
"workflows": {
"workflowLibrary": "Bibliothèque",
"loading": "Chargement des processus",
"searchWorkflows": "Rechercher des processus",
"workflowCleared": "Processus effacé",
"loading": "Chargement des Workflows",
"searchWorkflows": "Chercher des Workflows",
"workflowCleared": "Workflow effacé",
"noDescription": "Aucune description",
"deleteWorkflow": "Supprimer le processus",
"openWorkflow": "Ouvrir le processus",
"deleteWorkflow": "Supprimer le Workflow",
"openWorkflow": "Ouvrir le Workflow",
"uploadWorkflow": "Charger à partir d'un fichier",
"workflowName": "Nom du processus",
"unnamedWorkflow": "Processus sans nom",
"saveWorkflowAs": "Enregistrer le processus sous",
"workflows": "Processus",
"savingWorkflow": "Enregistrement du processus...",
"saveWorkflowToProject": "Enregistrer le processus dans le projet",
"workflowName": "Nom du Workflow",
"unnamedWorkflow": "Workflow sans nom",
"saveWorkflowAs": "Enregistrer le Workflow sous",
"workflows": "Workflows",
"savingWorkflow": "Enregistrement du Workflow...",
"saveWorkflowToProject": "Enregistrer le Workflow dans le projet",
"downloadWorkflow": "Enregistrer dans le fichier",
"saveWorkflow": "Enregistrer le processus",
"problemSavingWorkflow": "Problème de sauvegarde du processus",
"workflowEditorMenu": "Menu de l'Éditeur de Processus",
"newWorkflowCreated": "Nouveau processus créé",
"clearWorkflowSearchFilter": "Réinitialiser le filtre de recherche de processus",
"problemLoading": "Problème de chargement des processus",
"workflowSaved": "Processus enregistré",
"noWorkflows": "Pas de processus",
"saveWorkflow": "Enregistrer le Workflow",
"problemSavingWorkflow": "Problème de sauvegarde du Workflow",
"workflowEditorMenu": "Menu de l'Éditeur de Workflow",
"newWorkflowCreated": "Nouveau Workflow créé",
"clearWorkflowSearchFilter": "Réinitialiser le filtre de recherche de Workflow",
"problemLoading": "Problème de chargement des Workflows",
"workflowSaved": "Workflow enregistré",
"noWorkflows": "Pas de Workflows",
"ascending": "Ascendant",
"loadFromGraph": "Charger le processus à partir du graphique",
"loadFromGraph": "Charger le Workflow à partir du graphique",
"descending": "Descendant",
"created": "Créé",
"updated": "Mis à jour",
"loadWorkflow": "$t(common.load) Processus",
"loadWorkflow": "$t(common.load) Workflow",
"convertGraph": "Convertir le graphique",
"opened": "Ouvert",
"name": "Nom",
"autoLayout": "Mise en page automatique",
"defaultWorkflows": "Processus par défaut",
"userWorkflows": "Processus utilisateur",
"projectWorkflows": "Processus du projet",
"defaultWorkflows": "Workflows par défaut",
"userWorkflows": "Workflows de l'utilisateur",
"projectWorkflows": "Workflows du projet",
"copyShareLink": "Copier le lien de partage",
"chooseWorkflowFromLibrary": "Choisir le Processus dans la Bibliothèque",
"chooseWorkflowFromLibrary": "Choisir le Workflow dans la Bibliothèque",
"uploadAndSaveWorkflow": "Importer dans la bibliothèque",
"edit": "Modifer",
"deleteWorkflow2": "Êtes-vous sûr de vouloir supprimer ce processus? Ceci ne peut pas être annulé.",
"deleteWorkflow2": "Êtes-vous sûr de vouloir supprimer ce Workflow? Cette action ne peut pas être annulé.",
"download": "Télécharger",
"copyShareLinkForWorkflow": "Copier le lien de partage pour le processus",
"copyShareLinkForWorkflow": "Copier le lien de partage pour le Workflow",
"delete": "Supprimer"
},
"whatsNew": {
"canvasV2Announcement": {
"watchReleaseVideo": "Regarder la vidéo de lancement",
"newLayerTypes": "Nouveaux types de couches pour un contrôle encore plus précis",
"fluxSupport": "Support pour la famille de modèles Flux",
"readReleaseNotes": "Lire les notes de version",
"newCanvas": "Une nouvelle Toile de contrôle puissant",
"watchUiUpdatesOverview": "Regarder l'aperçu des mises à jour de l'UI"
},
"whatsNewInInvoke": "Quoi de neuf dans Invoke"
},
"ui": {
@@ -1657,7 +1691,7 @@
"gallery": "Galerie",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
"generation": "Génération",
"workflows": "Processus",
"workflows": "Workflows",
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
"models": "Modèles",
"modelsTab": "$t(ui.tabs.models) $t(common.tab)"
@@ -1767,7 +1801,9 @@
"bboxGroup": "Créer à partir de la bounding box",
"newRegionalReferenceImage": "Nouvelle image de référence régionale",
"newGlobalReferenceImage": "Nouvelle image de référence globale",
"newControlLayer": "Nouveau couche de contrôle"
"newControlLayer": "Nouveau couche de contrôle",
"newInpaintMask": "Nouveau Masque Inpaint",
"newRegionalGuidance": "Nouveau Guide Régional"
},
"bookmark": "Marque-page pour Changement Rapide",
"saveLayerToAssets": "Enregistrer la couche dans les ressources",
@@ -1780,8 +1816,6 @@
"on": "Activé",
"label": "Aligner sur la grille"
},
"isolatedFilteringPreview": "Aperçu de filtrage isolé",
"isolatedTransformingPreview": "Aperçu de transformation isolée",
"invertBrushSizeScrollDirection": "Inverser le défilement pour la taille du pinceau",
"pressureSensitivity": "Sensibilité à la pression",
"preserveMask": {
@@ -1789,9 +1823,10 @@
"alert": "Préserver la zone masquée"
},
"isolatedPreview": "Aperçu Isolé",
"isolatedStagingPreview": "Aperçu de l'attente isolé"
"isolatedStagingPreview": "Aperçu de l'attente isolé",
"isolatedLayerPreview": "Aperçu de la couche isolée",
"isolatedLayerPreviewDesc": "Pour afficher uniquement cette couche lors de l'exécution d'opérations telles que le filtrage ou la transformation."
},
"convertToRasterLayer": "Convertir en Couche de Rastérisation",
"transparency": "Transparence",
"moveBackward": "Reculer",
"rectangle": "Rectangle",
@@ -1914,7 +1949,6 @@
"globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)",
"globalReferenceImage_withCount_many": "Images de référence globales",
"globalReferenceImage_withCount_other": "Images de référence globales",
"convertToControlLayer": "Convertir en Couche de Contrôle",
"layer_withCount_one": "Couche {{count}}",
"layer_withCount_many": "Couches {{count}}",
"layer_withCount_other": "Couches {{count}}",
@@ -1977,7 +2011,41 @@
"pullBboxIntoReferenceImageOk": "Bounding Box insérée dans l'Image de référence",
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
"controlLayer_withCount_many": "Controler les couches",
"controlLayer_withCount_other": "Controler les couches"
"controlLayer_withCount_other": "Controler les couches",
"copyInpaintMaskTo": "Copier $t(controlLayers.inpaintMask) vers",
"copyRegionalGuidanceTo": "Copier $t(controlLayers.regionalGuidance) vers",
"convertRasterLayerTo": "Convertir $t(controlLayers.rasterLayer) vers",
"selectObject": {
"selectObject": "Sélectionner l'objet",
"clickToAdd": "Cliquez sur la couche pour ajouter un point",
"apply": "Appliquer",
"cancel": "Annuler",
"dragToMove": "Faites glisser un point pour le déplacer",
"clickToRemove": "Cliquez sur un point pour le supprimer",
"include": "Inclure",
"invertSelection": "Sélection Inversée",
"saveAs": "Enregistrer sous",
"neutral": "Neutre",
"pointType": "Type de point",
"exclude": "Exclure",
"process": "Traiter",
"reset": "Réinitialiser",
"help1": "Sélectionnez un seul objet cible. Ajoutez des points <Bold>Inclure</Bold> et <Bold>Exclure</Bold> pour indiquer quelles parties de la couche font partie de l'objet cible.",
"help2": "Commencez par un point <Bold>Inclure</Bold> au sein de l'objet cible. Ajoutez d'autres points pour affiner la sélection. Moins de points produisent généralement de meilleurs résultats.",
"help3": "Inversez la sélection pour sélectionner tout sauf l'objet cible."
},
"canvasAsControlLayer": "$t(controlLayers.canvas) en tant que $t(controlLayers.controlLayer)",
"convertRegionalGuidanceTo": "Convertir $t(controlLayers.regionalGuidance) vers",
"copyRasterLayerTo": "Copier $t(controlLayers.rasterLayer) vers",
"newControlLayer": "Nouveau $t(controlLayers.controlLayer)",
"newRegionalGuidance": "Nouveau $t(controlLayers.regionalGuidance)",
"replaceCurrent": "Remplacer Actuel",
"convertControlLayerTo": "Convertir $t(controlLayers.controlLayer) vers",
"convertInpaintMaskTo": "Convertir $t(controlLayers.inpaintMask) vers",
"copyControlLayerTo": "Copier $t(controlLayers.controlLayer) vers",
"newInpaintMask": "Nouveau $t(controlLayers.inpaintMask)",
"newRasterLayer": "Nouveau $t(controlLayers.rasterLayer)",
"canvasAsRasterLayer": "$t(controlLayers.canvas) en tant que $t(controlLayers.rasterLayer)"
},
"upscaling": {
"exceedsMaxSizeDetails": "La limite maximale d'agrandissement est de {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixels. Veuillez essayer une image plus petite ou réduire votre sélection d'échelle.",
@@ -2048,7 +2116,7 @@
"config": "Configuration",
"canvas": "Toile",
"generation": "Génération",
"workflows": "Processus",
"workflows": "Workflows",
"system": "Système",
"models": "Modèles",
"logNamespaces": "Journalisation des espaces de noms",
@@ -2071,9 +2139,9 @@
"newUserExperience": {
"toGetStarted": "Pour commencer, saisissez un prompt dans la boîte et cliquez sur <StrongComponent>Invoke</StrongComponent> pour générer votre première image. Sélectionnez un template de prompt pour améliorer les résultats. Vous pouvez choisir de sauvegarder vos images directement dans la <StrongComponent>Galerie</StrongComponent> ou de les modifier sur la <StrongComponent>Toile</StrongComponent>.",
"gettingStartedSeries": "Vous souhaitez plus de conseils? Consultez notre <LinkComponent>Série de démarrage</LinkComponent> pour des astuces sur l'exploitation du plein potentiel de l'Invoke Studio.",
"noModelsInstalled": "Il semblerait qu'aucun modèle ne soit installé",
"noModelsInstalled": "Il semble qu'aucun modèle ne soit installé",
"downloadStarterModels": "Télécharger les modèles de démarrage",
"importModels": "Importer Modèles",
"importModels": "Importer des Modèles",
"toGetStartedLocal": "Pour commencer, assurez-vous de télécharger ou d'importer des modèles nécessaires pour exécuter Invoke. Ensuite, saisissez le prompt dans la boîte et cliquez sur <StrongComponent>Invoke</StrongComponent> pour générer votre première image. Sélectionnez un template de prompt pour améliorer les résultats. Vous pouvez choisir de sauvegarder vos images directement sur <StrongComponent>Galerie</StrongComponent> ou les modifier sur la <StrongComponent>Toile</StrongComponent>."
},
"upsell": {

View File

@@ -92,7 +92,9 @@
"none": "Niente",
"new": "Nuovo",
"view": "Vista",
"close": "Chiudi"
"close": "Chiudi",
"clipboard": "Appunti",
"ok": "Ok"
},
"gallery": {
"galleryImageSize": "Dimensione dell'immagine",
@@ -542,7 +544,6 @@
"defaultSettingsSaved": "Impostazioni predefinite salvate",
"defaultSettings": "Impostazioni predefinite",
"metadata": "Metadati",
"useDefaultSettings": "Usa le impostazioni predefinite",
"triggerPhrases": "Frasi Trigger",
"deleteModelImage": "Elimina l'immagine del modello",
"localOnly": "solo locale",
@@ -588,7 +589,15 @@
"installingXModels_many": "Installazione di {{count}} modelli",
"installingXModels_other": "Installazione di {{count}} modelli",
"includesNModels": "Include {{n}} modelli e le loro dipendenze",
"starterBundleHelpText": "Installa facilmente tutti i modelli necessari per iniziare con un modello base, tra cui un modello principale, controlnet, adattatori IP e altro. Selezionando un pacchetto salterai tutti i modelli che hai già installato."
"starterBundleHelpText": "Installa facilmente tutti i modelli necessari per iniziare con un modello base, tra cui un modello principale, controlnet, adattatori IP e altro. Selezionando un pacchetto salterai tutti i modelli che hai già installato.",
"noDefaultSettings": "Nessuna impostazione predefinita configurata per questo modello. Visita Gestione Modelli per aggiungere impostazioni predefinite.",
"defaultSettingsOutOfSync": "Alcune impostazioni non corrispondono a quelle predefinite del modello:",
"restoreDefaultSettings": "Fare clic per utilizzare le impostazioni predefinite del modello.",
"usingDefaultSettings": "Utilizzo delle impostazioni predefinite del modello",
"huggingFace": "HuggingFace",
"huggingFaceRepoID": "HuggingFace Repository ID",
"clipEmbed": "CLIP Embed",
"t5Encoder": "T5 Encoder"
},
"parameters": {
"images": "Immagini",
@@ -689,7 +698,8 @@
"boxBlur": "Sfocatura Box",
"staged": "Maschera espansa",
"optimizedImageToImage": "Immagine-a-immagine ottimizzata",
"sendToCanvas": "Invia alla Tela"
"sendToCanvas": "Invia alla Tela",
"coherenceMinDenoise": "Riduzione minima del rumore"
},
"settings": {
"models": "Modelli",
@@ -724,7 +734,10 @@
"reloadingIn": "Ricaricando in",
"informationalPopoversDisabled": "Testo informativo a comparsa disabilitato",
"informationalPopoversDisabledDesc": "I testi informativi a comparsa sono disabilitati. Attivali nelle impostazioni.",
"confirmOnNewSession": "Conferma su nuova sessione"
"confirmOnNewSession": "Conferma su nuova sessione",
"enableModelDescriptions": "Abilita le descrizioni dei modelli nei menu a discesa",
"modelDescriptionsDisabled": "Descrizioni dei modelli nei menu a discesa disabilitate",
"modelDescriptionsDisabledDesc": "Le descrizioni dei modelli nei menu a discesa sono state disabilitate. Abilitale nelle Impostazioni."
},
"toast": {
"uploadFailed": "Caricamento fallito",
@@ -1076,7 +1089,8 @@
"noLoRAsInstalled": "Nessun LoRA installato",
"addLora": "Aggiungi LoRA",
"defaultVAE": "VAE predefinito",
"concepts": "Concetti"
"concepts": "Concetti",
"lora": "LoRA"
},
"invocationCache": {
"disable": "Disabilita",
@@ -1133,7 +1147,8 @@
"paragraphs": [
"Scegli quanti livelli del modello CLIP saltare.",
"Alcuni modelli funzionano meglio con determinate impostazioni di CLIP Skip."
]
],
"heading": "CLIP Skip"
},
"compositingCoherencePass": {
"heading": "Passaggio di Coerenza",
@@ -1492,6 +1507,42 @@
"Controlla quanto il prompt influenza il processo di generazione.",
"Valori di guida elevati possono causare sovrasaturazione e una guida elevata o bassa può causare risultati di generazione distorti. La guida si applica solo ai modelli FLUX DEV."
]
},
"regionalReferenceImage": {
"paragraphs": [
"Pennello per applicare un'immagine di riferimento ad aree specifiche."
],
"heading": "Immagine di riferimento Regionale"
},
"rasterLayer": {
"paragraphs": [
"Contenuto basato sui pixel della tua tela, utilizzato durante la generazione dell'immagine."
],
"heading": "Livello Raster"
},
"regionalGuidance": {
"heading": "Guida Regionale",
"paragraphs": [
"Pennello per guidare la posizione in cui devono apparire gli elementi dei prompt globali."
]
},
"regionalGuidanceAndReferenceImage": {
"heading": "Guida regionale e immagine di riferimento regionale",
"paragraphs": [
"Per la Guida Regionale, utilizzare il pennello per indicare dove devono apparire gli elementi dei prompt globali.",
"Per l'immagine di riferimento regionale, utilizzare il pennello per applicare un'immagine di riferimento ad aree specifiche."
]
},
"globalReferenceImage": {
"heading": "Immagine di riferimento Globale",
"paragraphs": [
"Applica un'immagine di riferimento per influenzare l'intera generazione."
]
},
"inpainting": {
"paragraphs": [
"Controlla quale area viene modificata, in base all'intensità di riduzione del rumore."
]
}
},
"sdxl": {
@@ -1513,7 +1564,6 @@
"refinerSteps": "Passi Affinamento"
},
"metadata": {
"seamless": "Senza giunture",
"positivePrompt": "Prompt positivo",
"negativePrompt": "Prompt negativo",
"generationMode": "Modalità generazione",
@@ -1541,7 +1591,10 @@
"parsingFailed": "Analisi non riuscita",
"recallParameter": "Richiama {{label}}",
"canvasV2Metadata": "Tela",
"guidance": "Guida"
"guidance": "Guida",
"seamlessXAxis": "Asse X senza giunte",
"seamlessYAxis": "Asse Y senza giunte",
"vae": "VAE"
},
"hrf": {
"enableHrf": "Abilita Correzione Alta Risoluzione",
@@ -1638,11 +1691,11 @@
"regionalGuidance": "Guida regionale",
"opacity": "Opacità",
"mergeVisible": "Fondi il visibile",
"mergeVisibleOk": "Livelli visibili uniti",
"mergeVisibleOk": "Livelli uniti",
"deleteReferenceImage": "Elimina l'immagine di riferimento",
"referenceImage": "Immagine di riferimento",
"fitBboxToLayers": "Adatta il riquadro di delimitazione ai livelli",
"mergeVisibleError": "Errore durante l'unione dei livelli visibili",
"mergeVisibleError": "Errore durante l'unione dei livelli",
"regionalReferenceImage": "Immagine di riferimento Regionale",
"newLayerFromImage": "Nuovo livello da immagine",
"newCanvasFromImage": "Nuova tela da immagine",
@@ -1734,7 +1787,7 @@
"composition": "Solo Composizione",
"ipAdapterMethod": "Metodo Adattatore IP"
},
"showingType": "Mostrare {{type}}",
"showingType": "Mostra {{type}}",
"dynamicGrid": "Griglia dinamica",
"tool": {
"view": "Muovi",
@@ -1862,8 +1915,6 @@
"layer_withCount_one": "Livello ({{count}})",
"layer_withCount_many": "Livelli ({{count}})",
"layer_withCount_other": "Livelli ({{count}})",
"convertToControlLayer": "Converti in livello di controllo",
"convertToRasterLayer": "Converti in livello raster",
"unlocked": "Sbloccato",
"enableTransparencyEffect": "Abilita l'effetto trasparenza",
"replaceLayer": "Sostituisci livello",
@@ -1876,9 +1927,7 @@
"newCanvasSession": "Nuova sessione Tela",
"deleteSelected": "Elimina selezione",
"settings": {
"isolatedFilteringPreview": "Anteprima del filtraggio isolata",
"isolatedStagingPreview": "Anteprima di generazione isolata",
"isolatedTransformingPreview": "Anteprima di trasformazione isolata",
"isolatedPreview": "Anteprima isolata",
"invertBrushSizeScrollDirection": "Inverti scorrimento per dimensione pennello",
"snapToGrid": {
@@ -1890,7 +1939,9 @@
"preserveMask": {
"alert": "Preservare la regione mascherata",
"label": "Preserva la regione mascherata"
}
},
"isolatedLayerPreview": "Anteprima livello isolato",
"isolatedLayerPreviewDesc": "Se visualizzare solo questo livello quando si eseguono operazioni come il filtraggio o la trasformazione."
},
"transform": {
"reset": "Reimposta",
@@ -1935,9 +1986,46 @@
"canvasGroup": "Tela",
"newRasterLayer": "Nuovo Livello Raster",
"saveCanvasToGallery": "Salva la Tela nella Galleria",
"saveToGalleryGroup": "Salva nella Galleria"
"saveToGalleryGroup": "Salva nella Galleria",
"newInpaintMask": "Nuova maschera Inpaint",
"newRegionalGuidance": "Nuova Guida Regionale"
},
"newImg2ImgCanvasFromImage": "Nuova Immagine da immagine"
"newImg2ImgCanvasFromImage": "Nuova Immagine da immagine",
"copyRasterLayerTo": "Copia $t(controlLayers.rasterLayer) in",
"copyControlLayerTo": "Copia $t(controlLayers.controlLayer) in",
"copyInpaintMaskTo": "Copia $t(controlLayers.inpaintMask) in",
"selectObject": {
"dragToMove": "Trascina un punto per spostarlo",
"clickToAdd": "Fare clic sul livello per aggiungere un punto",
"clickToRemove": "Clicca su un punto per rimuoverlo",
"help3": "Inverte la selezione per selezionare tutto tranne l'oggetto di destinazione.",
"pointType": "Tipo punto",
"apply": "Applica",
"reset": "Reimposta",
"cancel": "Annulla",
"selectObject": "Seleziona oggetto",
"invertSelection": "Inverti selezione",
"exclude": "Escludi",
"include": "Includi",
"neutral": "Neutro",
"saveAs": "Salva come",
"process": "Elabora",
"help1": "Seleziona un singolo oggetto di destinazione. Aggiungi i punti <Bold>Includi</Bold> e <Bold>Escludi</Bold> per indicare quali parti del livello fanno parte dell'oggetto di destinazione.",
"help2": "Inizia con un punto <Bold>Include</Bold> all'interno dell'oggetto di destinazione. Aggiungi altri punti per perfezionare la selezione. Meno punti in genere producono risultati migliori."
},
"convertControlLayerTo": "Converti $t(controlLayers.controlLayer) in",
"newRasterLayer": "Nuovo $t(controlLayers.rasterLayer)",
"newRegionalGuidance": "Nuova $t(controlLayers.regionalGuidance)",
"canvasAsRasterLayer": "$t(controlLayers.canvas) come $t(controlLayers.rasterLayer)",
"canvasAsControlLayer": "$t(controlLayers.canvas) come $t(controlLayers.controlLayer)",
"convertInpaintMaskTo": "Converti $t(controlLayers.inpaintMask) in",
"copyRegionalGuidanceTo": "Copia $t(controlLayers.regionalGuidance) in",
"convertRasterLayerTo": "Converti $t(controlLayers.rasterLayer) in",
"convertRegionalGuidanceTo": "Converti $t(controlLayers.regionalGuidance) in",
"newControlLayer": "Nuovo $t(controlLayers.controlLayer)",
"newInpaintMask": "Nuova $t(controlLayers.inpaintMask)",
"replaceCurrent": "Sostituisci corrente",
"mergeDown": "Unire in basso"
},
"ui": {
"tabs": {
@@ -2030,15 +2118,13 @@
"toGetStartedLocal": "Per iniziare, assicurati di scaricare o importare i modelli necessari per eseguire Invoke. Quindi, inserisci un prompt nella casella e fai clic su <StrongComponent>Invoke</StrongComponent> per generare la tua prima immagine. Seleziona un modello di prompt per migliorare i risultati. Puoi scegliere di salvare le tue immagini direttamente nella <StrongComponent>Galleria</StrongComponent> o modificarle nella <StrongComponent>Tela</StrongComponent>."
},
"whatsNew": {
"canvasV2Announcement": {
"readReleaseNotes": "Leggi le Note di Rilascio",
"fluxSupport": "Supporto per la famiglia di modelli Flux",
"newCanvas": "Una nuova potente tela di controllo",
"watchReleaseVideo": "Guarda il video di rilascio",
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia",
"newLayerTypes": "Nuovi tipi di livello per un miglior controllo"
},
"whatsNewInInvoke": "Novità in Invoke"
"whatsNewInInvoke": "Novità in Invoke",
"line2": "Supporto Flux esteso, ora con immagini di riferimento globali",
"line3": "Tooltip e menu contestuali migliorati",
"readReleaseNotes": "Leggi le note di rilascio",
"watchRecentReleaseVideos": "Guarda i video su questa versione",
"line1": "Strumento <ItalicComponent>Seleziona oggetto</ItalicComponent> per la selezione e la modifica precise degli oggetti",
"watchUiUpdatesOverview": "Guarda le novità dell'interfaccia"
},
"system": {
"logLevel": {

View File

@@ -229,7 +229,6 @@
"submitSupportTicket": "サポート依頼を送信する"
},
"metadata": {
"seamless": "シームレス",
"Threshold": "ノイズ閾値",
"seed": "シード",
"width": "幅",

View File

@@ -155,7 +155,6 @@
"path": "Pad",
"triggerPhrases": "Triggerzinnen",
"typePhraseHere": "Typ zin hier in",
"useDefaultSettings": "Gebruik standaardinstellingen",
"modelImageDeleteFailed": "Fout bij verwijderen modelafbeelding",
"modelImageUpdated": "Modelafbeelding bijgewerkt",
"modelImageUpdateFailed": "Fout bij bijwerken modelafbeelding",
@@ -666,7 +665,6 @@
}
},
"metadata": {
"seamless": "Naadloos",
"positivePrompt": "Positieve prompt",
"negativePrompt": "Negatieve prompt",
"generationMode": "Genereermodus",

View File

@@ -544,7 +544,6 @@
"scanResults": "Результаты сканирования",
"source": "Источник",
"triggerPhrases": "Триггерные фразы",
"useDefaultSettings": "Использовать стандартные настройки",
"modelName": "Название модели",
"modelSettings": "Настройки модели",
"upcastAttention": "Внимание",
@@ -573,7 +572,6 @@
"simpleModelPlaceholder": "URL или путь к локальному файлу или папке diffusers",
"urlOrLocalPath": "URL или локальный путь",
"urlOrLocalPathHelper": "URL-адреса должны указывать на один файл. Локальные пути могут указывать на один файл или папку для одной модели диффузоров.",
"hfToken": "Токен HuggingFace",
"starterModels": "Стартовые модели",
"textualInversions": "Текстовые инверсии",
"loraModels": "LoRAs",
@@ -1402,7 +1400,6 @@
}
},
"metadata": {
"seamless": "Бесшовность",
"positivePrompt": "Запрос",
"negativePrompt": "Негативный запрос",
"generationMode": "Режим генерации",
@@ -1836,14 +1833,12 @@
},
"settings": {
"isolatedPreview": "Изолированный предпросмотр",
"isolatedTransformingPreview": "Изолированный предпросмотр преобразования",
"invertBrushSizeScrollDirection": "Инвертировать прокрутку для размера кисти",
"snapToGrid": {
"label": "Привязка к сетке",
"on": "Вкл",
"off": "Выкл"
},
"isolatedFilteringPreview": "Изолированный предпросмотр фильтрации",
"pressureSensitivity": "Чувствительность к давлению",
"isolatedStagingPreview": "Изолированный предпросмотр на промежуточной стадии",
"preserveMask": {
@@ -1865,7 +1860,6 @@
"enableAutoNegative": "Включить авто негатив",
"maskFill": "Заполнение маски",
"viewProgressInViewer": "Просматривайте прогресс и результаты в <Btn>Просмотрщике изображений</Btn>.",
"convertToRasterLayer": "Конвертировать в растровый слой",
"tool": {
"move": "Двигать",
"bbox": "Ограничительная рамка",
@@ -1933,7 +1927,6 @@
"newGallerySession": "Новая сессия галереи",
"sendToCanvasDesc": "Нажатие кнопки Invoke отображает вашу текущую работу на холсте.",
"globalReferenceImages_withCount_hidden": "Глобальные эталонные изображения ({{count}} скрыто)",
"convertToControlLayer": "Конвертировать в контрольный слой",
"layer_withCount_one": "Слой ({{count}})",
"layer_withCount_few": "Слои ({{count}})",
"layer_withCount_many": "Слои ({{count}})",
@@ -2063,14 +2056,6 @@
}
},
"whatsNew": {
"canvasV2Announcement": {
"newLayerTypes": "Новые типы слоев для еще большего контроля",
"readReleaseNotes": "Прочитать информацию о выпуске",
"watchReleaseVideo": "Смотреть видео о выпуске",
"fluxSupport": "Поддержка семейства моделей Flux",
"newCanvas": "Новый мощный холст управления",
"watchUiUpdatesOverview": "Обзор обновлений пользовательского интерфейса"
},
"whatsNewInInvoke": "Что нового в Invoke"
},
"newUserExperience": {

View File

@@ -82,7 +82,21 @@
"dontShowMeThese": "请勿显示这些内容",
"beta": "测试版",
"toResolve": "解决",
"tab": "标签页"
"tab": "标签页",
"apply": "应用",
"edit": "编辑",
"off": "关",
"loadingImage": "正在加载图片",
"ok": "确定",
"placeholderSelectAModel": "选择一个模型",
"close": "关闭",
"reset": "重设",
"none": "无",
"new": "新建",
"view": "视图",
"alpha": "透明度通道",
"openInViewer": "在查看器中打开",
"clipboard": "剪贴板"
},
"gallery": {
"galleryImageSize": "预览大小",
@@ -124,7 +138,7 @@
"selectAllOnPage": "选择本页全部",
"swapImages": "交换图像",
"exitBoardSearch": "退出面板搜索",
"exitSearch": "退出搜索",
"exitSearch": "退出图像搜索",
"oldestFirst": "最旧在前",
"sortDirection": "排序方向",
"showStarredImagesFirst": "优先显示收藏的图片",
@@ -135,17 +149,333 @@
"searchImages": "按元数据搜索",
"jump": "跳过",
"compareHelp2": "按 <Kbd>M</Kbd> 键切换不同的比较模式。",
"displayBoardSearch": "显示面板搜索",
"displaySearch": "显示搜索",
"displayBoardSearch": "板搜索",
"displaySearch": "图像搜索",
"stretchToFit": "拉伸以适应",
"exitCompare": "退出对比",
"compareHelp1": "在点击图库中的图片或使用箭头键切换比较图片时,请按住<Kbd>Alt</Kbd> 键。",
"go": "运行"
"go": "运行",
"boardsSettings": "画板设置",
"imagesSettings": "画廊图片设置",
"gallery": "画廊",
"move": "移动",
"imagesTab": "您在Invoke中创建和保存的图片。",
"openViewer": "打开查看器",
"closeViewer": "关闭查看器",
"assetsTab": "您已上传用于项目的文件。"
},
"hotkeys": {
"searchHotkeys": "检索快捷键",
"noHotkeysFound": "未找到快捷键",
"clearSearch": "清除检索项"
"clearSearch": "清除检索项",
"app": {
"cancelQueueItem": {
"title": "取消",
"desc": "取消当前正在处理的队列项目。"
},
"selectQueueTab": {
"title": "选择队列标签",
"desc": "选择队列标签。"
},
"toggleLeftPanel": {
"desc": "显示或隐藏左侧面板。",
"title": "开关左侧面板"
},
"resetPanelLayout": {
"title": "重设面板布局",
"desc": "将左侧和右侧面板重置为默认大小和布局。"
},
"togglePanels": {
"title": "开关面板",
"desc": "同时显示或隐藏左右两侧的面板。"
},
"selectWorkflowsTab": {
"title": "选择工作流标签",
"desc": "选择工作流标签。"
},
"selectModelsTab": {
"title": "选择模型标签",
"desc": "选择模型标签。"
},
"toggleRightPanel": {
"title": "开关右侧面板",
"desc": "显示或隐藏右侧面板。"
},
"clearQueue": {
"title": "清除队列",
"desc": "取消并清除所有队列条目。"
},
"selectCanvasTab": {
"title": "选择画布标签",
"desc": "选择画布标签。"
},
"invokeFront": {
"desc": "将生成请求排队,添加到队列的前面。",
"title": "调用(前台)"
},
"selectUpscalingTab": {
"title": "选择放大选项卡",
"desc": "选择高清放大选项卡。"
},
"focusPrompt": {
"title": "聚焦提示",
"desc": "将光标焦点移动到正向提示。"
},
"title": "应用程序",
"invoke": {
"title": "调用",
"desc": "将生成请求排队,添加到队列的末尾。"
}
},
"canvas": {
"selectBrushTool": {
"title": "画笔工具",
"desc": "选择画笔工具。"
},
"selectEraserTool": {
"title": "橡皮擦工具",
"desc": "选择橡皮擦工具。"
},
"title": "画布",
"selectColorPickerTool": {
"title": "拾色器工具",
"desc": "选择拾色器工具。"
},
"fitBboxToCanvas": {
"title": "使边界框适应画布",
"desc": "缩放并调整视图以适应边界框。"
},
"setZoomTo400Percent": {
"title": "缩放到400%",
"desc": "将画布的缩放设置为400%。"
},
"setZoomTo800Percent": {
"desc": "将画布的缩放设置为800%。",
"title": "缩放到800%"
},
"redo": {
"desc": "重做上一次画布操作。",
"title": "重做"
},
"nextEntity": {
"title": "下一层",
"desc": "在列表中选择下一层。"
},
"selectRectTool": {
"title": "矩形工具",
"desc": "选择矩形工具。"
},
"selectViewTool": {
"title": "视图工具",
"desc": "选择视图工具。"
},
"prevEntity": {
"desc": "在列表中选择上一层。",
"title": "上一层"
},
"transformSelected": {
"desc": "变换所选图层。",
"title": "变换"
},
"selectBboxTool": {
"title": "边界框工具",
"desc": "选择边界框工具。"
},
"setZoomTo200Percent": {
"title": "缩放到200%",
"desc": "将画布的缩放设置为200%。"
},
"applyFilter": {
"title": "应用过滤器",
"desc": "将待处理的过滤器应用于所选图层。"
},
"filterSelected": {
"title": "过滤器",
"desc": "对所选图层进行过滤。仅适用于栅格层和控制层。"
},
"cancelFilter": {
"title": "取消过滤器",
"desc": "取消待处理的过滤器。"
},
"incrementToolWidth": {
"title": "增加工具宽度",
"desc": "增加所选的画笔或橡皮擦工具的宽度。"
},
"decrementToolWidth": {
"desc": "减少所选的画笔或橡皮擦工具的宽度。",
"title": "减少工具宽度"
},
"selectMoveTool": {
"title": "移动工具",
"desc": "选择移动工具。"
},
"setFillToWhite": {
"title": "将颜色设置为白色",
"desc": "将当前工具的颜色设置为白色。"
},
"cancelTransform": {
"desc": "取消待处理的变换。",
"title": "取消变换"
},
"applyTransform": {
"title": "应用变换",
"desc": "将待处理的变换应用于所选图层。"
},
"setZoomTo100Percent": {
"title": "缩放到100%",
"desc": "将画布的缩放设置为100%。"
},
"resetSelected": {
"title": "重置图层",
"desc": "重置选定的图层。仅适用于修复蒙版和区域指导。"
},
"undo": {
"title": "撤消",
"desc": "撤消上一次画布操作。"
},
"quickSwitch": {
"title": "图层快速切换",
"desc": "在最后两个选定的图层之间切换。如果某个图层被书签标记,则始终在该图层和最后一个未标记的图层之间切换。"
},
"fitLayersToCanvas": {
"title": "使图层适应画布",
"desc": "缩放并调整视图以适应所有可见图层。"
},
"deleteSelected": {
"title": "删除图层",
"desc": "删除选定的图层。"
}
},
"hotkeys": "快捷键",
"workflows": {
"pasteSelection": {
"title": "粘贴",
"desc": "粘贴复制的节点和边。"
},
"title": "工作流",
"addNode": {
"title": "添加节点",
"desc": "打开添加节点菜单。"
},
"copySelection": {
"desc": "复制选定的节点和边。",
"title": "复制"
},
"pasteSelectionWithEdges": {
"title": "带边缘的粘贴",
"desc": "粘贴复制的节点、边,以及与复制的节点连接的所有边。"
},
"selectAll": {
"title": "全选",
"desc": "选择所有节点和边。"
},
"deleteSelection": {
"title": "删除",
"desc": "删除选定的节点和边。"
},
"undo": {
"title": "撤销",
"desc": "撤销上一个工作流操作。"
},
"redo": {
"desc": "重做上一个工作流操作。",
"title": "重做"
}
},
"gallery": {
"title": "画廊",
"galleryNavUp": {
"title": "向上导航",
"desc": "在图库网格中向上导航,选择该图像。如果在页面顶部,则转到上一页。"
},
"galleryNavUpAlt": {
"title": "向上导航(比较图像)",
"desc": "与向上导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。"
},
"selectAllOnPage": {
"desc": "选择当前页面上的所有图像。",
"title": "选页面上的所有内容"
},
"galleryNavDownAlt": {
"title": "向下导航(比较图像)",
"desc": "与向下导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。"
},
"galleryNavLeftAlt": {
"title": "向左导航(比较图像)",
"desc": "与向左导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。"
},
"clearSelection": {
"title": "清除选择",
"desc": "清除当前的选择(如果有的话)。"
},
"deleteSelection": {
"title": "删除",
"desc": "删除所有选定的图像。默认情况下,系统会提示您确认删除。如果这些图像当前在应用中使用,系统将发出警告。"
},
"galleryNavLeft": {
"title": "向左导航",
"desc": "在图库网格中向左导航,选择该图像。如果处于行的第一张图像,转到上一行。如果处于页面的第一张图像,转到上一页。"
},
"galleryNavRight": {
"title": "向右导航",
"desc": "在图库网格中向右导航,选择该图像。如果在行的最后一张图像,转到下一行。如果在页面的最后一张图像,转到下一页。"
},
"galleryNavDown": {
"desc": "在图库网格中向下导航,选择该图像。如果在页面底部,则转到下一页。",
"title": "向下导航"
},
"galleryNavRightAlt": {
"title": "向右导航(比较图像)",
"desc": "与向右导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。"
}
},
"viewer": {
"toggleMetadata": {
"desc": "显示或隐藏当前图像的元数据覆盖。",
"title": "显示/隐藏元数据"
},
"recallPrompts": {
"desc": "召回当前图像的正面和负面提示。",
"title": "召回提示"
},
"toggleViewer": {
"title": "显示/隐藏图像查看器",
"desc": "显示或隐藏图像查看器。仅在画布选项卡上可用。"
},
"recallAll": {
"desc": "召回当前图像的所有元数据。",
"title": "召回所有元数据"
},
"recallSeed": {
"title": "召回种子",
"desc": "召回当前图像的种子。"
},
"swapImages": {
"title": "交换比较图像",
"desc": "交换正在比较的图像。"
},
"nextComparisonMode": {
"title": "下一个比较模式",
"desc": "环浏览比较模式。"
},
"loadWorkflow": {
"title": "加载工作流",
"desc": "加载当前图像的保存工作流程(如果有的话)。"
},
"title": "图像查看器",
"remix": {
"title": "混合",
"desc": "召回当前图像的所有元数据,除了种子。"
},
"useSize": {
"title": "使用尺寸",
"desc": "使用当前图像的尺寸作为边界框尺寸。"
},
"runPostprocessing": {
"title": "行后处理",
"desc": "对当前图像运行所选的后处理。"
}
}
},
"modelManager": {
"modelManager": "模型管理器",
@@ -210,7 +540,6 @@
"noModelsInstalled": "无已安装的模型",
"urlOrLocalPathHelper": "链接应该指向单个文件.本地路径可以指向单个文件,或者对于单个扩散模型(diffusers model),可以指向一个文件夹.",
"modelSettings": "模型设置",
"useDefaultSettings": "使用默认设置",
"scanPlaceholder": "本地文件夹路径",
"installRepo": "安装仓库",
"modelImageDeleted": "模型图像已删除",
@@ -249,7 +578,16 @@
"loraTriggerPhrases": "LoRA 触发词",
"ipAdapters": "IP适配器",
"spandrelImageToImage": "图生图(Spandrel)",
"starterModelsInModelManager": "您可以在模型管理器中找到初始模型"
"starterModelsInModelManager": "您可以在模型管理器中找到初始模型",
"noDefaultSettings": "此模型没有配置默认设置。请访问模型管理器添加默认设置。",
"clipEmbed": "CLIP 嵌入",
"defaultSettingsOutOfSync": "某些设置与模型的默认值不匹配:",
"restoreDefaultSettings": "点击以使用模型的默认设置。",
"usingDefaultSettings": "使用模型的默认设置",
"huggingFace": "HuggingFace",
"hfTokenInvalid": "HF 令牌无效或缺失",
"hfTokenLabel": "HuggingFace 令牌(某些模型所需)",
"hfTokenHelperText": "使用某些模型需要 HF 令牌。点击这里创建或获取你的令牌。"
},
"parameters": {
"images": "图像",
@@ -367,7 +705,7 @@
"uploadFailed": "上传失败",
"imageCopied": "图像已复制",
"parametersNotSet": "参数未恢复",
"uploadFailedInvalidUploadDesc": "必须是单张的 PNG 或 JPEG 图",
"uploadFailedInvalidUploadDesc": "必须是单 PNG 或 JPEG 图像。",
"connected": "服务器连接",
"parameterSet": "参数已恢复",
"parameterNotSet": "参数未恢复",
@@ -379,7 +717,7 @@
"setControlImage": "设为控制图像",
"setNodeField": "设为节点字段",
"imageUploaded": "图像已上传",
"addedToBoard": "添加到面板",
"addedToBoard": "添加到{{name}}的资产中",
"workflowLoaded": "工作流已加载",
"imageUploadFailed": "图像上传失败",
"baseModelChangedCleared_other": "已清除或禁用{{count}}个不兼容的子模型",
@@ -416,7 +754,9 @@
"createIssue": "创建问题",
"about": "关于",
"submitSupportTicket": "提交支持工单",
"toggleRightPanel": "切换右侧面板(G)"
"toggleRightPanel": "切换右侧面板(G)",
"uploadImages": "上传图片",
"toggleLeftPanel": "开关左侧面板(T)"
},
"nodes": {
"zoomInNodes": "放大",
@@ -569,7 +909,7 @@
"cancelSucceeded": "项目已取消",
"queue": "队列",
"batch": "批处理",
"clearQueueAlertDialog": "清队列时会立即取消所有处理的项目并且会完全清队列。",
"clearQueueAlertDialog": "清队列立即取消所有正在处理的项目并完全清队列。待处理的过滤器将被取消。",
"pending": "待定",
"completedIn": "完成于",
"resumeFailed": "恢复处理器时出现问题",
@@ -610,7 +950,15 @@
"openQueue": "打开队列",
"prompts_other": "提示词",
"iterations_other": "迭代",
"generations_other": "生成"
"generations_other": "生成",
"canvas": "画布",
"workflows": "工作流",
"generation": "生成",
"other": "其他",
"gallery": "画廊",
"destination": "目标存储",
"upscaling": "高清放大",
"origin": "来源"
},
"sdxl": {
"refinerStart": "Refiner 开始作用时机",
@@ -649,7 +997,6 @@
"workflow": "工作流",
"steps": "步数",
"scheduler": "调度器",
"seamless": "无缝",
"recallParameters": "召回参数",
"noRecallParameters": "未找到要召回的参数",
"vae": "VAE",
@@ -658,7 +1005,11 @@
"parsingFailed": "解析失败",
"recallParameter": "调用{{label}}",
"imageDimensions": "图像尺寸",
"parameterSet": "已设置参数{{parameter}}"
"parameterSet": "已设置参数{{parameter}}",
"guidance": "指导",
"seamlessXAxis": "无缝 X 轴",
"seamlessYAxis": "无缝 Y 轴",
"canvasV2Metadata": "画布"
},
"models": {
"noMatchingModels": "无相匹配的模型",
@@ -709,7 +1060,8 @@
"shared": "共享面板",
"archiveBoard": "归档面板",
"archived": "已归档",
"assetsWithCount_other": "{{count}}项资源"
"assetsWithCount_other": "{{count}}项资源",
"updateBoardError": "更新画板出错"
},
"dynamicPrompts": {
"seedBehaviour": {
@@ -1175,7 +1527,8 @@
},
"prompt": {
"addPromptTrigger": "添加提示词触发器",
"noMatchingTriggers": "没有匹配的触发器"
"noMatchingTriggers": "没有匹配的触发器",
"compatibleEmbeddings": "兼容的嵌入"
},
"controlLayers": {
"autoNegative": "自动反向",
@@ -1186,8 +1539,8 @@
"moveToFront": "移动到前面",
"addLayer": "添加层",
"deletePrompt": "删除提示词",
"addPositivePrompt": "添加 $t(common.positivePrompt)",
"addNegativePrompt": "添加 $t(common.negativePrompt)",
"addPositivePrompt": "添加 $t(controlLayers.prompt)",
"addNegativePrompt": "添加 $t(controlLayers.negativePrompt)",
"rectangle": "矩形",
"opacity": "透明度"
},

View File

@@ -58,7 +58,6 @@
"model": "模型",
"seed": "種子",
"vae": "VAE",
"seamless": "無縫",
"metadata": "元數據",
"width": "寬度",
"height": "高度"

View File

@@ -5,7 +5,7 @@ import { atom } from 'nanostores';
* A fallback non-writable atom that always returns `false`, used when a nanostores atom is only conditionally available
* in a hook or component.
*/
export const $false: ReadableAtom<boolean> = atom(false);
// export const $false: ReadableAtom<boolean> = atom(false);
/**
* A fallback non-writable atom that always returns `true`, used when a nanostores atom is only conditionally available
* in a hook or component.

View File

@@ -202,46 +202,6 @@ const createSelector = (
if (controlLayer.controlAdapter.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
}
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
if (controlLayer.controlAdapter.type === 't2i_adapter') {
const multiple = model?.base === 'sdxl' ? 32 : 64;
if (bbox.scaleMethod === 'none') {
if (bbox.rect.width % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleBboxWidth', {
multiple,
width: bbox.rect.width,
}),
});
}
if (bbox.rect.height % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleBboxHeight', {
multiple,
height: bbox.rect.height,
}),
});
}
} else {
if (bbox.scaledSize.width % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleScaledBboxWidth', {
multiple,
width: bbox.scaledSize.width,
}),
});
}
if (bbox.scaledSize.height % 16 !== 0) {
reasons.push({
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleScaledBboxHeight', {
multiple,
height: bbox.scaledSize.height,
}),
});
}
}
}
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });

View File

@@ -5,6 +5,7 @@ import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/componen
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
@@ -27,6 +28,7 @@ export const ControlLayerMenuItems = memo(() => {
<CanvasEntityMenuItemsSelectObject />
<ControlLayerMenuItemsTransparencyEffect />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<ControlLayerMenuItemsCopyToSubMenu />
<ControlLayerMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />

View File

@@ -2,7 +2,8 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import {
controlLayerConvertedToInpaintMask,
controlLayerConvertedToRasterLayer,
@@ -17,7 +18,8 @@ export const ControlLayerMenuItemsConvertToSubMenu = memo(() => {
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const convertToInpaintMask = useCallback(() => {
dispatch(controlLayerConvertedToInpaintMask({ entityIdentifier, replace: true }));
@@ -32,19 +34,19 @@ export const ControlLayerMenuItemsConvertToSubMenu = memo(() => {
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />}>
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertControlLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={!isInteractable}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={!isInteractable}>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem onClick={convertToRasterLayer} icon={<PiSwapBold />} isDisabled={!isInteractable}>
<MenuItem onClick={convertToRasterLayer} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
{t('controlLayers.rasterLayer')}
</MenuItem>
</MenuList>

View File

@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import {
controlLayerConvertedToInpaintMask,
controlLayerConvertedToRasterLayer,
@@ -18,7 +18,7 @@ export const ControlLayerMenuItemsCopyToSubMenu = memo(() => {
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const copyToInpaintMask = useCallback(() => {
dispatch(controlLayerConvertedToInpaintMask({ entityIdentifier }));
@@ -33,20 +33,20 @@ export const ControlLayerMenuItemsCopyToSubMenu = memo(() => {
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />}>
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyControlLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={!isInteractable}>
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={!isInteractable}>
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
<MenuItem onClick={copyToRasterLayer} icon={<PiCopyBold />} isDisabled={!isInteractable}>
<MenuItem onClick={copyToRasterLayer} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newRasterLayer')}
</MenuItem>
</MenuList>

View File

@@ -2,7 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
@@ -13,7 +13,7 @@ export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isLocked = useEntityIsLocked(entityIdentifier);
const selectWithTransparencyEffect = useMemo(
() =>
createSelector(selectCanvasSlice, (canvas) => {
@@ -28,7 +28,7 @@ export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={onToggle} icon={<PiDropHalfBold />} isDisabled={!isInteractable}>
<MenuItem onClick={onToggle} icon={<PiDropHalfBold />} isDisabled={isLocked}>
{withTransparencyEffect
? t('controlLayers.disableTransparencyEffect')
: t('controlLayers.enableTransparencyEffect')}

View File

@@ -4,6 +4,8 @@ import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
import { InpaintMaskMenuItemsConvertToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsConvertToSubMenu';
import { InpaintMaskMenuItemsCopyToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsCopyToSubMenu';
@@ -20,9 +22,11 @@ export const InpaintMaskMenuItems = memo(() => {
<MenuDivider />
<CanvasEntityMenuItemsTransform />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<InpaintMaskMenuItemsCopyToSubMenu />
<InpaintMaskMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsSave />
</>
);
});

View File

@@ -2,7 +2,8 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { inpaintMaskConvertedToRegionalGuidance } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -13,20 +14,21 @@ export const InpaintMaskMenuItemsConvertToSubMenu = memo(() => {
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const convertToRegionalGuidance = useCallback(() => {
dispatch(inpaintMaskConvertedToRegionalGuidance({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />}>
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertInpaintMaskTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={!isInteractable}>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
</MenuList>

View File

@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { inpaintMaskConvertedToRegionalGuidance } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -14,21 +14,21 @@ export const InpaintMaskMenuItemsCopyToSubMenu = memo(() => {
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const copyToRegionalGuidance = useCallback(() => {
dispatch(inpaintMaskConvertedToRegionalGuidance({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />}>
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyInpaintMaskTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={!isInteractable}>
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
</MenuList>

View File

@@ -5,6 +5,7 @@ import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/componen
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
@@ -25,6 +26,7 @@ export const RasterLayerMenuItems = memo(() => {
<CanvasEntityMenuItemsFilter />
<CanvasEntityMenuItemsSelectObject />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<RasterLayerMenuItemsCopyToSubMenu />
<RasterLayerMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />

View File

@@ -3,7 +3,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectDefaultControlAdapter } from 'features/controlLayers/hooks/addLayerHooks';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import {
rasterLayerConvertedToControlLayer,
rasterLayerConvertedToInpaintMask,
@@ -20,7 +21,8 @@ export const RasterLayerMenuItemsConvertToSubMenu = memo(() => {
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('raster_layer');
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const convertToInpaintMask = useCallback(() => {
dispatch(rasterLayerConvertedToInpaintMask({ entityIdentifier, replace: true }));
@@ -41,19 +43,19 @@ export const RasterLayerMenuItemsConvertToSubMenu = memo(() => {
}, [defaultControlAdapter, dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />}>
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertRasterLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={!isInteractable}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={!isInteractable}>
<MenuItem onClick={convertToRegionalGuidance} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem onClick={convertToControlLayer} icon={<PiSwapBold />} isDisabled={!isInteractable}>
<MenuItem onClick={convertToControlLayer} icon={<PiSwapBold />} isDisabled={isBusy || isLocked}>
{t('controlLayers.controlLayer')}
</MenuItem>
</MenuList>

View File

@@ -4,7 +4,7 @@ import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectDefaultControlAdapter } from 'features/controlLayers/hooks/addLayerHooks';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import {
rasterLayerConvertedToControlLayer,
rasterLayerConvertedToInpaintMask,
@@ -21,7 +21,7 @@ export const RasterLayerMenuItemsCopyToSubMenu = memo(() => {
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('raster_layer');
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const copyToInpaintMask = useCallback(() => {
dispatch(rasterLayerConvertedToInpaintMask({ entityIdentifier }));
@@ -41,20 +41,20 @@ export const RasterLayerMenuItemsCopyToSubMenu = memo(() => {
}, [defaultControlAdapter, dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />}>
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyRasterLayerTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={!isInteractable}>
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={!isInteractable}>
<MenuItem onClick={copyToRegionalGuidance} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newRegionalGuidance')}
</MenuItem>
<MenuItem onClick={copyToControlLayer} icon={<PiCopyBold />} isDisabled={!isInteractable}>
<MenuItem onClick={copyToControlLayer} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newControlLayer')}
</MenuItem>
</MenuList>

View File

@@ -4,6 +4,8 @@ import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
import { RegionalGuidanceMenuItemsAddPromptsAndIPAdapter } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter';
import { RegionalGuidanceMenuItemsAutoNegative } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative';
@@ -25,9 +27,11 @@ export const RegionalGuidanceMenuItems = memo(() => {
<CanvasEntityMenuItemsTransform />
<RegionalGuidanceMenuItemsAutoNegative />
<MenuDivider />
<CanvasEntityMenuItemsMergeDown />
<RegionalGuidanceMenuItemsCopyToSubMenu />
<RegionalGuidanceMenuItemsConvertToSubMenu />
<CanvasEntityMenuItemsCropToBbox />
<CanvasEntityMenuItemsSave />
</>
);
});

View File

@@ -2,7 +2,8 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { rgConvertedToInpaintMask } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -13,20 +14,21 @@ export const RegionalGuidanceMenuItemsConvertToSubMenu = memo(() => {
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const convertToInpaintMask = useCallback(() => {
dispatch(rgConvertedToInpaintMask({ entityIdentifier, replace: true }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />}>
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.convertRegionalGuidanceTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={!isInteractable}>
<MenuItem onClick={convertToInpaintMask} icon={<PiSwapBold />} isDisabled={isLocked || isBusy}>
{t('controlLayers.inpaintMask')}
</MenuItem>
</MenuList>

View File

@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { rgConvertedToInpaintMask } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -14,21 +14,21 @@ export const RegionalGuidanceMenuItemsCopyToSubMenu = memo(() => {
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const copyToInpaintMask = useCallback(() => {
dispatch(rgConvertedToInpaintMask({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />}>
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiCopyBold />} isDisabled={isBusy}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.copyRegionalGuidanceTo')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<CanvasEntityMenuItemsCopyToClipboard />
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={!isInteractable}>
<MenuItem onClick={copyToInpaintMask} icon={<PiCopyBold />} isDisabled={isBusy}>
{t('controlLayers.newInpaintMask')}
</MenuItem>
</MenuList>

View File

@@ -7,9 +7,9 @@ import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/component
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
import { useEntityTypeInformationalPopover } from 'features/controlLayers/hooks/useEntityTypeInformationalPopover';
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { type CanvasEntityIdentifier, isRenderableEntityType } from 'features/controlLayers/store/types';
import type { PropsWithChildren } from 'react';
import { memo, useMemo } from 'react';
import { memo } from 'react';
import { PiCaretDownBold } from 'react-icons/pi';
type Props = PropsWithChildren<{
@@ -25,8 +25,6 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
const title = useEntityTypeTitle(type);
const informationalPopoverFeature = useEntityTypeInformationalPopover(type);
const collapse = useBoolean(true);
const canMergeVisible = useMemo(() => type === 'raster_layer' || type === 'inpaint_mask', [type]);
const canHideAll = useMemo(() => type !== 'reference_image', [type]);
return (
<Flex flexDir="column" w="full">
@@ -76,8 +74,8 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
<Spacer />
</Flex>
{canMergeVisible && <CanvasEntityMergeVisibleButton type={type} />}
{canHideAll && <CanvasEntityTypeIsHiddenToggle type={type} />}
{isRenderableEntityType(type) && <CanvasEntityMergeVisibleButton type={type} />}
{isRenderableEntityType(type) && <CanvasEntityTypeIsHiddenToggle type={type} />}
<CanvasEntityAddOfTypeButton type={type} />
</Flex>
<Collapse in={collapse.isTrue}>

View File

@@ -2,7 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IconMenuItem } from 'common/components/IconMenuItem';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import {
entityArrangedBackwardOne,
entityArrangedForwardOne,
@@ -56,7 +56,7 @@ export const CanvasEntityMenuItemsArrange = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
@@ -92,28 +92,28 @@ export const CanvasEntityMenuItemsArrange = memo(() => {
aria-label={t('controlLayers.moveToFront')}
tooltip={t('controlLayers.moveToFront')}
onClick={moveToFront}
isDisabled={!validActions.canMoveToFront || !isInteractable}
isDisabled={!validActions.canMoveToFront || isBusy}
icon={<PiArrowLineUpBold />}
/>
<IconMenuItem
aria-label={t('controlLayers.moveForward')}
tooltip={t('controlLayers.moveForward')}
onClick={moveForwardOne}
isDisabled={!validActions.canMoveForwardOne || !isInteractable}
isDisabled={!validActions.canMoveForwardOne || isBusy}
icon={<PiArrowUpBold />}
/>
<IconMenuItem
aria-label={t('controlLayers.moveBackward')}
tooltip={t('controlLayers.moveBackward')}
onClick={moveBackwardOne}
isDisabled={!validActions.canMoveBackwardOne || !isInteractable}
isDisabled={!validActions.canMoveBackwardOne || isBusy}
icon={<PiArrowDownBold />}
/>
<IconMenuItem
aria-label={t('controlLayers.moveToBack')}
tooltip={t('controlLayers.moveToBack')}
onClick={moveToBack}
isDisabled={!validActions.canMoveToBack || !isInteractable}
isDisabled={!validActions.canMoveToBack || isBusy}
icon={<PiArrowLineDownBold />}
/>
</>

View File

@@ -1,9 +1,9 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useCopyLayerToClipboard } from 'features/controlLayers/hooks/useCopyLayerToClipboard';
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyBold } from 'react-icons/pi';
@@ -12,7 +12,7 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const isEmpty = useEntityIsEmpty(entityIdentifier);
const copyLayerToClipboard = useCopyLayerToClipboard();
@@ -21,7 +21,7 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
}, [copyLayerToClipboard, adapter]);
return (
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={!isInteractable || isEmpty}>
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={isBusy || isEmpty}>
{t('common.clipboard')}
</MenuItem>
);

View File

@@ -1,7 +1,8 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCropBold } from 'react-icons/pi';
@@ -10,7 +11,8 @@ export const CanvasEntityMenuItemsCropToBbox = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const isLocked = useEntityIsLocked(entityIdentifier);
const onClick = useCallback(() => {
if (!adapter) {
return;
@@ -19,7 +21,7 @@ export const CanvasEntityMenuItemsCropToBbox = memo(() => {
}, [adapter]);
return (
<MenuItem onClick={onClick} icon={<PiCropBold />} isDisabled={!isInteractable}>
<MenuItem onClick={onClick} icon={<PiCropBold />} isDisabled={isBusy || isLocked}>
{t('controlLayers.cropLayerToBbox')}
</MenuItem>
);

View File

@@ -2,7 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { IconMenuItem } from 'common/components/IconMenuItem';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -16,7 +16,7 @@ export const CanvasEntityMenuItemsDelete = memo(({ asIcon = false }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const deleteEntity = useCallback(() => {
dispatch(entityDeleted({ entityIdentifier }));
@@ -30,13 +30,13 @@ export const CanvasEntityMenuItemsDelete = memo(({ asIcon = false }: Props) => {
onClick={deleteEntity}
icon={<PiTrashSimpleBold />}
isDestructive
isDisabled={!isInteractable}
isDisabled={isBusy}
/>
);
}
return (
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} isDestructive isDisabled={!isInteractable}>
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} isDestructive isDisabled={isBusy}>
{t('common.delete')}
</MenuItem>
);

View File

@@ -1,7 +1,7 @@
import { useAppDispatch } from 'app/store/storeHooks';
import { IconMenuItem } from 'common/components/IconMenuItem';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { entityDuplicated } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,7 +11,7 @@ export const CanvasEntityMenuItemsDuplicate = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const onClick = useCallback(() => {
dispatch(entityDuplicated({ entityIdentifier }));
@@ -23,7 +23,7 @@ export const CanvasEntityMenuItemsDuplicate = memo(() => {
tooltip={t('controlLayers.duplicate')}
onClick={onClick}
icon={<PiCopyFill />}
isDisabled={!isInteractable}
isDisabled={isBusy}
/>
);
});

View File

@@ -0,0 +1,35 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIdentifierBelowThisOne } from 'features/controlLayers/hooks/useNextRenderableEntityIdentifier';
import type { CanvasRenderableEntityType } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStackSimpleBold } from 'react-icons/pi';
export const CanvasEntityMenuItemsMergeDown = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const isBusy = useCanvasIsBusy();
const entityIdentifier = useEntityIdentifierContext<CanvasRenderableEntityType>();
const entityIdentifierBelowThisOne = useEntityIdentifierBelowThisOne(entityIdentifier);
const mergeDown = useCallback(() => {
if (entityIdentifierBelowThisOne === null) {
return;
}
canvasManager.compositor.mergeByEntityIdentifiers([entityIdentifierBelowThisOne, entityIdentifier], true);
}, [canvasManager.compositor, entityIdentifier, entityIdentifierBelowThisOne]);
return (
<MenuItem
onClick={mergeDown}
icon={<PiStackSimpleBold />}
isDisabled={isBusy || entityIdentifierBelowThisOne === null}
>
{t('controlLayers.mergeDown')}
</MenuItem>
);
});
CanvasEntityMenuItemsMergeDown.displayName = 'CanvasEntityMenuItemsMergeDown';

View File

@@ -1,7 +1,7 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useSaveLayerToAssets } from 'features/controlLayers/hooks/useSaveLayerToAssets';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,14 +11,14 @@ export const CanvasEntityMenuItemsSave = memo(() => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isBusy = useCanvasIsBusy();
const saveLayerToAssets = useSaveLayerToAssets();
const onClick = useCallback(() => {
saveLayerToAssets(adapter);
}, [saveLayerToAssets, adapter]);
return (
<MenuItem onClick={onClick} icon={<PiFloppyDiskBold />} isDisabled={!isInteractable}>
<MenuItem onClick={onClick} icon={<PiFloppyDiskBold />} isDisabled={isBusy}>
{t('controlLayers.saveLayerToAssets')}
</MenuItem>
);

View File

@@ -1,80 +1,24 @@
import { IconButton } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger';
import { useAppDispatch } from 'app/store/storeHooks';
import { withResultAsync } from 'common/util/result';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityTypeCount } from 'features/controlLayers/hooks/useEntityTypeCount';
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { toast } from 'features/toast/toast';
import { useVisibleEntityCountByType } from 'features/controlLayers/hooks/useVisibleEntityCountByType';
import type { CanvasRenderableEntityType } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStackBold } from 'react-icons/pi';
import { serializeError } from 'serialize-error';
const log = logger('canvas');
type Props = {
type: CanvasEntityIdentifier['type'];
type: CanvasRenderableEntityType;
};
export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const isBusy = useCanvasIsBusy();
const entityCount = useEntityTypeCount(type);
const onClick = useCallback(async () => {
if (type === 'raster_layer') {
const rect = canvasManager.stage.getVisibleRect('raster_layer');
const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, { is_intermediate: true })
);
if (result.isOk()) {
dispatch(
rasterLayerAdded({
isSelected: true,
overrides: {
objects: [imageDTOToImageObject(result.value)],
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
},
isMergingVisible: true,
})
);
toast({ title: t('controlLayers.mergeVisibleOk') });
} else {
log.error({ error: serializeError(result.error) }, 'Failed to merge visible');
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
}
} else if (type === 'inpaint_mask') {
const rect = canvasManager.stage.getVisibleRect('inpaint_mask');
const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeInpaintMask(rect, false)
);
if (result.isOk()) {
dispatch(
inpaintMaskAdded({
isSelected: true,
overrides: {
objects: [imageDTOToImageObject(result.value)],
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
},
isMergingVisible: true,
})
);
toast({ title: t('controlLayers.mergeVisibleOk') });
} else {
log.error({ error: serializeError(result.error) }, 'Failed to merge visible');
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
}
} else {
log.error({ type }, 'Unsupported type for merge visible');
}
}, [canvasManager.compositor, canvasManager.stage, dispatch, t, type]);
const entityCount = useVisibleEntityCountByType(type);
const mergeVisible = useCallback(() => {
canvasManager.compositor.mergeVisibleOfType(type);
}, [canvasManager.compositor, type]);
return (
<IconButton
@@ -83,7 +27,7 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
tooltip={t('controlLayers.mergeVisible')}
variant="link"
icon={<PiStackBold />}
onClick={onClick}
onClick={mergeVisible}
alignSelf="stretch"
isDisabled={entityCount <= 1 || isBusy}
/>

View File

@@ -51,7 +51,9 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit
const saveCanvas = useCallback(async () => {
const rect =
region === 'bbox' ? canvasManager.stateApi.getBbox().rect : canvasManager.stage.getVisibleRect('raster_layer');
region === 'bbox'
? canvasManager.stateApi.getBbox().rect
: canvasManager.compositor.getVisibleRectOfType('raster_layer');
if (rect.width === 0 || rect.height === 0) {
toast({
@@ -68,12 +70,13 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit
metadata = selectCanvasMetadata(store.getState());
}
const result = await withResultAsync(() =>
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, {
const result = await withResultAsync(() => {
const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
return canvasManager.compositor.getCompositeImageDTO(rasterAdapters, rect, {
is_intermediate: !saveToGallery,
metadata,
})
);
});
});
if (result.isOk()) {
if (onSave) {
@@ -86,7 +89,6 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, wit
}
}, [
canvasManager.compositor,
canvasManager.stage,
canvasManager.stateApi,
onSave,
region,

View File

@@ -1,9 +1,8 @@
import { useStore } from '@nanostores/react';
import { $false } from 'app/store/nanostores/util';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import { entityReset } from 'features/controlLayers/store/canvasSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isMaskEntityIdentifier } from 'features/controlLayers/store/types';
@@ -14,30 +13,30 @@ import { useCallback, useMemo } from 'react';
export function useCanvasResetLayerHotkey() {
useAssertSingleton(useCanvasResetLayerHotkey.name);
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const entityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const isBusy = useCanvasIsBusy();
const adapter = useEntityAdapterSafe(selectedEntityIdentifier);
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
const adapter = useEntityAdapterSafe(entityIdentifier);
const isLocked = useEntityIsLocked(entityIdentifier);
const imageViewer = useImageViewer();
const resetSelectedLayer = useCallback(() => {
if (selectedEntityIdentifier === null || adapter === null) {
if (entityIdentifier === null || adapter === null) {
return;
}
adapter.bufferRenderer.clearBuffer();
dispatch(entityReset({ entityIdentifier: selectedEntityIdentifier }));
}, [adapter, dispatch, selectedEntityIdentifier]);
dispatch(entityReset({ entityIdentifier }));
}, [adapter, dispatch, entityIdentifier]);
const isResetEnabled = useMemo(
() => selectedEntityIdentifier !== null && isMaskEntityIdentifier(selectedEntityIdentifier),
[selectedEntityIdentifier]
const isResetAllowed = useMemo(
() => entityIdentifier !== null && isMaskEntityIdentifier(entityIdentifier),
[entityIdentifier]
);
useRegisteredHotkeys({
id: 'resetSelected',
category: 'canvas',
callback: resetSelectedLayer,
options: { enabled: isResetEnabled && !isBusy && isInteractable && !imageViewer.isOpen },
dependencies: [isResetEnabled, isBusy, isInteractable, resetSelectedLayer, imageViewer.isOpen],
options: { enabled: isResetAllowed && !isBusy && !isLocked && !imageViewer.isOpen },
dependencies: [isResetAllowed, isBusy, isLocked, resetSelectedLayer, imageViewer.isOpen],
});
}

View File

@@ -1,8 +1,8 @@
import { useStore } from '@nanostores/react';
import { $false } from 'app/store/nanostores/util';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
@@ -13,8 +13,8 @@ export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null)
const adapter = useEntityAdapterSafe(entityIdentifier);
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
const isEmpty = useStore(adapter?.$isEmpty ?? $false);
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
const isDisabled = useMemo(() => {
if (!entityIdentifier) {
@@ -29,14 +29,14 @@ export const useEntityFilter = (entityIdentifier: CanvasEntityIdentifier | null)
if (isBusy) {
return true;
}
if (!isInteractable) {
if (isLocked) {
return true;
}
if (isEmpty) {
return true;
}
return false;
}, [entityIdentifier, adapter, isBusy, isInteractable, isEmpty]);
}, [entityIdentifier, adapter, isBusy, isLocked, isEmpty]);
const start = useCallback(() => {
if (isDisabled) {

View File

@@ -3,8 +3,11 @@ import { buildSelectHasObjects } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntityIsEmpty = (entityIdentifier: CanvasEntityIdentifier) => {
const selectHasObjects = useMemo(() => buildSelectHasObjects(entityIdentifier), [entityIdentifier]);
export const useEntityIsEmpty = (entityIdentifier: CanvasEntityIdentifier | null) => {
const selectHasObjects = useMemo(
() => (entityIdentifier ? buildSelectHasObjects(entityIdentifier) : () => false),
[entityIdentifier]
);
const hasObjects = useAppSelector(selectHasObjects);
return !hasObjects;

View File

@@ -1,13 +0,0 @@
import { useStore } from '@nanostores/react';
import { $true } from 'app/store/nanostores/util';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
export const useIsEntityInteractable = (entityIdentifier: CanvasEntityIdentifier) => {
const isBusy = useCanvasIsBusy();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isInteractable = useStore(adapter?.$isInteractable ?? $true);
return !isBusy && isInteractable;
};

View File

@@ -4,10 +4,13 @@ import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/se
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntityIsLocked = (entityIdentifier: CanvasEntityIdentifier) => {
export const useEntityIsLocked = (entityIdentifier: CanvasEntityIdentifier | null) => {
const selectIsLocked = useMemo(
() =>
createSelector(selectCanvasSlice, (canvas) => {
if (!entityIdentifier) {
return false;
}
const entity = selectEntity(canvas, entityIdentifier);
if (!entity) {
return false;

View File

@@ -1,8 +1,8 @@
import { useStore } from '@nanostores/react';
import { $false } from 'app/store/nanostores/util';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isSegmentableEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
@@ -13,8 +13,8 @@ export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifie
const adapter = useEntityAdapterSafe(entityIdentifier);
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
const isEmpty = useStore(adapter?.$isEmpty ?? $false);
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
const isDisabled = useMemo(() => {
if (!entityIdentifier) {
@@ -29,14 +29,14 @@ export const useEntitySegmentAnything = (entityIdentifier: CanvasEntityIdentifie
if (isBusy) {
return true;
}
if (!isInteractable) {
if (isLocked) {
return true;
}
if (isEmpty) {
return true;
}
return false;
}, [entityIdentifier, adapter, isBusy, isInteractable, isEmpty]);
}, [entityIdentifier, adapter, isBusy, isLocked, isEmpty]);
const start = useCallback(() => {
if (isDisabled) {

View File

@@ -1,8 +1,8 @@
import { useStore } from '@nanostores/react';
import { $false } from 'app/store/nanostores/util';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
@@ -13,8 +13,8 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
const adapter = useEntityAdapterSafe(entityIdentifier);
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
const isEmpty = useStore(adapter?.$isEmpty ?? $false);
const isLocked = useEntityIsLocked(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
const isDisabled = useMemo(() => {
if (!entityIdentifier) {
@@ -29,14 +29,14 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
if (isBusy) {
return true;
}
if (!isInteractable) {
if (isLocked) {
return true;
}
if (isEmpty) {
return true;
}
return false;
}, [entityIdentifier, adapter, isBusy, isInteractable, isEmpty]);
}, [entityIdentifier, adapter, isBusy, isLocked, isEmpty]);
const start = useCallback(async () => {
if (isDisabled) {

View File

@@ -0,0 +1,25 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice, selectEntityIdentifierBelowThisOne } from 'features/controlLayers/store/selectors';
import type { CanvasRenderableEntityIdentifier } from 'features/controlLayers/store/types';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntityIdentifierBelowThisOne = <T extends CanvasRenderableEntityIdentifier>(
entityIdentifier: T
): T | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
const nextEntity = selectEntityIdentifierBelowThisOne(canvas, entityIdentifier);
if (!nextEntity) {
return null;
}
return getEntityIdentifier(nextEntity);
}),
[entityIdentifier]
);
const entityIdentifierBelowThisOne = useAppSelector(selector);
return entityIdentifierBelowThisOne as T | null;
};

View File

@@ -0,0 +1,33 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import {
selectActiveControlLayerEntities,
selectActiveInpaintMaskEntities,
selectActiveRasterLayerEntities,
selectActiveReferenceImageEntities,
selectActiveRegionalGuidanceEntities,
} from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useVisibleEntityCountByType = (type: CanvasEntityIdentifier['type']): number => {
const selectVisibleEntityCountByType = useMemo(() => {
switch (type) {
case 'control_layer':
return createSelector(selectActiveControlLayerEntities, (entities) => entities.length);
case 'raster_layer':
return createSelector(selectActiveRasterLayerEntities, (entities) => entities.length);
case 'inpaint_mask':
return createSelector(selectActiveInpaintMaskEntities, (entities) => entities.length);
case 'regional_guidance':
return createSelector(selectActiveRegionalGuidanceEntities, (entities) => entities.length);
case 'reference_image':
return createSelector(selectActiveReferenceImageEntities, (entities) => entities.length);
default:
assert(false, 'Invalid entity type');
}
}, [type]);
const visibleEntityCount = useAppSelector(selectVisibleEntityCountByType);
return visibleEntityCount;
};

View File

@@ -1,15 +1,32 @@
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import type { Transparency } from 'features/controlLayers/konva/util';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { GenerationMode } from 'features/controlLayers/store/types';
import { LRUCache } from 'lru-cache';
import type { Logger } from 'roarr';
type GetCacheEntryWithFallbackArg<T extends NonNullable<unknown>> = {
cache: LRUCache<string, T>;
key: string;
getValue: () => Promise<T>;
onHit?: (value: T) => void;
onMiss?: () => void;
};
type CanvasCacheModuleConfig = {
/**
* The maximum size of the image name cache.
*/
imageNameCacheSize: number;
/**
* The maximum size of the image data cache.
*/
imageDataCacheSize: number;
/**
* The maximum size of the transparency calculation cache.
*/
transparencyCalculationCacheSize: number;
/**
* The maximum size of the canvas element cache.
*/
@@ -21,7 +38,9 @@ type CanvasCacheModuleConfig = {
};
const DEFAULT_CONFIG: CanvasCacheModuleConfig = {
imageNameCacheSize: 100,
imageNameCacheSize: 1000,
imageDataCacheSize: 32,
transparencyCalculationCacheSize: 1000,
canvasElementCacheSize: 32,
generationModeCacheSize: 100,
};
@@ -41,26 +60,38 @@ export class CanvasCacheModule extends CanvasModuleBase {
config: CanvasCacheModuleConfig = DEFAULT_CONFIG;
/**
* A cache for storing image names. Used as a cache for results of layer/canvas/entity exports. For example, when we
* rasterize a layer and upload it to the server, we store the image name in this cache.
* A cache for storing image names.
*
* The cache key is a hash of the exported entity's state and the export rect.
* For example, the key might be a hash of a composite of entities with the uploaded image name as the value.
*/
imageNameCache = new LRUCache<string, string>({ max: this.config.imageNameCacheSize });
/**
* A cache for storing canvas elements. Similar to the image name cache, but for canvas elements. The primary use is
* for caching composite layers. For example, the canvas compositor module uses this to store the canvas elements for
* individual raster layers when creating a composite of the layers.
* A cache for storing canvas elements.
*
* The cache key is a hash of the exported entity's state and the export rect.
* For example, the key might be a hash of a composite of entities with the canvas element as the value.
*/
canvasElementCache = new LRUCache<string, HTMLCanvasElement>({ max: this.config.canvasElementCacheSize });
/**
* A cache for the generation mode calculation, which is fairly expensive.
* A cache for image data objects.
*
* The cache key is a hash of all the objects that contribute to the generation mode calculation (e.g. the composite
* raster layer, the composite inpaint mask, and bounding box), and the value is the generation mode.
* For example, the key might be a hash of a composite of entities with the image data as the value.
*/
imageDataCache = new LRUCache<string, ImageData>({ max: this.config.imageDataCacheSize });
/**
* A cache for transparency calculation results.
*
* For example, the key might be a hash of a composite of entities with the transparency as the value.
*/
transparencyCalculationCache = new LRUCache<string, Transparency>({ max: this.config.imageDataCacheSize });
/**
* A cache for generation mode calculation results.
*
* For example, the key might be a hash of a composite of raster and inpaint mask entities with the generation mode
* as the value.
*/
generationModeCache = new LRUCache<string, GenerationMode>({ max: this.config.generationModeCacheSize });
@@ -75,6 +106,33 @@ export class CanvasCacheModule extends CanvasModuleBase {
this.log.debug('Creating cache module');
}
/**
* A helper function for getting a cache entry with a fallback.
* @param param0.cache The LRUCache to get the entry from.
* @param param0.key The key to use to retrieve the entry.
* @param param0.getValue An async function to generate the value if the entry is not in the cache.
* @param param0.onHit An optional function to call when the entry is in the cache.
* @param param0.onMiss An optional function to call when the entry is not in the cache.
* @returns
*/
static getWithFallback = async <T extends NonNullable<unknown>>({
cache,
getValue,
key,
onHit,
onMiss,
}: GetCacheEntryWithFallbackArg<T>): Promise<T> => {
let value = cache.get(key);
if (value === undefined) {
onMiss?.();
value = await getValue();
cache.set(key, value);
} else {
onHit?.(value);
}
return value;
};
/**
* Clears all caches.
*/

View File

@@ -1,24 +1,55 @@
import type { SerializableObject } from 'common/types';
import { withResultAsync } from 'common/util/result';
import { CanvasCacheModule } from 'features/controlLayers/konva/CanvasCacheModule';
import type { CanvasEntityAdapter, CanvasEntityAdapterFromType } from 'features/controlLayers/konva/CanvasEntity/types';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import type { Transparency } from 'features/controlLayers/konva/util';
import {
canvasToBlob,
canvasToImageData,
getImageDataTransparency,
getPrefixedId,
getRectUnion,
mapId,
previewBlob,
} from 'features/controlLayers/konva/util';
import type { GenerationMode, Rect } from 'features/controlLayers/store/types';
import {
selectActiveControlLayerEntities,
selectActiveInpaintMaskEntities,
selectActiveRasterLayerEntities,
selectActiveRegionalGuidanceEntities,
} from 'features/controlLayers/store/selectors';
import type {
CanvasRenderableEntityIdentifier,
CanvasRenderableEntityState,
CanvasRenderableEntityType,
GenerationMode,
Rect,
} from 'features/controlLayers/store/types';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { atom, computed } from 'nanostores';
import type { Logger } from 'roarr';
import { serializeError } from 'serialize-error';
import type { UploadOptions } from 'services/api/endpoints/images';
import { getImageDTOSafe, uploadImage } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import stableHash from 'stable-hash';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
type CompositingOptions = {
/**
* The global composite operation to use when compositing each entity.
* See: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
*/
globalCompositeOperation?: GlobalCompositeOperation;
};
/**
* Handles compositing operations:
* - Rasterizing and uploading the composite raster layer
@@ -54,41 +85,98 @@ export class CanvasCompositorModule extends CanvasModuleBase {
}
/**
* Gets the entity IDs of all raster layers that should be included in the composite raster layer.
* A raster layer is included if it is enabled and has objects. The ids are sorted by draw order.
* @returns An array of raster layer entity IDs
* Gets the rect union of all visible entities of the given entity type. This is used for "merge visible".
*
* If no entity type is provided, all visible entities are included in the rect.
*
* @param type The optional entity type
* @returns The rect
*/
getCompositeRasterLayerEntityIds = (): string[] => {
const validSortedIds = [];
const sortedIds = this.manager.stateApi.getRasterLayersState().entities.map(({ id }) => id);
for (const id of sortedIds) {
const adapter = this.manager.adapters.rasterLayers.get(id);
if (!adapter) {
this.log.warn({ id }, 'Raster layer adapter not found');
getVisibleRectOfType = (type?: CanvasRenderableEntityType): Rect => {
const rects = [];
for (const adapter of this.manager.getAllAdapters()) {
if (!adapter.state.isEnabled) {
continue;
}
if (adapter.state.isEnabled && adapter.state.objects.length > 0) {
validSortedIds.push(adapter.id);
if (type && adapter.state.type !== type) {
continue;
}
if (adapter.renderer.hasObjects()) {
rects.push(adapter.transformer.getRelativeRect());
}
}
return validSortedIds;
return getRectUnion(...rects);
};
/**
* Gets a hash of the composite raster layer, which includes the state of all raster layers that are included in the
* composite plus arbitrary extra data that should contribute to the hash (e.g. a rect).
* @param extra Any extra data to include in the hash
* @returns A hash for the composite raster layer
* Gets the rect union of the given entity adapters. This is used for "merge down" and "merge selected".
*
* Unlike `getVisibleRectOfType`, **disabled entities are included in the rect**, per the conventional behaviour of
* these merge methods.
*
* @param adapters The entity adapters to include in the rect
* @returns The rect
*/
getCompositeRasterLayerHash = (extra: SerializableObject): string => {
getRectOfAdapters = (adapters: CanvasEntityAdapter[]): Rect => {
const rects = [];
for (const adapter of adapters) {
if (adapter.renderer.hasObjects()) {
rects.push(adapter.transformer.getRelativeRect());
}
}
return getRectUnion(...rects);
};
/**
* Gets all visible adapters for the given entity type. Visible adapters are those that are not disabled and have
* objects to render. This is used for "merge visible" functionality and for calculating the generation mode.
*
* This includes all adapters that are not disabled and have objects to render.
*
* @param type The entity type
* @returns The adapters for the given entity type that are eligible to be included in a composite
*/
getVisibleAdaptersOfType = <T extends CanvasRenderableEntityType>(type: T): CanvasEntityAdapterFromType<T>[] => {
let entities: CanvasRenderableEntityState[];
switch (type) {
case 'raster_layer':
entities = this.manager.stateApi.getRasterLayersState().entities;
break;
case 'inpaint_mask':
entities = this.manager.stateApi.getInpaintMasksState().entities;
break;
case 'control_layer':
entities = this.manager.stateApi.getControlLayersState().entities;
break;
case 'regional_guidance':
entities = this.manager.stateApi.getRegionsState().entities;
break;
default:
assert(false, `Unhandled entity type: ${type}`);
}
const adapters: CanvasEntityAdapter[] = entities
// Get the identifier for each entity
.map((entity) => getEntityIdentifier(entity))
// Get the adapter for each entity
.map(this.manager.getAdapter)
// Filter out null adapters
.filter((adapter) => !!adapter)
// Filter out adapters that are disabled or have no objects (and are thus not to be included in the composite)
.filter((adapter) => !adapter.$isDisabled.get() && adapter.renderer.hasObjects());
return adapters as CanvasEntityAdapterFromType<T>[];
};
getCompositeHash = (adapters: CanvasEntityAdapter[], extra: SerializableObject): string => {
const adapterHashes: SerializableObject[] = [];
for (const id of this.getCompositeRasterLayerEntityIds()) {
const adapter = this.manager.adapters.rasterLayers.get(id);
if (!adapter) {
this.log.warn({ id }, 'Raster layer adapter not found');
continue;
}
for (const adapter of adapters) {
adapterHashes.push(adapter.getHashableState());
}
@@ -101,23 +189,33 @@ export class CanvasCompositorModule extends CanvasModuleBase {
};
/**
* Gets a canvas element for the composite raster layer. Only the region defined by the rect is included in the canvas.
* Composites the given canvas entities for the given rect and returns the resulting canvas.
*
* If the hash of the composite raster layer is found in the cache, the cached canvas is returned.
* The canvas element is cached to avoid recomputing it when the canvas state has not changed.
*
* The canvas entities are drawn in the order they are provided.
*
* @param adapters The adapters for the canvas entities to composite, in the order they should be drawn
* @param rect The region to include in the canvas
* @returns A canvas element with the composite raster layer drawn on it
* @param compositingOptions Options for compositing the entities
* @returns The composite canvas
*/
getCompositeRasterLayerCanvas = (rect: Rect): HTMLCanvasElement => {
const hash = this.getCompositeRasterLayerHash({ rect });
getCompositeCanvas = (
adapters: CanvasEntityAdapter[],
rect: Rect,
compositingOptions?: CompositingOptions
): HTMLCanvasElement => {
const entityIdentifiers = adapters.map((adapter) => adapter.entityIdentifier);
const hash = this.getCompositeHash(adapters, { rect });
const cachedCanvas = this.manager.cache.canvasElementCache.get(hash);
if (cachedCanvas) {
this.log.trace({ rect }, 'Using cached composite raster layer canvas');
this.log.debug({ entityIdentifiers, rect }, 'Using cached composite canvas');
return cachedCanvas;
}
this.log.trace({ rect }, 'Building composite raster layer canvas');
this.log.debug({ entityIdentifiers, rect }, 'Building composite canvas');
this.$isCompositing.set(true);
const canvas = document.createElement('canvas');
@@ -129,13 +227,12 @@ export class CanvasCompositorModule extends CanvasModuleBase {
ctx.imageSmoothingEnabled = false;
for (const id of this.getCompositeRasterLayerEntityIds()) {
const adapter = this.manager.adapters.rasterLayers.get(id);
if (!adapter) {
this.log.warn({ id }, 'Raster layer adapter not found');
continue;
}
this.log.trace({ id }, 'Drawing raster layer to composite canvas');
if (compositingOptions?.globalCompositeOperation) {
ctx.globalCompositeOperation = compositingOptions.globalCompositeOperation;
}
for (const adapter of adapters) {
this.log.debug({ entityIdentifier: adapter.entityIdentifier }, 'Drawing entity to composite canvas');
const adapterCanvas = adapter.getCanvas(rect);
ctx.drawImage(adapterCanvas, 0, 0);
}
@@ -145,23 +242,42 @@ export class CanvasCompositorModule extends CanvasModuleBase {
};
/**
* Rasterizes the composite raster layer and uploads it to the server.
* Composites the given canvas entities for the given rect and uploads the resulting image.
*
* If the hash of the composite raster layer is found in the cache, the cached image DTO is returned.
* The uploaded image is cached to avoid recomputing it when the canvas state has not changed. The canvas elements
* created for each entity are also cached to avoid recomputing them when the canvas state has not changed.
*
* The canvas entities are drawn in the order they are provided.
*
* @param adapters The adapters for the canvas entities to composite, in the order they should be drawn
* @param rect The region to include in the rasterized image
* @param options Options for uploading the image
* @returns A promise that resolves to the uploaded image DTO
* @param uploadOptions Options for uploading the image
* @param compositingOptions Options for compositing the entities
* @returns A promise that resolves to the image DTO
*/
rasterizeAndUploadCompositeRasterLayer = async (
getCompositeImageDTO = async (
adapters: CanvasEntityAdapter[],
rect: Rect,
options: Pick<UploadOptions, 'is_intermediate' | 'metadata'>
uploadOptions: Pick<UploadOptions, 'is_intermediate' | 'metadata'>,
compositingOptions?: CompositingOptions
): Promise<ImageDTO> => {
this.log.trace({ rect }, 'Rasterizing composite raster layer');
assert(rect.width > 0 && rect.height > 0, 'Unable to rasterize empty rect');
const canvas = this.getCompositeRasterLayerCanvas(rect);
const hash = this.getCompositeHash(adapters, { rect });
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
let imageDTO: ImageDTO | null = null;
if (cachedImageName) {
imageDTO = await getImageDTOSafe(cachedImageName);
if (imageDTO) {
this.log.debug({ rect, imageName: cachedImageName, imageDTO }, 'Using cached composite image');
return imageDTO;
}
this.log.warn({ rect, imageName: cachedImageName }, 'Cached image name not found, recompositing');
}
const canvas = this.getCompositeCanvas(adapters, rect, compositingOptions);
this.$isProcessing.set(true);
const blobResult = await withResultAsync(() => canvasToBlob(canvas));
@@ -173,217 +289,163 @@ export class CanvasCompositorModule extends CanvasModuleBase {
const blob = blobResult.value;
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite raster layer canvas');
previewBlob(blob, 'Composite');
}
this.$isUploading.set(true);
const uploadResult = await withResultAsync(() =>
uploadImage({
blob,
fileName: 'composite-raster-layer.png',
fileName: 'canvas-composite.png',
image_category: 'general',
is_intermediate: options.is_intermediate,
board_id: options.is_intermediate ? undefined : selectAutoAddBoardId(this.manager.store.getState()),
metadata: options.metadata,
is_intermediate: uploadOptions.is_intermediate,
board_id: uploadOptions.is_intermediate ? undefined : selectAutoAddBoardId(this.manager.store.getState()),
metadata: uploadOptions.metadata,
})
);
this.$isUploading.set(false);
if (uploadResult.isErr()) {
throw uploadResult.error;
}
const imageDTO = uploadResult.value;
return imageDTO;
};
/**
* Gets the image DTO for the composite raster layer.
*
* If the image is found in the cache, the cached image DTO is returned.
*
* @param rect The region to include in the image
* @returns A promise that resolves to the image DTO
*/
getCompositeRasterLayerImageDTO = async (rect: Rect): Promise<ImageDTO> => {
let imageDTO: ImageDTO | null = null;
const hash = this.getCompositeRasterLayerHash({ rect });
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
if (cachedImageName) {
imageDTO = await getImageDTOSafe(cachedImageName);
if (imageDTO) {
this.log.trace({ rect, imageName: cachedImageName, imageDTO }, 'Using cached composite raster layer image');
return imageDTO;
}
}
imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, { is_intermediate: true });
imageDTO = uploadResult.value;
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
};
/**
* Gets the entity IDs of all inpaint masks that should be included in the composite inpaint mask.
* An inpaint mask is included if it is enabled and has objects. The ids are sorted by draw order.
* @returns An array of inpaint mask entity IDs
* Creates a merged composite image from the given entities. The entities are drawn in the order they are provided.
*
* The merged image is uploaded to the server and a new entity is created with the uploaded image as the only object.
*
* All entities must have the same type.
*
* @param entityIdentifiers The entity identifiers to merge
* @param deleteMergedEntities Whether to delete the merged entities after creating the new merged entity
* @returns A promise that resolves to the image DTO, or null if the merge failed
*/
getCompositeInpaintMaskEntityIds = (): string[] => {
const validSortedIds = [];
const sortedIds = this.manager.stateApi.getInpaintMasksState().entities.map(({ id }) => id);
for (const id of sortedIds) {
const adapter = this.manager.adapters.inpaintMasks.get(id);
if (!adapter) {
this.log.warn({ id }, 'Inpaint mask adapter not found');
continue;
}
if (adapter.state.isEnabled && adapter.state.objects.length > 0) {
validSortedIds.push(adapter.id);
}
mergeByEntityIdentifiers = async <T extends CanvasRenderableEntityIdentifier>(
entityIdentifiers: T[],
deleteMergedEntities: boolean
): Promise<ImageDTO | null> => {
if (entityIdentifiers.length <= 1) {
this.log.warn({ entityIdentifiers }, 'Cannot merge less than 2 entities');
return null;
}
return validSortedIds;
};
const type = entityIdentifiers[0]?.type;
assert(type, 'Cannot merge entities with no type (this should never happen)');
/**
* Gets a hash of the composite inpaint mask, which includes the state of all inpaint masks that are included in the
* composite plus arbitrary extra data that should contribute to the hash (e.g. a rect).
* @param extra Any extra data to include in the hash
* @returns A hash for the composite inpaint mask
*/
getCompositeInpaintMaskHash = (extra: SerializableObject): string => {
const adapterHashes: SerializableObject[] = [];
const adapters = this.manager.getAdapters(entityIdentifiers);
assert(adapters.length === entityIdentifiers.length, 'Failed to get all adapters for entity identifiers');
for (const id of this.getCompositeInpaintMaskEntityIds()) {
const adapter = this.manager.adapters.inpaintMasks.get(id);
if (!adapter) {
this.log.warn({ id }, 'Inpaint mask adapter not found');
continue;
}
adapterHashes.push(adapter.getHashableState());
}
const rect = this.getRectOfAdapters(adapters);
const data: SerializableObject = {
extra,
adapterHashes,
const compositingOptions: CompositingOptions = {
globalCompositeOperation: type === 'control_layer' ? 'lighter' : undefined,
};
return stableHash(data);
};
/**
* Gets a canvas element for the composite inpaint mask. Only the region defined by the rect is included in the canvas.
*
* If the hash of the composite inpaint mask is found in the cache, the cached canvas is returned.
*
* @param rect The region to include in the canvas
* @returns A canvas element with the composite inpaint mask drawn on it
*/
getCompositeInpaintMaskCanvas = (rect: Rect): HTMLCanvasElement => {
const hash = this.getCompositeInpaintMaskHash({ rect });
const cachedCanvas = this.manager.cache.canvasElementCache.get(hash);
if (cachedCanvas) {
this.log.trace({ rect }, 'Using cached composite inpaint mask canvas');
return cachedCanvas;
}
this.log.trace({ rect }, 'Building composite inpaint mask canvas');
this.$isCompositing.set(true);
const canvas = document.createElement('canvas');
canvas.width = rect.width;
canvas.height = rect.height;
const ctx = canvas.getContext('2d');
assert(ctx !== null);
ctx.imageSmoothingEnabled = false;
for (const id of this.getCompositeInpaintMaskEntityIds()) {
const adapter = this.manager.adapters.inpaintMasks.get(id);
if (!adapter) {
this.log.warn({ id }, 'Inpaint mask adapter not found');
continue;
}
this.log.trace({ id }, 'Drawing inpaint mask to composite canvas');
const adapterCanvas = adapter.getCanvas(rect);
ctx.drawImage(adapterCanvas, 0, 0);
}
this.manager.cache.canvasElementCache.set(hash, canvas);
this.$isCompositing.set(false);
return canvas;
};
/**
* Rasterizes the composite inpaint mask and uploads it to the server.
*
* If the hash of the composite inpaint mask is found in the cache, the cached image DTO is returned.
*
* @param rect The region to include in the rasterized image
* @param saveToGallery Whether to save the image to the gallery or just return the uploaded image DTO
* @returns A promise that resolves to the uploaded image DTO
*/
rasterizeAndUploadCompositeInpaintMask = async (rect: Rect, saveToGallery: boolean) => {
this.log.trace({ rect }, 'Rasterizing composite inpaint mask');
assert(rect.width > 0 && rect.height > 0, 'Unable to rasterize empty rect');
const canvas = this.getCompositeInpaintMaskCanvas(rect);
this.$isProcessing.set(true);
const blobResult = await withResultAsync(() => canvasToBlob(canvas));
this.$isProcessing.set(false);
if (blobResult.isErr()) {
throw blobResult.error;
}
const blob = blobResult.value;
if (this.manager._isDebugging) {
previewBlob(blob, 'Composite inpaint mask canvas');
}
this.$isUploading.set(true);
const uploadResult = await withResultAsync(() =>
uploadImage({
blob,
fileName: 'composite-inpaint-mask.png',
image_category: 'general',
is_intermediate: !saveToGallery,
board_id: saveToGallery ? selectAutoAddBoardId(this.manager.store.getState()) : undefined,
})
const result = await withResultAsync(() =>
this.getCompositeImageDTO(adapters, rect, { is_intermediate: true }, compositingOptions)
);
this.$isUploading.set(false);
if (uploadResult.isErr()) {
throw uploadResult.error;
if (result.isErr()) {
this.log.error({ error: serializeError(result.error) }, 'Failed to merge selected entities');
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
return null;
}
const imageDTO = uploadResult.value;
return imageDTO;
// All layer types have the same arg - create a new entity with the image as the only object, positioned at the
// top left corner of the visible rect for the given entity type.
const addEntityArg = {
isSelected: true,
overrides: {
objects: [imageDTOToImageObject(result.value)],
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
},
mergedEntitiesToDelete: deleteMergedEntities ? entityIdentifiers.map(mapId) : [],
};
switch (type) {
case 'raster_layer':
this.manager.stateApi.addRasterLayer(addEntityArg);
break;
case 'inpaint_mask':
this.manager.stateApi.addInpaintMask(addEntityArg);
break;
case 'regional_guidance':
this.manager.stateApi.addRegionalGuidance(addEntityArg);
break;
case 'control_layer':
this.manager.stateApi.addControlLayer(addEntityArg);
break;
default:
assert<Equals<typeof type, never>>(false, 'Unsupported type for merge');
}
toast({ title: t('controlLayers.mergeVisibleOk') });
return result.value;
};
/**
* Gets the image DTO for the composite inpaint mask.
* Merges all visible entities of the given type. This is used for "merge visible" functionality.
*
* If the image is found in the cache, the cached image DTO is returned.
*
* @param rect The region to include in the image
* @returns A promise that resolves to the image DTO
* @param type The type of entity to merge
* @returns A promise that resolves to the image DTO, or null if the merge failed
*/
getCompositeInpaintMaskImageDTO = async (rect: Rect): Promise<ImageDTO> => {
let imageDTO: ImageDTO | null = null;
mergeVisibleOfType = (type: CanvasRenderableEntityType): Promise<ImageDTO | null> => {
let entities: CanvasRenderableEntityState[];
const hash = this.getCompositeInpaintMaskHash({ rect });
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
if (cachedImageName) {
imageDTO = await getImageDTOSafe(cachedImageName);
if (imageDTO) {
this.log.trace({ rect, cachedImageName, imageDTO }, 'Using cached composite inpaint mask image');
return imageDTO;
}
switch (type) {
case 'raster_layer':
entities = this.manager.stateApi.runSelector(selectActiveRasterLayerEntities);
break;
case 'inpaint_mask':
entities = this.manager.stateApi.runSelector(selectActiveInpaintMaskEntities);
break;
case 'regional_guidance':
entities = this.manager.stateApi.runSelector(selectActiveRegionalGuidanceEntities);
break;
case 'control_layer':
entities = this.manager.stateApi.runSelector(selectActiveControlLayerEntities);
break;
default:
assert<Equals<typeof type, never>>(false, 'Unsupported type for merge');
}
imageDTO = await this.rasterizeAndUploadCompositeInpaintMask(rect, false);
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
const entityIdentifiers = entities.map(getEntityIdentifier);
return this.mergeByEntityIdentifiers(entityIdentifiers, false);
};
/**
* Calculates the transparency of the composite of the give adapters.
* @param adapters The adapters to composite
* @param rect The region to include in the composite
* @param hash The hash to use for caching the result
* @returns A promise that resolves to the transparency of the composite
*/
getTransparency = (adapters: CanvasEntityAdapter[], rect: Rect, hash: string): Promise<Transparency> => {
const entityIdentifiers = adapters.map((adapter) => adapter.entityIdentifier);
const logCtx = { entityIdentifiers, rect };
return CanvasCacheModule.getWithFallback({
cache: this.manager.cache.transparencyCalculationCache,
key: hash,
getValue: async () => {
const compositeInpaintMaskCanvas = this.getCompositeCanvas(adapters, rect);
const compositeInpaintMaskImageData = await CanvasCacheModule.getWithFallback({
cache: this.manager.cache.imageDataCache,
key: hash,
getValue: () => Promise.resolve(canvasToImageData(compositeInpaintMaskCanvas)),
onHit: () => this.log.trace(logCtx, 'Using cached image data'),
onMiss: () => this.log.trace(logCtx, 'Calculating image data'),
});
return getImageDataTransparency(compositeInpaintMaskImageData);
},
onHit: () => this.log.trace(logCtx, 'Using cached transparency'),
onMiss: () => this.log.trace(logCtx, 'Calculating transparency'),
});
};
/**
@@ -404,29 +466,37 @@ export class CanvasCompositorModule extends CanvasModuleBase {
*
* @returns The generation mode
*/
getGenerationMode(): GenerationMode {
getGenerationMode = async (): Promise<GenerationMode> => {
const { rect } = this.manager.stateApi.getBbox();
const compositeInpaintMaskHash = this.getCompositeInpaintMaskHash({ rect });
const compositeRasterLayerHash = this.getCompositeRasterLayerHash({ rect });
const rasterLayerAdapters = this.manager.compositor.getVisibleAdaptersOfType('raster_layer');
const compositeRasterLayerHash = this.getCompositeHash(rasterLayerAdapters, { rect });
const inpaintMaskAdapters = this.manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
const compositeInpaintMaskHash = this.getCompositeHash(inpaintMaskAdapters, { rect });
const hash = stableHash({ rect, compositeInpaintMaskHash, compositeRasterLayerHash });
const cachedGenerationMode = this.manager.cache.generationModeCache.get(hash);
if (cachedGenerationMode) {
this.log.trace({ rect, cachedGenerationMode }, 'Using cached generation mode');
this.log.debug({ rect, cachedGenerationMode }, 'Using cached generation mode');
return cachedGenerationMode;
}
const compositeInpaintMaskCanvas = this.getCompositeInpaintMaskCanvas(rect);
this.$isProcessing.set(true);
const compositeInpaintMaskImageData = canvasToImageData(compositeInpaintMaskCanvas);
const compositeInpaintMaskTransparency = getImageDataTransparency(compositeInpaintMaskImageData);
this.$isProcessing.set(false);
this.log.debug({ rect }, 'Calculating generation mode');
const compositeRasterLayerCanvas = this.getCompositeRasterLayerCanvas(rect);
this.$isProcessing.set(true);
const compositeRasterLayerImageData = canvasToImageData(compositeRasterLayerCanvas);
const compositeRasterLayerTransparency = getImageDataTransparency(compositeRasterLayerImageData);
const compositeRasterLayerTransparency = await this.getTransparency(
rasterLayerAdapters,
rect,
compositeRasterLayerHash
);
const compositeInpaintMaskTransparency = await this.getTransparency(
inpaintMaskAdapters,
rect,
compositeInpaintMaskHash
);
this.$isProcessing.set(false);
let generationMode: GenerationMode;
@@ -447,7 +517,7 @@ export class CanvasCompositorModule extends CanvasModuleBase {
this.manager.cache.generationModeCache.set(hash, generationMode);
return generationMode;
}
};
repr = () => {
return {

View File

@@ -12,17 +12,26 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import type { CanvasSegmentAnythingModule } from 'features/controlLayers/konva/CanvasSegmentAnythingModule';
import { getKonvaNodeDebugAttrs, getRectIntersection } from 'features/controlLayers/konva/util';
import { selectIsolatedLayerPreview } from 'features/controlLayers/store/canvasSettingsSlice';
import {
buildSelectIsHidden,
selectIsolatedLayerPreview,
selectIsolatedStagingPreview,
} from 'features/controlLayers/store/canvasSettingsSlice';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
buildSelectIsSelected,
getSelectIsTypeHidden,
selectBboxRect,
selectCanvasSlice,
selectEntity,
} from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier, CanvasRenderableEntityState, Rect } from 'features/controlLayers/store/types';
import {
type CanvasEntityIdentifier,
type CanvasRenderableEntityState,
isRasterLayerEntityIdentifier,
type Rect,
} from 'features/controlLayers/store/types';
import Konva from 'konva';
import { atom, computed } from 'nanostores';
import { atom } from 'nanostores';
import rafThrottle from 'raf-throttle';
import type { Logger } from 'roarr';
import type { ImageDTO } from 'services/api/types';
@@ -97,7 +106,10 @@ export abstract class CanvasEntityAdapterBase<
abstract getCanvas: (rect?: Rect) => HTMLCanvasElement;
/**
* Gets a hashable representation of the entity's state.
* Gets a hashable representation of the entity's _renderable_ state. This should exclude any properties that are not
* relevant to rendering the entity.
*
* This is used for caching.
*/
abstract getHashableState: () => SerializableObject;
@@ -172,7 +184,14 @@ export abstract class CanvasEntityAdapterBase<
}
};
selectIsHidden: Selector<RootState, boolean>;
/**
* A selector that selects whether the entity type is hidden.
*/
selectIsTypeHidden: Selector<RootState, boolean>;
/**
* A selector that selects whether the entity is selected.
*/
selectIsSelected: Selector<RootState, boolean>;
/**
@@ -206,17 +225,11 @@ export abstract class CanvasEntityAdapterBase<
/**
* Whether this entity is hidden. This is synced with the entity's group type visibility.
*/
$isHidden = atom(false);
$isEntityTypeHidden = atom(false);
/**
* Whether this entity is empty. This is computed based on the entity's objects.
*/
$isEmpty = atom(true);
/**
* Whether this entity is interactable. This is computed based on the entity's locked, disabled, and hidden states.
*/
$isInteractable = computed([this.$isLocked, this.$isDisabled, this.$isHidden], (isLocked, isDisabled, isHidden) => {
return !isLocked && !isDisabled && !isHidden;
});
/**
* A cache of the entity's canvas element. This is generated from a clone of the entity's Konva layer.
*/
@@ -257,22 +270,25 @@ export abstract class CanvasEntityAdapterBase<
assert(state !== undefined, 'Missing entity state on creation');
this.state = state;
this.selectIsHidden = buildSelectIsHidden(this.entityIdentifier);
this.selectIsTypeHidden = getSelectIsTypeHidden(this.entityIdentifier.type);
this.selectIsSelected = buildSelectIsSelected(this.entityIdentifier);
/**
* There are a number of reason we may need to show or hide a layer:
* - The entity is enabled/disabled
* - The entity type is hidden/shown
* - Staging status changes and `isolatedStagingPreview` is enabled
* - Global filtering status changes and `isolatedFilteringPreview` is enabled
* - Global transforming status changes and `isolatedTransformingPreview` is enabled
* - The entity is selected or deselected (only selected and onscreen entities are rendered)
* - `isolatedStagingPreview` is enabled and we start or stop staging
* - `isolatedLayerPreview` is enabled and we start or stop filtering, transforming, select-object-ing
* - The entity is selected or deselected (only selected and onscreen entities are rendered as a perf optimization)
*/
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectIsHidden, this.syncVisibility));
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectIsTypeHidden, this.syncVisibility));
this.subscriptions.add(
this.manager.stateApi.createStoreSubscription(selectIsolatedLayerPreview, this.syncVisibility)
);
this.subscriptions.add(
this.manager.stateApi.createStoreSubscription(selectIsolatedStagingPreview, this.syncVisibility)
);
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectIsStaging, this.syncVisibility));
this.subscriptions.add(this.manager.stateApi.$filteringAdapter.listen(this.syncVisibility));
this.subscriptions.add(this.manager.stateApi.$transformingAdapter.listen(this.syncVisibility));
this.subscriptions.add(this.manager.stateApi.$segmentingAdapter.listen(this.syncVisibility));
@@ -282,7 +298,9 @@ export abstract class CanvasEntityAdapterBase<
* The tool preview may need to be updated when the entity is locked or disabled. For example, when we disable the
* entity, we should hide the tool preview & change the cursor.
*/
this.subscriptions.add(this.$isInteractable.subscribe(this.manager.tool.render));
this.subscriptions.add(this.$isDisabled.subscribe(this.manager.tool.render));
this.subscriptions.add(this.$isLocked.subscribe(this.manager.tool.render));
this.subscriptions.add(this.$isEntityTypeHidden.subscribe(this.manager.tool.render));
/**
* When the stage is transformed in any way (panning, zooming, resizing) or the entity is moved, we need to update
@@ -401,10 +419,9 @@ export abstract class CanvasEntityAdapterBase<
*/
syncIsEnabled = () => {
this.log.trace('Updating visibility');
this.konva.layer.visible(this.state.isEnabled);
this.renderer.syncKonvaCache(this.state.isEnabled);
this.transformer.syncInteractionState();
this.$isDisabled.set(!this.state.isEnabled);
this.syncVisibility();
this.transformer.syncInteractionState();
};
/**
@@ -416,6 +433,7 @@ export abstract class CanvasEntityAdapterBase<
if (didRender) {
// If the objects have changed, we need to recalculate the transformer's bounding box.
this.transformer.requestRectCalculation();
this.transformer.syncInteractionState();
}
};
@@ -434,45 +452,70 @@ export abstract class CanvasEntityAdapterBase<
};
syncVisibility = rafThrottle(() => {
// Handle the base hidden state
if (this.manager.stateApi.runSelector(this.selectIsHidden)) {
/**
* If the entity type is hidden, so should the entity be hidden.
*/
if (this.manager.stateApi.runSelector(this.selectIsTypeHidden)) {
this.setVisibility(false);
return;
}
const isolatedLayerPreview = this.manager.stateApi.runSelector(selectIsolatedLayerPreview);
// Handle isolated preview modes - if another entity is filtering or transforming, we may need to hide this entity.
if (isolatedLayerPreview) {
const filteringEntityIdentifier = this.manager.stateApi.$filteringAdapter.get()?.entityIdentifier;
if (filteringEntityIdentifier && filteringEntityIdentifier.id !== this.id) {
if (this.manager.stateApi.runSelector(selectIsolatedStagingPreview)) {
/**
* When staging w/ isolatedStagingPreview enabled, we only show raster layers.
*
* This allows the user to easily see how the new generation fits in with the rest of the canvas without the
* other layer types getting in the way.
*/
const isStaging = this.manager.stateApi.runSelector(selectIsStaging);
const isRasterLayer = isRasterLayerEntityIdentifier(this.entityIdentifier);
if (isStaging && !isRasterLayer) {
this.setVisibility(false);
return;
}
}
if (isolatedLayerPreview) {
const transformingEntity = this.manager.stateApi.$transformingAdapter.get();
if (this.manager.stateApi.runSelector(selectIsolatedLayerPreview)) {
/**
* Handle isolated preview modes - if another entity is filtering, transforming, or select-object-ing, we may need
* to hide this entity.
*/
const filteringAdapter = this.manager.stateApi.$filteringAdapter.get();
if (filteringAdapter && filteringAdapter !== this) {
this.setVisibility(false);
return;
}
const transformingAdapter = this.manager.stateApi.$transformingAdapter.get();
if (
transformingEntity &&
transformingEntity.entityIdentifier.id !== this.id &&
transformingAdapter &&
transformingAdapter !== this &&
// Silent transforms should be transparent to the user, so we don't need to hide the entity.
!transformingEntity.transformer.$silentTransform.get()
!transformingAdapter.transformer.$silentTransform.get()
) {
this.setVisibility(false);
return;
}
}
if (isolatedLayerPreview) {
const segmentingEntity = this.manager.stateApi.$segmentingAdapter.get();
if (segmentingEntity && segmentingEntity.entityIdentifier.id !== this.id) {
const segmentingAdapter = this.manager.stateApi.$segmentingAdapter.get();
if (segmentingAdapter && segmentingAdapter !== this) {
this.setVisibility(false);
return;
}
}
// If the entity is not selected and offscreen, we can hide it
/**
* Disabled entities should be hidden.
*/
if (this.$isDisabled.get()) {
this.setVisibility(false);
return;
}
/**
* When the entity is offscreen and not selected, we should hide it. If it is selected and offscreen, it still needs
* to be visible so the user can interact with it.
*/
if (!this.$isOnScreen.get() && !this.manager.stateApi.getIsSelected(this.entityIdentifier.id)) {
this.setVisibility(false);
return;
@@ -482,17 +525,30 @@ export abstract class CanvasEntityAdapterBase<
});
setVisibility = (isVisible: boolean) => {
const isHidden = this.$isHidden.get();
const isLayerVisible = this.konva.layer.visible();
if (isHidden === !isVisible && isLayerVisible === isVisible) {
if (isLayerVisible === isVisible) {
// No change
return;
}
this.log.trace(isVisible ? 'Showing' : 'Hiding');
this.$isHidden.set(!isVisible);
this.konva.layer.visible(isVisible);
if (isVisible) {
/**
* When a layer is created and initially not visible, its compositing rect won't be set up properly. Then, when
* we show it in this method, it the layer will not render as it should.
*
* For example, if an inpaint mask is created via select-object while the isolated layer preview feature is
* enabled, it will be hidden on its first render, and the compositing rect will not be sized/positioned/filled.
* When next show the layer, the its underlying objects will be rendered directly, without the compositing rect
* providing the correct fill.
*
* The simplest way to ensure this doesn't happen is to always update the compositing rect when showing the layer.
*/
this.renderer.updateCompositingRectSize();
this.renderer.updateCompositingRectPosition();
this.renderer.updateCompositingRectFill();
}
this.renderer.syncKonvaCache();
};
@@ -502,8 +558,8 @@ export abstract class CanvasEntityAdapterBase<
syncIsLocked = () => {
// The only thing we need to do is update the transformer's interaction state. For tool interactions, like drawing
// shapes, we defer to the CanvasToolModule to handle the locked state.
this.transformer.syncInteractionState();
this.$isLocked.set(this.state.isLocked);
this.transformer.syncInteractionState();
};
/**
@@ -563,9 +619,8 @@ export abstract class CanvasEntityAdapterBase<
hasCache: this.$canvasCache.get() !== null,
isLocked: this.$isLocked.get(),
isDisabled: this.$isDisabled.get(),
isHidden: this.$isHidden.get(),
isEntityTypeHidden: this.$isEntityTypeHidden.get(),
isEmpty: this.$isEmpty.get(),
isInteractable: this.$isInteractable.get(),
isOnScreen: this.$isOnScreen.get(),
intersectsBbox: this.$intersectsBbox.get(),
konva: getKonvaNodeDebugAttrs(this.konva.layer),

View File

@@ -78,7 +78,12 @@ export class CanvasEntityAdapterControlLayer extends CanvasEntityAdapterBase<
};
getHashableState = (): SerializableObject => {
const keysToOmit: (keyof CanvasControlLayerState)[] = ['name', 'controlAdapter', 'withTransparencyEffect'];
const keysToOmit: (keyof CanvasControlLayerState)[] = [
'name',
'controlAdapter',
'withTransparencyEffect',
'isLocked',
];
return omit(this.state, keysToOmit);
};
}

View File

@@ -70,7 +70,7 @@ export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase<
};
getHashableState = (): SerializableObject => {
const keysToOmit: (keyof CanvasInpaintMaskState)[] = ['fill', 'name', 'opacity'];
const keysToOmit: (keyof CanvasInpaintMaskState)[] = ['fill', 'name', 'opacity', 'isLocked'];
return omit(this.state, keysToOmit);
};

View File

@@ -71,7 +71,7 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase<
};
getHashableState = (): SerializableObject => {
const keysToOmit: (keyof CanvasRasterLayerState)[] = ['name'];
const keysToOmit: (keyof CanvasRasterLayerState)[] = ['name', 'isLocked'];
return omit(this.state, keysToOmit);
};
}

View File

@@ -70,7 +70,16 @@ export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase
};
getHashableState = (): SerializableObject => {
const keysToOmit: (keyof CanvasRegionalGuidanceState)[] = ['fill', 'name', 'opacity'];
const keysToOmit: (keyof CanvasRegionalGuidanceState)[] = [
'fill',
'name',
'opacity',
'isLocked',
'autoNegative',
'positivePrompt',
'negativePrompt',
'referenceImages',
];
return omit(this.state, keysToOmit);
};

View File

@@ -9,7 +9,7 @@ import { addCoords, getKonvaNodeDebugAttrs, getPrefixedId } from 'features/contr
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
import type { FilterConfig } from 'features/controlLayers/store/filters';
import { getFilterForModel, IMAGE_FILTERS } from 'features/controlLayers/store/filters';
import type { CanvasEntityType, CanvasImageState } from 'features/controlLayers/store/types';
import type { CanvasImageState, CanvasRenderableEntityType } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import Konva from 'konva';
import { debounce } from 'lodash-es';
@@ -350,7 +350,7 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
* Saves the filtered image as a new entity of the given type.
* @param type The type of entity to save the filtered image as.
*/
saveAs = (type: Exclude<CanvasEntityType, 'reference_image'>) => {
saveAs = (type: CanvasRenderableEntityType) => {
const imageState = this.$imageState.get();
if (!imageState) {
this.log.warn('No image state to apply filter to');
@@ -386,10 +386,6 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
default:
assert<Equals<typeof type, never>>(false);
}
// Final cleanup and teardown, returning user to main canvas UI
this.resetEphemeralState();
this.teardown();
};
resetEphemeralState = () => {

View File

@@ -219,10 +219,18 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
return;
}
this.log.trace('Updating compositing rect fill');
if (!this.parent.konva.layer.visible()) {
return;
}
assert(this.konva.compositing, 'Missing compositing rect');
assert(this.parent.state.type === 'inpaint_mask' || this.parent.state.type === 'regional_guidance');
if (
!this.konva.compositing ||
(this.parent.state.type !== 'inpaint_mask' && this.parent.state.type !== 'regional_guidance')
) {
return;
}
this.log.trace('Updating compositing rect fill');
const fill = this.parent.state.fill;
@@ -244,9 +252,18 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
return;
}
this.log.trace('Updating compositing rect size');
if (!this.parent.konva.layer.visible()) {
return;
}
assert(this.konva.compositing, 'Missing compositing rect');
if (
!this.konva.compositing ||
(this.parent.state.type !== 'inpaint_mask' && this.parent.state.type !== 'regional_guidance')
) {
return;
}
this.log.trace('Updating compositing rect size');
const scale = this.manager.stage.unscale(1);
@@ -262,9 +279,18 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
return;
}
this.log.trace('Updating compositing rect position');
if (!this.parent.konva.layer.visible()) {
return;
}
assert(this.konva.compositing, 'Missing compositing rect');
if (
!this.konva.compositing ||
(this.parent.state.type !== 'inpaint_mask' && this.parent.state.type !== 'regional_guidance')
) {
return;
}
this.log.trace('Updating compositing rect position');
this.konva.compositing.rect.setAttrs({
...this.manager.stage.getScaledStageRect(),
@@ -272,6 +298,10 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
};
updateOpacity = throttle(() => {
if (!this.parent.konva.layer.visible()) {
return;
}
this.log.trace('Updating opacity');
const opacity = this.parent.state.opacity;

View File

@@ -296,6 +296,14 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
this.syncInteractionState();
};
syncCursorStyle = () => {
if (!this.parent.renderer.hasObjects()) {
this.manager.stage.setCursor('not-allowed');
} else {
this.manager.stage.setCursor('default');
}
};
anchorStyleFunc = (anchor: Konva.Rect): void => {
// Give the rotater special styling
if (anchor.hasName('rotater')) {
@@ -591,6 +599,13 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
syncInteractionState = () => {
this.log.trace('Syncing interaction state');
if (this.manager.stagingArea.$isStaging.get()) {
// While staging, the layer should not be interactable
this.parent.konva.layer.listening(false);
this._setInteractionMode('off');
return;
}
if (this.parent.segmentAnything?.$isSegmenting.get()) {
// When segmenting, the layer should listen but the transformer should not be interactable
this.parent.konva.layer.listening(true);
@@ -640,6 +655,13 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
return;
}
if (this.parent.$isLocked.get()) {
// The layer is locked, it should not be interactable
this.parent.konva.layer.listening(false);
this._setInteractionMode('off');
return;
}
if (!this.$isTransforming.get() && tool === 'move') {
// We are moving this layer, it must be listening
this.parent.konva.layer.listening(true);

View File

@@ -2,9 +2,15 @@ import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/kon
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance';
import type { CanvasRenderableEntityType } from 'features/controlLayers/store/types';
export type CanvasEntityAdapter =
| CanvasEntityAdapterRasterLayer
| CanvasEntityAdapterControlLayer
| CanvasEntityAdapterInpaintMask
| CanvasEntityAdapterRegionalGuidance;
export type CanvasEntityAdapterFromType<T extends CanvasRenderableEntityType> = Extract<
CanvasEntityAdapter,
{ state: { type: T } }
>;

View File

@@ -8,7 +8,7 @@ import { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/Ca
import { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
import { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
import type { CanvasEntityAdapter, CanvasEntityAdapterFromType } from 'features/controlLayers/konva/CanvasEntity/types';
import { CanvasEntityRendererModule } from 'features/controlLayers/konva/CanvasEntityRendererModule';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { CanvasProgressImageModule } from 'features/controlLayers/konva/CanvasProgressImageModule';
@@ -18,7 +18,11 @@ import { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/Canvas
import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
import type {
CanvasEntityIdentifier,
CanvasRenderableEntityIdentifier,
CanvasRenderableEntityType,
} from 'features/controlLayers/store/types';
import {
isControlLayerEntityIdentifier,
isInpaintMaskEntityIdentifier,
@@ -135,44 +139,35 @@ export class CanvasManager extends CanvasModuleBase {
this.konva.previewLayer.add(this.tool.konva.group);
}
getAdapter = <T extends CanvasEntityType = CanvasEntityType>(
getAdapter = <T extends CanvasRenderableEntityType = CanvasRenderableEntityType>(
entityIdentifier: CanvasEntityIdentifier<T>
): Extract<CanvasEntityAdapter, { state: { type: T } }> | null => {
): CanvasEntityAdapterFromType<T> | null => {
let adapter: CanvasEntityAdapter | undefined;
switch (entityIdentifier.type) {
case 'raster_layer':
return (
(this.adapters.rasterLayers.get(entityIdentifier.id) as Extract<
CanvasEntityAdapter,
{ state: { type: T } }
>) ?? null
);
adapter = this.adapters.rasterLayers.get(entityIdentifier.id);
break;
case 'control_layer':
return (
(this.adapters.controlLayers.get(entityIdentifier.id) as Extract<
CanvasEntityAdapter,
{ state: { type: T } }
>) ?? null
);
adapter = this.adapters.controlLayers.get(entityIdentifier.id);
break;
case 'regional_guidance':
return (
(this.adapters.regionMasks.get(entityIdentifier.id) as Extract<
CanvasEntityAdapter,
{ state: { type: T } }
>) ?? null
);
adapter = this.adapters.regionMasks.get(entityIdentifier.id);
break;
case 'inpaint_mask':
return (
(this.adapters.inpaintMasks.get(entityIdentifier.id) as Extract<
CanvasEntityAdapter,
{ state: { type: T } }
>) ?? null
);
adapter = this.adapters.inpaintMasks.get(entityIdentifier.id);
break;
default:
return null;
}
if (!adapter) {
return null;
}
return adapter as CanvasEntityAdapterFromType<T>;
};
deleteAdapter = (entityIdentifier: CanvasEntityIdentifier): boolean => {
deleteAdapter = (entityIdentifier: CanvasRenderableEntityIdentifier): boolean => {
switch (entityIdentifier.type) {
case 'raster_layer':
return this.adapters.rasterLayers.delete(entityIdentifier.id);
@@ -187,6 +182,18 @@ export class CanvasManager extends CanvasModuleBase {
}
};
getAdapters = (entityIdentifiers: CanvasRenderableEntityIdentifier[]): CanvasEntityAdapter[] => {
const adapters: CanvasEntityAdapter[] = [];
for (const entityIdentifier of entityIdentifiers) {
const adapter = this.getAdapter(entityIdentifier);
if (!adapter) {
continue;
}
adapters.push(adapter);
}
return adapters;
};
getAllAdapters = (): CanvasEntityAdapter[] => {
return [
...this.adapters.rasterLayers.values(),
@@ -196,7 +203,7 @@ export class CanvasManager extends CanvasModuleBase {
];
};
createAdapter = (entityIdentifier: CanvasEntityIdentifier): CanvasEntityAdapter => {
createAdapter = (entityIdentifier: CanvasRenderableEntityIdentifier): CanvasEntityAdapter => {
if (isRasterLayerEntityIdentifier(entityIdentifier)) {
const adapter = new CanvasEntityAdapterRasterLayer(entityIdentifier, this);
this.adapters.rasterLayers.set(adapter.id, adapter);

View File

@@ -59,7 +59,9 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
this.hasActiveGeneration = true;
} else {
this.hasActiveGeneration = false;
this.$lastProgressEvent.set(null);
if (!this.manager.stagingArea.$isStaging.get()) {
this.$lastProgressEvent.set(null);
}
}
})
);

View File

@@ -15,8 +15,8 @@ import {
} from 'features/controlLayers/konva/util';
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
import type {
CanvasEntityType,
CanvasImageState,
CanvasRenderableEntityType,
Coordinate,
RgbaColor,
SAMPointLabel,
@@ -697,7 +697,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
* Saves the segmented image as a new entity of the given type.
* @param type The type of entity to save the segmented image as.
*/
saveAs = (type: Exclude<CanvasEntityType, 'reference_image'>) => {
saveAs = (type: CanvasRenderableEntityType) => {
const imageState = this.$imageState.get();
if (!imageState) {
this.log.error('No image state to save as');
@@ -740,10 +740,6 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
default:
assert<Equals<typeof type, never>>(false);
}
// Final cleanup and teardown, returning user to main canvas UI
this.resetEphemeralState();
this.teardown();
};
/**

View File

@@ -1,14 +1,8 @@
import type { Property } from 'csstype';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import { getKonvaNodeDebugAttrs, getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util';
import type {
CanvasEntityIdentifier,
Coordinate,
Dimensions,
Rect,
StageAttrs,
} from 'features/controlLayers/store/types';
import { getKonvaNodeDebugAttrs, getPrefixedId } from 'features/controlLayers/konva/util';
import type { Coordinate, Dimensions, Rect, StageAttrs } from 'features/controlLayers/store/types';
import Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import { clamp } from 'lodash-es';
@@ -146,24 +140,6 @@ export class CanvasStageModule extends CanvasModuleBase {
}
};
getVisibleRect = (type?: Exclude<CanvasEntityIdentifier['type'], 'ip_adapter'>): Rect => {
const rects = [];
for (const adapter of this.manager.getAllAdapters()) {
if (!adapter.state.isEnabled) {
continue;
}
if (type && adapter.state.type !== type) {
continue;
}
if (adapter.renderer.hasObjects()) {
rects.push(adapter.transformer.getRelativeRect());
}
}
return getRectUnion(...rects);
};
/**
* Fits the bbox to the stage. This will center the bbox and scale it to fit the stage with some padding.
*/
@@ -177,7 +153,7 @@ export class CanvasStageModule extends CanvasModuleBase {
* Fits the visible canvas to the stage. This will center the canvas and scale it to fit the stage with some padding.
*/
fitLayersToStage = (): void => {
const rect = this.getVisibleRect();
const rect = this.manager.compositor.getVisibleRectOfType();
if (rect.width === 0 || rect.height === 0) {
this.fitBboxToStage();
} else {

View File

@@ -36,7 +36,6 @@ import {
selectGridSize,
} from 'features/controlLayers/store/selectors';
import type {
CanvasEntityType,
CanvasState,
EntityBrushLineAddedPayload,
EntityEraserLineAddedPayload,
@@ -47,7 +46,7 @@ import type {
Rect,
RgbaColor,
} from 'features/controlLayers/store/types';
import { RGBA_BLACK } from 'features/controlLayers/store/types';
import { isRenderableEntityIdentifier, RGBA_BLACK } from 'features/controlLayers/store/types';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { atom, computed } from 'nanostores';
import type { Logger } from 'roarr';
@@ -546,24 +545,6 @@ export class CanvasStateApiModule extends CanvasModuleBase {
return this.getCanvasState().selectedEntityIdentifier?.id === id;
};
/**
* Checks if an entity type is hidden. Individual entities are not hidden; the entire entity type is hidden.
*/
getIsTypeHidden = (type: CanvasEntityType): boolean => {
switch (type) {
case 'raster_layer':
return this.getRasterLayersState().isHidden;
case 'control_layer':
return this.getControlLayersState().isHidden;
case 'inpaint_mask':
return this.getInpaintMasksState().isHidden;
case 'regional_guidance':
return this.getRegionsState().isHidden;
default:
assert(false, 'Unhandled entity type');
}
};
/**
* Gets the number of entities that are currently rendered on the canvas.
*/
@@ -583,10 +564,13 @@ export class CanvasStateApiModule extends CanvasModuleBase {
*/
getSelectedEntityAdapter = (): CanvasEntityAdapter | null => {
const state = this.getCanvasState();
if (state.selectedEntityIdentifier) {
return this.manager.getAdapter(state.selectedEntityIdentifier);
if (!state.selectedEntityIdentifier) {
return null;
}
return null;
if (!isRenderableEntityIdentifier(state.selectedEntityIdentifier)) {
return null;
}
return this.manager.getAdapter(state.selectedEntityIdentifier);
};
/**
@@ -696,4 +680,20 @@ export class CanvasStateApiModule extends CanvasModuleBase {
* Whether the shift key is currently pressed.
*/
$shiftKey = $shift;
repr = () => {
return {
id: this.id,
type: this.type,
path: this.path,
$filteringAdapter: this.$filteringAdapter.get()?.entityIdentifier,
$isFiltering: this.$isFiltering.get(),
$transformingAdapter: this.$transformingAdapter.get()?.entityIdentifier,
$isTransforming: this.$isTransforming.get(),
$rasterizingAdapter: this.$rasterizingAdapter.get()?.entityIdentifier,
$isRasterizing: this.$isRasterizing.get(),
$segmentingAdapter: this.$segmentingAdapter.get()?.entityIdentifier,
$isSegmenting: this.$isSegmenting.get(),
};
};
}

View File

@@ -390,7 +390,7 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
};
fitToLayers = (): void => {
const visibleRect = this.manager.stage.getVisibleRect();
const visibleRect = this.manager.compositor.getVisibleRectOfType();
// Can't fit the bbox to nothing
if (visibleRect.height === 0 || visibleRect.width === 0) {

View File

@@ -2,7 +2,6 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
import type { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/CanvasToolModule';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { noop } from 'lodash-es';
import type { Logger } from 'roarr';
export class CanvasMoveToolModule extends CanvasModuleBase {
@@ -24,8 +23,13 @@ export class CanvasMoveToolModule extends CanvasModuleBase {
this.log.debug('Creating module');
}
/**
* This is a noop. Entity transformers handle cursor style when the move tool is active.
*/
syncCursorStyle = noop;
syncCursorStyle = () => {
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (!selectedEntity) {
this.manager.stage.setCursor('not-allowed');
} else {
// The cursor is on an entity, defer to transformer to handle the cursor
selectedEntity.transformer.syncCursorStyle();
}
};
}

View File

@@ -161,6 +161,7 @@ export class CanvasToolModule extends CanvasModuleBase {
const tool = this.$tool.get();
const segmentingAdapter = this.manager.stateApi.$segmentingAdapter.get();
const transformingAdapter = this.manager.stateApi.$transformingAdapter.get();
const selectedEntityAdapter = this.manager.stateApi.getSelectedEntityAdapter();
if (this.manager.stage.getIsDragging()) {
this.tools.view.syncCursorStyle();
@@ -169,7 +170,7 @@ export class CanvasToolModule extends CanvasModuleBase {
} else if (segmentingAdapter) {
segmentingAdapter.segmentAnything.syncCursorStyle();
} else if (transformingAdapter) {
// The transformer handles cursor style via events
transformingAdapter.transformer.syncCursorStyle();
} else if (this.manager.stateApi.$isFiltering.get()) {
stage.setCursor('not-allowed');
} else if (this.manager.stagingArea.$isStaging.get()) {
@@ -178,7 +179,11 @@ export class CanvasToolModule extends CanvasModuleBase {
this.tools.bbox.syncCursorStyle();
} else if (this.manager.stateApi.getRenderedEntityCount() === 0) {
stage.setCursor('not-allowed');
} else if (!this.manager.stateApi.getSelectedEntityAdapter()?.$isInteractable.get()) {
} else if (selectedEntityAdapter?.$isDisabled.get()) {
stage.setCursor('not-allowed');
} else if (selectedEntityAdapter?.$isEntityTypeHidden.get()) {
stage.setCursor('not-allowed');
} else if (selectedEntityAdapter?.$isLocked.get()) {
stage.setCursor('not-allowed');
} else if (tool === 'brush') {
this.tools.brush.syncCursorStyle();
@@ -301,7 +306,15 @@ export class CanvasToolModule extends CanvasModuleBase {
return false;
}
if (!selectedEntity.$isInteractable.get()) {
if (selectedEntity.$isDisabled.get()) {
return false;
}
if (selectedEntity.$isEntityTypeHidden.get()) {
return false;
}
if (selectedEntity.$isLocked.get()) {
return false;
}

View File

@@ -123,27 +123,28 @@ export const canvasSlice = createSlice({
id: string;
overrides?: Partial<CanvasRasterLayerState>;
isSelected?: boolean;
isMergingVisible?: boolean;
mergedEntitiesToDelete?: string[];
}>
) => {
const { id, overrides, isSelected, isMergingVisible } = action.payload;
const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
const entityState = getRasterLayerState(id, overrides);
if (isMergingVisible) {
// When merging visible, we delete all disabled layers
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => !layer.isEnabled);
}
state.rasterLayers.entities.push(entityState);
if (isSelected) {
if (mergedEntitiesToDelete.length > 0) {
state.rasterLayers.entities = state.rasterLayers.entities.filter(
(entity) => !mergedEntitiesToDelete.includes(entity.id)
);
}
if (isSelected || mergedEntitiesToDelete.length > 0) {
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
}
},
prepare: (payload: {
overrides?: Partial<CanvasRasterLayerState>;
isSelected?: boolean;
isMergingVisible?: boolean;
mergedEntitiesToDelete?: string[];
}) => ({
payload: { ...payload, id: getPrefixedId('raster_layer') },
}),
@@ -271,19 +272,34 @@ export const canvasSlice = createSlice({
controlLayerAdded: {
reducer: (
state,
action: PayloadAction<{ id: string; overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }>
action: PayloadAction<{
id: string;
overrides?: Partial<CanvasControlLayerState>;
isSelected?: boolean;
mergedEntitiesToDelete?: string[];
}>
) => {
const { id, overrides, isSelected } = action.payload;
const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
const entityState = getControlLayerState(id, overrides);
state.controlLayers.entities.push(entityState);
if (isSelected) {
if (mergedEntitiesToDelete.length > 0) {
state.controlLayers.entities = state.controlLayers.entities.filter(
(entity) => !mergedEntitiesToDelete.includes(entity.id)
);
}
if (isSelected || mergedEntitiesToDelete.length > 0) {
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
}
},
prepare: (payload: { overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }) => ({
prepare: (payload: {
overrides?: Partial<CanvasControlLayerState>;
isSelected?: boolean;
mergedEntitiesToDelete?: string[];
}) => ({
payload: { ...payload, id: getPrefixedId('control_layer') },
}),
},
@@ -595,19 +611,34 @@ export const canvasSlice = createSlice({
rgAdded: {
reducer: (
state,
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }>
action: PayloadAction<{
id: string;
overrides?: Partial<CanvasRegionalGuidanceState>;
isSelected?: boolean;
mergedEntitiesToDelete?: string[];
}>
) => {
const { id, overrides, isSelected } = action.payload;
const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
const entityState = getRegionalGuidanceState(id, overrides);
state.regionalGuidance.entities.push(entityState);
if (isSelected) {
if (mergedEntitiesToDelete.length > 0) {
state.regionalGuidance.entities = state.regionalGuidance.entities.filter(
(entity) => !mergedEntitiesToDelete.includes(entity.id)
);
}
if (isSelected || mergedEntitiesToDelete.length > 0) {
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
}
},
prepare: (payload?: { overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }) => ({
prepare: (payload?: {
overrides?: Partial<CanvasRegionalGuidanceState>;
isSelected?: boolean;
mergedEntitiesToDelete?: string[];
}) => ({
payload: { ...payload, id: getPrefixedId('regional_guidance') },
}),
},
@@ -822,28 +853,29 @@ export const canvasSlice = createSlice({
id: string;
overrides?: Partial<CanvasInpaintMaskState>;
isSelected?: boolean;
isMergingVisible?: boolean;
mergedEntitiesToDelete?: string[];
}>
) => {
const { id, overrides, isSelected, isMergingVisible } = action.payload;
const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
const entityState = getInpaintMaskState(id, overrides);
if (isMergingVisible) {
// When merging visible, we delete all disabled layers
state.inpaintMasks.entities = state.inpaintMasks.entities.filter((layer) => !layer.isEnabled);
}
state.inpaintMasks.entities.push(entityState);
if (isSelected) {
if (mergedEntitiesToDelete.length > 0) {
state.inpaintMasks.entities = state.inpaintMasks.entities.filter(
(entity) => !mergedEntitiesToDelete.includes(entity.id)
);
}
if (isSelected || mergedEntitiesToDelete.length > 0) {
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
}
},
prepare: (payload?: {
overrides?: Partial<CanvasInpaintMaskState>;
isSelected?: boolean;
isMergingVisible?: boolean;
mergedEntitiesToDelete?: string[];
}) => ({
payload: { ...payload, id: getPrefixedId('inpaint_mask') },
}),

View File

@@ -1,21 +1,21 @@
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import { selectIsolatedStagingPreview } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
CanvasEntityState,
CanvasEntityType,
CanvasInpaintMaskState,
CanvasMetadata,
CanvasRasterLayerState,
CanvasRegionalGuidanceState,
CanvasRenderableEntityIdentifier,
CanvasRenderableEntityState,
CanvasRenderableEntityType,
CanvasState,
} from 'features/controlLayers/store/types';
import { isRasterLayerEntityIdentifier } from 'features/controlLayers/store/types';
import { getGridSize, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
/**
@@ -43,23 +43,25 @@ const selectEntityCountAll = createSelector(selectCanvasSlice, (canvas) => {
);
});
const selectActiveRasterLayerEntities = createSelector(selectCanvasSlice, (canvas) =>
canvas.rasterLayers.entities.filter((e) => e.isEnabled && e.objects.length > 0)
const isVisibleEntity = (entity: CanvasRenderableEntityState) => entity.isEnabled && entity.objects.length > 0;
export const selectActiveRasterLayerEntities = createSelector(selectCanvasSlice, (canvas) =>
canvas.rasterLayers.entities.filter(isVisibleEntity)
);
const selectActiveControlLayerEntities = createSelector(selectCanvasSlice, (canvas) =>
canvas.controlLayers.entities.filter((e) => e.isEnabled && e.objects.length > 0)
export const selectActiveControlLayerEntities = createSelector(selectCanvasSlice, (canvas) =>
canvas.controlLayers.entities.filter(isVisibleEntity)
);
const selectActiveInpaintMaskEntities = createSelector(selectCanvasSlice, (canvas) =>
canvas.inpaintMasks.entities.filter((e) => e.isEnabled && e.objects.length > 0)
export const selectActiveInpaintMaskEntities = createSelector(selectCanvasSlice, (canvas) =>
canvas.inpaintMasks.entities.filter(isVisibleEntity)
);
const selectActiveRegionalGuidanceEntities = createSelector(selectCanvasSlice, (canvas) =>
canvas.regionalGuidance.entities.filter((e) => e.isEnabled && e.objects.length > 0)
export const selectActiveRegionalGuidanceEntities = createSelector(selectCanvasSlice, (canvas) =>
canvas.regionalGuidance.entities.filter(isVisibleEntity)
);
const selectActiveIPAdapterEntities = createSelector(selectCanvasSlice, (canvas) =>
export const selectActiveReferenceImageEntities = createSelector(selectCanvasSlice, (canvas) =>
canvas.referenceImages.entities.filter((e) => e.isEnabled)
);
@@ -78,7 +80,7 @@ export const selectEntityCountActive = createSelector(
selectActiveControlLayerEntities,
selectActiveInpaintMaskEntities,
selectActiveRegionalGuidanceEntities,
selectActiveIPAdapterEntities,
selectActiveReferenceImageEntities,
(
activeRasterLayerEntities,
activeControlLayerEntities,
@@ -148,7 +150,46 @@ export function selectEntity<T extends CanvasEntityIdentifier>(
}
// This cast is safe, but TS seems to be unable to infer the type
return entity as Extract<CanvasEntityState, T>;
return entity as Extract<CanvasEntityState, T> | undefined;
}
/**
* Selects the entity identifier for the entity that is below the given entity in terms of draw order.
*/
export function selectEntityIdentifierBelowThisOne<T extends CanvasRenderableEntityIdentifier>(
state: CanvasState,
entityIdentifier: T
): Extract<CanvasEntityState, T> | undefined {
const { id, type } = entityIdentifier;
let entities: CanvasRenderableEntityState[];
switch (type) {
case 'raster_layer': {
entities = state.rasterLayers.entities;
break;
}
case 'control_layer': {
entities = state.controlLayers.entities;
break;
}
case 'inpaint_mask': {
entities = state.inpaintMasks.entities;
break;
}
case 'regional_guidance': {
entities = state.regionalGuidance.entities;
break;
}
}
// Must reverse to get the draw order
const reversedEntities = entities.toReversed();
const idx = reversedEntities.findIndex((entity) => entity.id === id);
const entity = reversedEntities.at(idx + 1);
// This cast is safe, but TS seems to be unable to infer the type
return entity as Extract<CanvasEntityState, T> | undefined;
}
export const selectRasterLayerEntities = createSelector(selectCanvasSlice, (canvas) => canvas.rasterLayers.entities);
@@ -290,7 +331,7 @@ const selectRegionalGuidanceIsHidden = createSelector(selectCanvasSlice, (canvas
/**
* Returns the hidden selector for the given entity type.
*/
const getSelectIsTypeHidden = (type: CanvasEntityType) => {
export const getSelectIsTypeHidden = (type: CanvasRenderableEntityType) => {
switch (type) {
case 'raster_layer':
return selectRasterLayersIsHidden;
@@ -301,44 +342,10 @@ const getSelectIsTypeHidden = (type: CanvasEntityType) => {
case 'regional_guidance':
return selectRegionalGuidanceIsHidden;
default:
assert(false, 'Unhandled entity type');
assert<Equals<typeof type, never>>(false, 'Unhandled entity type');
}
};
/**
* Builds a selector taht selects if the entity is hidden.
*/
export const buildSelectIsHidden = (entityIdentifier: CanvasEntityIdentifier) => {
const selectIsTypeHidden = getSelectIsTypeHidden(entityIdentifier.type);
return createSelector(
[selectCanvasSlice, selectIsTypeHidden, selectIsStaging, selectIsolatedStagingPreview],
(canvas, isTypeHidden, isStaging, isolatedStagingPreview) => {
const entity = selectEntity(canvas, entityIdentifier);
// An entity is hidden if:
// - The entity type is hidden
// - The entity is disabled
// - The entity is not a raster layer and we are staging and the option to show only raster layers is enabled
if (!entity) {
return true;
}
if (isTypeHidden) {
return true;
}
if (!entity.isEnabled) {
return true;
}
if (isStaging && isolatedStagingPreview) {
// When staging, we only show raster layers. This allows the user to easily see how the new generation fits in
// with the rest of the canvas without the masks and control layers getting in the way.
return !isRasterLayerEntityIdentifier(entityIdentifier);
}
return false;
}
);
};
/**
* Builds a selector taht selects if the entity is selected.
*/

View File

@@ -332,6 +332,7 @@ const zCanvasRenderableEntityState = z.discriminatedUnion('type', [
zCanvasInpaintMaskState,
]);
export type CanvasRenderableEntityState = z.infer<typeof zCanvasRenderableEntityState>;
export type CanvasRenderableEntityType = CanvasRenderableEntityState['type'];
const zCanvasEntityType = z.union([
zCanvasRasterLayerState.shape.type,
@@ -347,7 +348,7 @@ export const zCanvasEntityIdentifer = z.object({
type: zCanvasEntityType,
});
export type CanvasEntityIdentifier<T extends CanvasEntityType = CanvasEntityType> = { id: string; type: T };
export type CanvasRenderableEntityIdentifier = CanvasEntityIdentifier<CanvasRenderableEntityType>;
export type LoRA = {
id: string;
isEnabled: boolean;
@@ -465,7 +466,7 @@ export type EntityRasterizedPayload = EntityIdentifierPayload<{
export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';
function isRenderableEntityType(
export function isRenderableEntityType(
entityType: CanvasEntityState['type']
): entityType is CanvasRenderableEntityState['type'] {
return (
@@ -537,6 +538,12 @@ export function isRenderableEntity(entity: CanvasEntityState): entity is CanvasR
return isRenderableEntityType(entity.type);
}
export function isRenderableEntityIdentifier(
entityIdentifier: CanvasEntityIdentifier
): entityIdentifier is CanvasRenderableEntityIdentifier {
return isRenderableEntityType(entityIdentifier.type);
}
export const getEntityIdentifier = <T extends CanvasEntityType>(
entity: Extract<CanvasEntityState, { type: T }>
): CanvasEntityIdentifier<T> => {

View File

@@ -25,7 +25,7 @@ export const ImageMenuItemMetadataRecallActions = memo(() => {
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiArrowBendUpLeftBold />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label="Recall Metadata" />
<SubMenuButtonContent label={t('parameters.recallMetadata')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={remix} isDisabled={!hasMetadata}>

View File

@@ -106,7 +106,7 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => {
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label="New from Image" />
<SubMenuButtonContent label={t('controlLayers.newFromImage')} />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasWithRasterLayerFromImage} isDisabled={isBusy}>

View File

@@ -0,0 +1,45 @@
import { ExternalLink, Text, useToast } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { atom } from 'nanostores';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
const TOAST_ID = 'hfForbidden';
/**
* Tracks whether or not the HF Login toast is showing
*/
export const $isHFForbiddenToastOpen = atom<{ isEnabled: boolean; source?: string }>({ isEnabled: false });
export const useHFForbiddenToast = () => {
const { t } = useTranslation();
const toast = useToast();
const isHFForbiddenToastOpen = useStore($isHFForbiddenToastOpen);
useEffect(() => {
if (!isHFForbiddenToastOpen.isEnabled) {
toast.close(TOAST_ID);
return;
}
if (isHFForbiddenToastOpen.isEnabled) {
toast({
id: TOAST_ID,
title: t('modelManager.hfForbidden'),
description: (
<Text fontSize="md">
{t('modelManager.hfForbiddenErrorMessage')}
<ExternalLink
label={isHFForbiddenToastOpen.source || ''}
href={`https://huggingface.co/${isHFForbiddenToastOpen.source}`}
/>
</Text>
),
status: 'error',
isClosable: true,
duration: null,
onCloseComplete: () => $isHFForbiddenToastOpen.set({ isEnabled: false }),
});
}
}, [isHFForbiddenToastOpen, t, toast]);
};

View File

@@ -0,0 +1,93 @@
import { Button, Text, useToast } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch } from 'app/store/storeHooks';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { t } from 'i18next';
import { atom } from 'nanostores';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetHFTokenStatusQuery } from 'services/api/endpoints/models';
import type { S } from 'services/api/types';
const FEATURE_ID = 'hfToken';
const TOAST_ID = 'hfTokenLogin';
/**
* Tracks whether or not the HF Login toast is showing
*/
export const $isHFLoginToastOpen = atom<boolean>(false);
const getTitle = (token_status: S['HFTokenStatus']) => {
switch (token_status) {
case 'invalid':
return t('modelManager.hfTokenInvalid');
case 'unknown':
return t('modelManager.hfTokenUnableToVerify');
}
};
export const useHFLoginToast = () => {
const isEnabled = useFeatureStatus(FEATURE_ID);
const { data } = useGetHFTokenStatusQuery(isEnabled ? undefined : skipToken);
const toast = useToast();
const isHFLoginToastOpen = useStore($isHFLoginToastOpen);
useEffect(() => {
if (!isHFLoginToastOpen) {
toast.close(TOAST_ID);
return;
}
if (isHFLoginToastOpen && data) {
const title = getTitle(data);
toast({
id: TOAST_ID,
title,
description: <ToastDescription token_status={data} />,
status: 'error',
isClosable: true,
duration: null,
onCloseComplete: () => $isHFLoginToastOpen.set(false),
});
}
}, [isHFLoginToastOpen, data, toast]);
};
type Props = {
token_status: S['HFTokenStatus'];
};
const ToastDescription = ({ token_status }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const toast = useToast();
const onClick = useCallback(() => {
dispatch(setActiveTab('models'));
toast.close(FEATURE_ID);
}, [dispatch, toast]);
if (token_status === 'invalid') {
return (
<Text fontSize="md">
{t('modelManager.hfTokenInvalidErrorMessage')} {t('modelManager.hfTokenRequired')}{' '}
{t('modelManager.hfTokenInvalidErrorMessage2')}
<Button onClick={onClick} variant="link" color="base.50" flexGrow={0}>
{t('modelManager.modelManager')}.
</Button>
</Text>
);
}
if (token_status === 'unknown') {
return (
<Text fontSize="md">
{t('modelManager.hfTokenUnableToErrorMessage')}{' '}
<Button onClick={onClick} variant="link" color="base.50" flexGrow={0}>
{t('modelManager.modelManager')}.
</Button>
</Text>
);
}
};

View File

@@ -0,0 +1,80 @@
import {
Button,
ExternalLink,
Flex,
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
Input,
useToast,
} from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { $isHFLoginToastOpen } from 'features/modelManagerV2/hooks/useHFLoginToast';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import type { ChangeEvent } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetHFTokenStatusQuery, useSetHFTokenMutation } from 'services/api/endpoints/models';
export const HFToken = () => {
const { t } = useTranslation();
const isHFTokenEnabled = useFeatureStatus('hfToken');
const [token, setToken] = useState('');
const { currentData } = useGetHFTokenStatusQuery(isHFTokenEnabled ? undefined : skipToken);
const [trigger, { isLoading, isUninitialized }] = useSetHFTokenMutation();
const toast = useToast();
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setToken(e.target.value);
}, []);
const onClick = useCallback(() => {
trigger({ token })
.unwrap()
.then((res) => {
if (res === 'valid') {
setToken('');
toast({
title: t('modelManager.hfTokenSaved'),
status: 'success',
duration: 3000,
});
$isHFLoginToastOpen.set(false);
}
});
}, [t, toast, token, trigger]);
const error = useMemo(() => {
if (!currentData || isUninitialized || isLoading) {
return null;
}
if (currentData === 'invalid') {
return t('modelManager.hfTokenInvalidErrorMessage');
}
if (currentData === 'unknown') {
return t('modelManager.hfTokenUnableToVerifyErrorMessage');
}
return null;
}, [currentData, isLoading, isUninitialized, t]);
if (!currentData || currentData === 'valid') {
return null;
}
return (
<Flex borderRadius="base" w="full">
<FormControl isInvalid={!isUninitialized && Boolean(error)} orientation="vertical">
<FormLabel>{t('modelManager.hfTokenLabel')}</FormLabel>
<Flex gap={3} alignItems="center" w="full">
<Input type="password" value={token} onChange={onChange} />
<Button onClick={onClick} size="sm" isDisabled={token.trim().length === 0} isLoading={isLoading}>
{t('common.save')}
</Button>
</Flex>
<FormHelperText>
<ExternalLink label={t('modelManager.hfTokenHelperText')} href="https://huggingface.co/settings/tokens" />
</FormHelperText>
<FormErrorMessage>{error}</FormErrorMessage>
</FormControl>
</Flex>
);
};

View File

@@ -1,10 +1,13 @@
import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import type { ChangeEventHandler } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';
import { useGetHFTokenStatusQuery, useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';
import { HFToken } from './HFToken';
import { HuggingFaceResults } from './HuggingFaceResults';
export const HuggingFaceForm = memo(() => {
@@ -12,6 +15,8 @@ export const HuggingFaceForm = memo(() => {
const [displayResults, setDisplayResults] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const { t } = useTranslation();
const isHFTokenEnabled = useFeatureStatus('hfToken');
const { currentData } = useGetHFTokenStatusQuery(isHFTokenEnabled ? undefined : skipToken);
const [_getHuggingFaceModels, { isLoading, data }] = useLazyGetHuggingFaceModelsQuery();
const [installModel] = useInstallModel();
@@ -41,7 +46,7 @@ export const HuggingFaceForm = memo(() => {
}, []);
return (
<Flex flexDir="column" height="100%" gap={3}>
<Flex flexDir="column" height="100%" gap={4}>
<FormControl isInvalid={!!errorMessage.length} w="full" orientation="vertical" flexShrink={0}>
<FormLabel>{t('modelManager.huggingFaceRepoID')}</FormLabel>
<Flex gap={3} alignItems="center" w="full">
@@ -63,6 +68,7 @@ export const HuggingFaceForm = memo(() => {
<FormHelperText>{t('modelManager.huggingFaceHelper')}</FormHelperText>
{!!errorMessage.length && <FormErrorMessage>{errorMessage}</FormErrorMessage>}
</FormControl>
{currentData !== 'valid' && <HFToken />}
{data && data.urls && displayResults && <HuggingFaceResults results={data.urls} />}
</Flex>
);

View File

@@ -1,5 +1,7 @@
import { Button, Flex, Heading } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useHFForbiddenToast } from 'features/modelManagerV2/hooks/useHFForbiddenToast';
import { useHFLoginToast } from 'features/modelManagerV2/hooks/useHFLoginToast';
import { selectSelectedModelKey, setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -16,6 +18,9 @@ export const ModelManager = memo(() => {
}, [dispatch]);
const selectedModelKey = useAppSelector(selectSelectedModelKey);
useHFLoginToast();
useHFForbiddenToast();
return (
<Flex flexDir="column" layerStyle="first" p={4} gap={4} borderRadius="base" w="50%" h="full">
<Flex w="full" gap={4} justifyContent="space-between" alignItems="center">

View File

@@ -32,8 +32,8 @@ export const addImageToImage = async ({
fp32,
}: AddImageToImageArg): Promise<Invocation<'img_resize' | 'l2i' | 'flux_vae_decode'>> => {
denoise.denoising_start = denoising_start;
const { image_name } = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
const adapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
const { image_name } = await manager.compositor.getCompositeImageDTO(adapters, bbox.rect, { is_intermediate: true });
if (!isEqual(scaledSize, originalSize)) {
// Resize the initial image to the scaled size, denoise, then resize back to the original size

View File

@@ -45,8 +45,15 @@ export const addInpaint = async ({
const { bbox } = canvas;
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, bbox.rect, {
is_intermediate: true,
});
const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
const maskImage = await manager.compositor.getCompositeImageDTO(inpaintMaskAdapters, bbox.rect, {
is_intermediate: true,
});
if (!isEqual(scaledSize, originalSize)) {
// Scale before processing requires some resizing

View File

@@ -45,8 +45,16 @@ export const addOutpaint = async ({
const { bbox } = canvas;
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, bbox.rect, {
is_intermediate: true,
});
const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
const maskImage = await manager.compositor.getCompositeImageDTO(inpaintMaskAdapters, bbox.rect, {
is_intermediate: true,
});
const infill = getInfill(g, params);
if (!isEqual(scaledSize, originalSize)) {

View File

@@ -34,7 +34,7 @@ export const buildFLUXGraph = async (
state: RootState,
manager: CanvasManager
): Promise<{ g: Graph; noise: Invocation<'noise' | 'flux_denoise'>; posCond: Invocation<'flux_text_encoder'> }> => {
const generationMode = manager.compositor.getGenerationMode();
const generationMode = await manager.compositor.getGenerationMode();
log.debug({ generationMode }, 'Building FLUX graph');
const params = selectParamsSlice(state);

View File

@@ -37,7 +37,7 @@ export const buildSD1Graph = async (
state: RootState,
manager: CanvasManager
): Promise<{ g: Graph; noise: Invocation<'noise'>; posCond: Invocation<'compel'> }> => {
const generationMode = manager.compositor.getGenerationMode();
const generationMode = await manager.compositor.getGenerationMode();
log.debug({ generationMode }, 'Building SD1/SD2 graph');
const params = selectParamsSlice(state);

View File

@@ -37,7 +37,7 @@ export const buildSDXLGraph = async (
state: RootState,
manager: CanvasManager
): Promise<{ g: Graph; noise: Invocation<'noise'>; posCond: Invocation<'sdxl_compel_prompt'> }> => {
const generationMode = manager.compositor.getGenerationMode();
const generationMode = await manager.compositor.getGenerationMode();
log.debug({ generationMode }, 'Building SDXL graph');
const params = selectParamsSlice(state);

View File

@@ -3,7 +3,7 @@ import { createEntityAdapter } from '@reduxjs/toolkit';
import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
import queryString from 'query-string';
import type { operations, paths } from 'services/api/schema';
import type { AnyModelConfig } from 'services/api/types';
import type { AnyModelConfig, GetHFTokenStatusResponse, SetHFTokenArg, SetHFTokenResponse } from 'services/api/types';
import type { ApiTagDescription } from '..';
import { api, buildV2Url, LIST_TAG } from '..';
@@ -259,6 +259,22 @@ export const modelsApi = api.injectEndpoints({
query: () => buildModelsUrl('starter_models'),
providesTags: [{ type: 'ModelConfig', id: LIST_TAG }],
}),
getHFTokenStatus: build.query<GetHFTokenStatusResponse, void>({
query: () => buildModelsUrl('hf_login'),
providesTags: ['HFTokenStatus'],
}),
setHFToken: build.mutation<SetHFTokenResponse, SetHFTokenArg>({
query: (body) => ({ url: buildModelsUrl('hf_login'), method: 'POST', body }),
invalidatesTags: ['HFTokenStatus'],
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
try {
const { data } = await queryFulfilled;
dispatch(modelsApi.util.updateQueryData('getHFTokenStatus', undefined, () => data));
} catch {
// no-op
}
},
}),
}),
});
@@ -277,6 +293,8 @@ export const {
useCancelModelInstallMutation,
usePruneCompletedModelInstallsMutation,
useGetStarterModelsQuery,
useGetHFTokenStatusQuery,
useSetHFTokenMutation,
} = modelsApi;
export const selectModelConfigsQuery = modelsApi.endpoints.getModelConfigs.select();

View File

@@ -344,6 +344,24 @@ export type paths = {
patch?: never;
trace?: never;
};
"/api/v2/models/hf_login": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get Hf Login Status */
get: operations["get_hf_login_status"];
put?: never;
/** Do Hf Login */
post: operations["do_hf_login"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/download_queue/": {
parameters: {
query?: never;
@@ -2157,6 +2175,14 @@ export type components = {
*/
image_names: string[];
};
/** Body_do_hf_login */
Body_do_hf_login: {
/**
* Token
* @description Hugging Face token to use for login
*/
token: string;
};
/** Body_download */
Body_download: {
/**
@@ -7322,6 +7348,11 @@ export type components = {
*/
type: "hf";
};
/**
* HFTokenStatus
* @enum {string}
*/
HFTokenStatus: "valid" | "invalid" | "unknown";
/** HTTPValidationError */
HTTPValidationError: {
/** Detail */
@@ -18274,6 +18305,59 @@ export interface operations {
};
};
};
get_hf_login_status: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HFTokenStatus"];
};
};
};
};
do_hf_login: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["Body_do_hf_login"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HFTokenStatus"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
list_downloads: {
parameters: {
query?: never;

View File

@@ -244,3 +244,12 @@ export type PostUploadAction =
export type BoardRecordOrderBy = S['BoardRecordOrderBy'];
export type StarterModel = S['StarterModel'];
export type GetHFTokenStatusResponse =
paths['/api/v2/models/hf_login']['get']['responses']['200']['content']['application/json'];
export type SetHFTokenResponse = NonNullable<
paths['/api/v2/models/hf_login']['post']['responses']['200']['content']['application/json']
>;
export type SetHFTokenArg = NonNullable<
paths['/api/v2/models/hf_login']['post']['requestBody']['content']['application/json']
>;

View File

@@ -7,6 +7,8 @@ import { $queueId } from 'app/store/nanostores/queueId';
import type { AppStore } from 'app/store/store';
import type { SerializableObject } from 'common/types';
import { deepClone } from 'common/util/deepClone';
import { $isHFForbiddenToastOpen } from 'features/modelManagerV2/hooks/useHFForbiddenToast';
import { $isHFLoginToastOpen } from 'features/modelManagerV2/hooks/useHFLoginToast';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation';
import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription';
@@ -295,6 +297,14 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
const { id, error, error_type } = data;
const installs = selectModelInstalls(getState()).data;
if (error === 'Unauthorized') {
$isHFLoginToastOpen.set(true);
}
if (error === 'Forbidden') {
$isHFForbiddenToastOpen.set({ isEnabled: true, source: data.source });
}
if (!installs?.find((install) => install.id === id)) {
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
} else {

View File

@@ -1 +1 @@
__version__ = "5.3.0"
__version__ = "5.3.1"

View File

@@ -42,7 +42,7 @@ dependencies = [
"gguf==0.10.0",
"invisible-watermark==0.2.0", # needed to install SDXL base and refiner using their repo_ids
"mediapipe>=0.10.7", # needed for "mediapipeface" controlnet model
"numpy==1.26.4", # >1.24.0 is needed to use the 'strict' argument to np.testing.assert_array_equal()
"numpy<2.0.0",
"onnx==1.16.1",
"onnxruntime==1.19.2",
"opencv-python==4.9.0.80",
@@ -52,10 +52,10 @@ dependencies = [
"sentencepiece==0.2.0",
"spandrel==0.3.4",
"timm==0.6.13", # needed to override timm latest in controlnet_aux, see https://github.com/isl-org/ZoeDepth/issues/26
"torch==2.4.1",
"torchmetrics==0.11.4",
"torchsde==0.2.6",
"torchvision==0.19.1",
"torch", # torch and related dependencies are not pinned, resolved as dependency of `diffusers[torch]` and so forth
"torchmetrics",
"torchsde",
"torchvision",
"transformers==4.41.1",
# Core application dependencies, pinned for reproducible builds.
@@ -73,7 +73,7 @@ dependencies = [
"click",
"datasets",
"Deprecated",
"dnspython~=2.4.0",
"dnspython",
"dynamicprompts",
"easing-functions",
"einops",
@@ -85,23 +85,23 @@ dependencies = [
"picklescan",
"pillow",
"prompt-toolkit",
"pympler~=1.0.1",
"pympler",
"pypatchmatch",
'pyperclip',
"pyperclip",
"pyreadline3",
"python-multipart",
"requests~=2.28.2",
"requests",
"rich~=13.3",
"scikit-image~=0.21.0",
"scikit-image",
"semver~=3.0.1",
"test-tube~=0.7.5",
"test-tube",
"windows-curses; sys_platform=='win32'",
]
[project.optional-dependencies]
"xformers" = [
# Core generation dependencies, pinned for reproducible builds.
"xformers==0.0.28.post1; sys_platform!='darwin'",
"xformers>=0.0.28.post1; sys_platform!='darwin'",
# Auxiliary dependencies, pinned only if necessary.
"triton; sys_platform=='linux'",
]

View File

@@ -3,15 +3,16 @@ from tempfile import TemporaryDirectory
from typing import Any
import pytest
from omegaconf import OmegaConf
from pydantic import ValidationError
from invokeai.app.invocations.baseinvocation import BaseInvocation
from invokeai.app.services.config.config_default import (
DefaultInvokeAIAppConfig,
InvokeAIAppConfig,
get_config,
load_and_migrate_config,
)
from invokeai.app.services.shared.graph import Graph
from invokeai.frontend.cli.arg_parser import InvokeAIArgs
v4_config = """
@@ -265,58 +266,34 @@ def test_get_config_writing(patch_rootdir: None, monkeypatch: pytest.MonkeyPatch
InvokeAIArgs.did_parse = False
@pytest.mark.xfail(
reason="""
This test fails when run as part of the full test suite.
This test needs to deny nodes from being included in the InvocationsUnion by providing
an app configuration as a test fixture. Pytest executes all test files before running
tests, so the app configuration is already initialized by the time this test runs, and
the InvocationUnion is already created and the denied nodes are not omitted from it.
This test passes when `test_config.py` is tested in isolation.
Perhaps a solution would be to call `get_app_config().parse_args()` in
other test files?
"""
)
def test_deny_nodes(patch_rootdir):
# Allow integer, string and float, but explicitly deny float
allow_deny_nodes_conf = OmegaConf.create(
"""
InvokeAI:
Nodes:
allow_nodes:
- integer
- string
- float
deny_nodes:
- float
"""
)
# must parse config before importing Graph, so its nodes union uses the config
get_config.cache_clear()
conf = get_config()
get_config.cache_clear()
conf.merge_from_file(conf=allow_deny_nodes_conf, argv=[])
from invokeai.app.services.shared.graph import Graph
conf.allow_nodes = ["integer", "string", "float"]
conf.deny_nodes = ["float"]
# We've changed the config, we need to invalidate the typeadapter cache so that the new config is used for
# subsequent graph validations
BaseInvocation.invalidate_typeadapter()
# confirm graph validation fails when using denied node
Graph(nodes={"1": {"id": "1", "type": "integer"}})
Graph(nodes={"1": {"id": "1", "type": "string"}})
Graph.model_validate({"nodes": {"1": {"id": "1", "type": "integer"}}})
Graph.model_validate({"nodes": {"1": {"id": "1", "type": "string"}}})
with pytest.raises(ValidationError):
Graph(nodes={"1": {"id": "1", "type": "float"}})
from invokeai.app.invocations.baseinvocation import BaseInvocation
Graph.model_validate({"nodes": {"1": {"id": "1", "type": "float"}}})
# confirm invocations union will not have denied nodes
all_invocations = BaseInvocation.get_invocations()
has_integer = len([i for i in all_invocations if i.model_fields.get("type").default == "integer"]) == 1
has_string = len([i for i in all_invocations if i.model_fields.get("type").default == "string"]) == 1
has_float = len([i for i in all_invocations if i.model_fields.get("type").default == "float"]) == 1
has_integer = len([i for i in all_invocations if i.get_type() == "integer"]) == 1
has_string = len([i for i in all_invocations if i.get_type() == "string"]) == 1
has_float = len([i for i in all_invocations if i.get_type() == "float"]) == 1
assert has_integer
assert has_string
assert not has_float
# Reset the config so that it doesn't affect other tests
get_config.cache_clear()
BaseInvocation.invalidate_typeadapter()

View File

@@ -34,6 +34,7 @@ from tests.test_nodes import (
PolymorphicStringTestInvocation,
PromptCollectionTestInvocation,
PromptTestInvocation,
PromptTestInvocationOutput,
TextToImageTestInvocation,
)
@@ -509,7 +510,7 @@ def test_invocation_decorator():
@invocation(invocation_type, title=title, tags=tags, category=category, version=version)
class TestInvocation(BaseInvocation):
def invoke(self):
def invoke(self) -> PromptTestInvocationOutput:
pass
schema = TestInvocation.model_json_schema()
@@ -527,7 +528,7 @@ def test_invocation_version_must_be_semver():
@invocation("test_invocation_version_valid", version=valid_version)
class ValidVersionInvocation(BaseInvocation):
def invoke(self):
def invoke(self) -> PromptTestInvocationOutput:
pass
with pytest.raises(InvalidVersionError):
@@ -714,3 +715,20 @@ def test_graph_can_generate_schema():
# Not throwing on this line is sufficient
# NOTE: if this test fails, it's PROBABLY because a new invocation type is breaking schema generation
models_json_schema([(Graph, "serialization")])
def test_nodes_must_implement_invoke_method():
with pytest.raises(ValueError, match='must implement the "invoke" method'):
@invocation("test_no_invoke_method", version="1.0.0")
class NoInvokeMethodInvocation(BaseInvocation):
pass
def test_nodes_must_return_invocation_output():
with pytest.raises(ValueError, match="must have a return annotation of a subclass of BaseInvocationOutput"):
@invocation("test_no_output", version="1.0.0")
class NoOutputInvocation(BaseInvocation):
def invoke(self) -> str:
return "foo"