mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-16 19:28:18 -05:00
Compare commits
73 Commits
psychedeli
...
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 | ||
|
|
34569a2410 | ||
|
|
acfa9c87ef | ||
|
|
f245d8e429 | ||
|
|
62cf0f54e0 | ||
|
|
5f015e76ba |
@@ -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!
|
||||
|
||||
@@ -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__,
|
||||
|
||||
@@ -622,7 +622,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
|
||||
for t2i_adapter_field in t2i_adapter:
|
||||
t2i_adapter_model_config = context.models.get_config(t2i_adapter_field.t2i_adapter_model.key)
|
||||
t2i_adapter_loaded_model = context.models.load(t2i_adapter_field.t2i_adapter_model)
|
||||
image = context.images.get_pil(t2i_adapter_field.image.image_name)
|
||||
image = context.images.get_pil(t2i_adapter_field.image.image_name, mode="RGB")
|
||||
|
||||
# The max_unet_downscale is the maximum amount that the UNet model downscales the latent image internally.
|
||||
if t2i_adapter_model_config.base == BaseModelType.StableDiffusion1:
|
||||
@@ -640,29 +640,39 @@ class DenoiseLatentsInvocation(BaseInvocation):
|
||||
with t2i_adapter_loaded_model as t2i_adapter_model:
|
||||
total_downscale_factor = t2i_adapter_model.total_downscale_factor
|
||||
|
||||
# Resize the T2I-Adapter input image.
|
||||
# We select the resize dimensions so that after the T2I-Adapter's total_downscale_factor is applied, the
|
||||
# result will match the latent image's dimensions after max_unet_downscale is applied.
|
||||
t2i_input_height = latents_shape[2] // max_unet_downscale * total_downscale_factor
|
||||
t2i_input_width = latents_shape[3] // max_unet_downscale * total_downscale_factor
|
||||
|
||||
# Note: We have hard-coded `do_classifier_free_guidance=False`. This is because we only want to prepare
|
||||
# a single image. If CFG is enabled, we will duplicate the resultant tensor after applying the
|
||||
# T2I-Adapter model.
|
||||
#
|
||||
# Note: We re-use the `prepare_control_image(...)` from ControlNet for T2I-Adapter, because it has many
|
||||
# of the same requirements (e.g. preserving binary masks during resize).
|
||||
|
||||
# Assuming fixed dimensional scaling of LATENT_SCALE_FACTOR.
|
||||
_, _, latent_height, latent_width = latents_shape
|
||||
control_height_resize = latent_height * LATENT_SCALE_FACTOR
|
||||
control_width_resize = latent_width * LATENT_SCALE_FACTOR
|
||||
t2i_image = prepare_control_image(
|
||||
image=image,
|
||||
do_classifier_free_guidance=False,
|
||||
width=t2i_input_width,
|
||||
height=t2i_input_height,
|
||||
width=control_width_resize,
|
||||
height=control_height_resize,
|
||||
num_channels=t2i_adapter_model.config["in_channels"], # mypy treats this as a FrozenDict
|
||||
device=t2i_adapter_model.device,
|
||||
dtype=t2i_adapter_model.dtype,
|
||||
resize_mode=t2i_adapter_field.resize_mode,
|
||||
)
|
||||
|
||||
# Resize the T2I-Adapter input image.
|
||||
# We select the resize dimensions so that after the T2I-Adapter's total_downscale_factor is applied, the
|
||||
# result will match the latent image's dimensions after max_unet_downscale is applied.
|
||||
# We crop the image to this size so that the positions match the input image on non-standard resolutions
|
||||
t2i_input_height = latents_shape[2] // max_unet_downscale * total_downscale_factor
|
||||
t2i_input_width = latents_shape[3] // max_unet_downscale * total_downscale_factor
|
||||
if t2i_image.shape[2] > t2i_input_height or t2i_image.shape[3] > t2i_input_width:
|
||||
t2i_image = t2i_image[
|
||||
:, :, : min(t2i_image.shape[2], t2i_input_height), : min(t2i_image.shape[3], t2i_input_width)
|
||||
]
|
||||
|
||||
adapter_state = t2i_adapter_model(t2i_image)
|
||||
|
||||
if do_classifier_free_guidance:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -733,7 +733,17 @@
|
||||
"huggingFacePlaceholder": "owner/model-name",
|
||||
"huggingFaceRepoID": "HuggingFace Repo ID",
|
||||
"huggingFaceHelper": "If multiple models are found in this repo, you will be prompted to select one to install.",
|
||||
"hfToken": "HuggingFace Token",
|
||||
"hfTokenLabel": "HuggingFace Token (Required for some models)",
|
||||
"hfTokenHelperText": "A HF token is required to use some models. Click here to create or get your token.",
|
||||
"hfTokenInvalid": "Invalid or Missing HF Token",
|
||||
"hfForbidden": "You do not have access to this HF model",
|
||||
"hfForbiddenErrorMessage": "We recommend visiting the repo page on HuggingFace.com. The owner may require acceptance of terms in order to download.",
|
||||
"hfTokenInvalidErrorMessage": "Invalid or missing HuggingFace token.",
|
||||
"hfTokenRequired": "You are trying to download a model that requires a valid HuggingFace Token.",
|
||||
"hfTokenInvalidErrorMessage2": "Update it in the ",
|
||||
"hfTokenUnableToVerify": "Unable to Verify HF Token",
|
||||
"hfTokenUnableToVerifyErrorMessage": "Unable to verify HuggingFace token. This is likely due to a network error. Please try again later.",
|
||||
"hfTokenSaved": "HF Token Saved",
|
||||
"imageEncoderModelId": "Image Encoder Model ID",
|
||||
"includesNModels": "Includes {{n}} models and their dependencies",
|
||||
"installQueue": "Install Queue",
|
||||
@@ -1037,6 +1047,7 @@
|
||||
"patchmatchDownScaleSize": "Downscale",
|
||||
"perlinNoise": "Perlin Noise",
|
||||
"positivePromptPlaceholder": "Positive Prompt",
|
||||
"recallMetadata": "Recall Metadata",
|
||||
"iterations": "Iterations",
|
||||
"scale": "Scale",
|
||||
"scaleBeforeProcessing": "Scale Before Processing",
|
||||
@@ -1641,14 +1652,16 @@
|
||||
"newControlLayerError": "Problem Creating Control Layer",
|
||||
"newRasterLayerOk": "Created Raster Layer",
|
||||
"newRasterLayerError": "Problem Creating Raster Layer",
|
||||
"newFromImage": "New from Image",
|
||||
"pullBboxIntoLayerOk": "Bbox Pulled Into Layer",
|
||||
"pullBboxIntoLayerError": "Problem Pulling BBox Into Layer",
|
||||
"pullBboxIntoReferenceImageOk": "Bbox Pulled Into ReferenceImage",
|
||||
"pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage",
|
||||
"regionIsEmpty": "Selected region is empty",
|
||||
"mergeVisible": "Merge Visible",
|
||||
"mergeVisibleOk": "Merged visible layers",
|
||||
"mergeVisibleError": "Error merging visible layers",
|
||||
"mergeDown": "Merge Down",
|
||||
"mergeVisibleOk": "Merged layers",
|
||||
"mergeVisibleError": "Error merging layers",
|
||||
"clearHistory": "Clear History",
|
||||
"bboxOverlay": "Show Bbox Overlay",
|
||||
"resetCanvas": "Reset Canvas",
|
||||
|
||||
@@ -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": "高度"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/componen
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
|
||||
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
|
||||
import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
@@ -27,6 +28,7 @@ export const ControlLayerMenuItems = memo(() => {
|
||||
<CanvasEntityMenuItemsSelectObject />
|
||||
<ControlLayerMenuItemsTransparencyEffect />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsMergeDown />
|
||||
<ControlLayerMenuItemsCopyToSubMenu />
|
||||
<ControlLayerMenuItemsConvertToSubMenu />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/
|
||||
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
|
||||
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import { InpaintMaskMenuItemsConvertToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsConvertToSubMenu';
|
||||
import { InpaintMaskMenuItemsCopyToSubMenu } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItemsCopyToSubMenu';
|
||||
@@ -20,9 +22,11 @@ export const InpaintMaskMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsTransform />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsMergeDown />
|
||||
<InpaintMaskMenuItemsCopyToSubMenu />
|
||||
<InpaintMaskMenuItemsConvertToSubMenu />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,6 +5,7 @@ import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/componen
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
|
||||
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
|
||||
import { CanvasEntityMenuItemsSelectObject } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSelectObject';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
@@ -25,6 +26,7 @@ export const RasterLayerMenuItems = memo(() => {
|
||||
<CanvasEntityMenuItemsFilter />
|
||||
<CanvasEntityMenuItemsSelectObject />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsMergeDown />
|
||||
<RasterLayerMenuItemsCopyToSubMenu />
|
||||
<RasterLayerMenuItemsConvertToSubMenu />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/
|
||||
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsMergeDown } from 'features/controlLayers/components/common/CanvasEntityMenuItemsMergeDown';
|
||||
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import { RegionalGuidanceMenuItemsAddPromptsAndIPAdapter } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter';
|
||||
import { RegionalGuidanceMenuItemsAutoNegative } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative';
|
||||
@@ -25,9 +27,11 @@ export const RegionalGuidanceMenuItems = memo(() => {
|
||||
<CanvasEntityMenuItemsTransform />
|
||||
<RegionalGuidanceMenuItemsAutoNegative />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsMergeDown />
|
||||
<RegionalGuidanceMenuItemsCopyToSubMenu />
|
||||
<RegionalGuidanceMenuItemsConvertToSubMenu />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,9 +1,9 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useCopyLayerToClipboard } from 'features/controlLayers/hooks/useCopyLayerToClipboard';
|
||||
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCopyBold } from 'react-icons/pi';
|
||||
@@ -12,7 +12,7 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const isEmpty = useEntityIsEmpty(entityIdentifier);
|
||||
const copyLayerToClipboard = useCopyLayerToClipboard();
|
||||
|
||||
@@ -21,7 +21,7 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
|
||||
}, [copyLayerToClipboard, adapter]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={!isInteractable || isEmpty}>
|
||||
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={isBusy || isEmpty}>
|
||||
{t('common.clipboard')}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -3,8 +3,11 @@ import { buildSelectHasObjects } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useEntityIsEmpty = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
const selectHasObjects = useMemo(() => buildSelectHasObjects(entityIdentifier), [entityIdentifier]);
|
||||
export const useEntityIsEmpty = (entityIdentifier: CanvasEntityIdentifier | null) => {
|
||||
const selectHasObjects = useMemo(
|
||||
() => (entityIdentifier ? buildSelectHasObjects(entityIdentifier) : () => false),
|
||||
[entityIdentifier]
|
||||
);
|
||||
const hasObjects = useAppSelector(selectHasObjects);
|
||||
|
||||
return !hasObjects;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { addCoords, getKonvaNodeDebugAttrs, getPrefixedId } from 'features/contr
|
||||
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import type { FilterConfig } from 'features/controlLayers/store/filters';
|
||||
import { getFilterForModel, IMAGE_FILTERS } from 'features/controlLayers/store/filters';
|
||||
import type { CanvasEntityType, CanvasImageState } from 'features/controlLayers/store/types';
|
||||
import type { CanvasImageState, CanvasRenderableEntityType } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import Konva from 'konva';
|
||||
import { debounce } from 'lodash-es';
|
||||
@@ -350,7 +350,7 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
* Saves the filtered image as a new entity of the given type.
|
||||
* @param type The type of entity to save the filtered image as.
|
||||
*/
|
||||
saveAs = (type: Exclude<CanvasEntityType, 'reference_image'>) => {
|
||||
saveAs = (type: CanvasRenderableEntityType) => {
|
||||
const imageState = this.$imageState.get();
|
||||
if (!imageState) {
|
||||
this.log.warn('No image state to apply filter to');
|
||||
@@ -386,10 +386,6 @@ export class CanvasEntityFilterer extends CanvasModuleBase {
|
||||
default:
|
||||
assert<Equals<typeof type, never>>(false);
|
||||
}
|
||||
|
||||
// Final cleanup and teardown, returning user to main canvas UI
|
||||
this.resetEphemeralState();
|
||||
this.teardown();
|
||||
};
|
||||
|
||||
resetEphemeralState = () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -59,7 +59,9 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
this.hasActiveGeneration = true;
|
||||
} else {
|
||||
this.hasActiveGeneration = false;
|
||||
this.$lastProgressEvent.set(null);
|
||||
if (!this.manager.stagingArea.$isStaging.get()) {
|
||||
this.$lastProgressEvent.set(null);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
} from 'features/controlLayers/konva/util';
|
||||
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import type {
|
||||
CanvasEntityType,
|
||||
CanvasImageState,
|
||||
CanvasRenderableEntityType,
|
||||
Coordinate,
|
||||
RgbaColor,
|
||||
SAMPointLabel,
|
||||
@@ -697,7 +697,7 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
|
||||
* Saves the segmented image as a new entity of the given type.
|
||||
* @param type The type of entity to save the segmented image as.
|
||||
*/
|
||||
saveAs = (type: Exclude<CanvasEntityType, 'reference_image'>) => {
|
||||
saveAs = (type: CanvasRenderableEntityType) => {
|
||||
const imageState = this.$imageState.get();
|
||||
if (!imageState) {
|
||||
this.log.error('No image state to save as');
|
||||
@@ -740,10 +740,6 @@ export class CanvasSegmentAnythingModule extends CanvasModuleBase {
|
||||
default:
|
||||
assert<Equals<typeof type, never>>(false);
|
||||
}
|
||||
|
||||
// Final cleanup and teardown, returning user to main canvas UI
|
||||
this.resetEphemeralState();
|
||||
this.teardown();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import type { Property } from 'csstype';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getKonvaNodeDebugAttrs, getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util';
|
||||
import type {
|
||||
CanvasEntityIdentifier,
|
||||
Coordinate,
|
||||
Dimensions,
|
||||
Rect,
|
||||
StageAttrs,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { getKonvaNodeDebugAttrs, getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { Coordinate, Dimensions, Rect, StageAttrs } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { clamp } from 'lodash-es';
|
||||
@@ -146,24 +140,6 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
getVisibleRect = (type?: Exclude<CanvasEntityIdentifier['type'], 'ip_adapter'>): Rect => {
|
||||
const rects = [];
|
||||
|
||||
for (const adapter of this.manager.getAllAdapters()) {
|
||||
if (!adapter.state.isEnabled) {
|
||||
continue;
|
||||
}
|
||||
if (type && adapter.state.type !== type) {
|
||||
continue;
|
||||
}
|
||||
if (adapter.renderer.hasObjects()) {
|
||||
rects.push(adapter.transformer.getRelativeRect());
|
||||
}
|
||||
}
|
||||
|
||||
return getRectUnion(...rects);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fits the bbox to the stage. This will center the bbox and scale it to fit the stage with some padding.
|
||||
*/
|
||||
@@ -177,7 +153,7 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
* Fits the visible canvas to the stage. This will center the canvas and scale it to fit the stage with some padding.
|
||||
*/
|
||||
fitLayersToStage = (): void => {
|
||||
const rect = this.getVisibleRect();
|
||||
const rect = this.manager.compositor.getVisibleRectOfType();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
this.fitBboxToStage();
|
||||
} else {
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
selectGridSize,
|
||||
} from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasEntityType,
|
||||
CanvasState,
|
||||
EntityBrushLineAddedPayload,
|
||||
EntityEraserLineAddedPayload,
|
||||
@@ -47,7 +46,7 @@ import type {
|
||||
Rect,
|
||||
RgbaColor,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { RGBA_BLACK } from 'features/controlLayers/store/types';
|
||||
import { isRenderableEntityIdentifier, RGBA_BLACK } from 'features/controlLayers/store/types';
|
||||
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
@@ -546,24 +545,6 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
return this.getCanvasState().selectedEntityIdentifier?.id === id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if an entity type is hidden. Individual entities are not hidden; the entire entity type is hidden.
|
||||
*/
|
||||
getIsTypeHidden = (type: CanvasEntityType): boolean => {
|
||||
switch (type) {
|
||||
case 'raster_layer':
|
||||
return this.getRasterLayersState().isHidden;
|
||||
case 'control_layer':
|
||||
return this.getControlLayersState().isHidden;
|
||||
case 'inpaint_mask':
|
||||
return this.getInpaintMasksState().isHidden;
|
||||
case 'regional_guidance':
|
||||
return this.getRegionsState().isHidden;
|
||||
default:
|
||||
assert(false, 'Unhandled entity type');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the number of entities that are currently rendered on the canvas.
|
||||
*/
|
||||
@@ -583,10 +564,13 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
*/
|
||||
getSelectedEntityAdapter = (): CanvasEntityAdapter | null => {
|
||||
const state = this.getCanvasState();
|
||||
if (state.selectedEntityIdentifier) {
|
||||
return this.manager.getAdapter(state.selectedEntityIdentifier);
|
||||
if (!state.selectedEntityIdentifier) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
if (!isRenderableEntityIdentifier(state.selectedEntityIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
return this.manager.getAdapter(state.selectedEntityIdentifier);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -696,4 +680,20 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
* Whether the shift key is currently pressed.
|
||||
*/
|
||||
$shiftKey = $shift;
|
||||
|
||||
repr = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
path: this.path,
|
||||
$filteringAdapter: this.$filteringAdapter.get()?.entityIdentifier,
|
||||
$isFiltering: this.$isFiltering.get(),
|
||||
$transformingAdapter: this.$transformingAdapter.get()?.entityIdentifier,
|
||||
$isTransforming: this.$isTransforming.get(),
|
||||
$rasterizingAdapter: this.$rasterizingAdapter.get()?.entityIdentifier,
|
||||
$isRasterizing: this.$isRasterizing.get(),
|
||||
$segmentingAdapter: this.$segmentingAdapter.get()?.entityIdentifier,
|
||||
$isSegmenting: this.$isSegmenting.get(),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -390,7 +390,7 @@ export class CanvasBboxToolModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
fitToLayers = (): void => {
|
||||
const visibleRect = this.manager.stage.getVisibleRect();
|
||||
const visibleRect = this.manager.compositor.getVisibleRectOfType();
|
||||
|
||||
// Can't fit the bbox to nothing
|
||||
if (visibleRect.height === 0 || visibleRect.width === 0) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import type { CanvasToolModule } from 'features/controlLayers/konva/CanvasTool/CanvasToolModule';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { noop } from 'lodash-es';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
export class CanvasMoveToolModule extends CanvasModuleBase {
|
||||
@@ -24,8 +23,13 @@ export class CanvasMoveToolModule extends CanvasModuleBase {
|
||||
this.log.debug('Creating module');
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a noop. Entity transformers handle cursor style when the move tool is active.
|
||||
*/
|
||||
syncCursorStyle = noop;
|
||||
syncCursorStyle = () => {
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
if (!selectedEntity) {
|
||||
this.manager.stage.setCursor('not-allowed');
|
||||
} else {
|
||||
// The cursor is on an entity, defer to transformer to handle the cursor
|
||||
selectedEntity.transformer.syncCursorStyle();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
const tool = this.$tool.get();
|
||||
const segmentingAdapter = this.manager.stateApi.$segmentingAdapter.get();
|
||||
const transformingAdapter = this.manager.stateApi.$transformingAdapter.get();
|
||||
const selectedEntityAdapter = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (this.manager.stage.getIsDragging()) {
|
||||
this.tools.view.syncCursorStyle();
|
||||
@@ -169,7 +170,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
} else if (segmentingAdapter) {
|
||||
segmentingAdapter.segmentAnything.syncCursorStyle();
|
||||
} else if (transformingAdapter) {
|
||||
// The transformer handles cursor style via events
|
||||
transformingAdapter.transformer.syncCursorStyle();
|
||||
} else if (this.manager.stateApi.$isFiltering.get()) {
|
||||
stage.setCursor('not-allowed');
|
||||
} else if (this.manager.stagingArea.$isStaging.get()) {
|
||||
@@ -178,7 +179,11 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
this.tools.bbox.syncCursorStyle();
|
||||
} else if (this.manager.stateApi.getRenderedEntityCount() === 0) {
|
||||
stage.setCursor('not-allowed');
|
||||
} else if (!this.manager.stateApi.getSelectedEntityAdapter()?.$isInteractable.get()) {
|
||||
} else if (selectedEntityAdapter?.$isDisabled.get()) {
|
||||
stage.setCursor('not-allowed');
|
||||
} else if (selectedEntityAdapter?.$isEntityTypeHidden.get()) {
|
||||
stage.setCursor('not-allowed');
|
||||
} else if (selectedEntityAdapter?.$isLocked.get()) {
|
||||
stage.setCursor('not-allowed');
|
||||
} else if (tool === 'brush') {
|
||||
this.tools.brush.syncCursorStyle();
|
||||
@@ -301,7 +306,15 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!selectedEntity.$isInteractable.get()) {
|
||||
if (selectedEntity.$isDisabled.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedEntity.$isEntityTypeHidden.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedEntity.$isLocked.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,27 +123,28 @@ export const canvasSlice = createSlice({
|
||||
id: string;
|
||||
overrides?: Partial<CanvasRasterLayerState>;
|
||||
isSelected?: boolean;
|
||||
isMergingVisible?: boolean;
|
||||
mergedEntitiesToDelete?: string[];
|
||||
}>
|
||||
) => {
|
||||
const { id, overrides, isSelected, isMergingVisible } = action.payload;
|
||||
const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
|
||||
const entityState = getRasterLayerState(id, overrides);
|
||||
|
||||
if (isMergingVisible) {
|
||||
// When merging visible, we delete all disabled layers
|
||||
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => !layer.isEnabled);
|
||||
}
|
||||
|
||||
state.rasterLayers.entities.push(entityState);
|
||||
|
||||
if (isSelected) {
|
||||
if (mergedEntitiesToDelete.length > 0) {
|
||||
state.rasterLayers.entities = state.rasterLayers.entities.filter(
|
||||
(entity) => !mergedEntitiesToDelete.includes(entity.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (isSelected || mergedEntitiesToDelete.length > 0) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
|
||||
}
|
||||
},
|
||||
prepare: (payload: {
|
||||
overrides?: Partial<CanvasRasterLayerState>;
|
||||
isSelected?: boolean;
|
||||
isMergingVisible?: boolean;
|
||||
mergedEntitiesToDelete?: string[];
|
||||
}) => ({
|
||||
payload: { ...payload, id: getPrefixedId('raster_layer') },
|
||||
}),
|
||||
@@ -271,19 +272,34 @@ export const canvasSlice = createSlice({
|
||||
controlLayerAdded: {
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }>
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
overrides?: Partial<CanvasControlLayerState>;
|
||||
isSelected?: boolean;
|
||||
mergedEntitiesToDelete?: string[];
|
||||
}>
|
||||
) => {
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
|
||||
|
||||
const entityState = getControlLayerState(id, overrides);
|
||||
|
||||
state.controlLayers.entities.push(entityState);
|
||||
|
||||
if (isSelected) {
|
||||
if (mergedEntitiesToDelete.length > 0) {
|
||||
state.controlLayers.entities = state.controlLayers.entities.filter(
|
||||
(entity) => !mergedEntitiesToDelete.includes(entity.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (isSelected || mergedEntitiesToDelete.length > 0) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
|
||||
}
|
||||
},
|
||||
prepare: (payload: { overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }) => ({
|
||||
prepare: (payload: {
|
||||
overrides?: Partial<CanvasControlLayerState>;
|
||||
isSelected?: boolean;
|
||||
mergedEntitiesToDelete?: string[];
|
||||
}) => ({
|
||||
payload: { ...payload, id: getPrefixedId('control_layer') },
|
||||
}),
|
||||
},
|
||||
@@ -595,19 +611,34 @@ export const canvasSlice = createSlice({
|
||||
rgAdded: {
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }>
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
overrides?: Partial<CanvasRegionalGuidanceState>;
|
||||
isSelected?: boolean;
|
||||
mergedEntitiesToDelete?: string[];
|
||||
}>
|
||||
) => {
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
|
||||
|
||||
const entityState = getRegionalGuidanceState(id, overrides);
|
||||
|
||||
state.regionalGuidance.entities.push(entityState);
|
||||
|
||||
if (isSelected) {
|
||||
if (mergedEntitiesToDelete.length > 0) {
|
||||
state.regionalGuidance.entities = state.regionalGuidance.entities.filter(
|
||||
(entity) => !mergedEntitiesToDelete.includes(entity.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (isSelected || mergedEntitiesToDelete.length > 0) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
|
||||
}
|
||||
},
|
||||
prepare: (payload?: { overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }) => ({
|
||||
prepare: (payload?: {
|
||||
overrides?: Partial<CanvasRegionalGuidanceState>;
|
||||
isSelected?: boolean;
|
||||
mergedEntitiesToDelete?: string[];
|
||||
}) => ({
|
||||
payload: { ...payload, id: getPrefixedId('regional_guidance') },
|
||||
}),
|
||||
},
|
||||
@@ -822,28 +853,29 @@ export const canvasSlice = createSlice({
|
||||
id: string;
|
||||
overrides?: Partial<CanvasInpaintMaskState>;
|
||||
isSelected?: boolean;
|
||||
isMergingVisible?: boolean;
|
||||
mergedEntitiesToDelete?: string[];
|
||||
}>
|
||||
) => {
|
||||
const { id, overrides, isSelected, isMergingVisible } = action.payload;
|
||||
const { id, overrides, isSelected, mergedEntitiesToDelete = [] } = action.payload;
|
||||
|
||||
const entityState = getInpaintMaskState(id, overrides);
|
||||
|
||||
if (isMergingVisible) {
|
||||
// When merging visible, we delete all disabled layers
|
||||
state.inpaintMasks.entities = state.inpaintMasks.entities.filter((layer) => !layer.isEnabled);
|
||||
}
|
||||
|
||||
state.inpaintMasks.entities.push(entityState);
|
||||
|
||||
if (isSelected) {
|
||||
if (mergedEntitiesToDelete.length > 0) {
|
||||
state.inpaintMasks.entities = state.inpaintMasks.entities.filter(
|
||||
(entity) => !mergedEntitiesToDelete.includes(entity.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (isSelected || mergedEntitiesToDelete.length > 0) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
|
||||
}
|
||||
},
|
||||
prepare: (payload?: {
|
||||
overrides?: Partial<CanvasInpaintMaskState>;
|
||||
isSelected?: boolean;
|
||||
isMergingVisible?: boolean;
|
||||
mergedEntitiesToDelete?: string[];
|
||||
}) => ({
|
||||
payload: { ...payload, id: getPrefixedId('inpaint_mask') },
|
||||
}),
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { selectIsolatedStagingPreview } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasEntityState,
|
||||
CanvasEntityType,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasMetadata,
|
||||
CanvasRasterLayerState,
|
||||
CanvasRegionalGuidanceState,
|
||||
CanvasRenderableEntityIdentifier,
|
||||
CanvasRenderableEntityState,
|
||||
CanvasRenderableEntityType,
|
||||
CanvasState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { isRasterLayerEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { getGridSize, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
@@ -43,23 +43,25 @@ const selectEntityCountAll = createSelector(selectCanvasSlice, (canvas) => {
|
||||
);
|
||||
});
|
||||
|
||||
const selectActiveRasterLayerEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.rasterLayers.entities.filter((e) => e.isEnabled && e.objects.length > 0)
|
||||
const isVisibleEntity = (entity: CanvasRenderableEntityState) => entity.isEnabled && entity.objects.length > 0;
|
||||
|
||||
export const selectActiveRasterLayerEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.rasterLayers.entities.filter(isVisibleEntity)
|
||||
);
|
||||
|
||||
const selectActiveControlLayerEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.controlLayers.entities.filter((e) => e.isEnabled && e.objects.length > 0)
|
||||
export const selectActiveControlLayerEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.controlLayers.entities.filter(isVisibleEntity)
|
||||
);
|
||||
|
||||
const selectActiveInpaintMaskEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.inpaintMasks.entities.filter((e) => e.isEnabled && e.objects.length > 0)
|
||||
export const selectActiveInpaintMaskEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.inpaintMasks.entities.filter(isVisibleEntity)
|
||||
);
|
||||
|
||||
const selectActiveRegionalGuidanceEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.regionalGuidance.entities.filter((e) => e.isEnabled && e.objects.length > 0)
|
||||
export const selectActiveRegionalGuidanceEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.regionalGuidance.entities.filter(isVisibleEntity)
|
||||
);
|
||||
|
||||
const selectActiveIPAdapterEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
export const selectActiveReferenceImageEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.referenceImages.entities.filter((e) => e.isEnabled)
|
||||
);
|
||||
|
||||
@@ -78,7 +80,7 @@ export const selectEntityCountActive = createSelector(
|
||||
selectActiveControlLayerEntities,
|
||||
selectActiveInpaintMaskEntities,
|
||||
selectActiveRegionalGuidanceEntities,
|
||||
selectActiveIPAdapterEntities,
|
||||
selectActiveReferenceImageEntities,
|
||||
(
|
||||
activeRasterLayerEntities,
|
||||
activeControlLayerEntities,
|
||||
@@ -148,7 +150,46 @@ export function selectEntity<T extends CanvasEntityIdentifier>(
|
||||
}
|
||||
|
||||
// This cast is safe, but TS seems to be unable to infer the type
|
||||
return entity as Extract<CanvasEntityState, T>;
|
||||
return entity as Extract<CanvasEntityState, T> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the entity identifier for the entity that is below the given entity in terms of draw order.
|
||||
*/
|
||||
export function selectEntityIdentifierBelowThisOne<T extends CanvasRenderableEntityIdentifier>(
|
||||
state: CanvasState,
|
||||
entityIdentifier: T
|
||||
): Extract<CanvasEntityState, T> | undefined {
|
||||
const { id, type } = entityIdentifier;
|
||||
|
||||
let entities: CanvasRenderableEntityState[];
|
||||
|
||||
switch (type) {
|
||||
case 'raster_layer': {
|
||||
entities = state.rasterLayers.entities;
|
||||
break;
|
||||
}
|
||||
case 'control_layer': {
|
||||
entities = state.controlLayers.entities;
|
||||
break;
|
||||
}
|
||||
case 'inpaint_mask': {
|
||||
entities = state.inpaintMasks.entities;
|
||||
break;
|
||||
}
|
||||
case 'regional_guidance': {
|
||||
entities = state.regionalGuidance.entities;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Must reverse to get the draw order
|
||||
const reversedEntities = entities.toReversed();
|
||||
const idx = reversedEntities.findIndex((entity) => entity.id === id);
|
||||
const entity = reversedEntities.at(idx + 1);
|
||||
|
||||
// This cast is safe, but TS seems to be unable to infer the type
|
||||
return entity as Extract<CanvasEntityState, T> | undefined;
|
||||
}
|
||||
|
||||
export const selectRasterLayerEntities = createSelector(selectCanvasSlice, (canvas) => canvas.rasterLayers.entities);
|
||||
@@ -290,7 +331,7 @@ const selectRegionalGuidanceIsHidden = createSelector(selectCanvasSlice, (canvas
|
||||
/**
|
||||
* Returns the hidden selector for the given entity type.
|
||||
*/
|
||||
const getSelectIsTypeHidden = (type: CanvasEntityType) => {
|
||||
export const getSelectIsTypeHidden = (type: CanvasRenderableEntityType) => {
|
||||
switch (type) {
|
||||
case 'raster_layer':
|
||||
return selectRasterLayersIsHidden;
|
||||
@@ -301,44 +342,10 @@ const getSelectIsTypeHidden = (type: CanvasEntityType) => {
|
||||
case 'regional_guidance':
|
||||
return selectRegionalGuidanceIsHidden;
|
||||
default:
|
||||
assert(false, 'Unhandled entity type');
|
||||
assert<Equals<typeof type, never>>(false, 'Unhandled entity type');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a selector taht selects if the entity is hidden.
|
||||
*/
|
||||
export const buildSelectIsHidden = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
const selectIsTypeHidden = getSelectIsTypeHidden(entityIdentifier.type);
|
||||
return createSelector(
|
||||
[selectCanvasSlice, selectIsTypeHidden, selectIsStaging, selectIsolatedStagingPreview],
|
||||
(canvas, isTypeHidden, isStaging, isolatedStagingPreview) => {
|
||||
const entity = selectEntity(canvas, entityIdentifier);
|
||||
|
||||
// An entity is hidden if:
|
||||
// - The entity type is hidden
|
||||
// - The entity is disabled
|
||||
// - The entity is not a raster layer and we are staging and the option to show only raster layers is enabled
|
||||
if (!entity) {
|
||||
return true;
|
||||
}
|
||||
if (isTypeHidden) {
|
||||
return true;
|
||||
}
|
||||
if (!entity.isEnabled) {
|
||||
return true;
|
||||
}
|
||||
if (isStaging && isolatedStagingPreview) {
|
||||
// When staging, we only show raster layers. This allows the user to easily see how the new generation fits in
|
||||
// with the rest of the canvas without the masks and control layers getting in the way.
|
||||
return !isRasterLayerEntityIdentifier(entityIdentifier);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a selector taht selects if the entity is selected.
|
||||
*/
|
||||
|
||||
@@ -332,6 +332,7 @@ const zCanvasRenderableEntityState = z.discriminatedUnion('type', [
|
||||
zCanvasInpaintMaskState,
|
||||
]);
|
||||
export type CanvasRenderableEntityState = z.infer<typeof zCanvasRenderableEntityState>;
|
||||
export type CanvasRenderableEntityType = CanvasRenderableEntityState['type'];
|
||||
|
||||
const zCanvasEntityType = z.union([
|
||||
zCanvasRasterLayerState.shape.type,
|
||||
@@ -347,7 +348,7 @@ export const zCanvasEntityIdentifer = z.object({
|
||||
type: zCanvasEntityType,
|
||||
});
|
||||
export type CanvasEntityIdentifier<T extends CanvasEntityType = CanvasEntityType> = { id: string; type: T };
|
||||
|
||||
export type CanvasRenderableEntityIdentifier = CanvasEntityIdentifier<CanvasRenderableEntityType>;
|
||||
export type LoRA = {
|
||||
id: string;
|
||||
isEnabled: boolean;
|
||||
@@ -465,7 +466,7 @@ export type EntityRasterizedPayload = EntityIdentifierPayload<{
|
||||
|
||||
export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';
|
||||
|
||||
function isRenderableEntityType(
|
||||
export function isRenderableEntityType(
|
||||
entityType: CanvasEntityState['type']
|
||||
): entityType is CanvasRenderableEntityState['type'] {
|
||||
return (
|
||||
@@ -537,6 +538,12 @@ export function isRenderableEntity(entity: CanvasEntityState): entity is CanvasR
|
||||
return isRenderableEntityType(entity.type);
|
||||
}
|
||||
|
||||
export function isRenderableEntityIdentifier(
|
||||
entityIdentifier: CanvasEntityIdentifier
|
||||
): entityIdentifier is CanvasRenderableEntityIdentifier {
|
||||
return isRenderableEntityType(entityIdentifier.type);
|
||||
}
|
||||
|
||||
export const getEntityIdentifier = <T extends CanvasEntityType>(
|
||||
entity: Extract<CanvasEntityState, { type: T }>
|
||||
): CanvasEntityIdentifier<T> => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ImageMenuItemMetadataRecallActions = memo(() => {
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiArrowBendUpLeftBold />}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label="Recall Metadata" />
|
||||
<SubMenuButtonContent label={t('parameters.recallMetadata')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<MenuItem icon={<PiArrowsCounterClockwiseBold />} onClick={remix} isDisabled={!hasMetadata}>
|
||||
|
||||
@@ -106,7 +106,7 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => {
|
||||
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>
|
||||
<Menu {...subMenu.menuProps}>
|
||||
<MenuButton {...subMenu.menuButtonProps}>
|
||||
<SubMenuButtonContent label="New from Image" />
|
||||
<SubMenuButtonContent label={t('controlLayers.newFromImage')} />
|
||||
</MenuButton>
|
||||
<MenuList {...subMenu.menuListProps}>
|
||||
<MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasWithRasterLayerFromImage} isDisabled={isBusy}>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ExternalLink, Text, useToast } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { atom } from 'nanostores';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const TOAST_ID = 'hfForbidden';
|
||||
|
||||
/**
|
||||
* Tracks whether or not the HF Login toast is showing
|
||||
*/
|
||||
export const $isHFForbiddenToastOpen = atom<{ isEnabled: boolean; source?: string }>({ isEnabled: false });
|
||||
|
||||
export const useHFForbiddenToast = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const isHFForbiddenToastOpen = useStore($isHFForbiddenToastOpen);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHFForbiddenToastOpen.isEnabled) {
|
||||
toast.close(TOAST_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHFForbiddenToastOpen.isEnabled) {
|
||||
toast({
|
||||
id: TOAST_ID,
|
||||
title: t('modelManager.hfForbidden'),
|
||||
description: (
|
||||
<Text fontSize="md">
|
||||
{t('modelManager.hfForbiddenErrorMessage')}
|
||||
<ExternalLink
|
||||
label={isHFForbiddenToastOpen.source || ''}
|
||||
href={`https://huggingface.co/${isHFForbiddenToastOpen.source}`}
|
||||
/>
|
||||
</Text>
|
||||
),
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
duration: null,
|
||||
onCloseComplete: () => $isHFForbiddenToastOpen.set({ isEnabled: false }),
|
||||
});
|
||||
}
|
||||
}, [isHFForbiddenToastOpen, t, toast]);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Button, Text, useToast } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { t } from 'i18next';
|
||||
import { atom } from 'nanostores';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetHFTokenStatusQuery } from 'services/api/endpoints/models';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
const FEATURE_ID = 'hfToken';
|
||||
const TOAST_ID = 'hfTokenLogin';
|
||||
/**
|
||||
* Tracks whether or not the HF Login toast is showing
|
||||
*/
|
||||
export const $isHFLoginToastOpen = atom<boolean>(false);
|
||||
|
||||
const getTitle = (token_status: S['HFTokenStatus']) => {
|
||||
switch (token_status) {
|
||||
case 'invalid':
|
||||
return t('modelManager.hfTokenInvalid');
|
||||
case 'unknown':
|
||||
return t('modelManager.hfTokenUnableToVerify');
|
||||
}
|
||||
};
|
||||
|
||||
export const useHFLoginToast = () => {
|
||||
const isEnabled = useFeatureStatus(FEATURE_ID);
|
||||
const { data } = useGetHFTokenStatusQuery(isEnabled ? undefined : skipToken);
|
||||
const toast = useToast();
|
||||
const isHFLoginToastOpen = useStore($isHFLoginToastOpen);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHFLoginToastOpen) {
|
||||
toast.close(TOAST_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHFLoginToastOpen && data) {
|
||||
const title = getTitle(data);
|
||||
toast({
|
||||
id: TOAST_ID,
|
||||
title,
|
||||
description: <ToastDescription token_status={data} />,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
duration: null,
|
||||
onCloseComplete: () => $isHFLoginToastOpen.set(false),
|
||||
});
|
||||
}
|
||||
}, [isHFLoginToastOpen, data, toast]);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
token_status: S['HFTokenStatus'];
|
||||
};
|
||||
|
||||
const ToastDescription = ({ token_status }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(setActiveTab('models'));
|
||||
toast.close(FEATURE_ID);
|
||||
}, [dispatch, toast]);
|
||||
|
||||
if (token_status === 'invalid') {
|
||||
return (
|
||||
<Text fontSize="md">
|
||||
{t('modelManager.hfTokenInvalidErrorMessage')} {t('modelManager.hfTokenRequired')}{' '}
|
||||
{t('modelManager.hfTokenInvalidErrorMessage2')}
|
||||
<Button onClick={onClick} variant="link" color="base.50" flexGrow={0}>
|
||||
{t('modelManager.modelManager')}.
|
||||
</Button>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (token_status === 'unknown') {
|
||||
return (
|
||||
<Text fontSize="md">
|
||||
{t('modelManager.hfTokenUnableToErrorMessage')}{' '}
|
||||
<Button onClick={onClick} variant="link" color="base.50" flexGrow={0}>
|
||||
{t('modelManager.modelManager')}.
|
||||
</Button>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
Button,
|
||||
ExternalLink,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Input,
|
||||
useToast,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { $isHFLoginToastOpen } from 'features/modelManagerV2/hooks/useHFLoginToast';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetHFTokenStatusQuery, useSetHFTokenMutation } from 'services/api/endpoints/models';
|
||||
|
||||
export const HFToken = () => {
|
||||
const { t } = useTranslation();
|
||||
const isHFTokenEnabled = useFeatureStatus('hfToken');
|
||||
const [token, setToken] = useState('');
|
||||
const { currentData } = useGetHFTokenStatusQuery(isHFTokenEnabled ? undefined : skipToken);
|
||||
const [trigger, { isLoading, isUninitialized }] = useSetHFTokenMutation();
|
||||
const toast = useToast();
|
||||
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setToken(e.target.value);
|
||||
}, []);
|
||||
const onClick = useCallback(() => {
|
||||
trigger({ token })
|
||||
.unwrap()
|
||||
.then((res) => {
|
||||
if (res === 'valid') {
|
||||
setToken('');
|
||||
toast({
|
||||
title: t('modelManager.hfTokenSaved'),
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
$isHFLoginToastOpen.set(false);
|
||||
}
|
||||
});
|
||||
}, [t, toast, token, trigger]);
|
||||
|
||||
const error = useMemo(() => {
|
||||
if (!currentData || isUninitialized || isLoading) {
|
||||
return null;
|
||||
}
|
||||
if (currentData === 'invalid') {
|
||||
return t('modelManager.hfTokenInvalidErrorMessage');
|
||||
}
|
||||
if (currentData === 'unknown') {
|
||||
return t('modelManager.hfTokenUnableToVerifyErrorMessage');
|
||||
}
|
||||
return null;
|
||||
}, [currentData, isLoading, isUninitialized, t]);
|
||||
|
||||
if (!currentData || currentData === 'valid') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex borderRadius="base" w="full">
|
||||
<FormControl isInvalid={!isUninitialized && Boolean(error)} orientation="vertical">
|
||||
<FormLabel>{t('modelManager.hfTokenLabel')}</FormLabel>
|
||||
<Flex gap={3} alignItems="center" w="full">
|
||||
<Input type="password" value={token} onChange={onChange} />
|
||||
<Button onClick={onClick} size="sm" isDisabled={token.trim().length === 0} isLoading={isLoading}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<FormHelperText>
|
||||
<ExternalLink label={t('modelManager.hfTokenHelperText')} href="https://huggingface.co/settings/tokens" />
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>{error}</FormErrorMessage>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';
|
||||
import { useGetHFTokenStatusQuery, useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';
|
||||
|
||||
import { HFToken } from './HFToken';
|
||||
import { HuggingFaceResults } from './HuggingFaceResults';
|
||||
|
||||
export const HuggingFaceForm = memo(() => {
|
||||
@@ -12,6 +15,8 @@ export const HuggingFaceForm = memo(() => {
|
||||
const [displayResults, setDisplayResults] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const isHFTokenEnabled = useFeatureStatus('hfToken');
|
||||
const { currentData } = useGetHFTokenStatusQuery(isHFTokenEnabled ? undefined : skipToken);
|
||||
|
||||
const [_getHuggingFaceModels, { isLoading, data }] = useLazyGetHuggingFaceModelsQuery();
|
||||
const [installModel] = useInstallModel();
|
||||
@@ -41,7 +46,7 @@ export const HuggingFaceForm = memo(() => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" height="100%" gap={3}>
|
||||
<Flex flexDir="column" height="100%" gap={4}>
|
||||
<FormControl isInvalid={!!errorMessage.length} w="full" orientation="vertical" flexShrink={0}>
|
||||
<FormLabel>{t('modelManager.huggingFaceRepoID')}</FormLabel>
|
||||
<Flex gap={3} alignItems="center" w="full">
|
||||
@@ -63,6 +68,7 @@ export const HuggingFaceForm = memo(() => {
|
||||
<FormHelperText>{t('modelManager.huggingFaceHelper')}</FormHelperText>
|
||||
{!!errorMessage.length && <FormErrorMessage>{errorMessage}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
{currentData !== 'valid' && <HFToken />}
|
||||
{data && data.urls && displayResults && <HuggingFaceResults results={data.urls} />}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Button, Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useHFForbiddenToast } from 'features/modelManagerV2/hooks/useHFForbiddenToast';
|
||||
import { useHFLoginToast } from 'features/modelManagerV2/hooks/useHFLoginToast';
|
||||
import { selectSelectedModelKey, setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -16,6 +18,9 @@ export const ModelManager = memo(() => {
|
||||
}, [dispatch]);
|
||||
const selectedModelKey = useAppSelector(selectSelectedModelKey);
|
||||
|
||||
useHFLoginToast();
|
||||
useHFForbiddenToast();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" layerStyle="first" p={4} gap={4} borderRadius="base" w="50%" h="full">
|
||||
<Flex w="full" gap={4} justifyContent="space-between" alignItems="center">
|
||||
|
||||
@@ -32,8 +32,8 @@ export const addImageToImage = async ({
|
||||
fp32,
|
||||
}: AddImageToImageArg): Promise<Invocation<'img_resize' | 'l2i' | 'flux_vae_decode'>> => {
|
||||
denoise.denoising_start = denoising_start;
|
||||
|
||||
const { image_name } = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||
const adapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
|
||||
const { image_name } = await manager.compositor.getCompositeImageDTO(adapters, bbox.rect, { is_intermediate: true });
|
||||
|
||||
if (!isEqual(scaledSize, originalSize)) {
|
||||
// Resize the initial image to the scaled size, denoise, then resize back to the original size
|
||||
|
||||
@@ -45,8 +45,15 @@ export const addInpaint = async ({
|
||||
|
||||
const { bbox } = canvas;
|
||||
|
||||
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
||||
const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
|
||||
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
});
|
||||
|
||||
const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
|
||||
const maskImage = await manager.compositor.getCompositeImageDTO(inpaintMaskAdapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
});
|
||||
|
||||
if (!isEqual(scaledSize, originalSize)) {
|
||||
// Scale before processing requires some resizing
|
||||
|
||||
@@ -45,8 +45,16 @@ export const addOutpaint = async ({
|
||||
|
||||
const { bbox } = canvas;
|
||||
|
||||
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
||||
const rasterAdapters = manager.compositor.getVisibleAdaptersOfType('raster_layer');
|
||||
const initialImage = await manager.compositor.getCompositeImageDTO(rasterAdapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
});
|
||||
|
||||
const inpaintMaskAdapters = manager.compositor.getVisibleAdaptersOfType('inpaint_mask');
|
||||
const maskImage = await manager.compositor.getCompositeImageDTO(inpaintMaskAdapters, bbox.rect, {
|
||||
is_intermediate: true,
|
||||
});
|
||||
|
||||
const infill = getInfill(g, params);
|
||||
|
||||
if (!isEqual(scaledSize, originalSize)) {
|
||||
|
||||
@@ -34,7 +34,7 @@ export const buildFLUXGraph = async (
|
||||
state: RootState,
|
||||
manager: CanvasManager
|
||||
): Promise<{ g: Graph; noise: Invocation<'noise' | 'flux_denoise'>; posCond: Invocation<'flux_text_encoder'> }> => {
|
||||
const generationMode = manager.compositor.getGenerationMode();
|
||||
const generationMode = await manager.compositor.getGenerationMode();
|
||||
log.debug({ generationMode }, 'Building FLUX graph');
|
||||
|
||||
const params = selectParamsSlice(state);
|
||||
|
||||
@@ -37,7 +37,7 @@ export const buildSD1Graph = async (
|
||||
state: RootState,
|
||||
manager: CanvasManager
|
||||
): Promise<{ g: Graph; noise: Invocation<'noise'>; posCond: Invocation<'compel'> }> => {
|
||||
const generationMode = manager.compositor.getGenerationMode();
|
||||
const generationMode = await manager.compositor.getGenerationMode();
|
||||
log.debug({ generationMode }, 'Building SD1/SD2 graph');
|
||||
|
||||
const params = selectParamsSlice(state);
|
||||
|
||||
@@ -37,7 +37,7 @@ export const buildSDXLGraph = async (
|
||||
state: RootState,
|
||||
manager: CanvasManager
|
||||
): Promise<{ g: Graph; noise: Invocation<'noise'>; posCond: Invocation<'sdxl_compel_prompt'> }> => {
|
||||
const generationMode = manager.compositor.getGenerationMode();
|
||||
const generationMode = await manager.compositor.getGenerationMode();
|
||||
log.debug({ generationMode }, 'Building SDXL graph');
|
||||
|
||||
const params = selectParamsSlice(state);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createEntityAdapter } from '@reduxjs/toolkit';
|
||||
import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
|
||||
import queryString from 'query-string';
|
||||
import type { operations, paths } from 'services/api/schema';
|
||||
import type { AnyModelConfig } from 'services/api/types';
|
||||
import type { AnyModelConfig, GetHFTokenStatusResponse, SetHFTokenArg, SetHFTokenResponse } from 'services/api/types';
|
||||
|
||||
import type { ApiTagDescription } from '..';
|
||||
import { api, buildV2Url, LIST_TAG } from '..';
|
||||
@@ -259,6 +259,22 @@ export const modelsApi = api.injectEndpoints({
|
||||
query: () => buildModelsUrl('starter_models'),
|
||||
providesTags: [{ type: 'ModelConfig', id: LIST_TAG }],
|
||||
}),
|
||||
getHFTokenStatus: build.query<GetHFTokenStatusResponse, void>({
|
||||
query: () => buildModelsUrl('hf_login'),
|
||||
providesTags: ['HFTokenStatus'],
|
||||
}),
|
||||
setHFToken: build.mutation<SetHFTokenResponse, SetHFTokenArg>({
|
||||
query: (body) => ({ url: buildModelsUrl('hf_login'), method: 'POST', body }),
|
||||
invalidatesTags: ['HFTokenStatus'],
|
||||
onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
dispatch(modelsApi.util.updateQueryData('getHFTokenStatus', undefined, () => data));
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -277,6 +293,8 @@ export const {
|
||||
useCancelModelInstallMutation,
|
||||
usePruneCompletedModelInstallsMutation,
|
||||
useGetStarterModelsQuery,
|
||||
useGetHFTokenStatusQuery,
|
||||
useSetHFTokenMutation,
|
||||
} = modelsApi;
|
||||
|
||||
export const selectModelConfigsQuery = modelsApi.endpoints.getModelConfigs.select();
|
||||
|
||||
@@ -344,6 +344,24 @@ export type paths = {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v2/models/hf_login": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get Hf Login Status */
|
||||
get: operations["get_hf_login_status"];
|
||||
put?: never;
|
||||
/** Do Hf Login */
|
||||
post: operations["do_hf_login"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/download_queue/": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -2157,6 +2175,14 @@ export type components = {
|
||||
*/
|
||||
image_names: string[];
|
||||
};
|
||||
/** Body_do_hf_login */
|
||||
Body_do_hf_login: {
|
||||
/**
|
||||
* Token
|
||||
* @description Hugging Face token to use for login
|
||||
*/
|
||||
token: string;
|
||||
};
|
||||
/** Body_download */
|
||||
Body_download: {
|
||||
/**
|
||||
@@ -7322,6 +7348,11 @@ export type components = {
|
||||
*/
|
||||
type: "hf";
|
||||
};
|
||||
/**
|
||||
* HFTokenStatus
|
||||
* @enum {string}
|
||||
*/
|
||||
HFTokenStatus: "valid" | "invalid" | "unknown";
|
||||
/** HTTPValidationError */
|
||||
HTTPValidationError: {
|
||||
/** Detail */
|
||||
@@ -18274,6 +18305,59 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
get_hf_login_status: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HFTokenStatus"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
do_hf_login: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Body_do_hf_login"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HFTokenStatus"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
list_downloads: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -244,3 +244,12 @@ export type PostUploadAction =
|
||||
|
||||
export type BoardRecordOrderBy = S['BoardRecordOrderBy'];
|
||||
export type StarterModel = S['StarterModel'];
|
||||
|
||||
export type GetHFTokenStatusResponse =
|
||||
paths['/api/v2/models/hf_login']['get']['responses']['200']['content']['application/json'];
|
||||
export type SetHFTokenResponse = NonNullable<
|
||||
paths['/api/v2/models/hf_login']['post']['responses']['200']['content']['application/json']
|
||||
>;
|
||||
export type SetHFTokenArg = NonNullable<
|
||||
paths['/api/v2/models/hf_login']['post']['requestBody']['content']['application/json']
|
||||
>;
|
||||
|
||||
@@ -7,6 +7,8 @@ import { $queueId } from 'app/store/nanostores/queueId';
|
||||
import type { AppStore } from 'app/store/store';
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { $isHFForbiddenToastOpen } from 'features/modelManagerV2/hooks/useHFForbiddenToast';
|
||||
import { $isHFLoginToastOpen } from 'features/modelManagerV2/hooks/useHFLoginToast';
|
||||
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription';
|
||||
@@ -295,6 +297,14 @@ export const setEventListeners = ({ socket, store, setIsConnected }: SetEventLis
|
||||
const { id, error, error_type } = data;
|
||||
const installs = selectModelInstalls(getState()).data;
|
||||
|
||||
if (error === 'Unauthorized') {
|
||||
$isHFLoginToastOpen.set(true);
|
||||
}
|
||||
|
||||
if (error === 'Forbidden') {
|
||||
$isHFForbiddenToastOpen.set({ isEnabled: true, source: data.source });
|
||||
}
|
||||
|
||||
if (!installs?.find((install) => install.id === id)) {
|
||||
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
|
||||
} else {
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "5.3.0"
|
||||
__version__ = "5.3.1"
|
||||
|
||||
@@ -42,7 +42,7 @@ dependencies = [
|
||||
"gguf==0.10.0",
|
||||
"invisible-watermark==0.2.0", # needed to install SDXL base and refiner using their repo_ids
|
||||
"mediapipe>=0.10.7", # needed for "mediapipeface" controlnet model
|
||||
"numpy==1.26.4", # >1.24.0 is needed to use the 'strict' argument to np.testing.assert_array_equal()
|
||||
"numpy<2.0.0",
|
||||
"onnx==1.16.1",
|
||||
"onnxruntime==1.19.2",
|
||||
"opencv-python==4.9.0.80",
|
||||
@@ -52,10 +52,10 @@ dependencies = [
|
||||
"sentencepiece==0.2.0",
|
||||
"spandrel==0.3.4",
|
||||
"timm==0.6.13", # needed to override timm latest in controlnet_aux, see https://github.com/isl-org/ZoeDepth/issues/26
|
||||
"torch==2.4.1",
|
||||
"torchmetrics==0.11.4",
|
||||
"torchsde==0.2.6",
|
||||
"torchvision==0.19.1",
|
||||
"torch", # torch and related dependencies are not pinned, resolved as dependency of `diffusers[torch]` and so forth
|
||||
"torchmetrics",
|
||||
"torchsde",
|
||||
"torchvision",
|
||||
"transformers==4.41.1",
|
||||
|
||||
# Core application dependencies, pinned for reproducible builds.
|
||||
@@ -73,7 +73,7 @@ dependencies = [
|
||||
"click",
|
||||
"datasets",
|
||||
"Deprecated",
|
||||
"dnspython~=2.4.0",
|
||||
"dnspython",
|
||||
"dynamicprompts",
|
||||
"easing-functions",
|
||||
"einops",
|
||||
@@ -85,23 +85,23 @@ dependencies = [
|
||||
"picklescan",
|
||||
"pillow",
|
||||
"prompt-toolkit",
|
||||
"pympler~=1.0.1",
|
||||
"pympler",
|
||||
"pypatchmatch",
|
||||
'pyperclip',
|
||||
"pyperclip",
|
||||
"pyreadline3",
|
||||
"python-multipart",
|
||||
"requests~=2.28.2",
|
||||
"requests",
|
||||
"rich~=13.3",
|
||||
"scikit-image~=0.21.0",
|
||||
"scikit-image",
|
||||
"semver~=3.0.1",
|
||||
"test-tube~=0.7.5",
|
||||
"test-tube",
|
||||
"windows-curses; sys_platform=='win32'",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
"xformers" = [
|
||||
# Core generation dependencies, pinned for reproducible builds.
|
||||
"xformers==0.0.28.post1; sys_platform!='darwin'",
|
||||
"xformers>=0.0.28.post1; sys_platform!='darwin'",
|
||||
# Auxiliary dependencies, pinned only if necessary.
|
||||
"triton; sys_platform=='linux'",
|
||||
]
|
||||
|
||||
@@ -3,15 +3,16 @@ from tempfile import TemporaryDirectory
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from omegaconf import OmegaConf
|
||||
from pydantic import ValidationError
|
||||
|
||||
from invokeai.app.invocations.baseinvocation import BaseInvocation
|
||||
from invokeai.app.services.config.config_default import (
|
||||
DefaultInvokeAIAppConfig,
|
||||
InvokeAIAppConfig,
|
||||
get_config,
|
||||
load_and_migrate_config,
|
||||
)
|
||||
from invokeai.app.services.shared.graph import Graph
|
||||
from invokeai.frontend.cli.arg_parser import InvokeAIArgs
|
||||
|
||||
v4_config = """
|
||||
@@ -265,58 +266,34 @@ def test_get_config_writing(patch_rootdir: None, monkeypatch: pytest.MonkeyPatch
|
||||
InvokeAIArgs.did_parse = False
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="""
|
||||
This test fails when run as part of the full test suite.
|
||||
|
||||
This test needs to deny nodes from being included in the InvocationsUnion by providing
|
||||
an app configuration as a test fixture. Pytest executes all test files before running
|
||||
tests, so the app configuration is already initialized by the time this test runs, and
|
||||
the InvocationUnion is already created and the denied nodes are not omitted from it.
|
||||
|
||||
This test passes when `test_config.py` is tested in isolation.
|
||||
|
||||
Perhaps a solution would be to call `get_app_config().parse_args()` in
|
||||
other test files?
|
||||
"""
|
||||
)
|
||||
def test_deny_nodes(patch_rootdir):
|
||||
# Allow integer, string and float, but explicitly deny float
|
||||
allow_deny_nodes_conf = OmegaConf.create(
|
||||
"""
|
||||
InvokeAI:
|
||||
Nodes:
|
||||
allow_nodes:
|
||||
- integer
|
||||
- string
|
||||
- float
|
||||
deny_nodes:
|
||||
- float
|
||||
"""
|
||||
)
|
||||
# must parse config before importing Graph, so its nodes union uses the config
|
||||
get_config.cache_clear()
|
||||
conf = get_config()
|
||||
get_config.cache_clear()
|
||||
conf.merge_from_file(conf=allow_deny_nodes_conf, argv=[])
|
||||
from invokeai.app.services.shared.graph import Graph
|
||||
conf.allow_nodes = ["integer", "string", "float"]
|
||||
conf.deny_nodes = ["float"]
|
||||
|
||||
# We've changed the config, we need to invalidate the typeadapter cache so that the new config is used for
|
||||
# subsequent graph validations
|
||||
BaseInvocation.invalidate_typeadapter()
|
||||
|
||||
# confirm graph validation fails when using denied node
|
||||
Graph(nodes={"1": {"id": "1", "type": "integer"}})
|
||||
Graph(nodes={"1": {"id": "1", "type": "string"}})
|
||||
Graph.model_validate({"nodes": {"1": {"id": "1", "type": "integer"}}})
|
||||
Graph.model_validate({"nodes": {"1": {"id": "1", "type": "string"}}})
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
Graph(nodes={"1": {"id": "1", "type": "float"}})
|
||||
|
||||
from invokeai.app.invocations.baseinvocation import BaseInvocation
|
||||
Graph.model_validate({"nodes": {"1": {"id": "1", "type": "float"}}})
|
||||
|
||||
# confirm invocations union will not have denied nodes
|
||||
all_invocations = BaseInvocation.get_invocations()
|
||||
|
||||
has_integer = len([i for i in all_invocations if i.model_fields.get("type").default == "integer"]) == 1
|
||||
has_string = len([i for i in all_invocations if i.model_fields.get("type").default == "string"]) == 1
|
||||
has_float = len([i for i in all_invocations if i.model_fields.get("type").default == "float"]) == 1
|
||||
has_integer = len([i for i in all_invocations if i.get_type() == "integer"]) == 1
|
||||
has_string = len([i for i in all_invocations if i.get_type() == "string"]) == 1
|
||||
has_float = len([i for i in all_invocations if i.get_type() == "float"]) == 1
|
||||
|
||||
assert has_integer
|
||||
assert has_string
|
||||
assert not has_float
|
||||
|
||||
# Reset the config so that it doesn't affect other tests
|
||||
get_config.cache_clear()
|
||||
BaseInvocation.invalidate_typeadapter()
|
||||
|
||||
@@ -34,6 +34,7 @@ from tests.test_nodes import (
|
||||
PolymorphicStringTestInvocation,
|
||||
PromptCollectionTestInvocation,
|
||||
PromptTestInvocation,
|
||||
PromptTestInvocationOutput,
|
||||
TextToImageTestInvocation,
|
||||
)
|
||||
|
||||
@@ -509,7 +510,7 @@ def test_invocation_decorator():
|
||||
|
||||
@invocation(invocation_type, title=title, tags=tags, category=category, version=version)
|
||||
class TestInvocation(BaseInvocation):
|
||||
def invoke(self):
|
||||
def invoke(self) -> PromptTestInvocationOutput:
|
||||
pass
|
||||
|
||||
schema = TestInvocation.model_json_schema()
|
||||
@@ -527,7 +528,7 @@ def test_invocation_version_must_be_semver():
|
||||
|
||||
@invocation("test_invocation_version_valid", version=valid_version)
|
||||
class ValidVersionInvocation(BaseInvocation):
|
||||
def invoke(self):
|
||||
def invoke(self) -> PromptTestInvocationOutput:
|
||||
pass
|
||||
|
||||
with pytest.raises(InvalidVersionError):
|
||||
@@ -714,3 +715,20 @@ def test_graph_can_generate_schema():
|
||||
# Not throwing on this line is sufficient
|
||||
# NOTE: if this test fails, it's PROBABLY because a new invocation type is breaking schema generation
|
||||
models_json_schema([(Graph, "serialization")])
|
||||
|
||||
|
||||
def test_nodes_must_implement_invoke_method():
|
||||
with pytest.raises(ValueError, match='must implement the "invoke" method'):
|
||||
|
||||
@invocation("test_no_invoke_method", version="1.0.0")
|
||||
class NoInvokeMethodInvocation(BaseInvocation):
|
||||
pass
|
||||
|
||||
|
||||
def test_nodes_must_return_invocation_output():
|
||||
with pytest.raises(ValueError, match="must have a return annotation of a subclass of BaseInvocationOutput"):
|
||||
|
||||
@invocation("test_no_output", version="1.0.0")
|
||||
class NoOutputInvocation(BaseInvocation):
|
||||
def invoke(self) -> str:
|
||||
return "foo"
|
||||
|
||||
Reference in New Issue
Block a user