mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-17 02:48:01 -05:00
Compare commits
137 Commits
ebr/pin-py
...
v5.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cf94d602f | ||
|
|
016a6f182f | ||
|
|
6fbc019142 | ||
|
|
26f95d6a97 | ||
|
|
40f7b0d171 | ||
|
|
4904700751 | ||
|
|
83538c4b2b | ||
|
|
eb7b559529 | ||
|
|
4945465cf0 | ||
|
|
7eed7282a9 | ||
|
|
47f0781822 | ||
|
|
88b8e3e3d5 | ||
|
|
47c3ab9214 | ||
|
|
d6d436b59c | ||
|
|
6ff7057967 | ||
|
|
e032ab1179 | ||
|
|
65bddfcd93 | ||
|
|
2d3ce418dd | ||
|
|
548d72f7b9 | ||
|
|
19837a0f29 | ||
|
|
483b65a1dc | ||
|
|
b85931c7ab | ||
|
|
9225f47338 | ||
|
|
bccac5e4a6 | ||
|
|
7cb07fdc04 | ||
|
|
b137450026 | ||
|
|
dc5090469a | ||
|
|
e0ae2ace89 | ||
|
|
269faae04b | ||
|
|
e282acd41c | ||
|
|
a266668348 | ||
|
|
3bb3e142fc | ||
|
|
6ac6d70a22 | ||
|
|
b0acf33ba5 | ||
|
|
b3eb64b64c | ||
|
|
95f8ab1a29 | ||
|
|
4e043384db | ||
|
|
0f5df8ba17 | ||
|
|
2826ab48a2 | ||
|
|
7ff1b635c8 | ||
|
|
dfb5e8b5e5 | ||
|
|
7259da799c | ||
|
|
965069fce1 | ||
|
|
90232806d9 | ||
|
|
81bc153399 | ||
|
|
c63e526f99 | ||
|
|
2b74263007 | ||
|
|
d3a82f7119 | ||
|
|
291c5a0341 | ||
|
|
bcb41399ca | ||
|
|
6f0f53849b | ||
|
|
4e7d63761a | ||
|
|
198c84105d | ||
|
|
2453b9f443 | ||
|
|
b091aca986 | ||
|
|
8f02ce54a0 | ||
|
|
f4b7c63002 | ||
|
|
a4629280b5 | ||
|
|
855fb007da | ||
|
|
d805b52c1f | ||
|
|
2ea55685bb | ||
|
|
bd6ff3deaa | ||
|
|
82dd53ec88 | ||
|
|
71d749541d | ||
|
|
48a57fc4b9 | ||
|
|
530e0910fc | ||
|
|
2fdf8fc0a2 | ||
|
|
91db9c9300 | ||
|
|
bc42205593 | ||
|
|
2e3cba6416 | ||
|
|
7852aacd11 | ||
|
|
6cccd67ecd | ||
|
|
a7a89c9de1 | ||
|
|
5ca8eed89e | ||
|
|
c885c3c9a6 | ||
|
|
d81c38c350 | ||
|
|
92d5b73215 | ||
|
|
097e92db6a | ||
|
|
84c6209a45 | ||
|
|
107e48808a | ||
|
|
47168b5505 | ||
|
|
58152ec981 | ||
|
|
c74afbf332 | ||
|
|
7cdda00a54 | ||
|
|
a74282bce6 | ||
|
|
107f048c7a | ||
|
|
a2486a5f06 | ||
|
|
07ab116efb | ||
|
|
1a13af3c7a | ||
|
|
f2966a2594 | ||
|
|
58bb97e3c6 | ||
|
|
34569a2410 | ||
|
|
a84aa5c049 | ||
|
|
acfa9c87ef | ||
|
|
f245d8e429 | ||
|
|
62cf0f54e0 | ||
|
|
5f015e76ba | ||
|
|
aebcec28e0 | ||
|
|
db1c5a94f7 | ||
|
|
56222a8493 | ||
|
|
b7510ce709 | ||
|
|
5739799e2e | ||
|
|
813cf87920 | ||
|
|
c95b151daf | ||
|
|
a0f823a3cf | ||
|
|
64e0f6d688 | ||
|
|
ddd5b1087c | ||
|
|
008be9b846 | ||
|
|
8e7cabdc04 | ||
|
|
a4c4237f99 | ||
|
|
bda3740dcd | ||
|
|
5b4633baa9 | ||
|
|
96351181cb | ||
|
|
957d591d99 | ||
|
|
75f605ba1a | ||
|
|
ab898a7180 | ||
|
|
c9a4516ab1 | ||
|
|
fe97c0d5eb | ||
|
|
6056764840 | ||
|
|
8747c0dbb0 | ||
|
|
c5cdd5f9c6 | ||
|
|
abc5d53159 | ||
|
|
2f76019a89 | ||
|
|
3f45beb1ed | ||
|
|
bc1126a85b | ||
|
|
380017041e | ||
|
|
ab7cdbb7e0 | ||
|
|
e5b78d0221 | ||
|
|
1acaa6c486 | ||
|
|
b0381076b7 | ||
|
|
ffff2d6dbb | ||
|
|
afa9f07649 | ||
|
|
addb5c49ea | ||
|
|
a112d2d55b | ||
|
|
619a271c8a | ||
|
|
909f2ee36d | ||
|
|
b4cf3d9d03 |
@@ -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!
|
||||
|
||||
@@ -17,46 +17,49 @@ If you just want to use Invoke, you should use the [installer][installer link].
|
||||
## Setup
|
||||
|
||||
1. Run through the [requirements][requirements link].
|
||||
1. [Fork and clone][forking link] the [InvokeAI repo][repo link].
|
||||
1. Create an directory for user data (images, models, db, etc). This is typically at `~/invokeai`, but if you already have a non-dev install, you may want to create a separate directory for the dev install.
|
||||
1. Create a python virtual environment inside the directory you just created:
|
||||
2. [Fork and clone][forking link] the [InvokeAI repo][repo link].
|
||||
3. Create an directory for user data (images, models, db, etc). This is typically at `~/invokeai`, but if you already have a non-dev install, you may want to create a separate directory for the dev install.
|
||||
4. Create a python virtual environment inside the directory you just created:
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv --prompt InvokeAI-Dev
|
||||
```
|
||||
```sh
|
||||
python3 -m venv .venv --prompt InvokeAI-Dev
|
||||
```
|
||||
|
||||
1. Activate the venv (you'll need to do this every time you want to run the app):
|
||||
5. Activate the venv (you'll need to do this every time you want to run the app):
|
||||
|
||||
```sh
|
||||
source .venv/bin/activate
|
||||
```
|
||||
```sh
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
1. Install the repo as an [editable install][editable install link]:
|
||||
6. Install the repo as an [editable install][editable install link]:
|
||||
|
||||
```sh
|
||||
pip install -e ".[dev,test,xformers]" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121
|
||||
```
|
||||
```sh
|
||||
pip install -e ".[dev,test,xformers]" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121
|
||||
```
|
||||
|
||||
Refer to the [manual installation][manual install link]] instructions for more determining the correct install options. `xformers` is optional, but `dev` and `test` are not.
|
||||
Refer to the [manual installation][manual install link]] instructions for more determining the correct install options. `xformers` is optional, but `dev` and `test` are not.
|
||||
|
||||
1. Install the frontend dev toolchain:
|
||||
7. Install the frontend dev toolchain:
|
||||
|
||||
- [`nodejs`](https://nodejs.org/) (recommend v20 LTS)
|
||||
- [`pnpm`](https://pnpm.io/installation#installing-a-specific-version) (must be v8 - not v9!)
|
||||
- [`pnpm`](https://pnpm.io/8.x/installation) (must be v8 - not v9!)
|
||||
|
||||
1. Do a production build of the frontend:
|
||||
8. Do a production build of the frontend:
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
```
|
||||
```sh
|
||||
cd PATH_TO_INVOKEAI_REPO/invokeai/frontend/web
|
||||
pnpm i
|
||||
pnpm build
|
||||
```
|
||||
|
||||
1. Start the application:
|
||||
9. Start the application:
|
||||
|
||||
```sh
|
||||
python scripts/invokeai-web.py
|
||||
```
|
||||
```sh
|
||||
cd PATH_TO_INVOKEAI_REPO
|
||||
python scripts/invokeai-web.py
|
||||
```
|
||||
|
||||
1. Access the UI at `localhost:9090`.
|
||||
10. Access the UI at `localhost:9090`.
|
||||
|
||||
## Updating the UI
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]🠴[/]",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__,
|
||||
|
||||
@@ -13,6 +13,7 @@ from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel
|
||||
from diffusers.schedulers.scheduling_dpmsolver_sde import DPMSolverSDEScheduler
|
||||
from diffusers.schedulers.scheduling_tcd import TCDScheduler
|
||||
from diffusers.schedulers.scheduling_utils import SchedulerMixin as Scheduler
|
||||
from PIL import Image
|
||||
from pydantic import field_validator
|
||||
from torchvision.transforms.functional import resize as tv_resize
|
||||
from transformers import CLIPVisionModelWithProjection
|
||||
@@ -510,6 +511,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
|
||||
context: InvocationContext,
|
||||
t2i_adapters: Optional[Union[T2IAdapterField, list[T2IAdapterField]]],
|
||||
ext_manager: ExtensionsManager,
|
||||
bgr_mode: bool = False,
|
||||
) -> None:
|
||||
if t2i_adapters is None:
|
||||
return
|
||||
@@ -519,6 +521,10 @@ class DenoiseLatentsInvocation(BaseInvocation):
|
||||
t2i_adapters = [t2i_adapters]
|
||||
|
||||
for t2i_adapter_field in t2i_adapters:
|
||||
image = context.images.get_pil(t2i_adapter_field.image.image_name)
|
||||
if bgr_mode: # SDXL t2i trained on cv2's BGR outputs, but PIL won't convert straight to BGR
|
||||
r, g, b = image.split()
|
||||
image = Image.merge("RGB", (b, g, r))
|
||||
ext_manager.add_extension(
|
||||
T2IAdapterExt(
|
||||
node_context=context,
|
||||
@@ -616,13 +622,17 @@ 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:
|
||||
max_unet_downscale = 8
|
||||
elif t2i_adapter_model_config.base == BaseModelType.StableDiffusionXL:
|
||||
max_unet_downscale = 4
|
||||
|
||||
# SDXL adapters are trained on cv2's BGR outputs
|
||||
r, g, b = image.split()
|
||||
image = Image.merge("RGB", (b, g, r))
|
||||
else:
|
||||
raise ValueError(f"Unexpected T2I-Adapter base model type: '{t2i_adapter_model_config.base}'.")
|
||||
|
||||
@@ -630,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:
|
||||
@@ -900,7 +920,8 @@ class DenoiseLatentsInvocation(BaseInvocation):
|
||||
# ext = extension_field.to_extension(exit_stack, context, ext_manager)
|
||||
# ext_manager.add_extension(ext)
|
||||
self.parse_controlnet_field(exit_stack, context, self.control, ext_manager)
|
||||
self.parse_t2i_adapter_field(exit_stack, context, self.t2i_adapter, ext_manager)
|
||||
bgr_mode = self.unet.unet.base == BaseModelType.StableDiffusionXL
|
||||
self.parse_t2i_adapter_field(exit_stack, context, self.t2i_adapter, ext_manager, bgr_mode)
|
||||
|
||||
# ext: t2i/ip adapter
|
||||
ext_manager.run_callback(ExtensionCallbackType.SETUP, denoise_ctx)
|
||||
|
||||
@@ -165,6 +165,7 @@ class ApplyMaskTensorToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
|
||||
mask: TensorField = InputField(description="The mask tensor to apply.")
|
||||
image: ImageField = InputField(description="The image to apply the mask to.")
|
||||
invert: bool = InputField(default=False, description="Whether to invert the mask.")
|
||||
|
||||
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||
image = context.images.get_pil(self.image.image_name, mode="RGBA")
|
||||
@@ -179,6 +180,9 @@ class ApplyMaskTensorToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
mask = mask > 0.5
|
||||
mask_np = (mask.float() * 255).byte().cpu().numpy().astype(np.uint8)
|
||||
|
||||
if self.invert:
|
||||
mask_np = 255 - mask_np
|
||||
|
||||
# Apply the mask only to the alpha channel where the original alpha is non-zero. This preserves the original
|
||||
# image's transparency - else the transparent regions would end up as opaque black.
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Union
|
||||
@@ -221,7 +222,7 @@ class ImagesInterface(InvocationContextInterface):
|
||||
)
|
||||
|
||||
def get_pil(self, image_name: str, mode: IMAGE_MODES | None = None) -> Image:
|
||||
"""Gets an image as a PIL Image object.
|
||||
"""Gets an image as a PIL Image object. This method returns a copy of the image.
|
||||
|
||||
Args:
|
||||
image_name: The name of the image to get.
|
||||
@@ -233,11 +234,15 @@ class ImagesInterface(InvocationContextInterface):
|
||||
image = self._services.images.get_pil_image(image_name)
|
||||
if mode and mode != image.mode:
|
||||
try:
|
||||
# convert makes a copy!
|
||||
image = image.convert(mode)
|
||||
except ValueError:
|
||||
self._services.logger.warning(
|
||||
f"Could not convert image from {image.mode} to {mode}. Using original mode instead."
|
||||
)
|
||||
else:
|
||||
# copy the image to prevent the user from modifying the original
|
||||
image = image.copy()
|
||||
return image
|
||||
|
||||
def get_metadata(self, image_name: str) -> Optional[MetadataField]:
|
||||
@@ -290,15 +295,15 @@ class TensorsInterface(InvocationContextInterface):
|
||||
return name
|
||||
|
||||
def load(self, name: str) -> Tensor:
|
||||
"""Loads a tensor by name.
|
||||
"""Loads a tensor by name. This method returns a copy of the tensor.
|
||||
|
||||
Args:
|
||||
name: The name of the tensor to load.
|
||||
|
||||
Returns:
|
||||
The loaded tensor.
|
||||
The tensor.
|
||||
"""
|
||||
return self._services.tensors.load(name)
|
||||
return self._services.tensors.load(name).clone()
|
||||
|
||||
|
||||
class ConditioningInterface(InvocationContextInterface):
|
||||
@@ -316,16 +321,16 @@ class ConditioningInterface(InvocationContextInterface):
|
||||
return name
|
||||
|
||||
def load(self, name: str) -> ConditioningFieldData:
|
||||
"""Loads conditioning data by name.
|
||||
"""Loads conditioning data by name. This method returns a copy of the conditioning data.
|
||||
|
||||
Args:
|
||||
name: The name of the conditioning data to load.
|
||||
|
||||
Returns:
|
||||
The loaded conditioning data.
|
||||
The conditioning data.
|
||||
"""
|
||||
|
||||
return self._services.conditioning.load(name)
|
||||
return deepcopy(self._services.conditioning.load(name))
|
||||
|
||||
|
||||
class ModelsInterface(InvocationContextInterface):
|
||||
|
||||
@@ -117,8 +117,6 @@ class StableDiffusionDiffusersModel(GenericDiffusersLoader):
|
||||
load_class = load_classes[config.base][config.variant]
|
||||
except KeyError as e:
|
||||
raise Exception(f"No diffusers pipeline known for base={config.base}, variant={config.variant}") from e
|
||||
prediction_type = config.prediction_type.value
|
||||
upcast_attention = config.upcast_attention
|
||||
|
||||
# Without SilenceWarnings we get log messages like this:
|
||||
# site-packages/huggingface_hub/file_download.py:1132: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`.
|
||||
@@ -129,13 +127,7 @@ class StableDiffusionDiffusersModel(GenericDiffusersLoader):
|
||||
# ['text_model.embeddings.position_ids']
|
||||
|
||||
with SilenceWarnings():
|
||||
pipeline = load_class.from_single_file(
|
||||
config.path,
|
||||
torch_dtype=self._torch_dtype,
|
||||
prediction_type=prediction_type,
|
||||
upcast_attention=upcast_attention,
|
||||
load_safety_checker=False,
|
||||
)
|
||||
pipeline = load_class.from_single_file(config.path, torch_dtype=self._torch_dtype)
|
||||
|
||||
if not submodel_type:
|
||||
return pipeline
|
||||
|
||||
@@ -20,7 +20,7 @@ from typing import Optional
|
||||
|
||||
import requests
|
||||
from huggingface_hub import HfApi, configure_http_backend, hf_hub_url
|
||||
from huggingface_hub.utils._errors import RepositoryNotFoundError, RevisionNotFoundError
|
||||
from huggingface_hub.errors import RepositoryNotFoundError, RevisionNotFoundError
|
||||
from pydantic.networks import AnyHttpUrl
|
||||
from requests.sessions import Session
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -33,7 +33,7 @@ class PreviewExt(ExtensionBase):
|
||||
def initial_preview(self, ctx: DenoiseContext):
|
||||
self.callback(
|
||||
PipelineIntermediateState(
|
||||
step=-1,
|
||||
step=0,
|
||||
order=ctx.scheduler.order,
|
||||
total_steps=len(ctx.inputs.timesteps),
|
||||
timestep=int(ctx.scheduler.config.num_train_timesteps), # TODO: is there any code which uses it?
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
import diffusers
|
||||
import torch
|
||||
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
||||
from diffusers.loaders import FromOriginalControlNetMixin
|
||||
from diffusers.loaders.single_file_model import FromOriginalModelMixin
|
||||
from diffusers.models.attention_processor import AttentionProcessor, AttnProcessor
|
||||
from diffusers.models.controlnet import ControlNetConditioningEmbedding, ControlNetOutput, zero_module
|
||||
from diffusers.models.embeddings import (
|
||||
@@ -32,7 +32,9 @@ from invokeai.backend.util.logging import InvokeAILogger
|
||||
logger = InvokeAILogger.get_logger(__name__)
|
||||
|
||||
|
||||
class ControlNetModel(ModelMixin, ConfigMixin, FromOriginalControlNetMixin):
|
||||
# NOTE(ryand): I'm not the origina author of this code, but for future reference, it appears that this class was copied
|
||||
# from diffusers in order to add support for the encoder_attention_mask argument.
|
||||
class ControlNetModel(ModelMixin, ConfigMixin, FromOriginalModelMixin):
|
||||
"""
|
||||
A ControlNet model.
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -682,7 +682,8 @@
|
||||
"recallParameters": "Recall Parameters",
|
||||
"recallParameter": "Recall {{label}}",
|
||||
"scheduler": "Scheduler",
|
||||
"seamless": "Seamless",
|
||||
"seamlessXAxis": "Seamless X Axis",
|
||||
"seamlessYAxis": "Seamless Y Axis",
|
||||
"seed": "Seed",
|
||||
"steps": "Steps",
|
||||
"strength": "Image to image strength",
|
||||
@@ -713,8 +714,12 @@
|
||||
"convertToDiffusersHelpText4": "This is a one time process only. It might take around 30s-60s depending on the specifications of your computer.",
|
||||
"convertToDiffusersHelpText5": "Please make sure you have enough disk space. Models generally vary between 2GB-7GB in size.",
|
||||
"convertToDiffusersHelpText6": "Do you wish to convert this model?",
|
||||
"noDefaultSettings": "No default settings configured for this model. Visit the Model Manager to add default settings.",
|
||||
"defaultSettings": "Default Settings",
|
||||
"defaultSettingsSaved": "Default Settings Saved",
|
||||
"defaultSettingsOutOfSync": "Some settings do not match the model's defaults:",
|
||||
"restoreDefaultSettings": "Click to use the model's default settings.",
|
||||
"usingDefaultSettings": "Using model's default settings",
|
||||
"delete": "Delete",
|
||||
"deleteConfig": "Delete Config",
|
||||
"deleteModel": "Delete Model",
|
||||
@@ -728,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",
|
||||
@@ -799,7 +814,6 @@
|
||||
"uploadImage": "Upload Image",
|
||||
"urlOrLocalPath": "URL or Local Path",
|
||||
"urlOrLocalPathHelper": "URLs should point to a single file. Local paths can point to a single file or folder for a single diffusers model.",
|
||||
"useDefaultSettings": "Use Default Settings",
|
||||
"vae": "VAE",
|
||||
"vaePrecision": "VAE Precision",
|
||||
"variant": "Variant",
|
||||
@@ -1033,6 +1047,7 @@
|
||||
"patchmatchDownScaleSize": "Downscale",
|
||||
"perlinNoise": "Perlin Noise",
|
||||
"positivePromptPlaceholder": "Positive Prompt",
|
||||
"recallMetadata": "Recall Metadata",
|
||||
"iterations": "Iterations",
|
||||
"scale": "Scale",
|
||||
"scaleBeforeProcessing": "Scale Before Processing",
|
||||
@@ -1109,6 +1124,9 @@
|
||||
"enableInformationalPopovers": "Enable Informational Popovers",
|
||||
"informationalPopoversDisabled": "Informational Popovers Disabled",
|
||||
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
|
||||
"enableModelDescriptions": "Enable Model Descriptions in Dropdowns",
|
||||
"modelDescriptionsDisabled": "Model Descriptions in Dropdowns Disabled",
|
||||
"modelDescriptionsDisabledDesc": "Model descriptions in dropdowns have been disabled. Enable them in Settings.",
|
||||
"enableInvisibleWatermark": "Enable Invisible Watermark",
|
||||
"enableNSFWChecker": "Enable NSFW Checker",
|
||||
"general": "General",
|
||||
@@ -1634,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",
|
||||
@@ -1676,6 +1696,8 @@
|
||||
"controlLayer": "Control Layer",
|
||||
"inpaintMask": "Inpaint Mask",
|
||||
"regionalGuidance": "Regional Guidance",
|
||||
"canvasAsRasterLayer": "$t(controlLayers.canvas) as $t(controlLayers.rasterLayer)",
|
||||
"canvasAsControlLayer": "$t(controlLayers.canvas) as $t(controlLayers.controlLayer)",
|
||||
"referenceImage": "Reference Image",
|
||||
"regionalReferenceImage": "Regional Reference Image",
|
||||
"globalReferenceImage": "Global Reference Image",
|
||||
@@ -1751,6 +1773,7 @@
|
||||
"newGallerySessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be sent to the gallery.",
|
||||
"newCanvasSession": "New Canvas Session",
|
||||
"newCanvasSessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be staged on the canvas.",
|
||||
"replaceCurrent": "Replace Current",
|
||||
"controlMode": {
|
||||
"controlMode": "Control Mode",
|
||||
"balanced": "Balanced",
|
||||
@@ -1880,16 +1903,24 @@
|
||||
"apply": "Apply",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"segment": {
|
||||
"autoMask": "Auto Mask",
|
||||
"selectObject": {
|
||||
"selectObject": "Select Object",
|
||||
"pointType": "Point Type",
|
||||
"invertSelection": "Invert Selection",
|
||||
"include": "Include",
|
||||
"exclude": "Exclude",
|
||||
"neutral": "Neutral",
|
||||
"apply": "Apply",
|
||||
"reset": "Reset",
|
||||
"saveAs": "Save As",
|
||||
"cancel": "Cancel",
|
||||
"process": "Process"
|
||||
"process": "Process",
|
||||
"help1": "Select a single target object. Add <Bold>Include</Bold> and <Bold>Exclude</Bold> points to indicate which parts of the layer are part of the target object.",
|
||||
"help2": "Start with one <Bold>Include</Bold> point within the target object. Add more points to refine the selection. Fewer points typically produce better results.",
|
||||
"help3": "Invert the selection to select everything except the target object.",
|
||||
"clickToAdd": "Click on the layer to add a point",
|
||||
"dragToMove": "Drag a point to move it",
|
||||
"clickToRemove": "Click on a point to remove it"
|
||||
},
|
||||
"settings": {
|
||||
"snapToGrid": {
|
||||
@@ -1930,6 +1961,8 @@
|
||||
"newRegionalReferenceImage": "New Regional Reference Image",
|
||||
"newControlLayer": "New Control Layer",
|
||||
"newRasterLayer": "New Raster Layer",
|
||||
"newInpaintMask": "New Inpaint Mask",
|
||||
"newRegionalGuidance": "New Regional Guidance",
|
||||
"cropCanvasToBbox": "Crop Canvas to Bbox"
|
||||
},
|
||||
"stagingArea": {
|
||||
@@ -2062,13 +2095,11 @@
|
||||
},
|
||||
"whatsNew": {
|
||||
"whatsNewInInvoke": "What's New in Invoke",
|
||||
"canvasV2Announcement": {
|
||||
"newCanvas": "A powerful new control canvas",
|
||||
"newLayerTypes": "New layer types for even more control",
|
||||
"fluxSupport": "Support for the Flux family of models",
|
||||
"readReleaseNotes": "Read Release Notes",
|
||||
"watchReleaseVideo": "Watch Release Video",
|
||||
"watchUiUpdatesOverview": "Watch UI Updates Overview"
|
||||
}
|
||||
"line1": "<ItalicComponent>Select Object</ItalicComponent> tool for precise object selection and editing",
|
||||
"line2": "Expanded Flux support, now with Global Reference Images",
|
||||
"line3": "Improved tooltips and context menus",
|
||||
"readReleaseNotes": "Read Release Notes",
|
||||
"watchRecentReleaseVideos": "Watch Recent Release Videos",
|
||||
"watchUiUpdatesOverview": "Watch UI Updates Overview"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -229,7 +229,6 @@
|
||||
"submitSupportTicket": "サポート依頼を送信する"
|
||||
},
|
||||
"metadata": {
|
||||
"seamless": "シームレス",
|
||||
"Threshold": "ノイズ閾値",
|
||||
"seed": "シード",
|
||||
"width": "幅",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": "透明度"
|
||||
},
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
"model": "模型",
|
||||
"seed": "種子",
|
||||
"vae": "VAE",
|
||||
"seamless": "無縫",
|
||||
"metadata": "元數據",
|
||||
"width": "寬度",
|
||||
"height": "高度"
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
controlLayerAdded,
|
||||
entityRasterized,
|
||||
entitySelected,
|
||||
inpaintMaskAdded,
|
||||
rasterLayerAdded,
|
||||
referenceImageAdded,
|
||||
referenceImageIPAdapterImageChanged,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasReferenceImageState,
|
||||
CanvasRegionalGuidanceState,
|
||||
@@ -110,6 +112,46 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Image dropped on Inpaint Mask
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'ADD_INPAINT_MASK_FROM_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
|
||||
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
|
||||
const overrides: Partial<CanvasInpaintMaskState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Image dropped on Regional Guidance
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'ADD_REGIONAL_GUIDANCE_FROM_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
|
||||
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
|
||||
const overrides: Partial<CanvasRegionalGuidanceState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on Raster layer
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import type { GroupBase } from 'chakra-react-select';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import type { ModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { selectSystemShouldEnableModelDescriptions } from 'features/system/store/systemSlice';
|
||||
import { groupBy, reduce } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -16,7 +17,6 @@ type UseGroupedModelComboboxArg<T extends AnyModelConfig> = {
|
||||
getIsDisabled?: (model: T) => boolean;
|
||||
isLoading?: boolean;
|
||||
groupByType?: boolean;
|
||||
showDescriptions?: boolean;
|
||||
};
|
||||
|
||||
type UseGroupedModelComboboxReturn = {
|
||||
@@ -38,15 +38,8 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
|
||||
): UseGroupedModelComboboxReturn => {
|
||||
const { t } = useTranslation();
|
||||
const base = useAppSelector(selectBaseWithSDXLFallback);
|
||||
const {
|
||||
modelConfigs,
|
||||
selectedModel,
|
||||
getIsDisabled,
|
||||
onChange,
|
||||
isLoading,
|
||||
groupByType = false,
|
||||
showDescriptions = false,
|
||||
} = arg;
|
||||
const shouldShowModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions);
|
||||
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg;
|
||||
const options = useMemo<GroupBase<ComboboxOption>[]>(() => {
|
||||
if (!modelConfigs) {
|
||||
return [];
|
||||
@@ -60,7 +53,7 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
|
||||
options: val.map((model) => ({
|
||||
label: model.name,
|
||||
value: model.key,
|
||||
description: (showDescriptions && model.description) || undefined,
|
||||
description: (shouldShowModelDescriptions && model.description) || undefined,
|
||||
isDisabled: getIsDisabled ? getIsDisabled(model) : false,
|
||||
})),
|
||||
});
|
||||
@@ -70,7 +63,7 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
|
||||
);
|
||||
_options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base) ? -1 : 1));
|
||||
return _options;
|
||||
}, [modelConfigs, groupByType, getIsDisabled, base, showDescriptions]);
|
||||
}, [modelConfigs, groupByType, getIsDisabled, base, shouldShowModelDescriptions]);
|
||||
|
||||
const value = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import type { ModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { selectSystemShouldEnableModelDescriptions } from 'features/system/store/systemSlice';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AnyModelConfig } from 'services/api/types';
|
||||
@@ -24,13 +26,16 @@ type UseModelComboboxReturn = {
|
||||
export const useModelCombobox = <T extends AnyModelConfig>(arg: UseModelComboboxArg<T>): UseModelComboboxReturn => {
|
||||
const { t } = useTranslation();
|
||||
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, optionsFilter = () => true } = arg;
|
||||
const shouldShowModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions);
|
||||
|
||||
const options = useMemo<ComboboxOption[]>(() => {
|
||||
return modelConfigs.filter(optionsFilter).map((model) => ({
|
||||
label: model.name,
|
||||
value: model.key,
|
||||
description: (shouldShowModelDescriptions && model.description) || undefined,
|
||||
isDisabled: getIsDisabled ? getIsDisabled(model) : false,
|
||||
}));
|
||||
}, [optionsFilter, getIsDisabled, modelConfigs]);
|
||||
}, [optionsFilter, getIsDisabled, modelConfigs, shouldShowModelDescriptions]);
|
||||
|
||||
const value = useMemo(
|
||||
() => options.find((m) => (selectedModel ? m.value === selectedModel.key : false)),
|
||||
|
||||
@@ -13,7 +13,7 @@ export const CanvasAlertsPreserveMask = memo(() => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert status="warning" borderRadius="base" fontSize="sm" shadow="md" w="fit-content" alignSelf="flex-end">
|
||||
<Alert status="warning" borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
|
||||
<AlertIcon />
|
||||
<AlertTitle>{t('controlLayers.settings.preserveMask.alert')}</AlertTitle>
|
||||
</Alert>
|
||||
|
||||
@@ -98,7 +98,7 @@ const CanvasAlertsSelectedEntityStatusContent = memo(({ entityIdentifier, adapte
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert status={alert.status} borderRadius="base" fontSize="sm" shadow="md" w="fit-content" alignSelf="flex-end">
|
||||
<Alert status={alert.status} borderRadius="base" fontSize="sm" shadow="md" w="fit-content">
|
||||
<AlertIcon />
|
||||
<AlertTitle>{alert.title}</AlertTitle>
|
||||
</Alert>
|
||||
|
||||
@@ -132,7 +132,6 @@ const AlertWrapper = ({
|
||||
fontSize="sm"
|
||||
shadow="md"
|
||||
w="fit-content"
|
||||
alignSelf="flex-end"
|
||||
>
|
||||
<Flex w="full" alignItems="center">
|
||||
<AlertIcon />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MenuGroup } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems';
|
||||
import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems';
|
||||
@@ -8,7 +9,9 @@ import {
|
||||
EntityIdentifierContext,
|
||||
useEntityIdentifierContext,
|
||||
} from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityTypeString } from 'features/controlLayers/hooks/useEntityTypeString';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
@@ -46,9 +49,20 @@ export const CanvasContextMenuSelectedEntityMenuItems = memo(() => {
|
||||
|
||||
return (
|
||||
<EntityIdentifierContext.Provider value={selectedEntityIdentifier}>
|
||||
<CanvasContextMenuSelectedEntityMenuItemsContent />
|
||||
<CanvasContextMenuSelectedEntityMenuGroup>
|
||||
<CanvasContextMenuSelectedEntityMenuItemsContent />
|
||||
</CanvasContextMenuSelectedEntityMenuGroup>
|
||||
</EntityIdentifierContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasContextMenuSelectedEntityMenuItems.displayName = 'CanvasContextMenuSelectedEntityMenuItems';
|
||||
|
||||
const CanvasContextMenuSelectedEntityMenuGroup = memo((props: PropsWithChildren) => {
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const title = useEntityTypeString(entityIdentifier.type);
|
||||
|
||||
return <MenuGroup title={title}>{props.children}</MenuGroup>;
|
||||
});
|
||||
|
||||
CanvasContextMenuSelectedEntityMenuGroup.displayName = 'CanvasContextMenuSelectedEntityMenuGroup';
|
||||
|
||||
@@ -62,6 +62,7 @@ export const CanvasDropArea = memo(() => {
|
||||
data={addControlLayerFromImageDropData}
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable
|
||||
dropLabel={t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
size="sm"
|
||||
minW={8}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
tooltip={t('controlLayers.addLayer')}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu';
|
||||
import { EntityListSelectedEntityActionBarAutoMaskButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarAutoMaskButton';
|
||||
import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton';
|
||||
import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill';
|
||||
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
|
||||
import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity';
|
||||
import { EntityListSelectedEntityActionBarSelectObjectButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton';
|
||||
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
|
||||
import { memo } from 'react';
|
||||
|
||||
@@ -17,7 +17,7 @@ export const EntityListSelectedEntityActionBar = memo(() => {
|
||||
<Spacer />
|
||||
<EntityListSelectedEntityActionBarFill />
|
||||
<Flex h="full">
|
||||
<EntityListSelectedEntityActionBarAutoMaskButton />
|
||||
<EntityListSelectedEntityActionBarSelectObjectButton />
|
||||
<EntityListSelectedEntityActionBarFilterButton />
|
||||
<EntityListSelectedEntityActionBarTransformButton />
|
||||
<EntityListSelectedEntityActionBarSaveToAssetsButton />
|
||||
|
||||
@@ -23,7 +23,7 @@ export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => {
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={!selectedEntityIdentifier || isBusy}
|
||||
size="sm"
|
||||
minW={8}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.duplicate')}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/sel
|
||||
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShootingStarBold } from 'react-icons/pi';
|
||||
import { PiShootingStarFill } from 'react-icons/pi';
|
||||
|
||||
export const EntityListSelectedEntityActionBarFilterButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
@@ -24,12 +24,12 @@ export const EntityListSelectedEntityActionBarFilterButton = memo(() => {
|
||||
<IconButton
|
||||
onClick={filter.start}
|
||||
isDisabled={filter.isDisabled}
|
||||
size="sm"
|
||||
minW={8}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.filter.filter')}
|
||||
tooltip={t('controlLayers.filter.filter')}
|
||||
icon={<PiShootingStarBold />}
|
||||
icon={<PiShootingStarFill />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ export const EntityListSelectedEntityActionBarSaveToAssetsButton = memo(() => {
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={!selectedEntityIdentifier || isBusy}
|
||||
size="sm"
|
||||
minW={8}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.saveLayerToAssets')}
|
||||
|
||||
@@ -5,9 +5,9 @@ import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/sel
|
||||
import { isSegmentableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiMaskHappyBold } from 'react-icons/pi';
|
||||
import { PiShapesFill } from 'react-icons/pi';
|
||||
|
||||
export const EntityListSelectedEntityActionBarAutoMaskButton = memo(() => {
|
||||
export const EntityListSelectedEntityActionBarSelectObjectButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const segment = useEntitySegmentAnything(selectedEntityIdentifier);
|
||||
@@ -24,14 +24,14 @@ export const EntityListSelectedEntityActionBarAutoMaskButton = memo(() => {
|
||||
<IconButton
|
||||
onClick={segment.start}
|
||||
isDisabled={segment.isDisabled}
|
||||
size="sm"
|
||||
minW={8}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.segment.autoMask')}
|
||||
tooltip={t('controlLayers.segment.autoMask')}
|
||||
icon={<PiMaskHappyBold />}
|
||||
aria-label={t('controlLayers.selectObject.selectObject')}
|
||||
tooltip={t('controlLayers.selectObject.selectObject')}
|
||||
icon={<PiShapesFill />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBarAutoMaskButton.displayName = 'EntityListSelectedEntityActionBarAutoMaskButton';
|
||||
EntityListSelectedEntityActionBarSelectObjectButton.displayName = 'EntityListSelectedEntityActionBarSelectObjectButton';
|
||||
@@ -24,7 +24,7 @@ export const EntityListSelectedEntityActionBarTransformButton = memo(() => {
|
||||
<IconButton
|
||||
onClick={transform.start}
|
||||
isDisabled={transform.isDisabled}
|
||||
size="sm"
|
||||
minW={8}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.transform.transform')}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea
|
||||
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
||||
import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
|
||||
import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
|
||||
import { SegmentAnything } from 'features/controlLayers/components/SegmentAnything/SegmentAnything';
|
||||
import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
|
||||
import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
|
||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
|
||||
@@ -71,12 +71,16 @@ export const CanvasMainPanelContent = memo(() => {
|
||||
>
|
||||
<InvokeCanvasComponent />
|
||||
<CanvasManagerProviderGate>
|
||||
{showHUD && (
|
||||
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none">
|
||||
<CanvasHUD />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex flexDir="column" position="absolute" top={1} insetInlineEnd={1} pointerEvents="none" gap={2}>
|
||||
<Flex
|
||||
position="absolute"
|
||||
flexDir="column"
|
||||
top={1}
|
||||
insetInlineStart={1}
|
||||
pointerEvents="none"
|
||||
gap={2}
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{showHUD && <CanvasHUD />}
|
||||
<CanvasAlertsSelectedEntityStatus />
|
||||
<CanvasAlertsPreserveMask />
|
||||
<CanvasAlertsSendingToGallery />
|
||||
@@ -102,7 +106,7 @@ export const CanvasMainPanelContent = memo(() => {
|
||||
<CanvasManagerProviderGate>
|
||||
<Filter />
|
||||
<Transform />
|
||||
<SegmentAnything />
|
||||
<SelectObject />
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
<CanvasDropArea />
|
||||
|
||||
@@ -21,7 +21,7 @@ import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/s
|
||||
import type { CanvasEntityIdentifier, ControlModeV2 } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold, PiShootingStarBold, PiUploadBold } from 'react-icons/pi';
|
||||
import { PiBoundingBoxBold, PiShootingStarFill, PiUploadBold } from 'react-icons/pi';
|
||||
import type { ControlNetModelConfig, PostUploadAction, T2IAdapterModelConfig } from 'services/api/types';
|
||||
|
||||
const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => {
|
||||
@@ -93,7 +93,7 @@ export const ControlLayerControlAdapter = memo(() => {
|
||||
variant="link"
|
||||
aria-label={t('controlLayers.filter.filter')}
|
||||
tooltip={t('controlLayers.filter.filter')}
|
||||
icon={<PiShootingStarBold />}
|
||||
icon={<PiShootingStarFill />}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={pullBboxIntoLayer}
|
||||
|
||||
@@ -5,8 +5,9 @@ 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 { CanvasEntityMenuItemsSegment } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSegment';
|
||||
import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import { ControlLayerMenuItemsConvertToSubMenu } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsConvertToSubMenu';
|
||||
import { ControlLayerMenuItemsCopyToSubMenu } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsCopyToSubMenu';
|
||||
@@ -24,14 +25,14 @@ export const ControlLayerMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsTransform />
|
||||
<CanvasEntityMenuItemsFilter />
|
||||
<CanvasEntityMenuItemsSegment />
|
||||
<CanvasEntityMenuItemsSelectObject />
|
||||
<ControlLayerMenuItemsTransparencyEffect />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsMergeDown />
|
||||
<ControlLayerMenuItemsCopyToSubMenu />
|
||||
<ControlLayerMenuItemsConvertToSubMenu />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
<MenuDivider />
|
||||
<ControlLayerMenuItemsConvertToSubMenu />
|
||||
<ControlLayerMenuItemsCopyToSubMenu />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
Heading,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Spacer,
|
||||
Spinner,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
|
||||
@@ -15,7 +26,7 @@ import { IMAGE_FILTERS } from 'features/controlLayers/store/filters';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiShootingStarBold, PiXBold } from 'react-icons/pi';
|
||||
import { PiCaretDownBold } from 'react-icons/pi';
|
||||
|
||||
const FilterContent = memo(
|
||||
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
|
||||
@@ -25,7 +36,7 @@ const FilterContent = memo(
|
||||
const config = useStore(adapter.filterer.$filterConfig);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
const isProcessing = useStore(adapter.filterer.$isProcessing);
|
||||
const hasProcessed = useStore(adapter.filterer.$hasProcessed);
|
||||
const hasImageState = useStore(adapter.filterer.$hasImageState);
|
||||
const autoProcess = useAppSelector(selectAutoProcess);
|
||||
|
||||
const onChangeFilterConfig = useCallback(
|
||||
@@ -46,6 +57,22 @@ const FilterContent = memo(
|
||||
return IMAGE_FILTERS[config.type].validateConfig?.(config as never) ?? true;
|
||||
}, [config]);
|
||||
|
||||
const saveAsInpaintMask = useCallback(() => {
|
||||
adapter.filterer.saveAs('inpaint_mask');
|
||||
}, [adapter.filterer]);
|
||||
|
||||
const saveAsRegionalGuidance = useCallback(() => {
|
||||
adapter.filterer.saveAs('regional_guidance');
|
||||
}, [adapter.filterer]);
|
||||
|
||||
const saveAsRasterLayer = useCallback(() => {
|
||||
adapter.filterer.saveAs('raster_layer');
|
||||
}, [adapter.filterer]);
|
||||
|
||||
const saveAsControlLayer = useCallback(() => {
|
||||
adapter.filterer.saveAs('control_layer');
|
||||
}, [adapter.filterer]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'applyFilter',
|
||||
category: 'canvas',
|
||||
@@ -89,40 +116,56 @@ const FilterContent = memo(
|
||||
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiShootingStarBold />}
|
||||
onClick={adapter.filterer.processImmediate}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.process')}
|
||||
isDisabled={!isValid || autoProcess}
|
||||
isDisabled={isProcessing || !isValid || (autoProcess && hasImageState)}
|
||||
>
|
||||
{t('controlLayers.filter.process')}
|
||||
{isProcessing && <Spinner ms={3} boxSize={5} color="base.600" />}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
leftIcon={<PiArrowsCounterClockwiseBold />}
|
||||
onClick={adapter.filterer.reset}
|
||||
isLoading={isProcessing}
|
||||
isDisabled={isProcessing}
|
||||
loadingText={t('controlLayers.filter.reset')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('controlLayers.filter.reset')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiCheckBold />}
|
||||
onClick={adapter.filterer.apply}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.apply')}
|
||||
isDisabled={!isValid || !hasProcessed}
|
||||
variant="ghost"
|
||||
isDisabled={isProcessing || !isValid || !hasImageState}
|
||||
>
|
||||
{t('controlLayers.filter.apply')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiXBold />}
|
||||
onClick={adapter.filterer.cancel}
|
||||
loadingText={t('controlLayers.filter.cancel')}
|
||||
>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
loadingText={t('controlLayers.selectObject.saveAs')}
|
||||
variant="ghost"
|
||||
isDisabled={isProcessing || !isValid || !hasImageState}
|
||||
rightIcon={<PiCaretDownBold />}
|
||||
>
|
||||
{t('controlLayers.selectObject.saveAs')}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem isDisabled={isProcessing || !isValid || !hasImageState} onClick={saveAsInpaintMask}>
|
||||
{t('controlLayers.newInpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem isDisabled={isProcessing || !isValid || !hasImageState} onClick={saveAsRegionalGuidance}>
|
||||
{t('controlLayers.newRegionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem isDisabled={isProcessing || !isValid || !hasImageState} onClick={saveAsControlLayer}>
|
||||
{t('controlLayers.newControlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem isDisabled={isProcessing || !isValid || !hasImageState} onClick={saveAsRasterLayer}>
|
||||
{t('controlLayers.newRasterLayer')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<Button variant="ghost" onClick={adapter.filterer.cancel} loadingText={t('controlLayers.filter.cancel')}>
|
||||
{t('controlLayers.filter.cancel')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -14,7 +14,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const InpaintMask = memo(({ id }: Props) => {
|
||||
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'inpaint_mask' }), [id]);
|
||||
const entityIdentifier = useMemo<CanvasEntityIdentifier<'inpaint_mask'>>(() => ({ id, type: 'inpaint_mask' }), [id]);
|
||||
|
||||
return (
|
||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||
|
||||
@@ -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,10 +22,11 @@ export const InpaintMaskMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsTransform />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<MenuDivider />
|
||||
<InpaintMaskMenuItemsConvertToSubMenu />
|
||||
<CanvasEntityMenuItemsMergeDown />
|
||||
<InpaintMaskMenuItemsCopyToSubMenu />
|
||||
<InpaintMaskMenuItemsConvertToSubMenu />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,8 +5,9 @@ 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 { CanvasEntityMenuItemsSegment } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSegment';
|
||||
import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import { RasterLayerMenuItemsConvertToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsConvertToSubMenu';
|
||||
import { RasterLayerMenuItemsCopyToSubMenu } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsCopyToSubMenu';
|
||||
@@ -23,13 +24,13 @@ export const RasterLayerMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsTransform />
|
||||
<CanvasEntityMenuItemsFilter />
|
||||
<CanvasEntityMenuItemsSegment />
|
||||
<CanvasEntityMenuItemsSelectObject />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsMergeDown />
|
||||
<RasterLayerMenuItemsCopyToSubMenu />
|
||||
<RasterLayerMenuItemsConvertToSubMenu />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
<MenuDivider />
|
||||
<RasterLayerMenuItemsConvertToSubMenu />
|
||||
<RasterLayerMenuItemsCopyToSubMenu />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,7 +16,10 @@ type Props = {
|
||||
};
|
||||
|
||||
export const RegionalGuidance = memo(({ id }: Props) => {
|
||||
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'regional_guidance' }), [id]);
|
||||
const entityIdentifier = useMemo<CanvasEntityIdentifier<'regional_guidance'>>(
|
||||
() => ({ id, type: 'regional_guidance' }),
|
||||
[id]
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||
|
||||
@@ -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,10 +27,11 @@ export const RegionalGuidanceMenuItems = memo(() => {
|
||||
<CanvasEntityMenuItemsTransform />
|
||||
<RegionalGuidanceMenuItemsAutoNegative />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<MenuDivider />
|
||||
<RegionalGuidanceMenuItemsConvertToSubMenu />
|
||||
<CanvasEntityMenuItemsMergeDown />
|
||||
<RegionalGuidanceMenuItemsCopyToSubMenu />
|
||||
<RegionalGuidanceMenuItemsConvertToSubMenu />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,28 +3,36 @@ import {
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
Heading,
|
||||
Icon,
|
||||
ListItem,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Spacer,
|
||||
Spinner,
|
||||
Text,
|
||||
Tooltip,
|
||||
UnorderedList,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { CanvasAutoProcessSwitch } from 'features/controlLayers/components/CanvasAutoProcessSwitch';
|
||||
import { CanvasOperationIsolatedLayerPreviewSwitch } from 'features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch';
|
||||
import { SegmentAnythingPointType } from 'features/controlLayers/components/SegmentAnything/SegmentAnythingPointType';
|
||||
import { SelectObjectInvert } from 'features/controlLayers/components/SelectObject/SelectObjectInvert';
|
||||
import { SelectObjectPointType } from 'features/controlLayers/components/SelectObject/SelectObjectPointType';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
|
||||
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
|
||||
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsCounterClockwiseBold, PiFloppyDiskBold, PiStarBold, PiXBold } from 'react-icons/pi';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { PiCaretDownBold, PiInfoBold } from 'react-icons/pi';
|
||||
|
||||
const SegmentAnythingContent = memo(
|
||||
const SelectObjectContent = memo(
|
||||
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -81,72 +89,86 @@ const SegmentAnythingContent = memo(
|
||||
transitionProperty="height"
|
||||
transitionDuration="normal"
|
||||
>
|
||||
<Flex w="full" gap={4}>
|
||||
<Heading size="md" color="base.300" userSelect="none">
|
||||
{t('controlLayers.segment.autoMask')}
|
||||
</Heading>
|
||||
<Flex w="full" gap={4} alignItems="center">
|
||||
<Flex gap={2}>
|
||||
<Heading size="md" color="base.300" userSelect="none">
|
||||
{t('controlLayers.selectObject.selectObject')}
|
||||
</Heading>
|
||||
<Tooltip label={<SelectObjectHelpTooltipContent />}>
|
||||
<Flex alignItems="center">
|
||||
<Icon as={PiInfoBold} color="base.500" />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Spacer />
|
||||
<CanvasAutoProcessSwitch />
|
||||
<CanvasOperationIsolatedLayerPreviewSwitch />
|
||||
</Flex>
|
||||
|
||||
<SegmentAnythingPointType adapter={adapter} />
|
||||
<Flex w="full" justifyContent="space-between" py={2}>
|
||||
<SelectObjectPointType adapter={adapter} />
|
||||
<SelectObjectInvert adapter={adapter} />
|
||||
</Flex>
|
||||
|
||||
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||
<Button
|
||||
leftIcon={<PiStarBold />}
|
||||
onClick={adapter.segmentAnything.processImmediate}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.segment.process')}
|
||||
loadingText={t('controlLayers.selectObject.process')}
|
||||
variant="ghost"
|
||||
isDisabled={!hasPoints || autoProcess}
|
||||
isDisabled={isProcessing || !hasPoints || (autoProcess && hasImageState)}
|
||||
>
|
||||
{t('controlLayers.segment.process')}
|
||||
{t('controlLayers.selectObject.process')}
|
||||
{isProcessing && <Spinner ms={3} boxSize={5} color="base.600" />}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
leftIcon={<PiArrowsCounterClockwiseBold />}
|
||||
onClick={adapter.segmentAnything.reset}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.segment.reset')}
|
||||
isDisabled={isProcessing || !hasPoints}
|
||||
loadingText={t('controlLayers.selectObject.reset')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('controlLayers.segment.reset')}
|
||||
{t('controlLayers.selectObject.reset')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={adapter.segmentAnything.apply}
|
||||
loadingText={t('controlLayers.selectObject.apply')}
|
||||
variant="ghost"
|
||||
isDisabled={isProcessing || !hasImageState}
|
||||
>
|
||||
{t('controlLayers.selectObject.apply')}
|
||||
</Button>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
leftIcon={<PiFloppyDiskBold />}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.segment.saveAs')}
|
||||
loadingText={t('controlLayers.selectObject.saveAs')}
|
||||
variant="ghost"
|
||||
isDisabled={!hasImageState}
|
||||
isDisabled={isProcessing || !hasImageState}
|
||||
rightIcon={<PiCaretDownBold />}
|
||||
>
|
||||
{t('controlLayers.segment.saveAs')}
|
||||
{t('controlLayers.selectObject.saveAs')}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem isDisabled={!hasImageState} onClick={saveAsInpaintMask}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
<MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsInpaintMask}>
|
||||
{t('controlLayers.newInpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem isDisabled={!hasImageState} onClick={saveAsRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
<MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsRegionalGuidance}>
|
||||
{t('controlLayers.newRegionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem isDisabled={!hasImageState} onClick={saveAsControlLayer}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
<MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsControlLayer}>
|
||||
{t('controlLayers.newControlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem isDisabled={!hasImageState} onClick={saveAsRasterLayer}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
<MenuItem isDisabled={isProcessing || !hasImageState} onClick={saveAsRasterLayer}>
|
||||
{t('controlLayers.newRasterLayer')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<Button
|
||||
leftIcon={<PiXBold />}
|
||||
onClick={adapter.segmentAnything.cancel}
|
||||
isLoading={isProcessing}
|
||||
isDisabled={isProcessing}
|
||||
loadingText={t('common.cancel')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('controlLayers.segment.cancel')}
|
||||
{t('controlLayers.selectObject.cancel')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
@@ -154,9 +176,9 @@ const SegmentAnythingContent = memo(
|
||||
}
|
||||
);
|
||||
|
||||
SegmentAnythingContent.displayName = 'SegmentAnythingContent';
|
||||
SelectObjectContent.displayName = 'SegmentAnythingContent';
|
||||
|
||||
export const SegmentAnything = () => {
|
||||
export const SelectObject = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const adapter = useStore(canvasManager.stateApi.$segmentingAdapter);
|
||||
|
||||
@@ -164,5 +186,38 @@ export const SegmentAnything = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SegmentAnythingContent adapter={adapter} />;
|
||||
};
|
||||
return <SelectObjectContent adapter={adapter} />;
|
||||
});
|
||||
|
||||
SelectObject.displayName = 'SelectObject';
|
||||
|
||||
const Bold = (props: PropsWithChildren) => (
|
||||
<Text as="span" fontWeight="semibold">
|
||||
{props.children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const SelectObjectHelpTooltipContent = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex gap={3} flexDir="column">
|
||||
<Text>
|
||||
<Trans i18nKey="controlLayers.selectObject.help1" components={{ Bold: <Bold /> }} />
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans i18nKey="controlLayers.selectObject.help2" components={{ Bold: <Bold /> }} />
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans i18nKey="controlLayers.selectObject.help3" />
|
||||
</Text>
|
||||
<UnorderedList>
|
||||
<ListItem>{t('controlLayers.selectObject.clickToAdd')}</ListItem>
|
||||
<ListItem>{t('controlLayers.selectObject.dragToMove')}</ListItem>
|
||||
<ListItem>{t('controlLayers.selectObject.clickToRemove')}</ListItem>
|
||||
</UnorderedList>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
SelectObjectHelpTooltipContent.displayName = 'SelectObjectHelpTooltipContent';
|
||||
@@ -0,0 +1,26 @@
|
||||
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
|
||||
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const SelectObjectInvert = memo(
|
||||
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
|
||||
const { t } = useTranslation();
|
||||
const invert = useStore(adapter.segmentAnything.$invert);
|
||||
|
||||
const onChange = useCallback(() => {
|
||||
adapter.segmentAnything.$invert.set(!adapter.segmentAnything.$invert.get());
|
||||
}, [adapter.segmentAnything.$invert]);
|
||||
|
||||
return (
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>{t('controlLayers.selectObject.invertSelection')}</FormLabel>
|
||||
<Switch size="sm" isChecked={invert} onChange={onChange} />
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SelectObjectInvert.displayName = 'SelectObjectInvert';
|
||||
@@ -6,7 +6,7 @@ import { SAM_POINT_LABEL_STRING_TO_NUMBER, zSAMPointLabelString } from 'features
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const SegmentAnythingPointType = memo(
|
||||
export const SelectObjectPointType = memo(
|
||||
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
|
||||
const { t } = useTranslation();
|
||||
const pointType = useStore(adapter.segmentAnything.$pointTypeString);
|
||||
@@ -21,15 +21,15 @@ export const SegmentAnythingPointType = memo(
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl w="full">
|
||||
<FormLabel>{t('controlLayers.segment.pointType')}</FormLabel>
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>{t('controlLayers.selectObject.pointType')}</FormLabel>
|
||||
<RadioGroup value={pointType} onChange={onChange} w="full" size="md">
|
||||
<Flex alignItems="center" w="full" gap={4} fontWeight="semibold" color="base.300">
|
||||
<Radio value="foreground">
|
||||
<Text>{t('controlLayers.segment.include')}</Text>
|
||||
<Text>{t('controlLayers.selectObject.include')}</Text>
|
||||
</Radio>
|
||||
<Radio value="background">
|
||||
<Text>{t('controlLayers.segment.exclude')}</Text>
|
||||
<Text>{t('controlLayers.selectObject.exclude')}</Text>
|
||||
</Radio>
|
||||
</Flex>
|
||||
</RadioGroup>
|
||||
@@ -38,4 +38,4 @@ export const SegmentAnythingPointType = memo(
|
||||
}
|
||||
);
|
||||
|
||||
SegmentAnythingPointType.displayName = 'SegmentAnythingPointType';
|
||||
SelectObjectPointType.displayName = 'SelectObject';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
|
||||
import { Button, ButtonGroup, Flex, Heading, Spacer, Spinner } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { CanvasOperationIsolatedLayerPreviewSwitch } from 'features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch';
|
||||
@@ -8,7 +8,6 @@ import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEnt
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -62,30 +61,28 @@ const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) =>
|
||||
|
||||
<TransformFitToBboxButtons adapter={adapter} />
|
||||
|
||||
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||
<ButtonGroup isAttached={false} size="sm" w="full" alignItems="center">
|
||||
{isProcessing && <Spinner ms={3} boxSize={5} color="base.600" />}
|
||||
<Spacer />
|
||||
<Button
|
||||
leftIcon={<PiArrowsCounterClockwiseBold />}
|
||||
onClick={adapter.transformer.resetTransform}
|
||||
isLoading={isProcessing}
|
||||
isDisabled={isProcessing}
|
||||
loadingText={t('controlLayers.transform.reset')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('controlLayers.transform.reset')}
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<PiCheckBold />}
|
||||
onClick={adapter.transformer.applyTransform}
|
||||
isLoading={isProcessing}
|
||||
isDisabled={isProcessing}
|
||||
loadingText={t('controlLayers.transform.apply')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('controlLayers.transform.apply')}
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<PiXBold />}
|
||||
onClick={adapter.transformer.stopTransform}
|
||||
isLoading={isProcessing}
|
||||
isDisabled={isProcessing}
|
||||
loadingText={t('common.cancel')}
|
||||
variant="ghost"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useStore } from '@nanostores/react';
|
||||
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsOutBold } from 'react-icons/pi';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
import { z } from 'zod';
|
||||
@@ -60,10 +59,9 @@ export const TransformFitToBboxButtons = memo(({ adapter }: { adapter: CanvasEnt
|
||||
<Combobox options={options} value={value} onChange={onChange} isSearchable={false} isClearable={false} />
|
||||
</FormControl>
|
||||
<Button
|
||||
leftIcon={<PiArrowsOutBold />}
|
||||
size="sm"
|
||||
onClick={onClick}
|
||||
isLoading={isProcessing}
|
||||
isDisabled={isProcessing}
|
||||
loadingText={t('controlLayers.transform.fitToBbox')}
|
||||
variant="ghost"
|
||||
>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 />}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,8 +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 { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCopyBold } from 'react-icons/pi';
|
||||
@@ -11,7 +12,8 @@ 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();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
@@ -19,7 +21,7 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
|
||||
}, [copyLayerToClipboard, adapter]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={!isInteractable}>
|
||||
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={isBusy || isEmpty}>
|
||||
{t('common.clipboard')}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti
|
||||
import { useEntityFilter } from 'features/controlLayers/hooks/useEntityFilter';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShootingStarBold } from 'react-icons/pi';
|
||||
import { PiShootingStarFill } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityMenuItemsFilter = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
@@ -11,7 +11,7 @@ export const CanvasEntityMenuItemsFilter = memo(() => {
|
||||
const filter = useEntityFilter(entityIdentifier);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={filter.start} icon={<PiShootingStarBold />} isDisabled={filter.isDisabled}>
|
||||
<MenuItem onClick={filter.start} icon={<PiShootingStarFill />} isDisabled={filter.isDisabled}>
|
||||
{t('controlLayers.filter.filter')}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -3,18 +3,18 @@ import { useEntityIdentifierContext } from 'features/controlLayers/contexts/Enti
|
||||
import { useEntitySegmentAnything } from 'features/controlLayers/hooks/useEntitySegmentAnything';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiMaskHappyBold } from 'react-icons/pi';
|
||||
import { PiShapesFill } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityMenuItemsSegment = memo(() => {
|
||||
export const CanvasEntityMenuItemsSelectObject = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const segmentAnything = useEntitySegmentAnything(entityIdentifier);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={segmentAnything.start} icon={<PiMaskHappyBold />} isDisabled={segmentAnything.isDisabled}>
|
||||
{t('controlLayers.segment.autoMask')}
|
||||
<MenuItem onClick={segmentAnything.start} icon={<PiShapesFill />} isDisabled={segmentAnything.isDisabled}>
|
||||
{t('controlLayers.selectObject.selectObject')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityMenuItemsSegment.displayName = 'CanvasEntityMenuItemsSegment';
|
||||
CanvasEntityMenuItemsSelectObject.displayName = 'CanvasEntityMenuItemsSelectObject';
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,9 @@ import {
|
||||
selectEntityOrThrow,
|
||||
} from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasRegionalGuidanceState,
|
||||
ControlNetConfig,
|
||||
@@ -44,6 +46,8 @@ import { useCallback } from 'react';
|
||||
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
|
||||
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
||||
import { isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig } from 'services/api/types';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const selectDefaultControlAdapter = createSelector(
|
||||
selectModelConfigsQuery,
|
||||
@@ -124,6 +128,60 @@ export const useNewRasterLayerFromImage = () => {
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useNewControlLayerFromImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const bboxRect = useAppSelector(selectBboxRect);
|
||||
const func = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const overrides: Partial<CanvasControlLayerState> = {
|
||||
position: { x: bboxRect.x, y: bboxRect.y },
|
||||
objects: [imageObject],
|
||||
};
|
||||
dispatch(controlLayerAdded({ overrides, isSelected: true }));
|
||||
},
|
||||
[bboxRect.x, bboxRect.y, dispatch]
|
||||
);
|
||||
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useNewInpaintMaskFromImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const bboxRect = useAppSelector(selectBboxRect);
|
||||
const func = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const overrides: Partial<CanvasInpaintMaskState> = {
|
||||
position: { x: bboxRect.x, y: bboxRect.y },
|
||||
objects: [imageObject],
|
||||
};
|
||||
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
|
||||
},
|
||||
[bboxRect.x, bboxRect.y, dispatch]
|
||||
);
|
||||
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useNewRegionalGuidanceFromImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const bboxRect = useAppSelector(selectBboxRect);
|
||||
const func = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const overrides: Partial<CanvasRegionalGuidanceState> = {
|
||||
position: { x: bboxRect.x, y: bboxRect.y },
|
||||
objects: [imageObject],
|
||||
};
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
},
|
||||
[bboxRect.x, bboxRect.y, dispatch]
|
||||
);
|
||||
|
||||
return func;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function that adds a new canvas with the given image as the initial image, replicating the img2img flow:
|
||||
* - Reset the canvas
|
||||
@@ -138,18 +196,31 @@ export const useNewCanvasFromImage = () => {
|
||||
const bboxRect = useAppSelector(selectBboxRect);
|
||||
const base = useAppSelector(selectBboxModelBase);
|
||||
const func = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
(imageDTO: ImageDTO, type: CanvasRasterLayerState['type'] | CanvasControlLayerState['type']) => {
|
||||
// Calculate the new bbox dimensions to fit the image's aspect ratio at the optimal size
|
||||
const ratio = imageDTO.width / imageDTO.height;
|
||||
const optimalDimension = getOptimalDimension(base);
|
||||
const { width, height } = calculateNewSize(ratio, optimalDimension ** 2, base);
|
||||
|
||||
// The overrides need to include the layer's ID so we can transform the layer it is initialized
|
||||
const overrides = {
|
||||
id: getPrefixedId('raster_layer'),
|
||||
position: { x: bboxRect.x, y: bboxRect.y },
|
||||
objects: [imageDTOToImageObject(imageDTO)],
|
||||
} satisfies Partial<CanvasRasterLayerState>;
|
||||
let overrides: Partial<CanvasRasterLayerState> | Partial<CanvasControlLayerState>;
|
||||
|
||||
if (type === 'raster_layer') {
|
||||
overrides = {
|
||||
id: getPrefixedId('raster_layer'),
|
||||
position: { x: bboxRect.x, y: bboxRect.y },
|
||||
objects: [imageDTOToImageObject(imageDTO)],
|
||||
} satisfies Partial<CanvasRasterLayerState>;
|
||||
} else if (type === 'control_layer') {
|
||||
overrides = {
|
||||
id: getPrefixedId('control_layer'),
|
||||
position: { x: bboxRect.x, y: bboxRect.y },
|
||||
objects: [imageDTOToImageObject(imageDTO)],
|
||||
} satisfies Partial<CanvasControlLayerState>;
|
||||
} else {
|
||||
// Catch unhandled types
|
||||
assert<Equals<typeof type, never>>(false);
|
||||
}
|
||||
|
||||
CanvasEntityAdapterBase.registerInitCallback(async (adapter) => {
|
||||
// Skip the callback if the adapter is not the one we are creating
|
||||
@@ -166,7 +237,16 @@ export const useNewCanvasFromImage = () => {
|
||||
dispatch(canvasReset());
|
||||
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
||||
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
|
||||
// The type casts are safe because the type is checked above
|
||||
if (type === 'raster_layer') {
|
||||
dispatch(rasterLayerAdded({ overrides: overrides as Partial<CanvasRasterLayerState>, isSelected: true }));
|
||||
} else if (type === 'control_layer') {
|
||||
dispatch(controlLayerAdded({ overrides: overrides as Partial<CanvasControlLayerState>, isSelected: true }));
|
||||
} else {
|
||||
// Catch unhandled types
|
||||
assert<Equals<typeof type, never>>(false);
|
||||
}
|
||||
},
|
||||
[base, bboxRect.x, bboxRect.y, dispatch]
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
|
||||
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
|
||||
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
|
||||
@@ -7,6 +8,9 @@ import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { serializeError } from 'serialize-error';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
export const useCopyLayerToClipboard = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -26,11 +30,13 @@ export const useCopyLayerToClipboard = () => {
|
||||
const canvas = adapter.getCanvas();
|
||||
const blob = await canvasToBlob(canvas);
|
||||
copyBlobToClipboard(blob);
|
||||
log.trace('Layer copied to clipboard');
|
||||
toast({
|
||||
status: 'info',
|
||||
title: t('toast.layerCopiedToClipboard'),
|
||||
});
|
||||
} catch (error) {
|
||||
log.error({ error: serializeError(error) }, 'Problem copying layer to clipboard');
|
||||
toast({
|
||||
status: 'error',
|
||||
title: t('toast.problemCopyingLayer'),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { buildSelectHasObjects } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useEntityIsEmpty = (entityIdentifier: CanvasEntityIdentifier | null) => {
|
||||
const selectHasObjects = useMemo(
|
||||
() => (entityIdentifier ? buildSelectHasObjects(entityIdentifier) : () => false),
|
||||
[entityIdentifier]
|
||||
);
|
||||
const hasObjects = useAppSelector(selectHasObjects);
|
||||
|
||||
return !hasObjects;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
@@ -52,8 +52,9 @@ export const useEntityTransform = (entityIdentifier: CanvasEntityIdentifier | nu
|
||||
if (!adapter) {
|
||||
return;
|
||||
}
|
||||
imageViewer.close();
|
||||
await adapter.transformer.startTransform();
|
||||
}, [isDisabled, entityIdentifier, canvasManager]);
|
||||
}, [isDisabled, entityIdentifier, canvasManager, imageViewer]);
|
||||
|
||||
const fitToBbox = useCallback(async () => {
|
||||
if (isDisabled) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { withResult, withResultAsync } from 'common/util/result';
|
||||
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
|
||||
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import { addCoords, getKonvaNodeDebugAttrs, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
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 { 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';
|
||||
import { atom } from 'nanostores';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { buildSelectModelConfig } from 'services/api/hooks/modelsByType';
|
||||
import { isControlNetOrT2IAdapterModelConfig } from 'services/api/types';
|
||||
import stableHash from 'stable-hash';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
type CanvasEntityFiltererConfig = {
|
||||
processDebounceMs: number;
|
||||
/**
|
||||
* The debounce time in milliseconds for processing the filter.
|
||||
*/
|
||||
PROCESS_DEBOUNCE_MS: number;
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: CanvasEntityFiltererConfig = {
|
||||
processDebounceMs: 1000,
|
||||
PROCESS_DEBOUNCE_MS: 1000,
|
||||
};
|
||||
|
||||
export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
@@ -32,20 +41,65 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
imageState: CanvasImageState | null = null;
|
||||
subscriptions = new Set<() => void>();
|
||||
config: CanvasEntityFiltererConfig = DEFAULT_CONFIG;
|
||||
|
||||
subscriptions = new Set<() => void>();
|
||||
|
||||
/**
|
||||
* The AbortController used to cancel the filter processing.
|
||||
*/
|
||||
abortController: AbortController | null = null;
|
||||
|
||||
/**
|
||||
* Whether the module is currently filtering an image.
|
||||
*/
|
||||
$isFiltering = atom<boolean>(false);
|
||||
$hasProcessed = atom<boolean>(false);
|
||||
/**
|
||||
* The hash of the last processed config. This is used to prevent re-processing the same config.
|
||||
*/
|
||||
$lastProcessedHash = atom<string>('');
|
||||
|
||||
/**
|
||||
* Whether the module is currently processing the filter.
|
||||
*/
|
||||
$isProcessing = atom<boolean>(false);
|
||||
|
||||
/**
|
||||
* The config for the filter.
|
||||
*/
|
||||
$filterConfig = atom<FilterConfig>(IMAGE_FILTERS.canny_edge_detection.buildDefaults());
|
||||
|
||||
/**
|
||||
* The initial filter config, used to reset the filter config.
|
||||
*/
|
||||
$initialFilterConfig = atom<FilterConfig | null>(null);
|
||||
|
||||
/**
|
||||
* The ephemeral image state of the filtered image.
|
||||
*/
|
||||
$imageState = atom<CanvasImageState | null>(null);
|
||||
|
||||
/**
|
||||
* Whether the module has an image state. This is a computed value based on $imageState.
|
||||
*/
|
||||
$hasImageState = computed(this.$imageState, (imageState) => imageState !== null);
|
||||
/**
|
||||
* The filtered image object module, if it exists.
|
||||
*/
|
||||
imageModule: CanvasObjectImage | null = null;
|
||||
|
||||
/**
|
||||
* The Konva nodes for the module.
|
||||
*/
|
||||
konva: {
|
||||
/**
|
||||
* The main Konva group node for the module. This is added to the parent layer on start, and removed on teardown.
|
||||
*/
|
||||
group: Konva.Group;
|
||||
};
|
||||
|
||||
KONVA_GROUP_NAME = `${this.type}:group`;
|
||||
|
||||
constructor(parent: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer) {
|
||||
super();
|
||||
this.id = getPrefixedId(this.type);
|
||||
@@ -55,9 +109,17 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
this.log = this.manager.buildLogger(this);
|
||||
|
||||
this.log.debug('Creating filter module');
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({ name: this.KONVA_GROUP_NAME }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event listeners needed while filtering the entity.
|
||||
*/
|
||||
subscribe = () => {
|
||||
// As the filter config changes, process the filter
|
||||
this.subscriptions.add(
|
||||
this.$filterConfig.listen(() => {
|
||||
if (this.manager.stateApi.getSettings().autoProcess && this.$isFiltering.get()) {
|
||||
@@ -65,6 +127,7 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
}
|
||||
})
|
||||
);
|
||||
// When auto-process is enabled, process the filter
|
||||
this.subscriptions.add(
|
||||
this.manager.stateApi.createStoreSubscription(selectAutoProcess, (autoProcess) => {
|
||||
if (autoProcess && this.$isFiltering.get()) {
|
||||
@@ -74,11 +137,18 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes event listeners used while filtering the entity.
|
||||
*/
|
||||
unsubscribe = () => {
|
||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
this.subscriptions.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts the filter module.
|
||||
* @param config The filter config to start with. If omitted, the default filter config is used.
|
||||
*/
|
||||
start = (config?: FilterConfig) => {
|
||||
const filteringAdapter = this.manager.stateApi.$filteringAdapter.get();
|
||||
if (filteringAdapter) {
|
||||
@@ -88,30 +158,57 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
|
||||
this.log.trace('Initializing filter');
|
||||
|
||||
this.subscribe();
|
||||
// Reset any previous state
|
||||
this.resetEphemeralState();
|
||||
this.$isFiltering.set(true);
|
||||
|
||||
// Update the konva group's position to match the parent entity
|
||||
const pixelRect = this.parent.transformer.$pixelRect.get();
|
||||
const position = addCoords(this.parent.state.position, pixelRect);
|
||||
this.konva.group.setAttrs(position);
|
||||
|
||||
// Add the group to the parent layer
|
||||
this.parent.konva.layer.add(this.konva.group);
|
||||
|
||||
if (config) {
|
||||
// If a config is provided, use it
|
||||
this.$filterConfig.set(config);
|
||||
} else if (this.parent.type === 'control_layer_adapter' && this.parent.state.controlAdapter.model) {
|
||||
this.$initialFilterConfig.set(config);
|
||||
} else {
|
||||
this.$filterConfig.set(this.createInitialFilterConfig());
|
||||
}
|
||||
|
||||
this.$initialFilterConfig.set(this.$filterConfig.get());
|
||||
|
||||
this.subscribe();
|
||||
|
||||
this.manager.stateApi.$filteringAdapter.set(this.parent);
|
||||
|
||||
if (this.manager.stateApi.getSettings().autoProcess) {
|
||||
this.processImmediate();
|
||||
}
|
||||
};
|
||||
|
||||
createInitialFilterConfig = (): FilterConfig => {
|
||||
if (this.parent.type === 'control_layer_adapter' && this.parent.state.controlAdapter.model) {
|
||||
// If the parent is a control layer adapter, we should check if the model has a default filter and set it if so
|
||||
const selectModelConfig = buildSelectModelConfig(
|
||||
this.parent.state.controlAdapter.model.key,
|
||||
isControlNetOrT2IAdapterModelConfig
|
||||
);
|
||||
const modelConfig = this.manager.stateApi.runSelector(selectModelConfig);
|
||||
// This always returns a filter
|
||||
const filter = getFilterForModel(modelConfig);
|
||||
this.$filterConfig.set(filter.buildDefaults());
|
||||
return filter.buildDefaults();
|
||||
} else {
|
||||
// Otherwise, set the default filter
|
||||
this.$filterConfig.set(IMAGE_FILTERS.canny_edge_detection.buildDefaults());
|
||||
}
|
||||
this.$isFiltering.set(true);
|
||||
this.manager.stateApi.$filteringAdapter.set(this.parent);
|
||||
if (this.manager.stateApi.getSettings().autoProcess) {
|
||||
this.processImmediate();
|
||||
// Otherwise, used the default filter
|
||||
return IMAGE_FILTERS.canny_edge_detection.buildDefaults();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes the filter, updating the module's state and rendering the filtered image.
|
||||
*/
|
||||
processImmediate = async () => {
|
||||
const config = this.$filterConfig.get();
|
||||
const filterData = IMAGE_FILTERS[config.type];
|
||||
@@ -123,6 +220,12 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
const hash = stableHash({ config });
|
||||
if (hash === this.$lastProcessedHash.get()) {
|
||||
this.log.trace('Already processed config');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.trace({ config }, 'Processing filter');
|
||||
const rect = this.parent.transformer.getRelativeRect();
|
||||
|
||||
@@ -156,91 +259,177 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
this.manager.stateApi.runGraphAndReturnImageOutput({
|
||||
graph,
|
||||
outputNodeId,
|
||||
// The filter graph should always be prepended to the queue so it's processed ASAP.
|
||||
prepend: true,
|
||||
/**
|
||||
* The filter node may need to download a large model. Currently, the models required by the filter nodes are
|
||||
* downloaded just-in-time, as required by the filter. If we use a timeout here, we might get into a catch-22
|
||||
* where the filter node is waiting for the model to download, but the download gets canceled if the filter
|
||||
* node times out.
|
||||
*
|
||||
* (I suspect the model download will actually _not_ be canceled if the graph is canceled, but let's not chance it!)
|
||||
*
|
||||
* TODO(psyche): Figure out a better way to handle this. Probably need to download the models ahead of time.
|
||||
*/
|
||||
// timeout: 5000,
|
||||
/**
|
||||
* The filter node should be able to cancel the request if it's taking too long. This will cancel the graph's
|
||||
* queue item and clear any event listeners on the request.
|
||||
*/
|
||||
signal: controller.signal,
|
||||
})
|
||||
);
|
||||
|
||||
// If there is an error, log it and bail out of this processing run
|
||||
if (filterResult.isErr()) {
|
||||
this.log.error({ error: serializeError(filterResult.error) }, 'Error processing filter');
|
||||
this.log.error({ error: serializeError(filterResult.error) }, 'Error filtering');
|
||||
this.$isProcessing.set(false);
|
||||
// Clean up the abort controller as needed
|
||||
if (!this.abortController.signal.aborted) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
this.abortController = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.trace({ imageDTO: filterResult.value }, 'Filter processed');
|
||||
this.imageState = imageDTOToImageObject(filterResult.value);
|
||||
this.log.trace({ imageDTO: filterResult.value }, 'Filtered');
|
||||
|
||||
await this.parent.bufferRenderer.setBuffer(this.imageState, true);
|
||||
// Prepare the ephemeral image state
|
||||
const imageState = imageDTOToImageObject(filterResult.value);
|
||||
this.$imageState.set(imageState);
|
||||
|
||||
// Destroy any existing masked image and create a new one
|
||||
if (this.imageModule) {
|
||||
this.imageModule.destroy();
|
||||
}
|
||||
|
||||
this.imageModule = new CanvasObjectImage(imageState, this);
|
||||
|
||||
// Force update the masked image - after awaiting, the image will be rendered (in memory)
|
||||
await this.imageModule.update(imageState, true);
|
||||
|
||||
this.konva.group.add(this.imageModule.konva.group);
|
||||
|
||||
// The porcessing is complete, set can set the last processed hash and isProcessing to false
|
||||
this.$lastProcessedHash.set(hash);
|
||||
|
||||
this.$isProcessing.set(false);
|
||||
this.$hasProcessed.set(true);
|
||||
|
||||
// Clean up the abort controller as needed
|
||||
if (!this.abortController.signal.aborted) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
|
||||
this.abortController = null;
|
||||
};
|
||||
|
||||
process = debounce(this.processImmediate, this.config.processDebounceMs);
|
||||
/**
|
||||
* Debounced version of processImmediate.
|
||||
*/
|
||||
process = debounce(this.processImmediate, this.config.PROCESS_DEBOUNCE_MS);
|
||||
|
||||
/**
|
||||
* Applies the filter image to the entity, replacing the entity's objects with the filtered image.
|
||||
*/
|
||||
apply = () => {
|
||||
const imageState = this.imageState;
|
||||
if (!imageState) {
|
||||
const filteredImageObjectState = this.$imageState.get();
|
||||
if (!filteredImageObjectState) {
|
||||
this.log.warn('No image state to apply filter to');
|
||||
return;
|
||||
}
|
||||
this.log.trace('Applying filter');
|
||||
this.parent.bufferRenderer.commitBuffer();
|
||||
this.log.trace('Applying');
|
||||
|
||||
// Have the parent adopt the image module - this prevents a flash of the original layer content before the filtered
|
||||
// image is rendered
|
||||
if (this.imageModule) {
|
||||
this.parent.renderer.adoptObjectRenderer(this.imageModule);
|
||||
}
|
||||
|
||||
// Rasterize the entity, replacing the objects with the masked image
|
||||
const rect = this.parent.transformer.getRelativeRect();
|
||||
this.manager.stateApi.rasterizeEntity({
|
||||
entityIdentifier: this.parent.entityIdentifier,
|
||||
imageObject: imageState,
|
||||
imageObject: filteredImageObjectState,
|
||||
position: {
|
||||
x: Math.round(rect.x),
|
||||
y: Math.round(rect.y),
|
||||
},
|
||||
replaceObjects: true,
|
||||
});
|
||||
this.imageState = null;
|
||||
|
||||
// Final cleanup and teardown, returning user to main canvas UI
|
||||
this.resetEphemeralState();
|
||||
this.teardown();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: CanvasRenderableEntityType) => {
|
||||
const imageState = this.$imageState.get();
|
||||
if (!imageState) {
|
||||
this.log.warn('No image state to apply filter to');
|
||||
return;
|
||||
}
|
||||
this.log.trace(`Saving as ${type}`);
|
||||
|
||||
const rect = this.parent.transformer.getRelativeRect();
|
||||
const arg = {
|
||||
overrides: {
|
||||
objects: [imageState],
|
||||
position: {
|
||||
x: Math.round(rect.x),
|
||||
y: Math.round(rect.y),
|
||||
},
|
||||
},
|
||||
isSelected: true,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'raster_layer':
|
||||
this.manager.stateApi.addRasterLayer(arg);
|
||||
break;
|
||||
case 'control_layer':
|
||||
this.manager.stateApi.addControlLayer(arg);
|
||||
break;
|
||||
case 'inpaint_mask':
|
||||
this.manager.stateApi.addInpaintMask(arg);
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
this.manager.stateApi.addRegionalGuidance(arg);
|
||||
break;
|
||||
default:
|
||||
assert<Equals<typeof type, never>>(false);
|
||||
}
|
||||
};
|
||||
|
||||
resetEphemeralState = () => {
|
||||
// First we need to bail out of any processing
|
||||
if (this.abortController && !this.abortController.signal.aborted) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
this.abortController = null;
|
||||
|
||||
// If the image module exists, and is a child of the group, destroy it. It might not be a child of the group if
|
||||
// the user has applied the filter and the image has been adopted by the parent entity.
|
||||
if (this.imageModule && this.imageModule.konva.group.parent === this.konva.group) {
|
||||
this.imageModule.destroy();
|
||||
this.imageModule = null;
|
||||
}
|
||||
const initialFilterConfig = this.$initialFilterConfig.get() ?? this.createInitialFilterConfig();
|
||||
this.$filterConfig.set(initialFilterConfig);
|
||||
this.$imageState.set(null);
|
||||
this.$lastProcessedHash.set('');
|
||||
this.$isProcessing.set(false);
|
||||
};
|
||||
|
||||
teardown = () => {
|
||||
this.$initialFilterConfig.set(null);
|
||||
this.konva.group.remove();
|
||||
this.unsubscribe();
|
||||
this.$isFiltering.set(false);
|
||||
this.$hasProcessed.set(false);
|
||||
this.manager.stateApi.$filteringAdapter.set(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets the module (e.g. remove all points and the mask image).
|
||||
*
|
||||
* Does not cancel or otherwise complete the segmenting process.
|
||||
*/
|
||||
reset = () => {
|
||||
this.log.trace('Resetting filter');
|
||||
|
||||
this.abortController?.abort();
|
||||
this.abortController = null;
|
||||
this.parent.bufferRenderer.clearBuffer();
|
||||
this.parent.transformer.updatePosition();
|
||||
this.parent.renderer.syncKonvaCache(true);
|
||||
this.imageState = null;
|
||||
this.$hasProcessed.set(false);
|
||||
this.log.trace('Resetting');
|
||||
this.resetEphemeralState();
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
this.log.trace('Cancelling filter');
|
||||
|
||||
this.reset();
|
||||
this.unsubscribe();
|
||||
this.$isProcessing.set(false);
|
||||
this.$isFiltering.set(false);
|
||||
this.$hasProcessed.set(false);
|
||||
this.manager.stateApi.$filteringAdapter.set(null);
|
||||
this.log.trace('Canceling');
|
||||
this.resetEphemeralState();
|
||||
this.teardown();
|
||||
};
|
||||
|
||||
repr = () => {
|
||||
@@ -248,11 +437,14 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
path: this.path,
|
||||
parent: this.parent.id,
|
||||
config: this.config,
|
||||
imageState: deepClone(this.$imageState.get()),
|
||||
$isFiltering: this.$isFiltering.get(),
|
||||
$hasProcessed: this.$hasProcessed.get(),
|
||||
$lastProcessedHash: this.$lastProcessedHash.get(),
|
||||
$isProcessing: this.$isProcessing.get(),
|
||||
$filterConfig: this.$filterConfig.get(),
|
||||
konva: { group: getKonvaNodeDebugAttrs(this.konva.group) },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -263,5 +455,6 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
}
|
||||
this.abortController = null;
|
||||
this.unsubscribe();
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 } }
|
||||
>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user