mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-21 14:08:42 -05:00
Compare commits
139 Commits
ryan/flux-
...
v5.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e1b9567c1 | ||
|
|
56ef754292 | ||
|
|
2de99ec32d | ||
|
|
889e63d585 | ||
|
|
56de2b3a51 | ||
|
|
eb40bdb810 | ||
|
|
0840e5fa65 | ||
|
|
b79f2a4e4f | ||
|
|
76a533e67e | ||
|
|
188974988c | ||
|
|
b47aae2165 | ||
|
|
7105a22e0f | ||
|
|
eee4175e4d | ||
|
|
e0b63559d0 | ||
|
|
aa54c1f969 | ||
|
|
87fdea4cc6 | ||
|
|
53443084c5 | ||
|
|
8d2e5bfd77 | ||
|
|
05e285c95a | ||
|
|
25f19a35d7 | ||
|
|
01bbd32598 | ||
|
|
0e2761d5c6 | ||
|
|
d5b51cca56 | ||
|
|
a303777777 | ||
|
|
e90b3de706 | ||
|
|
3ce94e5b84 | ||
|
|
42e5ec3916 | ||
|
|
ffa00d1d9a | ||
|
|
1648a2af6e | ||
|
|
852e9e280a | ||
|
|
af72412d3f | ||
|
|
72f715e688 | ||
|
|
3b567bef3d | ||
|
|
3d867db315 | ||
|
|
a8c7dd74d0 | ||
|
|
2dc069d759 | ||
|
|
2a90f4f59e | ||
|
|
af5f342347 | ||
|
|
6dd53b6a32 | ||
|
|
0ca8351911 | ||
|
|
b14cbfde13 | ||
|
|
46dc633df9 | ||
|
|
d4a981fc1c | ||
|
|
e0474ce822 | ||
|
|
9e5ce6b2d4 | ||
|
|
98fa946f77 | ||
|
|
ef80d40b63 | ||
|
|
7a9f923d35 | ||
|
|
fd982fa7c2 | ||
|
|
df86ed653a | ||
|
|
0be8aacee6 | ||
|
|
4f993a4f32 | ||
|
|
0158320940 | ||
|
|
bb2dc6c78b | ||
|
|
80d7d69c2f | ||
|
|
1010c9877c | ||
|
|
8fd8994ee8 | ||
|
|
262c2f1fc7 | ||
|
|
150d3239e3 | ||
|
|
e49e5e9782 | ||
|
|
2d1e745594 | ||
|
|
b793328edd | ||
|
|
e79b316645 | ||
|
|
8297e7964c | ||
|
|
26832c1a0e | ||
|
|
c29259ccdb | ||
|
|
3d4bd71098 | ||
|
|
814be44cd7 | ||
|
|
d328eaf743 | ||
|
|
b502c05009 | ||
|
|
0f333388bb | ||
|
|
bc63e2acc5 | ||
|
|
ec7e771942 | ||
|
|
fe84013392 | ||
|
|
710f81266b | ||
|
|
446e2884bc | ||
|
|
7d9f125232 | ||
|
|
66bbd62758 | ||
|
|
0875e861f5 | ||
|
|
0267d73dfc | ||
|
|
c9ab7c5233 | ||
|
|
f06765dfba | ||
|
|
f347b26999 | ||
|
|
c665cf3525 | ||
|
|
8cf19c4124 | ||
|
|
f7112ae57b | ||
|
|
2bfb0ddff5 | ||
|
|
950c9f5d0c | ||
|
|
db283d21f9 | ||
|
|
70cca7a431 | ||
|
|
3c3938cfc8 | ||
|
|
4455fc4092 | ||
|
|
4b7e920612 | ||
|
|
433146d08f | ||
|
|
324a46d0c8 | ||
|
|
c4421241f6 | ||
|
|
43b417be6b | ||
|
|
4a135c1017 | ||
|
|
dd591abc2b | ||
|
|
0e65f295ac | ||
|
|
ab7fbb7b30 | ||
|
|
92aed5e4fc | ||
|
|
d9b0697d1f | ||
|
|
34a9409bc1 | ||
|
|
319d82751a | ||
|
|
9b90834248 | ||
|
|
a8957aa50d | ||
|
|
807f458f13 | ||
|
|
68dbe45315 | ||
|
|
bd3d1dcdf9 | ||
|
|
386c01ede1 | ||
|
|
c224971cb4 | ||
|
|
ca55ef1da5 | ||
|
|
3072d80171 | ||
|
|
d5f2f4dc4e | ||
|
|
b2552323b8 | ||
|
|
61d217e377 | ||
|
|
57a80c456a | ||
|
|
211b2f84ed | ||
|
|
ea4104c7c4 | ||
|
|
8b46c2dc53 | ||
|
|
9812b9676b | ||
|
|
6efa1597eb | ||
|
|
cd6ef3edb3 | ||
|
|
fcdbb729d3 | ||
|
|
c0657072ec | ||
|
|
7167a5d3f4 | ||
|
|
8cf0d8c8d3 | ||
|
|
48311f38ba | ||
|
|
7631d55c2a | ||
|
|
ea0dc09c64 | ||
|
|
a424552c82 | ||
|
|
ba8ef6ff0f | ||
|
|
3463a968c7 | ||
|
|
c256826015 | ||
|
|
7d38a9b7fb | ||
|
|
249da858df | ||
|
|
d332d81866 | ||
|
|
21017edcde |
@@ -105,7 +105,7 @@ Invoke features an organized gallery system for easily storing, accessing, and r
|
||||
### Other features
|
||||
|
||||
- Support for both ckpt and diffusers models
|
||||
- SD1.5, SD2.0, and SDXL support
|
||||
- SD1.5, SD2.0, SDXL, and FLUX support
|
||||
- Upscaling Tools
|
||||
- Embedding Manager & Support
|
||||
- Model Manager & Support
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2023 Eugene Brodsky https://github.com/ebr
|
||||
|
||||
x-invokeai: &invokeai
|
||||
image: "local/invokeai:latest"
|
||||
image: "ghcr.io/invoke-ai/invokeai:latest"
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
|
||||
@@ -39,7 +39,9 @@ To use a community workflow, download the `.json` node graph file and load it in
|
||||
+ [Match Histogram](#match-histogram)
|
||||
+ [Metadata-Linked](#metadata-linked-nodes)
|
||||
+ [Negative Image](#negative-image)
|
||||
+ [Nightmare Promptgen](#nightmare-promptgen)
|
||||
+ [Nightmare Promptgen](#nightmare-promptgen)
|
||||
+ [Ollama](#ollama-node)
|
||||
+ [One Button Prompt](#one-button-prompt)
|
||||
+ [Oobabooga](#oobabooga)
|
||||
+ [Prompt Tools](#prompt-tools)
|
||||
+ [Remote Image](#remote-image)
|
||||
@@ -389,6 +391,34 @@ View:
|
||||
|
||||
**Node Link:** [https://github.com/gogurtenjoyer/nightmare-promptgen](https://github.com/gogurtenjoyer/nightmare-promptgen)
|
||||
|
||||
--------------------------------
|
||||
### Ollama Node
|
||||
|
||||
**Description:** Uses Ollama API to expand text prompts for text-to-image generation using local LLMs. Works great for expanding basic prompts into detailed natural language prompts for Flux. Also provides a toggle to unload the LLM model immediately after expanding, to free up VRAM for Invoke to continue the image generation workflow.
|
||||
|
||||
**Node Link:** https://github.com/Jonseed/Ollama-Node
|
||||
|
||||
**Example Node Graph:** https://github.com/Jonseed/Ollama-Node/blob/main/Ollama-Node-Flux-example.json
|
||||
|
||||
**View:**
|
||||
|
||||

|
||||
|
||||
--------------------------------
|
||||
### One Button Prompt
|
||||
|
||||
<img src="https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI/blob/main/images/background.png" width="800" />
|
||||
|
||||
**Description:** an extensive suite of auto prompt generation and prompt helper nodes based on extensive logic. Get creative with the best prompt generator in the world.
|
||||
|
||||
The main node generates interesting prompts based on a set of parameters. There are also some additional nodes such as Auto Negative Prompt, One Button Artify, Create Prompt Variant and other cool prompt toys to play around with.
|
||||
|
||||
**Node Link:** [https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI](https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI)
|
||||
|
||||
**Nodes:**
|
||||
|
||||
<img src="https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI/blob/main/images/OBP_nodes_invokeai.png" width="800" />
|
||||
|
||||
--------------------------------
|
||||
### Oobabooga
|
||||
|
||||
|
||||
@@ -421,7 +421,7 @@ def get_torch_source() -> Tuple[str | None, str | None]:
|
||||
optional_modules = "[xformers,onnx-cuda]"
|
||||
elif OS == "Windows":
|
||||
if device.value == "cuda":
|
||||
url = "https://download.pytorch.org/whl/cu121"
|
||||
url = "https://download.pytorch.org/whl/cu124"
|
||||
optional_modules = "[xformers,onnx-cuda]"
|
||||
elif device.value == "cpu":
|
||||
# CPU uses the default PyPi index, no optional modules
|
||||
|
||||
@@ -7,13 +7,14 @@ from pathlib import Path
|
||||
|
||||
import torch
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi_events.handlers.local import local_handler
|
||||
from fastapi_events.middleware import EventHandlerASGIMiddleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||
from torch.backends.mps import is_available as is_mps_available
|
||||
|
||||
# for PyCharm:
|
||||
@@ -78,6 +79,29 @@ app = FastAPI(
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
|
||||
class RedirectRootWithQueryStringMiddleware(BaseHTTPMiddleware):
|
||||
"""When a request is made to the root path with a query string, redirect to the root path without the query string.
|
||||
|
||||
For example, to force a Gradio app to use dark mode, users may append `?__theme=dark` to the URL. Their browser may
|
||||
have this query string saved in history or a bookmark, so when the user navigates to `http://127.0.0.1:9090/`, the
|
||||
browser takes them to `http://127.0.0.1:9090/?__theme=dark`.
|
||||
|
||||
This breaks the static file serving in the UI, so we redirect the user to the root path without the query string.
|
||||
"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
|
||||
if request.url.path == "/" and request.url.query:
|
||||
return RedirectResponse(url="/")
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
# Add the middleware
|
||||
app.add_middleware(RedirectRootWithQueryStringMiddleware)
|
||||
|
||||
|
||||
# Add event handler
|
||||
event_handler_id: int = id(app)
|
||||
app.add_middleware(
|
||||
|
||||
@@ -30,6 +30,7 @@ from invokeai.backend.flux.sampling_utils import (
|
||||
pack,
|
||||
unpack,
|
||||
)
|
||||
from invokeai.backend.lora.conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX
|
||||
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
|
||||
from invokeai.backend.lora.lora_patcher import LoRAPatcher
|
||||
from invokeai.backend.model_manager.config import ModelFormat
|
||||
@@ -208,18 +209,22 @@ class FluxDenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
LoRAPatcher.apply_lora_patches(
|
||||
model=transformer,
|
||||
patches=self._lora_iterator(context),
|
||||
prefix="",
|
||||
prefix=FLUX_LORA_TRANSFORMER_PREFIX,
|
||||
cached_weights=cached_weights,
|
||||
)
|
||||
)
|
||||
elif config.format in [ModelFormat.BnbQuantizedLlmInt8b, ModelFormat.BnbQuantizednf4b]:
|
||||
elif config.format in [
|
||||
ModelFormat.BnbQuantizedLlmInt8b,
|
||||
ModelFormat.BnbQuantizednf4b,
|
||||
ModelFormat.GGUFQuantized,
|
||||
]:
|
||||
# The model is quantized, so apply the LoRA weights as sidecar layers. This results in slower inference,
|
||||
# than directly patching the weights, but is agnostic to the quantization format.
|
||||
exit_stack.enter_context(
|
||||
LoRAPatcher.apply_lora_sidecar_patches(
|
||||
model=transformer,
|
||||
patches=self._lora_iterator(context),
|
||||
prefix="",
|
||||
prefix=FLUX_LORA_TRANSFORMER_PREFIX,
|
||||
dtype=inference_dtype,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from invokeai.app.invocations.baseinvocation import (
|
||||
invocation_output,
|
||||
)
|
||||
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType
|
||||
from invokeai.app.invocations.model import LoRAField, ModelIdentifierField, TransformerField
|
||||
from invokeai.app.invocations.model import CLIPField, LoRAField, ModelIdentifierField, TransformerField
|
||||
from invokeai.app.services.shared.invocation_context import InvocationContext
|
||||
from invokeai.backend.model_manager.config import BaseModelType
|
||||
|
||||
@@ -20,6 +20,7 @@ class FluxLoRALoaderOutput(BaseInvocationOutput):
|
||||
transformer: Optional[TransformerField] = OutputField(
|
||||
default=None, description=FieldDescriptions.transformer, title="FLUX Transformer"
|
||||
)
|
||||
clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP")
|
||||
|
||||
|
||||
@invocation(
|
||||
@@ -27,21 +28,28 @@ class FluxLoRALoaderOutput(BaseInvocationOutput):
|
||||
title="FLUX LoRA",
|
||||
tags=["lora", "model", "flux"],
|
||||
category="model",
|
||||
version="1.0.0",
|
||||
version="1.1.0",
|
||||
classification=Classification.Prototype,
|
||||
)
|
||||
class FluxLoRALoaderInvocation(BaseInvocation):
|
||||
"""Apply a LoRA model to a FLUX transformer."""
|
||||
"""Apply a LoRA model to a FLUX transformer and/or text encoder."""
|
||||
|
||||
lora: ModelIdentifierField = InputField(
|
||||
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
|
||||
)
|
||||
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
|
||||
transformer: TransformerField = InputField(
|
||||
transformer: TransformerField | None = InputField(
|
||||
default=None,
|
||||
description=FieldDescriptions.transformer,
|
||||
input=Input.Connection,
|
||||
title="FLUX Transformer",
|
||||
)
|
||||
clip: CLIPField | None = InputField(
|
||||
default=None,
|
||||
title="CLIP",
|
||||
description=FieldDescriptions.clip,
|
||||
input=Input.Connection,
|
||||
)
|
||||
|
||||
def invoke(self, context: InvocationContext) -> FluxLoRALoaderOutput:
|
||||
lora_key = self.lora.key
|
||||
@@ -49,18 +57,33 @@ class FluxLoRALoaderInvocation(BaseInvocation):
|
||||
if not context.models.exists(lora_key):
|
||||
raise ValueError(f"Unknown lora: {lora_key}!")
|
||||
|
||||
if any(lora.lora.key == lora_key for lora in self.transformer.loras):
|
||||
# Check for existing LoRAs with the same key.
|
||||
if self.transformer and any(lora.lora.key == lora_key for lora in self.transformer.loras):
|
||||
raise ValueError(f'LoRA "{lora_key}" already applied to transformer.')
|
||||
if self.clip and any(lora.lora.key == lora_key for lora in self.clip.loras):
|
||||
raise ValueError(f'LoRA "{lora_key}" already applied to CLIP encoder.')
|
||||
|
||||
transformer = self.transformer.model_copy(deep=True)
|
||||
transformer.loras.append(
|
||||
LoRAField(
|
||||
lora=self.lora,
|
||||
weight=self.weight,
|
||||
output = FluxLoRALoaderOutput()
|
||||
|
||||
# Attach LoRA layers to the models.
|
||||
if self.transformer is not None:
|
||||
output.transformer = self.transformer.model_copy(deep=True)
|
||||
output.transformer.loras.append(
|
||||
LoRAField(
|
||||
lora=self.lora,
|
||||
weight=self.weight,
|
||||
)
|
||||
)
|
||||
if self.clip is not None:
|
||||
output.clip = self.clip.model_copy(deep=True)
|
||||
output.clip.loras.append(
|
||||
LoRAField(
|
||||
lora=self.lora,
|
||||
weight=self.weight,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return FluxLoRALoaderOutput(transformer=transformer)
|
||||
return output
|
||||
|
||||
|
||||
@invocation(
|
||||
@@ -68,7 +91,7 @@ class FluxLoRALoaderInvocation(BaseInvocation):
|
||||
title="FLUX LoRA Collection Loader",
|
||||
tags=["lora", "model", "flux"],
|
||||
category="model",
|
||||
version="1.0.0",
|
||||
version="1.1.0",
|
||||
classification=Classification.Prototype,
|
||||
)
|
||||
class FLUXLoRACollectionLoader(BaseInvocation):
|
||||
@@ -84,6 +107,12 @@ class FLUXLoRACollectionLoader(BaseInvocation):
|
||||
input=Input.Connection,
|
||||
title="Transformer",
|
||||
)
|
||||
clip: CLIPField | None = InputField(
|
||||
default=None,
|
||||
title="CLIP",
|
||||
description=FieldDescriptions.clip,
|
||||
input=Input.Connection,
|
||||
)
|
||||
|
||||
def invoke(self, context: InvocationContext) -> FluxLoRALoaderOutput:
|
||||
output = FluxLoRALoaderOutput()
|
||||
@@ -106,4 +135,9 @@ class FLUXLoRACollectionLoader(BaseInvocation):
|
||||
output.transformer = self.transformer.model_copy(deep=True)
|
||||
output.transformer.loras.append(lora)
|
||||
|
||||
if self.clip is not None:
|
||||
if output.clip is None:
|
||||
output.clip = self.clip.model_copy(deep=True)
|
||||
output.clip.loras.append(lora)
|
||||
|
||||
return output
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Literal
|
||||
from contextlib import ExitStack
|
||||
from typing import Iterator, Literal, Tuple
|
||||
|
||||
import torch
|
||||
from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5Tokenizer
|
||||
@@ -9,6 +10,10 @@ from invokeai.app.invocations.model import CLIPField, T5EncoderField
|
||||
from invokeai.app.invocations.primitives import FluxConditioningOutput
|
||||
from invokeai.app.services.shared.invocation_context import InvocationContext
|
||||
from invokeai.backend.flux.modules.conditioner import HFEncoder
|
||||
from invokeai.backend.lora.conversions.flux_lora_constants import FLUX_LORA_CLIP_PREFIX
|
||||
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
|
||||
from invokeai.backend.lora.lora_patcher import LoRAPatcher
|
||||
from invokeai.backend.model_manager.config import ModelFormat
|
||||
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData, FLUXConditioningInfo
|
||||
|
||||
|
||||
@@ -17,7 +22,7 @@ from invokeai.backend.stable_diffusion.diffusion.conditioning_data import Condit
|
||||
title="FLUX Text Encoding",
|
||||
tags=["prompt", "conditioning", "flux"],
|
||||
category="conditioning",
|
||||
version="1.0.0",
|
||||
version="1.1.0",
|
||||
classification=Classification.Prototype,
|
||||
)
|
||||
class FluxTextEncoderInvocation(BaseInvocation):
|
||||
@@ -78,15 +83,42 @@ class FluxTextEncoderInvocation(BaseInvocation):
|
||||
prompt = [self.prompt]
|
||||
|
||||
with (
|
||||
clip_text_encoder_info as clip_text_encoder,
|
||||
clip_text_encoder_info.model_on_device() as (cached_weights, clip_text_encoder),
|
||||
clip_tokenizer_info as clip_tokenizer,
|
||||
ExitStack() as exit_stack,
|
||||
):
|
||||
assert isinstance(clip_text_encoder, CLIPTextModel)
|
||||
assert isinstance(clip_tokenizer, CLIPTokenizer)
|
||||
|
||||
clip_text_encoder_config = clip_text_encoder_info.config
|
||||
assert clip_text_encoder_config is not None
|
||||
|
||||
# Apply LoRA models to the CLIP encoder.
|
||||
# Note: We apply the LoRA after the transformer has been moved to its target device for faster patching.
|
||||
if clip_text_encoder_config.format in [ModelFormat.Diffusers]:
|
||||
# The model is non-quantized, so we can apply the LoRA weights directly into the model.
|
||||
exit_stack.enter_context(
|
||||
LoRAPatcher.apply_lora_patches(
|
||||
model=clip_text_encoder,
|
||||
patches=self._clip_lora_iterator(context),
|
||||
prefix=FLUX_LORA_CLIP_PREFIX,
|
||||
cached_weights=cached_weights,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# There are currently no supported CLIP quantized models. Add support here if needed.
|
||||
raise ValueError(f"Unsupported model format: {clip_text_encoder_config.format}")
|
||||
|
||||
clip_encoder = HFEncoder(clip_text_encoder, clip_tokenizer, True, 77)
|
||||
|
||||
pooled_prompt_embeds = clip_encoder(prompt)
|
||||
|
||||
assert isinstance(pooled_prompt_embeds, torch.Tensor)
|
||||
return pooled_prompt_embeds
|
||||
|
||||
def _clip_lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[LoRAModelRaw, float]]:
|
||||
for lora in self.clip.loras:
|
||||
lora_info = context.models.load(lora.lora)
|
||||
assert isinstance(lora_info.model, LoRAModelRaw)
|
||||
yield (lora_info.model, lora.weight)
|
||||
del lora_info
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import Dict
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.lora.conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX
|
||||
from invokeai.backend.lora.layers.any_lora_layer import AnyLoRALayer
|
||||
from invokeai.backend.lora.layers.concatenated_lora_layer import ConcatenatedLoRALayer
|
||||
from invokeai.backend.lora.layers.lora_layer import LoRALayer
|
||||
@@ -189,7 +190,9 @@ def lora_model_from_flux_diffusers_state_dict(state_dict: Dict[str, torch.Tensor
|
||||
# Assert that all keys were processed.
|
||||
assert len(grouped_state_dict) == 0
|
||||
|
||||
return LoRAModelRaw(layers=layers)
|
||||
layers_with_prefix = {f"{FLUX_LORA_TRANSFORMER_PREFIX}{k}": v for k, v in layers.items()}
|
||||
|
||||
return LoRAModelRaw(layers=layers_with_prefix)
|
||||
|
||||
|
||||
def _group_by_layer(state_dict: Dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]:
|
||||
|
||||
@@ -3,18 +3,25 @@ from typing import Any, Dict, TypeVar
|
||||
|
||||
import torch
|
||||
|
||||
from invokeai.backend.lora.conversions.flux_lora_constants import FLUX_LORA_CLIP_PREFIX, FLUX_LORA_TRANSFORMER_PREFIX
|
||||
from invokeai.backend.lora.layers.any_lora_layer import AnyLoRALayer
|
||||
from invokeai.backend.lora.layers.utils import any_lora_layer_from_state_dict
|
||||
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
|
||||
|
||||
# A regex pattern that matches all of the keys in the Kohya FLUX LoRA format.
|
||||
# A regex pattern that matches all of the transformer keys in the Kohya FLUX LoRA format.
|
||||
# Example keys:
|
||||
# lora_unet_double_blocks_0_img_attn_proj.alpha
|
||||
# lora_unet_double_blocks_0_img_attn_proj.lora_down.weight
|
||||
# lora_unet_double_blocks_0_img_attn_proj.lora_up.weight
|
||||
FLUX_KOHYA_KEY_REGEX = (
|
||||
FLUX_KOHYA_TRANSFORMER_KEY_REGEX = (
|
||||
r"lora_unet_(\w+_blocks)_(\d+)_(img_attn|img_mlp|img_mod|txt_attn|txt_mlp|txt_mod|linear1|linear2|modulation)_?(.*)"
|
||||
)
|
||||
# A regex pattern that matches all of the CLIP keys in the Kohya FLUX LoRA format.
|
||||
# Example keys:
|
||||
# lora_te1_text_model_encoder_layers_0_mlp_fc1.alpha
|
||||
# lora_te1_text_model_encoder_layers_0_mlp_fc1.lora_down.weight
|
||||
# lora_te1_text_model_encoder_layers_0_mlp_fc1.lora_up.weight
|
||||
FLUX_KOHYA_CLIP_KEY_REGEX = r"lora_te1_text_model_encoder_layers_(\d+)_(mlp|self_attn)_(\w+)\.?.*"
|
||||
|
||||
|
||||
def is_state_dict_likely_in_flux_kohya_format(state_dict: Dict[str, Any]) -> bool:
|
||||
@@ -23,7 +30,10 @@ def is_state_dict_likely_in_flux_kohya_format(state_dict: Dict[str, Any]) -> boo
|
||||
This is intended to be a high-precision detector, but it is not guaranteed to have perfect precision. (A
|
||||
perfect-precision detector would require checking all keys against a whitelist and verifying tensor shapes.)
|
||||
"""
|
||||
return all(re.match(FLUX_KOHYA_KEY_REGEX, k) for k in state_dict.keys())
|
||||
return all(
|
||||
re.match(FLUX_KOHYA_TRANSFORMER_KEY_REGEX, k) or re.match(FLUX_KOHYA_CLIP_KEY_REGEX, k)
|
||||
for k in state_dict.keys()
|
||||
)
|
||||
|
||||
|
||||
def lora_model_from_flux_kohya_state_dict(state_dict: Dict[str, torch.Tensor]) -> LoRAModelRaw:
|
||||
@@ -35,13 +45,27 @@ def lora_model_from_flux_kohya_state_dict(state_dict: Dict[str, torch.Tensor]) -
|
||||
grouped_state_dict[layer_name] = {}
|
||||
grouped_state_dict[layer_name][param_name] = value
|
||||
|
||||
# Convert the state dict to the InvokeAI format.
|
||||
grouped_state_dict = convert_flux_kohya_state_dict_to_invoke_format(grouped_state_dict)
|
||||
# Split the grouped state dict into transformer and CLIP state dicts.
|
||||
transformer_grouped_sd: dict[str, dict[str, torch.Tensor]] = {}
|
||||
clip_grouped_sd: dict[str, dict[str, torch.Tensor]] = {}
|
||||
for layer_name, layer_state_dict in grouped_state_dict.items():
|
||||
if layer_name.startswith("lora_unet"):
|
||||
transformer_grouped_sd[layer_name] = layer_state_dict
|
||||
elif layer_name.startswith("lora_te1"):
|
||||
clip_grouped_sd[layer_name] = layer_state_dict
|
||||
else:
|
||||
raise ValueError(f"Layer '{layer_name}' does not match the expected pattern for FLUX LoRA weights.")
|
||||
|
||||
# Convert the state dicts to the InvokeAI format.
|
||||
transformer_grouped_sd = _convert_flux_transformer_kohya_state_dict_to_invoke_format(transformer_grouped_sd)
|
||||
clip_grouped_sd = _convert_flux_clip_kohya_state_dict_to_invoke_format(clip_grouped_sd)
|
||||
|
||||
# Create LoRA layers.
|
||||
layers: dict[str, AnyLoRALayer] = {}
|
||||
for layer_key, layer_state_dict in grouped_state_dict.items():
|
||||
layers[layer_key] = any_lora_layer_from_state_dict(layer_state_dict)
|
||||
for layer_key, layer_state_dict in transformer_grouped_sd.items():
|
||||
layers[FLUX_LORA_TRANSFORMER_PREFIX + layer_key] = any_lora_layer_from_state_dict(layer_state_dict)
|
||||
for layer_key, layer_state_dict in clip_grouped_sd.items():
|
||||
layers[FLUX_LORA_CLIP_PREFIX + layer_key] = any_lora_layer_from_state_dict(layer_state_dict)
|
||||
|
||||
# Create and return the LoRAModelRaw.
|
||||
return LoRAModelRaw(layers=layers)
|
||||
@@ -50,16 +74,34 @@ def lora_model_from_flux_kohya_state_dict(state_dict: Dict[str, torch.Tensor]) -
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def convert_flux_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) -> Dict[str, T]:
|
||||
"""Converts a state dict from the Kohya FLUX LoRA format to LoRA weight format used internally by InvokeAI.
|
||||
def _convert_flux_clip_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) -> Dict[str, T]:
|
||||
"""Converts a CLIP LoRA state dict from the Kohya FLUX LoRA format to LoRA weight format used internally by
|
||||
InvokeAI.
|
||||
|
||||
Example key conversions:
|
||||
|
||||
"lora_te1_text_model_encoder_layers_0_mlp_fc1" -> "text_model.encoder.layers.0.mlp.fc1",
|
||||
"lora_te1_text_model_encoder_layers_0_self_attn_k_proj" -> "text_model.encoder.layers.0.self_attn.k_proj"
|
||||
"""
|
||||
converted_sd: dict[str, T] = {}
|
||||
for k, v in state_dict.items():
|
||||
match = re.match(FLUX_KOHYA_CLIP_KEY_REGEX, k)
|
||||
if match:
|
||||
new_key = f"text_model.encoder.layers.{match.group(1)}.{match.group(2)}.{match.group(3)}"
|
||||
converted_sd[new_key] = v
|
||||
else:
|
||||
raise ValueError(f"Key '{k}' does not match the expected pattern for FLUX LoRA weights.")
|
||||
|
||||
return converted_sd
|
||||
|
||||
|
||||
def _convert_flux_transformer_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) -> Dict[str, T]:
|
||||
"""Converts a FLUX tranformer LoRA state dict from the Kohya FLUX LoRA format to LoRA weight format used internally
|
||||
by InvokeAI.
|
||||
|
||||
Example key conversions:
|
||||
"lora_unet_double_blocks_0_img_attn_proj" -> "double_blocks.0.img_attn.proj"
|
||||
"lora_unet_double_blocks_0_img_attn_proj" -> "double_blocks.0.img_attn.proj"
|
||||
"lora_unet_double_blocks_0_img_attn_proj" -> "double_blocks.0.img_attn.proj"
|
||||
"lora_unet_double_blocks_0_img_attn_qkv" -> "double_blocks.0.img_attn.qkv"
|
||||
"lora_unet_double_blocks_0_img_attn_qkv" -> "double_blocks.0.img.attn.qkv"
|
||||
"lora_unet_double_blocks_0_img_attn_qkv" -> "double_blocks.0.img.attn.qkv"
|
||||
"""
|
||||
|
||||
def replace_func(match: re.Match[str]) -> str:
|
||||
@@ -70,9 +112,9 @@ def convert_flux_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) ->
|
||||
|
||||
converted_dict: dict[str, T] = {}
|
||||
for k, v in state_dict.items():
|
||||
match = re.match(FLUX_KOHYA_KEY_REGEX, k)
|
||||
match = re.match(FLUX_KOHYA_TRANSFORMER_KEY_REGEX, k)
|
||||
if match:
|
||||
new_key = re.sub(FLUX_KOHYA_KEY_REGEX, replace_func, k)
|
||||
new_key = re.sub(FLUX_KOHYA_TRANSFORMER_KEY_REGEX, replace_func, k)
|
||||
converted_dict[new_key] = v
|
||||
else:
|
||||
raise ValueError(f"Key '{k}' does not match the expected pattern for FLUX LoRA weights.")
|
||||
|
||||
3
invokeai/backend/lora/conversions/flux_lora_constants.py
Normal file
3
invokeai/backend/lora/conversions/flux_lora_constants.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# Prefixes used to distinguish between transformer and CLIP text encoder keys in the FLUX InvokeAI LoRA format.
|
||||
FLUX_LORA_TRANSFORMER_PREFIX = "lora_transformer-"
|
||||
FLUX_LORA_CLIP_PREFIX = "lora_clip-"
|
||||
@@ -114,6 +114,7 @@ class ModelFormat(str, Enum):
|
||||
T5Encoder = "t5_encoder"
|
||||
BnbQuantizedLlmInt8b = "bnb_quantized_int8b"
|
||||
BnbQuantizednf4b = "bnb_quantized_nf4b"
|
||||
GGUFQuantized = "gguf_quantized"
|
||||
|
||||
|
||||
class SchedulerPredictionType(str, Enum):
|
||||
@@ -157,6 +158,7 @@ class MainModelDefaultSettings(BaseModel):
|
||||
)
|
||||
width: int | None = Field(default=None, multiple_of=8, ge=64, description="Default width for this model")
|
||||
height: int | None = Field(default=None, multiple_of=8, ge=64, description="Default height for this model")
|
||||
guidance: float | None = Field(default=None, ge=1, description="Default Guidance for this model")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@@ -196,7 +198,7 @@ class ModelConfigBase(BaseModel):
|
||||
class CheckpointConfigBase(ModelConfigBase):
|
||||
"""Model config for checkpoint-style models."""
|
||||
|
||||
format: Literal[ModelFormat.Checkpoint, ModelFormat.BnbQuantizednf4b] = Field(
|
||||
format: Literal[ModelFormat.Checkpoint, ModelFormat.BnbQuantizednf4b, ModelFormat.GGUFQuantized] = Field(
|
||||
description="Format of the provided checkpoint model", default=ModelFormat.Checkpoint
|
||||
)
|
||||
config_path: str = Field(description="path to the checkpoint model config file")
|
||||
@@ -362,6 +364,21 @@ class MainBnbQuantized4bCheckpointConfig(CheckpointConfigBase, MainConfigBase):
|
||||
return Tag(f"{ModelType.Main.value}.{ModelFormat.BnbQuantizednf4b.value}")
|
||||
|
||||
|
||||
class MainGGUFCheckpointConfig(CheckpointConfigBase, MainConfigBase):
|
||||
"""Model config for main checkpoint models."""
|
||||
|
||||
prediction_type: SchedulerPredictionType = SchedulerPredictionType.Epsilon
|
||||
upcast_attention: bool = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.format = ModelFormat.GGUFQuantized
|
||||
|
||||
@staticmethod
|
||||
def get_tag() -> Tag:
|
||||
return Tag(f"{ModelType.Main.value}.{ModelFormat.GGUFQuantized.value}")
|
||||
|
||||
|
||||
class MainDiffusersConfig(DiffusersConfigBase, MainConfigBase):
|
||||
"""Model config for main diffusers models."""
|
||||
|
||||
@@ -465,6 +482,7 @@ AnyModelConfig = Annotated[
|
||||
Annotated[MainDiffusersConfig, MainDiffusersConfig.get_tag()],
|
||||
Annotated[MainCheckpointConfig, MainCheckpointConfig.get_tag()],
|
||||
Annotated[MainBnbQuantized4bCheckpointConfig, MainBnbQuantized4bCheckpointConfig.get_tag()],
|
||||
Annotated[MainGGUFCheckpointConfig, MainGGUFCheckpointConfig.get_tag()],
|
||||
Annotated[VAEDiffusersConfig, VAEDiffusersConfig.get_tag()],
|
||||
Annotated[VAECheckpointConfig, VAECheckpointConfig.get_tag()],
|
||||
Annotated[ControlNetDiffusersConfig, ControlNetDiffusersConfig.get_tag()],
|
||||
|
||||
@@ -26,6 +26,7 @@ from invokeai.backend.model_manager.config import (
|
||||
CLIPEmbedDiffusersConfig,
|
||||
MainBnbQuantized4bCheckpointConfig,
|
||||
MainCheckpointConfig,
|
||||
MainGGUFCheckpointConfig,
|
||||
T5EncoderBnbQuantizedLlmInt8bConfig,
|
||||
T5EncoderConfig,
|
||||
VAECheckpointConfig,
|
||||
@@ -35,6 +36,8 @@ from invokeai.backend.model_manager.load.model_loader_registry import ModelLoade
|
||||
from invokeai.backend.model_manager.util.model_util import (
|
||||
convert_bundle_to_flux_transformer_checkpoint,
|
||||
)
|
||||
from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
|
||||
from invokeai.backend.quantization.gguf.utils import TORCH_COMPATIBLE_QTYPES
|
||||
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
||||
|
||||
try:
|
||||
@@ -204,6 +207,52 @@ class FluxCheckpointModel(ModelLoader):
|
||||
return model
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.Main, format=ModelFormat.GGUFQuantized)
|
||||
class FluxGGUFCheckpointModel(ModelLoader):
|
||||
"""Class to load GGUF main models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if not isinstance(config, CheckpointConfigBase):
|
||||
raise ValueError("Only CheckpointConfigBase models are currently supported here.")
|
||||
|
||||
match submodel_type:
|
||||
case SubModelType.Transformer:
|
||||
return self._load_from_singlefile(config)
|
||||
|
||||
raise ValueError(
|
||||
f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}"
|
||||
)
|
||||
|
||||
def _load_from_singlefile(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
) -> AnyModel:
|
||||
assert isinstance(config, MainGGUFCheckpointConfig)
|
||||
model_path = Path(config.path)
|
||||
|
||||
with SilenceWarnings():
|
||||
model = Flux(params[config.config_path])
|
||||
|
||||
# HACK(ryand): We shouldn't be hard-coding the compute_dtype here.
|
||||
sd = gguf_sd_loader(model_path, compute_dtype=torch.bfloat16)
|
||||
|
||||
# HACK(ryand): There are some broken GGUF models in circulation that have the wrong shape for img_in.weight.
|
||||
# We override the shape here to fix the issue.
|
||||
# Example model with this issue (Q4_K_M): https://civitai.com/models/705823/ggufk-flux-unchained-km-quants
|
||||
img_in_weight = sd.get("img_in.weight", None)
|
||||
if img_in_weight is not None and img_in_weight._ggml_quantization_type in TORCH_COMPATIBLE_QTYPES:
|
||||
expected_img_in_weight_shape = model.img_in.weight.shape
|
||||
img_in_weight.quantized_data = img_in_weight.quantized_data.view(expected_img_in_weight_shape)
|
||||
img_in_weight.tensor_shape = expected_img_in_weight_shape
|
||||
|
||||
model.load_state_dict(sd, assign=True)
|
||||
return model
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.Main, format=ModelFormat.BnbQuantizednf4b)
|
||||
class FluxBnbQuantizednf4bCheckpointModel(ModelLoader):
|
||||
"""Class to load main models."""
|
||||
|
||||
@@ -30,6 +30,8 @@ from invokeai.backend.model_manager.config import (
|
||||
SchedulerPredictionType,
|
||||
)
|
||||
from invokeai.backend.model_manager.util.model_util import lora_token_vector_length, read_checkpoint_meta
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
|
||||
from invokeai.backend.spandrel_image_to_image_model import SpandrelImageToImageModel
|
||||
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
||||
|
||||
@@ -187,6 +189,7 @@ class ModelProbe(object):
|
||||
if fields["type"] in [ModelType.Main, ModelType.ControlNet, ModelType.VAE] and fields["format"] in [
|
||||
ModelFormat.Checkpoint,
|
||||
ModelFormat.BnbQuantizednf4b,
|
||||
ModelFormat.GGUFQuantized,
|
||||
]:
|
||||
ckpt_config_path = cls._get_checkpoint_config_path(
|
||||
model_path,
|
||||
@@ -220,7 +223,7 @@ class ModelProbe(object):
|
||||
|
||||
@classmethod
|
||||
def get_model_type_from_checkpoint(cls, model_path: Path, checkpoint: Optional[CkptType] = None) -> ModelType:
|
||||
if model_path.suffix not in (".bin", ".pt", ".ckpt", ".safetensors", ".pth"):
|
||||
if model_path.suffix not in (".bin", ".pt", ".ckpt", ".safetensors", ".pth", ".gguf"):
|
||||
raise InvalidModelConfigException(f"{model_path}: unrecognized suffix")
|
||||
|
||||
if model_path.name == "learned_embeds.bin":
|
||||
@@ -278,12 +281,10 @@ class ModelProbe(object):
|
||||
return ModelType.SpandrelImageToImage
|
||||
except spandrel.UnsupportedModelError:
|
||||
pass
|
||||
except RuntimeError as e:
|
||||
if "No such file or directory" in str(e):
|
||||
# This error is expected if the model_path does not exist (which is the case in some unit tests).
|
||||
pass
|
||||
else:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Encountered error while probing to determine if {model_path} is a Spandrel model. Ignoring. Error: {e}"
|
||||
)
|
||||
|
||||
raise InvalidModelConfigException(f"Unable to determine model type for {model_path}")
|
||||
|
||||
@@ -408,6 +409,8 @@ class ModelProbe(object):
|
||||
model = torch.load(model_path, map_location="cpu")
|
||||
assert isinstance(model, dict)
|
||||
return model
|
||||
elif model_path.suffix.endswith(".gguf"):
|
||||
return gguf_sd_loader(model_path, compute_dtype=torch.float32)
|
||||
else:
|
||||
return safetensors.torch.load_file(model_path)
|
||||
|
||||
@@ -477,6 +480,8 @@ class CheckpointProbeBase(ProbeBase):
|
||||
or "model.diffusion_model.double_blocks.0.img_attn.proj.weight.quant_state.bitsandbytes__nf4" in state_dict
|
||||
):
|
||||
return ModelFormat.BnbQuantizednf4b
|
||||
elif any(isinstance(v, GGMLTensor) for v in state_dict.values()):
|
||||
return ModelFormat.GGUFQuantized
|
||||
return ModelFormat("checkpoint")
|
||||
|
||||
def get_variant_type(self) -> ModelVariantType:
|
||||
|
||||
@@ -130,7 +130,7 @@ class ModelSearch:
|
||||
return
|
||||
|
||||
for n in file_names:
|
||||
if n.endswith((".ckpt", ".bin", ".pth", ".safetensors", ".pt")):
|
||||
if n.endswith((".ckpt", ".bin", ".pth", ".safetensors", ".pt", ".gguf")):
|
||||
try:
|
||||
self.model_found(absolute_path / n)
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -8,6 +8,8 @@ import safetensors
|
||||
import torch
|
||||
from picklescan.scanner import scan_file_path
|
||||
|
||||
from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
|
||||
|
||||
|
||||
def _fast_safetensors_reader(path: str) -> Dict[str, torch.Tensor]:
|
||||
checkpoint = {}
|
||||
@@ -54,7 +56,11 @@ def read_checkpoint_meta(path: Union[str, Path], scan: bool = False) -> Dict[str
|
||||
scan_result = scan_file_path(path)
|
||||
if scan_result.infected_files != 0:
|
||||
raise Exception(f'The model file "{path}" is potentially infected by malware. Aborting import.')
|
||||
checkpoint = torch.load(path, map_location=torch.device("meta"))
|
||||
if str(path).endswith(".gguf"):
|
||||
# The GGUF reader used here uses numpy memmap, so these tensors are not loaded into memory during this function
|
||||
checkpoint = gguf_sd_loader(Path(path), compute_dtype=torch.float32)
|
||||
else:
|
||||
checkpoint = torch.load(path, map_location=torch.device("meta"))
|
||||
return checkpoint
|
||||
|
||||
|
||||
|
||||
152
invokeai/backend/quantization/gguf/ggml_tensor.py
Normal file
152
invokeai/backend/quantization/gguf/ggml_tensor.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from typing import overload
|
||||
|
||||
import gguf
|
||||
import torch
|
||||
|
||||
from invokeai.backend.quantization.gguf.utils import (
|
||||
DEQUANTIZE_FUNCTIONS,
|
||||
TORCH_COMPATIBLE_QTYPES,
|
||||
dequantize,
|
||||
)
|
||||
|
||||
|
||||
def dequantize_and_run(func, args, kwargs):
|
||||
"""A helper function for running math ops on GGMLTensor inputs.
|
||||
|
||||
Dequantizes the inputs, and runs the function.
|
||||
"""
|
||||
dequantized_args = [a.get_dequantized_tensor() if hasattr(a, "get_dequantized_tensor") else a for a in args]
|
||||
dequantized_kwargs = {
|
||||
k: v.get_dequantized_tensor() if hasattr(v, "get_dequantized_tensor") else v for k, v in kwargs.items()
|
||||
}
|
||||
return func(*dequantized_args, **dequantized_kwargs)
|
||||
|
||||
|
||||
def apply_to_quantized_tensor(func, args, kwargs):
|
||||
"""A helper function to apply a function to a quantized GGML tensor, and re-wrap the result in a GGMLTensor.
|
||||
|
||||
Assumes that the first argument is a GGMLTensor.
|
||||
"""
|
||||
# We expect the first argument to be a GGMLTensor, and all other arguments to be non-GGMLTensors.
|
||||
ggml_tensor = args[0]
|
||||
assert isinstance(ggml_tensor, GGMLTensor)
|
||||
assert all(not isinstance(a, GGMLTensor) for a in args[1:])
|
||||
assert all(not isinstance(v, GGMLTensor) for v in kwargs.values())
|
||||
|
||||
new_data = func(ggml_tensor.quantized_data, *args[1:], **kwargs)
|
||||
|
||||
if new_data.dtype != ggml_tensor.quantized_data.dtype:
|
||||
# This is intended to catch calls such as `.to(dtype-torch.float32)`, which are not supported on GGMLTensors.
|
||||
raise ValueError("Operation changed the dtype of GGMLTensor unexpectedly.")
|
||||
|
||||
return GGMLTensor(
|
||||
new_data, ggml_tensor._ggml_quantization_type, ggml_tensor.tensor_shape, ggml_tensor.compute_dtype
|
||||
)
|
||||
|
||||
|
||||
GGML_TENSOR_OP_TABLE = {
|
||||
# Ops to run on the quantized tensor.
|
||||
torch.ops.aten.detach.default: apply_to_quantized_tensor, # pyright: ignore
|
||||
torch.ops.aten._to_copy.default: apply_to_quantized_tensor, # pyright: ignore
|
||||
# Ops to run on dequantized tensors.
|
||||
torch.ops.aten.t.default: dequantize_and_run, # pyright: ignore
|
||||
torch.ops.aten.addmm.default: dequantize_and_run, # pyright: ignore
|
||||
torch.ops.aten.mul.Tensor: dequantize_and_run, # pyright: ignore
|
||||
}
|
||||
|
||||
|
||||
class GGMLTensor(torch.Tensor):
|
||||
"""A torch.Tensor sub-class holding a quantized GGML tensor.
|
||||
|
||||
The underlying tensor is quantized, but the GGMLTensor class provides a dequantized view of the tensor on-the-fly
|
||||
when it is used in operations.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def __new__(
|
||||
cls,
|
||||
data: torch.Tensor,
|
||||
ggml_quantization_type: gguf.GGMLQuantizationType,
|
||||
tensor_shape: torch.Size,
|
||||
compute_dtype: torch.dtype,
|
||||
):
|
||||
# Type hinting is not supported for torch.Tensor._make_wrapper_subclass, so we ignore the errors.
|
||||
return torch.Tensor._make_wrapper_subclass( # pyright: ignore
|
||||
cls,
|
||||
data.shape,
|
||||
dtype=data.dtype,
|
||||
layout=data.layout,
|
||||
device=data.device,
|
||||
strides=data.stride(),
|
||||
storage_offset=data.storage_offset(),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: torch.Tensor,
|
||||
ggml_quantization_type: gguf.GGMLQuantizationType,
|
||||
tensor_shape: torch.Size,
|
||||
compute_dtype: torch.dtype,
|
||||
):
|
||||
self.quantized_data = data
|
||||
self._ggml_quantization_type = ggml_quantization_type
|
||||
# The dequantized shape of the tensor.
|
||||
self.tensor_shape = tensor_shape
|
||||
self.compute_dtype = compute_dtype
|
||||
|
||||
def __repr__(self, *, tensor_contents=None):
|
||||
return f"GGMLTensor(type={self._ggml_quantization_type.name}, dequantized_shape=({self.tensor_shape})"
|
||||
|
||||
@overload
|
||||
def size(self, dim: None = None) -> torch.Size: ...
|
||||
|
||||
@overload
|
||||
def size(self, dim: int) -> int: ...
|
||||
|
||||
def size(self, dim: int | None = None):
|
||||
"""Return the size of the tensor after dequantization. I.e. the shape that will be used in any math ops."""
|
||||
if dim is not None:
|
||||
return self.tensor_shape[dim]
|
||||
return self.tensor_shape
|
||||
|
||||
@property
|
||||
def shape(self) -> torch.Size: # pyright: ignore[reportIncompatibleVariableOverride] pyright doesn't understand this for some reason.
|
||||
"""The shape of the tensor after dequantization. I.e. the shape that will be used in any math ops."""
|
||||
return self.size()
|
||||
|
||||
@property
|
||||
def quantized_shape(self) -> torch.Size:
|
||||
"""The shape of the quantized tensor."""
|
||||
return self.quantized_data.shape
|
||||
|
||||
def requires_grad_(self, mode: bool = True) -> torch.Tensor:
|
||||
"""The GGMLTensor class is currently only designed for inference (not training). Setting requires_grad to True
|
||||
is not supported. This method is a no-op.
|
||||
"""
|
||||
return self
|
||||
|
||||
def get_dequantized_tensor(self):
|
||||
"""Return the dequantized tensor.
|
||||
|
||||
Args:
|
||||
dtype: The dtype of the dequantized tensor.
|
||||
"""
|
||||
if self._ggml_quantization_type in TORCH_COMPATIBLE_QTYPES:
|
||||
return self.quantized_data.to(self.compute_dtype)
|
||||
elif self._ggml_quantization_type in DEQUANTIZE_FUNCTIONS:
|
||||
# TODO(ryand): Look into how the dtype param is intended to be used.
|
||||
return dequantize(
|
||||
data=self.quantized_data, qtype=self._ggml_quantization_type, oshape=self.tensor_shape, dtype=None
|
||||
).to(self.compute_dtype)
|
||||
else:
|
||||
# There is no GPU implementation for this quantization type, so fallback to the numpy implementation.
|
||||
new = gguf.quants.dequantize(self.quantized_data.cpu().numpy(), self._ggml_quantization_type)
|
||||
return torch.from_numpy(new).to(self.quantized_data.device, dtype=self.compute_dtype)
|
||||
|
||||
@classmethod
|
||||
def __torch_dispatch__(cls, func, types, args, kwargs):
|
||||
# We will likely hit cases here in the future where a new op is encountered that is not yet supported.
|
||||
# The new op simply needs to be added to the GGML_TENSOR_OP_TABLE.
|
||||
if func in GGML_TENSOR_OP_TABLE:
|
||||
return GGML_TENSOR_OP_TABLE[func](func, args, kwargs)
|
||||
return NotImplemented
|
||||
22
invokeai/backend/quantization/gguf/loaders.py
Normal file
22
invokeai/backend/quantization/gguf/loaders.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from pathlib import Path
|
||||
|
||||
import gguf
|
||||
import torch
|
||||
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
from invokeai.backend.quantization.gguf.utils import TORCH_COMPATIBLE_QTYPES
|
||||
|
||||
|
||||
def gguf_sd_loader(path: Path, compute_dtype: torch.dtype) -> dict[str, GGMLTensor]:
|
||||
reader = gguf.GGUFReader(path)
|
||||
|
||||
sd: dict[str, GGMLTensor] = {}
|
||||
for tensor in reader.tensors:
|
||||
torch_tensor = torch.from_numpy(tensor.data)
|
||||
shape = torch.Size(tuple(int(v) for v in reversed(tensor.shape)))
|
||||
if tensor.tensor_type in TORCH_COMPATIBLE_QTYPES:
|
||||
torch_tensor = torch_tensor.view(*shape)
|
||||
sd[tensor.name] = GGMLTensor(
|
||||
torch_tensor, ggml_quantization_type=tensor.tensor_type, tensor_shape=shape, compute_dtype=compute_dtype
|
||||
)
|
||||
return sd
|
||||
308
invokeai/backend/quantization/gguf/utils.py
Normal file
308
invokeai/backend/quantization/gguf/utils.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# Largely based on https://github.com/city96/ComfyUI-GGUF
|
||||
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
import gguf
|
||||
import torch
|
||||
|
||||
TORCH_COMPATIBLE_QTYPES = {None, gguf.GGMLQuantizationType.F32, gguf.GGMLQuantizationType.F16}
|
||||
|
||||
# K Quants #
|
||||
QK_K = 256
|
||||
K_SCALE_SIZE = 12
|
||||
|
||||
|
||||
def get_scale_min(scales: torch.Tensor):
|
||||
n_blocks = scales.shape[0]
|
||||
scales = scales.view(torch.uint8)
|
||||
scales = scales.reshape((n_blocks, 3, 4))
|
||||
|
||||
d, m, m_d = torch.split(scales, scales.shape[-2] // 3, dim=-2)
|
||||
|
||||
sc = torch.cat([d & 0x3F, (m_d & 0x0F) | ((d >> 2) & 0x30)], dim=-1)
|
||||
min = torch.cat([m & 0x3F, (m_d >> 4) | ((m >> 2) & 0x30)], dim=-1)
|
||||
|
||||
return (sc.reshape((n_blocks, 8)), min.reshape((n_blocks, 8)))
|
||||
|
||||
|
||||
# Legacy Quants #
|
||||
def dequantize_blocks_Q8_0(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
d, x = split_block_dims(blocks, 2)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
x = x.view(torch.int8)
|
||||
return d * x
|
||||
|
||||
|
||||
def dequantize_blocks_Q5_1(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, m, qh, qs = split_block_dims(blocks, 2, 2, 4)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
m = m.view(torch.float16).to(dtype)
|
||||
qh = to_uint32(qh)
|
||||
|
||||
qh = qh.reshape((n_blocks, 1)) >> torch.arange(32, device=d.device, dtype=torch.int32).reshape(1, 32)
|
||||
ql = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor(
|
||||
[0, 4], device=d.device, dtype=torch.uint8
|
||||
).reshape(1, 1, 2, 1)
|
||||
qh = (qh & 1).to(torch.uint8)
|
||||
ql = (ql & 0x0F).reshape((n_blocks, -1))
|
||||
|
||||
qs = ql | (qh << 4)
|
||||
return (d * qs) + m
|
||||
|
||||
|
||||
def dequantize_blocks_Q5_0(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, qh, qs = split_block_dims(blocks, 2, 4)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
qh = to_uint32(qh)
|
||||
|
||||
qh = qh.reshape(n_blocks, 1) >> torch.arange(32, device=d.device, dtype=torch.int32).reshape(1, 32)
|
||||
ql = qs.reshape(n_blocks, -1, 1, block_size // 2) >> torch.tensor(
|
||||
[0, 4], device=d.device, dtype=torch.uint8
|
||||
).reshape(1, 1, 2, 1)
|
||||
|
||||
qh = (qh & 1).to(torch.uint8)
|
||||
ql = (ql & 0x0F).reshape(n_blocks, -1)
|
||||
|
||||
qs = (ql | (qh << 4)).to(torch.int8) - 16
|
||||
return d * qs
|
||||
|
||||
|
||||
def dequantize_blocks_Q4_1(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, m, qs = split_block_dims(blocks, 2, 2)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
m = m.view(torch.float16).to(dtype)
|
||||
|
||||
qs = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor(
|
||||
[0, 4], device=d.device, dtype=torch.uint8
|
||||
).reshape(1, 1, 2, 1)
|
||||
qs = (qs & 0x0F).reshape(n_blocks, -1)
|
||||
|
||||
return (d * qs) + m
|
||||
|
||||
|
||||
def dequantize_blocks_Q4_0(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, qs = split_block_dims(blocks, 2)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
|
||||
qs = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor(
|
||||
[0, 4], device=d.device, dtype=torch.uint8
|
||||
).reshape((1, 1, 2, 1))
|
||||
qs = (qs & 0x0F).reshape((n_blocks, -1)).to(torch.int8) - 8
|
||||
return d * qs
|
||||
|
||||
|
||||
def dequantize_blocks_BF16(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
return (blocks.view(torch.int16).to(torch.int32) << 16).view(torch.float32)
|
||||
|
||||
|
||||
def dequantize_blocks_Q6_K(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
(
|
||||
ql,
|
||||
qh,
|
||||
scales,
|
||||
d,
|
||||
) = split_block_dims(blocks, QK_K // 2, QK_K // 4, QK_K // 16)
|
||||
|
||||
scales = scales.view(torch.int8).to(dtype)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
d = (d * scales).reshape((n_blocks, QK_K // 16, 1))
|
||||
|
||||
ql = ql.reshape((n_blocks, -1, 1, 64)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 2, 1)
|
||||
)
|
||||
ql = (ql & 0x0F).reshape((n_blocks, -1, 32))
|
||||
qh = qh.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 4, 1)
|
||||
)
|
||||
qh = (qh & 0x03).reshape((n_blocks, -1, 32))
|
||||
q = (ql | (qh << 4)).to(torch.int8) - 32
|
||||
q = q.reshape((n_blocks, QK_K // 16, -1))
|
||||
|
||||
return (d * q).reshape((n_blocks, QK_K))
|
||||
|
||||
|
||||
def dequantize_blocks_Q5_K(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, dmin, scales, qh, qs = split_block_dims(blocks, 2, 2, K_SCALE_SIZE, QK_K // 8)
|
||||
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
dmin = dmin.view(torch.float16).to(dtype)
|
||||
|
||||
sc, m = get_scale_min(scales)
|
||||
|
||||
d = (d * sc).reshape((n_blocks, -1, 1))
|
||||
dm = (dmin * m).reshape((n_blocks, -1, 1))
|
||||
|
||||
ql = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 2, 1)
|
||||
)
|
||||
qh = qh.reshape((n_blocks, -1, 1, 32)) >> torch.tensor(list(range(8)), device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 8, 1)
|
||||
)
|
||||
ql = (ql & 0x0F).reshape((n_blocks, -1, 32))
|
||||
qh = (qh & 0x01).reshape((n_blocks, -1, 32))
|
||||
q = ql | (qh << 4)
|
||||
|
||||
return (d * q - dm).reshape((n_blocks, QK_K))
|
||||
|
||||
|
||||
def dequantize_blocks_Q4_K(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, dmin, scales, qs = split_block_dims(blocks, 2, 2, K_SCALE_SIZE)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
dmin = dmin.view(torch.float16).to(dtype)
|
||||
|
||||
sc, m = get_scale_min(scales)
|
||||
|
||||
d = (d * sc).reshape((n_blocks, -1, 1))
|
||||
dm = (dmin * m).reshape((n_blocks, -1, 1))
|
||||
|
||||
qs = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 2, 1)
|
||||
)
|
||||
qs = (qs & 0x0F).reshape((n_blocks, -1, 32))
|
||||
|
||||
return (d * qs - dm).reshape((n_blocks, QK_K))
|
||||
|
||||
|
||||
def dequantize_blocks_Q3_K(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
hmask, qs, scales, d = split_block_dims(blocks, QK_K // 8, QK_K // 4, 12)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
|
||||
lscales, hscales = scales[:, :8], scales[:, 8:]
|
||||
lscales = lscales.reshape((n_blocks, 1, 8)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 2, 1)
|
||||
)
|
||||
lscales = lscales.reshape((n_blocks, 16))
|
||||
hscales = hscales.reshape((n_blocks, 1, 4)) >> torch.tensor(
|
||||
[0, 2, 4, 6], device=d.device, dtype=torch.uint8
|
||||
).reshape((1, 4, 1))
|
||||
hscales = hscales.reshape((n_blocks, 16))
|
||||
scales = (lscales & 0x0F) | ((hscales & 0x03) << 4)
|
||||
scales = scales.to(torch.int8) - 32
|
||||
|
||||
dl = (d * scales).reshape((n_blocks, 16, 1))
|
||||
|
||||
ql = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 4, 1)
|
||||
)
|
||||
qh = hmask.reshape(n_blocks, -1, 1, 32) >> torch.tensor(list(range(8)), device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 8, 1)
|
||||
)
|
||||
ql = ql.reshape((n_blocks, 16, QK_K // 16)) & 3
|
||||
qh = (qh.reshape((n_blocks, 16, QK_K // 16)) & 1) ^ 1
|
||||
q = ql.to(torch.int8) - (qh << 2).to(torch.int8)
|
||||
|
||||
return (dl * q).reshape((n_blocks, QK_K))
|
||||
|
||||
|
||||
def dequantize_blocks_Q2_K(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
scales, qs, d, dmin = split_block_dims(blocks, QK_K // 16, QK_K // 4, 2)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
dmin = dmin.view(torch.float16).to(dtype)
|
||||
|
||||
# (n_blocks, 16, 1)
|
||||
dl = (d * (scales & 0xF)).reshape((n_blocks, QK_K // 16, 1))
|
||||
ml = (dmin * (scales >> 4)).reshape((n_blocks, QK_K // 16, 1))
|
||||
|
||||
shift = torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 1, 4, 1))
|
||||
|
||||
qs = (qs.reshape((n_blocks, -1, 1, 32)) >> shift) & 3
|
||||
qs = qs.reshape((n_blocks, QK_K // 16, 16))
|
||||
qs = dl * qs - ml
|
||||
|
||||
return qs.reshape((n_blocks, -1))
|
||||
|
||||
|
||||
DEQUANTIZE_FUNCTIONS: dict[
|
||||
gguf.GGMLQuantizationType, Callable[[torch.Tensor, int, int, Optional[torch.dtype]], torch.Tensor]
|
||||
] = {
|
||||
gguf.GGMLQuantizationType.BF16: dequantize_blocks_BF16,
|
||||
gguf.GGMLQuantizationType.Q8_0: dequantize_blocks_Q8_0,
|
||||
gguf.GGMLQuantizationType.Q5_1: dequantize_blocks_Q5_1,
|
||||
gguf.GGMLQuantizationType.Q5_0: dequantize_blocks_Q5_0,
|
||||
gguf.GGMLQuantizationType.Q4_1: dequantize_blocks_Q4_1,
|
||||
gguf.GGMLQuantizationType.Q4_0: dequantize_blocks_Q4_0,
|
||||
gguf.GGMLQuantizationType.Q6_K: dequantize_blocks_Q6_K,
|
||||
gguf.GGMLQuantizationType.Q5_K: dequantize_blocks_Q5_K,
|
||||
gguf.GGMLQuantizationType.Q4_K: dequantize_blocks_Q4_K,
|
||||
gguf.GGMLQuantizationType.Q3_K: dequantize_blocks_Q3_K,
|
||||
gguf.GGMLQuantizationType.Q2_K: dequantize_blocks_Q2_K,
|
||||
}
|
||||
|
||||
|
||||
def is_torch_compatible(tensor: Optional[torch.Tensor]):
|
||||
return getattr(tensor, "tensor_type", None) in TORCH_COMPATIBLE_QTYPES
|
||||
|
||||
|
||||
def is_quantized(tensor: torch.Tensor):
|
||||
return not is_torch_compatible(tensor)
|
||||
|
||||
|
||||
def dequantize(
|
||||
data: torch.Tensor, qtype: gguf.GGMLQuantizationType, oshape: torch.Size, dtype: Optional[torch.dtype] = None
|
||||
):
|
||||
"""
|
||||
Dequantize tensor back to usable shape/dtype
|
||||
"""
|
||||
block_size, type_size = gguf.GGML_QUANT_SIZES[qtype]
|
||||
dequantize_blocks = DEQUANTIZE_FUNCTIONS[qtype]
|
||||
|
||||
rows = data.reshape((-1, data.shape[-1])).view(torch.uint8)
|
||||
|
||||
n_blocks = rows.numel() // type_size
|
||||
blocks = rows.reshape((n_blocks, type_size))
|
||||
blocks = dequantize_blocks(blocks, block_size, type_size, dtype)
|
||||
return blocks.reshape(oshape)
|
||||
|
||||
|
||||
def to_uint32(x: torch.Tensor) -> torch.Tensor:
|
||||
x = x.view(torch.uint8).to(torch.int32)
|
||||
return (x[:, 0] | x[:, 1] << 8 | x[:, 2] << 16 | x[:, 3] << 24).unsqueeze(1)
|
||||
|
||||
|
||||
def split_block_dims(blocks: torch.Tensor, *args):
|
||||
n_max = blocks.shape[1]
|
||||
dims = list(args) + [n_max - sum(args)]
|
||||
return torch.split(blocks, dims, dim=1)
|
||||
|
||||
|
||||
PATCH_TYPES = Union[torch.Tensor, list[torch.Tensor], tuple[torch.Tensor]]
|
||||
@@ -52,49 +52,51 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.1.3",
|
||||
"@dagrejs/graphlib": "^2.2.3",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@dagrejs/graphlib": "^2.2.4",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource-variable/inter": "^5.0.20",
|
||||
"@invoke-ai/ui-library": "^0.0.37",
|
||||
"@fontsource-variable/inter": "^5.1.0",
|
||||
"@invoke-ai/ui-library": "^0.0.41",
|
||||
"@nanostores/react": "^0.7.3",
|
||||
"@reduxjs/toolkit": "2.2.3",
|
||||
"@roarr/browser-log-writer": "^1.3.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chakra-react-select": "^4.9.1",
|
||||
"chakra-react-select": "^4.9.2",
|
||||
"cmdk": "^1.0.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
"dateformat": "^5.0.3",
|
||||
"fracturedjsonjs": "^4.0.2",
|
||||
"framer-motion": "^11.3.24",
|
||||
"i18next": "^23.12.2",
|
||||
"i18next-http-backend": "^2.5.2",
|
||||
"framer-motion": "^11.10.0",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"konva": "^9.3.14",
|
||||
"konva": "^9.3.15",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lru-cache": "^11.0.0",
|
||||
"lru-cache": "^11.0.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"nanostores": "^0.11.2",
|
||||
"nanostores": "^0.11.3",
|
||||
"new-github-issue-url": "^1.0.0",
|
||||
"overlayscrollbars": "^2.10.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"perfect-freehand": "^1.2.2",
|
||||
"query-string": "^9.1.0",
|
||||
"raf-throttle": "^2.0.6",
|
||||
"react": "^18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-dropzone": "^14.2.9",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-hotkeys-hook": "4.5.0",
|
||||
"react-i18next": "^14.1.3",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-i18next": "^15.0.2",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-redux": "9.1.2",
|
||||
"react-resizable-panels": "^2.1.2",
|
||||
"react-resizable-panels": "^2.1.4",
|
||||
"react-use": "^17.5.1",
|
||||
"react-virtuoso": "^4.9.0",
|
||||
"react-virtuoso": "^4.10.4",
|
||||
"reactflow": "^11.11.4",
|
||||
"redux-dynamic-middlewares": "^2.2.0",
|
||||
"redux-remember": "^5.1.0",
|
||||
@@ -102,13 +104,13 @@
|
||||
"rfdc": "^1.4.1",
|
||||
"roarr": "^7.21.1",
|
||||
"serialize-error": "^11.0.3",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"stable-hash": "^0.0.4",
|
||||
"use-debounce": "^10.0.2",
|
||||
"use-debounce": "^10.0.3",
|
||||
"use-device-pixel-ratio": "^1.1.2",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.23.8",
|
||||
"zod-validation-error": "^3.3.1"
|
||||
"zod-validation-error": "^3.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
@@ -118,40 +120,40 @@
|
||||
"devDependencies": {
|
||||
"@invoke-ai/eslint-config-react": "^0.0.14",
|
||||
"@invoke-ai/prettier-config-react": "^0.0.7",
|
||||
"@storybook/addon-essentials": "^8.2.8",
|
||||
"@storybook/addon-interactions": "^8.2.8",
|
||||
"@storybook/addon-links": "^8.2.8",
|
||||
"@storybook/addon-storysource": "^8.2.8",
|
||||
"@storybook/manager-api": "^8.2.8",
|
||||
"@storybook/react": "^8.2.8",
|
||||
"@storybook/react-vite": "^8.2.8",
|
||||
"@storybook/theming": "^8.2.8",
|
||||
"@storybook/addon-essentials": "^8.3.4",
|
||||
"@storybook/addon-interactions": "^8.3.4",
|
||||
"@storybook/addon-links": "^8.3.4",
|
||||
"@storybook/addon-storysource": "^8.3.4",
|
||||
"@storybook/manager-api": "^8.3.4",
|
||||
"@storybook/react": "^8.3.4",
|
||||
"@storybook/react-vite": "^8.3.4",
|
||||
"@storybook/theming": "^8.3.4",
|
||||
"@types/dateformat": "^5.0.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.14.15",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/node": "^20.16.10",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"@vitest/coverage-v8": "^1.5.0",
|
||||
"@vitest/ui": "^1.5.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.1",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"csstype": "^3.1.3",
|
||||
"dpdm": "^3.14.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-i18next": "^6.0.9",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-i18next": "^6.1.0",
|
||||
"eslint-plugin-path": "^1.3.0",
|
||||
"knip": "^5.27.2",
|
||||
"knip": "^5.31.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"openapi-typescript": "^7.3.0",
|
||||
"openapi-typescript": "^7.4.1",
|
||||
"prettier": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"storybook": "^8.2.8",
|
||||
"storybook": "^8.3.4",
|
||||
"ts-toolbelt": "^9.6.0",
|
||||
"tsafe": "^1.7.2",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-css-injected-by-js": "^3.5.1",
|
||||
"tsafe": "^1.7.5",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.8",
|
||||
"vite-plugin-css-injected-by-js": "^3.5.2",
|
||||
"vite-plugin-dts": "^3.9.1",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
|
||||
6075
invokeai/frontend/web/pnpm-lock.yaml
generated
6075
invokeai/frontend/web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"reportBugLabel": "Fehler melden",
|
||||
"settingsLabel": "Einstellungen",
|
||||
"img2img": "Bild zu Bild",
|
||||
"nodes": "Workflows",
|
||||
"nodes": "Arbeitsabläufe",
|
||||
"upload": "Hochladen",
|
||||
"load": "Laden",
|
||||
"statusDisconnected": "Getrennt",
|
||||
@@ -83,7 +83,17 @@
|
||||
"tab": "Tabulator",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Ausgeschaltet",
|
||||
"dontShowMeThese": "Zeig mir diese nicht"
|
||||
"dontShowMeThese": "Zeig mir diese nicht",
|
||||
"apply": "Anwenden",
|
||||
"edit": "Ändern",
|
||||
"openInViewer": "Im Viewer öffnen",
|
||||
"loadingImage": "Lade Bild",
|
||||
"off": "Aus",
|
||||
"view": "Anzeigen",
|
||||
"placeholderSelectAModel": "Modell auswählen",
|
||||
"reset": "Zurücksetzen",
|
||||
"none": "Keine",
|
||||
"new": "Neu"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Bildgröße",
|
||||
@@ -117,16 +127,337 @@
|
||||
"alwaysShowImageSizeBadge": "Zeige immer Bilder Größe Abzeichen",
|
||||
"selectForCompare": "Zum Vergleichen auswählen",
|
||||
"compareImage": "Bilder vergleichen",
|
||||
"exitSearch": "Suche beenden",
|
||||
"exitSearch": "Bildsuche beenden",
|
||||
"newestFirst": "Neueste zuerst",
|
||||
"oldestFirst": "Älteste zuerst",
|
||||
"openInViewer": "Im Viewer öffnen",
|
||||
"swapImages": "Bilder tauschen"
|
||||
"swapImages": "Bilder tauschen",
|
||||
"slider": "Slider",
|
||||
"showStarredImagesFirst": "Mit * markierte Bilder zuerst zeigen",
|
||||
"compareHelp1": "Halten Sie <Kbd>Alt</Kbd> gedrückt, während Sie auf ein Galeriebild klicken oder die Pfeiltasten verwenden, um das Vergleichsbild zu ändern.",
|
||||
"compareHelp4": "Drücken Sie <Kbd>Z</Kbd> oder <Kbd>Esc</Kbd> zum Beenden.",
|
||||
"move": "Bewegen",
|
||||
"exitBoardSearch": "Suchen beenden",
|
||||
"searchImages": "Suche mit Metadaten",
|
||||
"selectAllOnPage": "Alle auf Seite auswählen",
|
||||
"showArchivedBoards": "Archivierte Boards anzeigen",
|
||||
"hover": "Schweben",
|
||||
"compareHelp2": "Drücken Sie <Kbd>M</Kbd>, um durch alle Vergleichsmodi zu wechseln.",
|
||||
"compareHelp3": "Drücken Sie <Kbd>C</Kbd>, um die verglichenen Bilder zu wechseln.",
|
||||
"gallery": "Galerie",
|
||||
"sortDirection": "Sortierreihenfolge",
|
||||
"sideBySide": "Nebeneinander",
|
||||
"openViewer": "Viewer öffnen",
|
||||
"viewerImage": "Viewer-Bild",
|
||||
"exitCompare": "Vergleichen beenden",
|
||||
"closeViewer": "Viewer schließen",
|
||||
"selectAnImageToCompare": "Wählen Sie ein Bild zum Vergleichen",
|
||||
"stretchToFit": "Strecken bis es passt",
|
||||
"displayBoardSearch": "Board durchsuchen",
|
||||
"displaySearch": "Bild suchen",
|
||||
"go": "Los",
|
||||
"jump": "Springen"
|
||||
},
|
||||
"hotkeys": {
|
||||
"noHotkeysFound": "Kein Hotkey gefunden",
|
||||
"searchHotkeys": "Hotkeys durchsuchen",
|
||||
"clearSearch": "Suche leeren"
|
||||
"clearSearch": "Suche leeren",
|
||||
"canvas": {
|
||||
"fitBboxToCanvas": {
|
||||
"desc": "Skalierung und Positionierung der Ansicht auf Bbox-Größe.",
|
||||
"title": "Bbox auf Arbeitsfläche skalieren"
|
||||
},
|
||||
"selectBboxTool": {
|
||||
"title": "Bbox Werkzeug",
|
||||
"desc": "Bbox Werkzeug auswählen."
|
||||
},
|
||||
"setFillToWhite": {
|
||||
"title": "Farbe auf Weiß einstellen",
|
||||
"desc": "Setzt die aktuelle Werkzeugfarbe auf weiß."
|
||||
},
|
||||
"title": "Leinwand",
|
||||
"selectBrushTool": {
|
||||
"title": "Pinselwerkzeug",
|
||||
"desc": "Wählen Sie das Pinselwerkzeug aus."
|
||||
},
|
||||
"decrementToolWidth": {
|
||||
"title": "Werkzeugbreite verringern",
|
||||
"desc": "Verringern Sie die Breite des Pinsels oder Radiergummis, je nachdem, welches ausgewählt ist."
|
||||
},
|
||||
"incrementToolWidth": {
|
||||
"title": "Werkzeugbreite erhöhen",
|
||||
"desc": "Vergrößern Sie die Breite des Pinsels oder Radiergummis, je nachdem, welches ausgewählt ist."
|
||||
},
|
||||
"selectColorPickerTool": {
|
||||
"title": "Farbwähler-Werkzeug",
|
||||
"desc": "Farbwähler-Werkzeug auswählen."
|
||||
},
|
||||
"selectEraserTool": {
|
||||
"title": "Radiergummi-Werkzeug",
|
||||
"desc": "Radiergummi-Werkzeug auswählen."
|
||||
},
|
||||
"fitLayersToCanvas": {
|
||||
"title": "Ebenen an die Leinwand anpassen",
|
||||
"desc": "Alle sichtbaren Ebenen in der Ansicht einpassen."
|
||||
},
|
||||
"filterSelected": {
|
||||
"title": "Filter",
|
||||
"desc": "Gewählte Ebene filtern. Nur bei \"Raster\" und Kontroll-Ebenen."
|
||||
},
|
||||
"transformSelected": {
|
||||
"title": "Umwandeln",
|
||||
"desc": "Transformieren Sie die ausgewählte Ebene."
|
||||
},
|
||||
"setZoomTo100Percent": {
|
||||
"title": "Auf 100 % zoomen",
|
||||
"desc": "Leinwand-Zoom auf 100 % setzen."
|
||||
},
|
||||
"setZoomTo200Percent": {
|
||||
"title": "Auf 200 % zoomen",
|
||||
"desc": "Leinwand-Zoom auf 200 % setzen."
|
||||
},
|
||||
"setZoomTo400Percent": {
|
||||
"title": "Auf 400 % zoomen",
|
||||
"desc": "Leinwand-Zoom auf 400 % setzen."
|
||||
},
|
||||
"setZoomTo800Percent": {
|
||||
"title": "Auf 800 % zoomen",
|
||||
"desc": "Leinwand-Zoom auf 800 % setzen."
|
||||
},
|
||||
"deleteSelected": {
|
||||
"title": "Ebene löschen",
|
||||
"desc": "Ausgewählte Ebene löschen."
|
||||
},
|
||||
"undo": {
|
||||
"title": "Rückgängig",
|
||||
"desc": "Letzte Aktion rückgängig machen."
|
||||
},
|
||||
"redo": {
|
||||
"title": "Wiederholen",
|
||||
"desc": "Letzte Aktion wiederholen."
|
||||
},
|
||||
"nextEntity": {
|
||||
"title": "Nächste Ebene",
|
||||
"desc": "Nächste Ebene in der Liste auswählen."
|
||||
},
|
||||
"resetSelected": {
|
||||
"title": "Ebene zurücksetzen",
|
||||
"desc": "Ausgewählte Ebene zurücksetzen. Gilt nur für Malmaske bei \"Inpaint\" und \"Regionaler Führung\"."
|
||||
},
|
||||
"prevEntity": {
|
||||
"title": "Vorherige Ebene",
|
||||
"desc": "Vorherige Ebene in der Liste auswählen."
|
||||
},
|
||||
"selectMoveTool": {
|
||||
"title": "Verschieben-Werkzeug",
|
||||
"desc": "Verschieben-Werkzeug auswählen."
|
||||
},
|
||||
"selectRectTool": {
|
||||
"title": "Rechteck-Werkzeug",
|
||||
"desc": "Rechteck-Werkzeug auswählen."
|
||||
},
|
||||
"selectViewTool": {
|
||||
"desc": "Wählen Sie das Ansichts-Tool.",
|
||||
"title": "Ansichts-Tool"
|
||||
},
|
||||
"quickSwitch": {
|
||||
"title": "Ebenen schnell umschalten",
|
||||
"desc": "Wechseln Sie zwischen den beiden zuletzt gewählten Ebenen. Wenn eine Ebene mit einem Lesezeichen versehen ist, wird zwischen ihr und der letzten nicht markierten Ebene gewechselt."
|
||||
},
|
||||
"applyFilter": {
|
||||
"title": "Filter anwenden",
|
||||
"desc": "Wende den ausstehenden Filter auf die ausgewählte Ebene an."
|
||||
}
|
||||
},
|
||||
"viewer": {
|
||||
"useSize": {
|
||||
"desc": "Aktuelle Bildgröße als Bbox-Größe verwenden.",
|
||||
"title": "Maße übernehmen"
|
||||
},
|
||||
"title": "Bildbetrachter",
|
||||
"toggleViewer": {
|
||||
"title": "Bildbetrachter anzeigen/ausblenden",
|
||||
"desc": "Zeigen oder verbergen Sie den Bildbetrachter. Nur auf der Arbeitsflächen-Registerkarte."
|
||||
},
|
||||
"nextComparisonMode": {
|
||||
"title": "Nächster Vergleichsmodus",
|
||||
"desc": "Alle Vergleichsmodi durchlaufen."
|
||||
},
|
||||
"swapImages": {
|
||||
"title": "Vergleichsbilder tauschen",
|
||||
"desc": "Vergleichs-Bilder tauschen."
|
||||
},
|
||||
"runPostprocessing": {
|
||||
"title": "Nachbearbeitung ausführen",
|
||||
"desc": "Ausgewählte Nachbearbeitung/en auf aktuelles Bild anwenden."
|
||||
},
|
||||
"toggleMetadata": {
|
||||
"title": "Metadaten anzeigen/ausblenden",
|
||||
"desc": "Zeigen oder verbergen der Metadaten des Bildes."
|
||||
},
|
||||
"recallPrompts": {
|
||||
"title": "Prompts abrufen",
|
||||
"desc": "Rufen Sie die positiven und negativen Prompts für das aktuelle Bild ab."
|
||||
},
|
||||
"recallSeed": {
|
||||
"desc": "Seed für aktuelles Bild abrufen.",
|
||||
"title": "Seed abrufen"
|
||||
},
|
||||
"loadWorkflow": {
|
||||
"title": "Lade Arbeitsablauf/Workflow",
|
||||
"desc": "Laden Sie den gespeicherten Workflow des aktuellen Bildes (falls es einen hat)."
|
||||
},
|
||||
"recallAll": {
|
||||
"title": "Alle Metadaten abrufen",
|
||||
"desc": "Alle Metadaten für das aktuelle Bild abrufen."
|
||||
},
|
||||
"remix": {
|
||||
"desc": "Rufen Sie alle Metadaten außer dem Seed für das aktuelle Bild ab.",
|
||||
"title": "Remixen"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"invoke": {
|
||||
"title": "Invoke",
|
||||
"desc": "Stellt eine Generierung in die Warteschlange und fügt sie am Ende hinzu."
|
||||
},
|
||||
"invokeFront": {
|
||||
"title": "Invoke (Front)",
|
||||
"desc": "Stellt eine Generierung in die Warteschlange und fügt sie am Anfang hinzu."
|
||||
},
|
||||
"cancelQueueItem": {
|
||||
"title": "Abbrechen",
|
||||
"desc": "Aktuelles Warteschlangenelement abbrechen."
|
||||
},
|
||||
"clearQueue": {
|
||||
"title": "Warteschlange löschen",
|
||||
"desc": "Warteschlange abbrechen und komplett löschen."
|
||||
},
|
||||
"selectUpscalingTab": {
|
||||
"title": "Wählen Sie die Registerkarte Hochskalieren",
|
||||
"desc": "Wählt die Registerkarte Hochskalieren."
|
||||
},
|
||||
"selectCanvasTab": {
|
||||
"desc": "Wählt die Arbeitsflächen-Registerkarte.",
|
||||
"title": "Wählen Sie die Arbeitsflächen-Registerkarte"
|
||||
},
|
||||
"selectWorkflowsTab": {
|
||||
"title": "Wählt die Registerkarte Arbeitsabläufe",
|
||||
"desc": "Wählt die Registerkarte Arbeitsabläufe."
|
||||
},
|
||||
"selectModelsTab": {
|
||||
"title": "Wählt die Registerkarte Modelle",
|
||||
"desc": "Wählt die Registerkarte Modelle."
|
||||
},
|
||||
"selectQueueTab": {
|
||||
"title": "Wählt die Registerkarte Warteschlange",
|
||||
"desc": "Wählt die Registerkarte Warteschlange."
|
||||
},
|
||||
"focusPrompt": {
|
||||
"desc": "Bewegt den Cursor-Fokus auf den positiven Prompt.",
|
||||
"title": "Fokus-Prompt"
|
||||
},
|
||||
"toggleLeftPanel": {
|
||||
"title": "Linkes Panel ein-/ausblenden",
|
||||
"desc": "Linke Seite zeigen/verbergen."
|
||||
},
|
||||
"toggleRightPanel": {
|
||||
"title": "Rechte Seite umschalten",
|
||||
"desc": "Rechte Seite zeigen/verbergen."
|
||||
},
|
||||
"resetPanelLayout": {
|
||||
"title": "Layout zurücksetzen",
|
||||
"desc": "Beide Seiten auf Standard zurücksetzen."
|
||||
},
|
||||
"title": "Anwendung",
|
||||
"togglePanels": {
|
||||
"title": "Seiten umschalten",
|
||||
"desc": "Zeigen oder verbergen Sie beide Panels auf einmal."
|
||||
}
|
||||
},
|
||||
"hotkeys": "Tastaturbefehle",
|
||||
"gallery": {
|
||||
"title": "Galerie",
|
||||
"selectAllOnPage": {
|
||||
"title": "Alle auf der Seite auswählen",
|
||||
"desc": "Alle Bilder auf der aktuellen Seite auswählen."
|
||||
},
|
||||
"galleryNavRight": {
|
||||
"title": "Nach rechts navigieren",
|
||||
"desc": "Navigieren Sie im Galerieraster nach rechts, und wählen Sie das Bild aus. Wenn es sich um das letzte Bild in der Reihe handelt, gehen Sie zur nächsten Reihe. Wenn Sie sich beim letzten Bild der Seite befinden, gehen Sie zur nächsten Seite."
|
||||
},
|
||||
"galleryNavDownAlt": {
|
||||
"title": "Nach unten navigieren (Bild vergleichen)",
|
||||
"desc": "Wie \"Abwärts navigieren\", wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist."
|
||||
},
|
||||
"galleryNavUp": {
|
||||
"title": "Nach oben navigieren",
|
||||
"desc": "Navigieren Sie im Galerieraster nach oben, und wählen Sie das Bild aus. Wenn Sie sich oben auf der Seite befinden, gehen Sie zur vorherigen Seite."
|
||||
},
|
||||
"galleryNavDown": {
|
||||
"title": "Nach unten navigieren",
|
||||
"desc": "Navigieren Sie im Galerieraster nach unten, und wählen Sie das Bild aus. Wenn Sie sich am Ende der Seite befinden, gehen Sie zur nächsten Seite."
|
||||
},
|
||||
"galleryNavLeft": {
|
||||
"title": "Nach links navigieren",
|
||||
"desc": "Navigieren Sie im Galerieraster nach links, und wählen Sie das Bild aus. Wenn Sie sich im ersten Bild der Reihe befinden, gehen Sie zur vorherigen Reihe. Wenn Sie sich beim ersten Bild der Seite befinden, gehen Sie zur vorherigen Seite."
|
||||
},
|
||||
"galleryNavUpAlt": {
|
||||
"title": "Nach oben navigieren (Bild vergleichen)",
|
||||
"desc": "Wie „Nach oben navigieren“, wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist."
|
||||
},
|
||||
"galleryNavRightAlt": {
|
||||
"title": "Nach rechts navigieren (Bild vergleichen)",
|
||||
"desc": "Wie \"Navigieren nach rechts\", wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist."
|
||||
},
|
||||
"clearSelection": {
|
||||
"title": "Auswahl aufheben",
|
||||
"desc": "Aktuelle Auswahl aufheben, falls vorhanden."
|
||||
},
|
||||
"galleryNavLeftAlt": {
|
||||
"title": "Nach links navigieren (Bild vergleichen)",
|
||||
"desc": "Wie „Nach links navigieren“, wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist."
|
||||
},
|
||||
"deleteSelection": {
|
||||
"title": "Löschen",
|
||||
"desc": "Alle ausgewählten Bilder löschen. Standardmäßig werden Sie aufgefordert, den Löschvorgang zu bestätigen. Wenn die Bilder derzeit in der App verwendet werden, werden Sie gewarnt."
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
"redo": {
|
||||
"title": "Wiederholen",
|
||||
"desc": "Letzte Workflow-Aktion wiederherstellen."
|
||||
},
|
||||
"copySelection": {
|
||||
"title": "Kopieren",
|
||||
"desc": "Ausgewählte Knoten und Kanten kopieren."
|
||||
},
|
||||
"title": "Arbeitsabläufe",
|
||||
"addNode": {
|
||||
"title": "Knoten hinzufügen",
|
||||
"desc": "Öffnen Sie das \"Knoten zufügen\"-Menü."
|
||||
},
|
||||
"pasteSelection": {
|
||||
"title": "Einfügen",
|
||||
"desc": "Kopierte Knoten und Kanten einfügen."
|
||||
},
|
||||
"selectAll": {
|
||||
"title": "Alles auswählen",
|
||||
"desc": "Alle Knoten und Kanten auswählen."
|
||||
},
|
||||
"deleteSelection": {
|
||||
"title": "Löschen",
|
||||
"desc": "Lösche ausgewählte Knoten und Kanten."
|
||||
},
|
||||
"undo": {
|
||||
"title": "Rückgängig",
|
||||
"desc": "Letzte Workflow-Aktion rückgängig machen."
|
||||
},
|
||||
"pasteSelectionWithEdges": {
|
||||
"desc": "Kopierte Knoten, Kanten und alle mit den kopierten Knoten verbundenen Kanten einfügen.",
|
||||
"title": "Einfügen mit Kanten"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelManager": {
|
||||
"modelUpdated": "Model aktualisiert",
|
||||
@@ -164,7 +495,7 @@
|
||||
"baseModel": "Basis Modell",
|
||||
"convertToDiffusers": "Konvertiere zu Diffusers",
|
||||
"vae": "VAE",
|
||||
"predictionType": "Vorhersagetyp (für Stable Diffusion 2.x-Modelle und gelegentliche Stable Diffusion 1.x-Modelle)",
|
||||
"predictionType": "Vorhersagetyp",
|
||||
"selectModel": "Wählen Sie Modell aus",
|
||||
"repo_id": "Repo-ID",
|
||||
"modelDeleted": "Modell gelöscht",
|
||||
@@ -187,7 +518,52 @@
|
||||
"deleteModelImage": "Lösche Model Bild",
|
||||
"huggingFaceRepoID": "HuggingFace Repo ID",
|
||||
"hfToken": "HuggingFace Schlüssel",
|
||||
"huggingFacePlaceholder": "besitzer/model-name"
|
||||
"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",
|
||||
"textualInversions": "Textuelle Inversionen",
|
||||
"ipAdapters": "IP-Adapter",
|
||||
"modelImageUpdated": "Modellbild aktualisiert",
|
||||
"path": "Pfad",
|
||||
"pathToConfig": "Pfad zur Konfiguration",
|
||||
"scanPlaceholder": "Pfad zu einem lokalen Ordner",
|
||||
"noMatchingModels": "Keine passenden Modelle",
|
||||
"localOnly": "nur lokal",
|
||||
"installAll": "Alles installieren",
|
||||
"main": "Haupt",
|
||||
"metadata": "Metadaten",
|
||||
"modelImageDeleted": "Modellbild gelöscht",
|
||||
"modelName": "Modellname",
|
||||
"noModelsInstalled": "Keine Modelle installiert",
|
||||
"source": "Quelle",
|
||||
"simpleModelPlaceholder": "URL oder Pfad zu einem lokalen Datei- oder Diffusers-Ordner",
|
||||
"imageEncoderModelId": "Bild Encoder Modell ID",
|
||||
"installRepo": "Repo installieren",
|
||||
"huggingFaceHelper": "Wenn mehrere Modelle in diesem Repo gefunden werden, werden Sie aufgefordert, eines für die Installation auszuwählen.",
|
||||
"inplaceInstall": "In-place-Installation",
|
||||
"modelImageDeleteFailed": "Modellbild konnte nicht gelöscht werden",
|
||||
"repoVariant": "Repo Variante",
|
||||
"learnMoreAboutSupportedModels": "Erfahren Sie mehr über die Modelle, die wir unterstützen",
|
||||
"clipEmbed": "CLIP einbetten",
|
||||
"starterModelsInModelManager": "Modelle für Ihren Start finden Sie im Modell-Manager",
|
||||
"noModelsInstalledDesc1": "Installiere Modelle mit dem",
|
||||
"modelImageUpdateFailed": "Modellbild-Update fehlgeschlagen",
|
||||
"prune": "Bereinigen",
|
||||
"loraModels": "LoRAs",
|
||||
"scanFolder": "Ordner scannen",
|
||||
"installQueue": "Installations-Warteschlange",
|
||||
"pruneTooltip": "Abgeschlossene Importe aus Warteschlange entfernen",
|
||||
"scanResults": "Ergebnisse des Scans",
|
||||
"urlOrLocalPathHelper": "URLs sollten auf eine einzelne Datei deuten. Lokale Pfade können zusätzlich auch auf einen Ordner für ein einzelnes Diffusers-Modell hinweisen.",
|
||||
"inplaceInstallDesc": "Installieren Sie Modelle, ohne die Dateien zu kopieren. Wenn Sie das Modell verwenden, wird es direkt von seinem Speicherort geladen. Wenn deaktiviert, werden die Dateien während der Installation in das von Invoke verwaltete Modellverzeichnis kopiert.",
|
||||
"scanFolderHelper": "Der Ordner wird rekursiv nach Modellen durchsucht. Dies kann bei sehr großen Ordnern etwas dauern."
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Bilder",
|
||||
@@ -226,7 +602,19 @@
|
||||
"setToOptimalSize": "Optimiere Größe für Modell",
|
||||
"useSize": "Maße übernehmen",
|
||||
"remixImage": "Remix des Bilds erstellen",
|
||||
"imageActions": "Weitere Bildaktionen"
|
||||
"imageActions": "Weitere Bildaktionen",
|
||||
"invoke": {
|
||||
"layer": {
|
||||
"t2iAdapterIncompatibleBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, Bbox-Breite ist {{width}}",
|
||||
"t2iAdapterIncompatibleScaledBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, Skalierte Bbox-Breite ist {{width}}",
|
||||
"t2iAdapterIncompatibleScaledBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, Skalierte Bbox-Höhe ist {{height}}",
|
||||
"t2iAdapterIncompatibleBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, Bbox-Höhe ist {{height}}"
|
||||
},
|
||||
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Skalierte Bbox-Breite ist {{width}}",
|
||||
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Skalierte Bbox-Höhe ist {{height}}",
|
||||
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Bbox-Breite ist {{width}}",
|
||||
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Bbox-Höhe ist {{height}}"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"displayInProgress": "Zwischenbilder anzeigen",
|
||||
@@ -263,22 +651,23 @@
|
||||
"imageCopied": "Bild kopiert",
|
||||
"parametersNotSet": "Parameter nicht festgelegt",
|
||||
"addedToBoard": "Dem Board hinzugefügt",
|
||||
"loadedWithWarnings": "Workflow mit Warnungen geladen"
|
||||
"loadedWithWarnings": "Workflow mit Warnungen geladen",
|
||||
"imageSaved": "Bild gespeichert"
|
||||
},
|
||||
"accessibility": {
|
||||
"uploadImage": "Bild hochladen",
|
||||
"previousImage": "Vorheriges Bild",
|
||||
"showOptionsPanel": "Seitenpanel anzeigen",
|
||||
"reset": "Zurücksetzten",
|
||||
"nextImage": "Nächstes Bild",
|
||||
"showGalleryPanel": "Galerie-Panel anzeigen",
|
||||
"menu": "Menü",
|
||||
"invokeProgressBar": "Invoke Fortschrittsanzeige",
|
||||
"mode": "Modus",
|
||||
"resetUI": "$t(accessibility.reset) von UI",
|
||||
"createIssue": "Ticket erstellen",
|
||||
"about": "Über",
|
||||
"submitSupportTicket": "Support-Ticket senden"
|
||||
"submitSupportTicket": "Support-Ticket senden",
|
||||
"toggleRightPanel": "Rechtes Bedienfeld umschalten (G)",
|
||||
"toggleLeftPanel": "Linkes Bedienfeld umschalten (T)"
|
||||
},
|
||||
"boards": {
|
||||
"autoAddBoard": "Board automatisch erstellen",
|
||||
@@ -300,7 +689,6 @@
|
||||
"deleteBoardOnly": "Nur Ordner löschen",
|
||||
"deleteBoard": "Lösche Ordner",
|
||||
"deleteBoardAndImages": "Lösche Ordner und Bilder",
|
||||
"deletedBoardsCannotbeRestored": "Gelöschte Ordner können nicht wiederhergestellt werden",
|
||||
"movingImagesToBoard_one": "Verschiebe {{count}} Bild in Ordner:",
|
||||
"movingImagesToBoard_other": "Verschiebe {{count}} Bilder in Ordner:",
|
||||
"selectedForAutoAdd": "Ausgewählt für Automatisches hinzufügen",
|
||||
@@ -317,7 +705,10 @@
|
||||
"noBoards": "Kein {boardType}} Ordner",
|
||||
"hideBoards": "Ordner verstecken",
|
||||
"viewBoards": "Ordner ansehen",
|
||||
"deletedPrivateBoardsCannotbeRestored": "Gelöschte Boards können nicht wiederhergestellt werden. Wenn Sie „Nur Board löschen“ wählen, werden die Bilder in einen privaten, nicht kategorisierten Status für den Ersteller des Bildes versetzt."
|
||||
"deletedPrivateBoardsCannotbeRestored": "Gelöschte Boards können nicht wiederhergestellt werden. Wenn Sie „Nur Board löschen“ wählen, werden die Bilder in einen privaten, nicht kategorisierten Status für den Ersteller des Bildes versetzt.",
|
||||
"assetsWithCount_one": "{{count}} in der Sammlung",
|
||||
"assetsWithCount_other": "{{count}} in der Sammlung",
|
||||
"deletedBoardsCannotbeRestored": "Gelöschte Ordner können nicht wiederhergestellt werden. Die Auswahl von \"Nur Ordner löschen\" verschiebt Bilder in einen unkategorisierten Zustand."
|
||||
},
|
||||
"queue": {
|
||||
"status": "Status",
|
||||
@@ -377,7 +768,19 @@
|
||||
"graphQueued": "Graph eingereiht",
|
||||
"graphFailedToQueue": "Fehler beim Einreihen des Graphen",
|
||||
"generations_one": "Generation",
|
||||
"generations_other": "Generationen"
|
||||
"generations_other": "Generationen",
|
||||
"iterations_one": "Iteration",
|
||||
"iterations_other": "Iterationen",
|
||||
"gallery": "Galerie",
|
||||
"generation": "Erstellung",
|
||||
"workflows": "Arbeitsabläufe",
|
||||
"other": "Sonstige",
|
||||
"origin": "Ursprung",
|
||||
"destination": "Ziel",
|
||||
"upscaling": "Hochskalierung",
|
||||
"canvas": "Leinwand",
|
||||
"prompts_one": "Prompt",
|
||||
"prompts_other": "Prompts"
|
||||
},
|
||||
"metadata": {
|
||||
"negativePrompt": "Negativ Beschreibung",
|
||||
@@ -407,7 +810,8 @@
|
||||
"imageDimensions": "Bilder Auslösungen",
|
||||
"parameterSet": "Parameter {{parameter}} setzen",
|
||||
"recallParameter": "{{label}} Abrufen",
|
||||
"parsingFailed": "Parsing Fehlgeschlagen"
|
||||
"parsingFailed": "Parsing Fehlgeschlagen",
|
||||
"canvasV2Metadata": "Leinwand"
|
||||
},
|
||||
"popovers": {
|
||||
"noiseUseCPU": {
|
||||
@@ -555,6 +959,9 @@
|
||||
"paragraphs": [
|
||||
"Die Skalierung steuert die Größe des Ausgabebildes und basiert auf einem Vielfachen der Auflösung des Originalbildes. So würde z. B. eine 2-fache Hochskalierung eines 1024x1024px Bildes eine 2048x2048px große Ausgabe erzeugen."
|
||||
]
|
||||
},
|
||||
"ipAdapterMethod": {
|
||||
"heading": "Methode"
|
||||
}
|
||||
},
|
||||
"invocationCache": {
|
||||
@@ -588,7 +995,7 @@
|
||||
"cannotConnectToSelf": "Es kann keine Verbindung zu sich selbst hergestellt werden",
|
||||
"colorCodeEdges": "Farbkodierte Kanten",
|
||||
"addNodeToolTip": "Knoten hinzufügen (Umschalt+A, Leertaste)",
|
||||
"collectionFieldType": "{{name}} Sammlung",
|
||||
"collectionFieldType": "{{name}} (Sammlung)",
|
||||
"connectionWouldCreateCycle": "Verbindung würde einen Kreislauf/cycle schaffen",
|
||||
"inputMayOnlyHaveOneConnection": "Eingang darf nur eine Verbindung haben",
|
||||
"hideLegendNodes": "Feldtyp-Legende ausblenden",
|
||||
@@ -652,7 +1059,28 @@
|
||||
"enum": "Aufzählung",
|
||||
"fullyContainNodes": "Vollständig ausgewählte Nodes auswählen",
|
||||
"editMode": "Im Workflow-Editor bearbeiten",
|
||||
"resetToDefaultValue": "Auf Standardwert zurücksetzen"
|
||||
"resetToDefaultValue": "Auf Standardwert zurücksetzen",
|
||||
"singleFieldType": "{{name}} (Einzeln)",
|
||||
"collectionOrScalarFieldType": "{{name}} (Einzeln oder Sammlung)",
|
||||
"missingFieldTemplate": "Fehlende Feldvorlage",
|
||||
"missingNode": "Fehlender Aufrufknoten",
|
||||
"missingInvocationTemplate": "Fehlende Aufrufvorlage",
|
||||
"edit": "Bearbeiten",
|
||||
"workflowAuthor": "Autor",
|
||||
"graph": "Graph",
|
||||
"workflowDescription": "Kurze Beschreibung",
|
||||
"versionUnknown": " Version unbekannt",
|
||||
"workflow": "Arbeitsablauf",
|
||||
"noGraph": "Kein Graph",
|
||||
"version": "Version",
|
||||
"zoomInNodes": "Hineinzoomen",
|
||||
"zoomOutNodes": "Herauszoomen",
|
||||
"workflowName": "Name",
|
||||
"unknownNode": "Unbekannter Knoten",
|
||||
"workflowContact": "Kontaktdaten",
|
||||
"workflowNotes": "Notizen",
|
||||
"workflowTags": "Tags",
|
||||
"workflowVersion": "Version"
|
||||
},
|
||||
"hrf": {
|
||||
"enableHrf": "Korrektur für hohe Auflösungen",
|
||||
@@ -674,7 +1102,8 @@
|
||||
"noLoRAsInstalled": "Keine LoRAs installiert",
|
||||
"addLora": "LoRA hinzufügen",
|
||||
"defaultVAE": "Standard VAE",
|
||||
"lora": "LoRA"
|
||||
"lora": "LoRA",
|
||||
"concepts": "Konzepte"
|
||||
},
|
||||
"accordions": {
|
||||
"generation": {
|
||||
@@ -719,7 +1148,17 @@
|
||||
"openWorkflow": "Arbeitsablauf öffnen",
|
||||
"saveWorkflowToProject": "Arbeitsablauf in Projekt speichern",
|
||||
"workflowCleared": "Arbeitsablauf gelöscht",
|
||||
"loading": "Lade Arbeitsabläufe"
|
||||
"loading": "Lade Arbeitsabläufe",
|
||||
"name": "Name",
|
||||
"ascending": "Aufsteigend",
|
||||
"defaultWorkflows": "Standard Arbeitsabläufe",
|
||||
"userWorkflows": "Benutzer Arbeitsabläufe",
|
||||
"projectWorkflows": "Projekt Arbeitsabläufe",
|
||||
"opened": "Geöffnet",
|
||||
"loadWorkflow": "Arbeitsablauf $t(common.load)",
|
||||
"updated": "Aktualisiert",
|
||||
"created": "Erstellt",
|
||||
"descending": "Absteigend"
|
||||
},
|
||||
"sdxl": {
|
||||
"concatPromptStyle": "Verknüpfen von Prompt & Stil",
|
||||
@@ -736,7 +1175,180 @@
|
||||
},
|
||||
"ui": {
|
||||
"tabs": {
|
||||
"queue": "Warteschlange"
|
||||
"queue": "Warteschlange",
|
||||
"generation": "Erzeugung",
|
||||
"gallery": "Galerie",
|
||||
"models": "Modelle",
|
||||
"upscaling": "Hochskalierung",
|
||||
"workflows": "Arbeitsabläufe",
|
||||
"canvas": "Leinwand"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"logNamespaces": {
|
||||
"logNamespaces": "Namespaces loggen",
|
||||
"models": "Modelle",
|
||||
"gallery": "Galerie",
|
||||
"events": "Ereignisse",
|
||||
"queue": "Warteschlange",
|
||||
"system": "System",
|
||||
"workflows": "Arbeitsabläufe",
|
||||
"generation": "Erstellung",
|
||||
"metadata": "Metadaten",
|
||||
"config": "Konfiguration",
|
||||
"canvas": "Leinwand"
|
||||
},
|
||||
"logLevel": {
|
||||
"fatal": "Fatal",
|
||||
"trace": "Trace",
|
||||
"logLevel": "Protokollierungsstufe",
|
||||
"error": "Fehler",
|
||||
"info": "Infos",
|
||||
"warn": "Warnung",
|
||||
"debug": "Fehlerdiagnose"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"stylePresets": {
|
||||
"name": "Name",
|
||||
"acceptedColumnsKeys": "Akzeptierte Spalten/Schlüssel:",
|
||||
"noTemplates": "Keine Vorlagen",
|
||||
"promptTemplatesDesc2": "Verwenden Sie die Platzhalterzeichenfolge<Pre>{{placeholder}}</Pre>, um anzugeben, wo Ihre Eingabeaufforderung in die Vorlage aufgenommen werden soll.",
|
||||
"noMatchingTemplates": "Keine passenden Vorlagen",
|
||||
"myTemplates": "Meine Vorlagen",
|
||||
"toggleViewMode": "Ansicht umschalten",
|
||||
"viewModeTooltip": "So sieht Ihr Prompt mit der aktuell ausgewählten Vorlage aus. Um Ihren Prompt zu bearbeiten, klicken Sie irgendwo in das Textfeld.",
|
||||
"templateDeleted": "Promptvorlage gelöscht",
|
||||
"unableToDeleteTemplate": "Promptvorlage kann nicht gelöscht werden",
|
||||
"insertPlaceholder": "Platzhalter einfügen",
|
||||
"type": "Typ",
|
||||
"uploadImage": "Bild hochladen",
|
||||
"updatePromptTemplate": "Promptvorlage aktualisieren",
|
||||
"exportFailed": "CSV kann nicht generiert und heruntergeladen werden",
|
||||
"viewList": "Vorlagenliste anzeigen",
|
||||
"useForTemplate": "Für Promptvorlage nutzen",
|
||||
"shared": "Geteilt",
|
||||
"private": "Privat",
|
||||
"promptTemplatesDesc1": "Promptvorlagen fügen den Prompts, die Sie in das Prompt-Feld schreiben, Text hinzu.",
|
||||
"negativePrompt": "Negativ-Prompt",
|
||||
"positivePromptColumn": "'prompt' oder 'positive_prompt'",
|
||||
"promptTemplatesDesc3": "Wenn Sie den Platzhalter weglassen, wird die Vorlage an das Ende Ihres Prompts angehängt.",
|
||||
"sharedTemplates": "Geteilte Vorlagen",
|
||||
"importTemplates": "Promptvorlagen importieren (CSV/JSON)",
|
||||
"flatten": "Ausgewählte Vorlage in aktuelle Eingabeaufforderung einblenden",
|
||||
"searchByName": "Nach Name suchen",
|
||||
"promptTemplateCleared": "Promptvorlage gelöscht",
|
||||
"preview": "Vorschau",
|
||||
"positivePrompt": "Positiv-Prompt"
|
||||
},
|
||||
"newUserExperience": {
|
||||
"gettingStartedSeries": "Wünschen Sie weitere Anleitungen? In unserer <LinkComponent>Einführungsserie</LinkComponent> finden Sie Tipps, wie Sie das Potenzial von Invoke Studio voll ausschöpfen können.",
|
||||
"toGetStarted": "Um zu beginnen, geben Sie einen Prompt in das Feld ein und klicken Sie auf <StrongComponent>Invoke</StrongComponent>, um Ihr erstes Bild zu erzeugen. Sie können Ihre Bilder direkt in der <StrongComponent>Galerie</StrongComponent> speichern oder sie auf der <StrongComponent>Leinwand</StrongComponent> bearbeiten."
|
||||
},
|
||||
"controlLayers": {
|
||||
"pullBboxIntoLayerOk": "Bbox in die Ebene gezogen",
|
||||
"saveBboxToGallery": "Bbox in Galerie speichern",
|
||||
"tool": {
|
||||
"bbox": "Bbox"
|
||||
},
|
||||
"transform": {
|
||||
"fitToBbox": "An Bbox anpassen"
|
||||
},
|
||||
"pullBboxIntoLayerError": "Problem, Bbox in die Ebene zu ziehen",
|
||||
"pullBboxIntoLayer": "Bbox in Ebene ziehen",
|
||||
"HUD": {
|
||||
"bbox": "Bbox",
|
||||
"scaledBbox": "Skalierte Bbox"
|
||||
},
|
||||
"fitBboxToLayers": "Bbox an Ebenen anpassen",
|
||||
"pullBboxIntoReferenceImage": "Bbox ins Referenzbild ziehen",
|
||||
"pullBboxIntoReferenceImageOk": "Bbox in Referenzbild gezogen",
|
||||
"pullBboxIntoReferenceImageError": "Problem, Bbox ins Referenzbild zu ziehen",
|
||||
"bboxOverlay": "Bbox Overlay anzeigen",
|
||||
"clipToBbox": "Pinselstriche auf Bbox beschränken",
|
||||
"canvasContextMenu": {
|
||||
"saveBboxToGallery": "Bbox in Galerie speichern",
|
||||
"bboxGroup": "Aus Bbox erstellen"
|
||||
},
|
||||
"rectangle": "Rechteck",
|
||||
"saveCanvasToGallery": "Leinwand in Galerie speichern",
|
||||
"newRasterLayerError": "Problem beim Erstellen einer Raster-Ebene",
|
||||
"saveLayerToAssets": "Ebene in Galerie speichern",
|
||||
"deleteReferenceImage": "Referenzbild löschen",
|
||||
"referenceImage": "Referenzbild",
|
||||
"opacity": "Opazität",
|
||||
"resetCanvas": "Leinwand zurücksetzen",
|
||||
"removeBookmark": "Lesezeichen entfernen",
|
||||
"rasterLayer": "Raster-Ebene",
|
||||
"rasterLayers_withCount_visible": "Raster-Ebenen ({{count}})",
|
||||
"controlLayers_withCount_visible": "Kontroll-Ebenen ({{count}})",
|
||||
"deleteSelected": "Ausgewählte löschen",
|
||||
"newRegionalReferenceImageError": "Problem beim Erstellen eines regionalen Referenzbilds",
|
||||
"newControlLayerOk": "Kontroll-Ebene erstellt",
|
||||
"newControlLayerError": "Problem beim Erstellen einer Kontroll-Ebene",
|
||||
"newRasterLayerOk": "Raster-Layer erstellt",
|
||||
"moveToFront": "Nach vorne bringen",
|
||||
"copyToClipboard": "In die Zwischenablage kopieren",
|
||||
"controlLayers_withCount_hidden": "Kontroll-Ebenen ({{count}} ausgeblendet)",
|
||||
"clearCaches": "Cache leeren",
|
||||
"controlLayer": "Kontroll-Ebene",
|
||||
"rasterLayers_withCount_hidden": "Raster-Ebenen ({{count}} ausgeblendet)",
|
||||
"transparency": "Transparenz",
|
||||
"canvas": "Leinwand",
|
||||
"global": "Global",
|
||||
"regional": "Regional",
|
||||
"newGlobalReferenceImageOk": "Globales Referenzbild erstellt",
|
||||
"savedToGalleryError": "Fehler beim Speichern in der Galerie",
|
||||
"savedToGalleryOk": "In Galerie speichern",
|
||||
"newGlobalReferenceImageError": "Problem beim Erstellen eines globalen Referenzbilds",
|
||||
"newRegionalReferenceImageOk": "Regionales Referenzbild erstellt",
|
||||
"duplicate": "Duplizieren",
|
||||
"regionalReferenceImage": "Regionales Referenzbild",
|
||||
"globalReferenceImage": "Globales Referenzbild",
|
||||
"regionIsEmpty": "Ausgewählte Region is leer",
|
||||
"mergeVisible": "Sichtbare vereinen",
|
||||
"mergeVisibleOk": "Sichtbare Ebenen vereinen",
|
||||
"mergeVisibleError": "Fehler beim Vereinen sichtbarer Ebenen",
|
||||
"clearHistory": "Verlauf leeren",
|
||||
"addLayer": "Ebene hinzufügen",
|
||||
"width": "Breite",
|
||||
"weight": "Gewichtung",
|
||||
"addReferenceImage": "$t(controlLayers.referenceImage) hinzufügen",
|
||||
"addInpaintMask": "$t(controlLayers.inpaintMask) hinzufügen",
|
||||
"addGlobalReferenceImage": "$t(controlLayers.globalReferenceImage) hinzufügen",
|
||||
"regionalGuidance": "Regionale Führung",
|
||||
"globalReferenceImages_withCount_visible": "Globale Referenzbilder ({{count}})",
|
||||
"addPositivePrompt": "$t(controlLayers.prompt) hinzufügen",
|
||||
"locked": "Gesperrt",
|
||||
"showHUD": "HUD anzeigen",
|
||||
"addNegativePrompt": "$t(controlLayers.negativePrompt) hinzufügen",
|
||||
"addRasterLayer": "$t(controlLayers.rasterLayer) hinzufügen",
|
||||
"addRegionalGuidance": "$t(controlLayers.regionalGuidance) hinzufügen",
|
||||
"addControlLayer": "$t(controlLayers.controlLayer) hinzufügen",
|
||||
"newCanvasSession": "Neue Leinwand-Sitzung",
|
||||
"replaceLayer": "Ebene ersetzen",
|
||||
"newGallerySession": "Neue Galerie-Sitzung",
|
||||
"unlocked": "Entsperrt",
|
||||
"showProgressOnCanvas": "Fortschritt auf Leinwand anzeigen",
|
||||
"controlMode": {
|
||||
"balanced": "Ausgewogen"
|
||||
}
|
||||
},
|
||||
"upsell": {
|
||||
"shareAccess": "Zugang teilen",
|
||||
"professional": "Professionell",
|
||||
"inviteTeammates": "Teamkollegen einladen",
|
||||
"professionalUpsell": "Verfügbar in der Professional Edition von Invoke. Klicken Sie hier oder besuchen Sie invoke.com/pricing für weitere Details."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"previousImage": "Previous Image",
|
||||
"reset": "Reset",
|
||||
"resetUI": "$t(accessibility.reset) UI",
|
||||
"showGalleryPanel": "Show Gallery Panel",
|
||||
"showOptionsPanel": "Show Side Panel",
|
||||
"toggleRightPanel": "Toggle Right Panel (G)",
|
||||
"toggleLeftPanel": "Toggle Left Panel (T)",
|
||||
"uploadImage": "Upload Image"
|
||||
},
|
||||
"boards": {
|
||||
@@ -53,7 +53,8 @@
|
||||
"imagesWithCount_one": "{{count}} image",
|
||||
"imagesWithCount_other": "{{count}} images",
|
||||
"assetsWithCount_one": "{{count}} asset",
|
||||
"assetsWithCount_other": "{{count}} assets"
|
||||
"assetsWithCount_other": "{{count}} assets",
|
||||
"updateBoardError": "Error updating board"
|
||||
},
|
||||
"accordions": {
|
||||
"generation": {
|
||||
@@ -94,6 +95,7 @@
|
||||
"on": "On",
|
||||
"off": "Off",
|
||||
"or": "or",
|
||||
"ok": "Ok",
|
||||
"checkpoint": "Checkpoint",
|
||||
"communityLabel": "Community",
|
||||
"controlNet": "ControlNet",
|
||||
@@ -503,6 +505,22 @@
|
||||
"transformSelected": {
|
||||
"title": "Transform",
|
||||
"desc": "Transform the selected layer."
|
||||
},
|
||||
"applyFilter": {
|
||||
"title": "Apply Filter",
|
||||
"desc": "Apply the pending filter to the selected layer."
|
||||
},
|
||||
"cancelFilter": {
|
||||
"title": "Cancel Filter",
|
||||
"desc": "Cancel the pending filter."
|
||||
},
|
||||
"applyTransform": {
|
||||
"title": "Apply Transform",
|
||||
"desc": "Apply the pending transform to the selected layer."
|
||||
},
|
||||
"cancelTransform": {
|
||||
"title": "Cancel Transform",
|
||||
"desc": "Cancel the pending transform."
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
@@ -1065,6 +1083,7 @@
|
||||
"antialiasProgressImages": "Antialias Progress Images",
|
||||
"beta": "Beta",
|
||||
"confirmOnDelete": "Confirm On Delete",
|
||||
"confirmOnNewSession": "Confirm On New Session",
|
||||
"developer": "Developer",
|
||||
"displayInProgress": "Display Progress Images",
|
||||
"enableInformationalPopovers": "Enable Informational Popovers",
|
||||
@@ -1545,6 +1564,7 @@
|
||||
"saveCanvasToGallery": "Save Canvas to Gallery",
|
||||
"saveBboxToGallery": "Save Bbox to Gallery",
|
||||
"saveLayerToAssets": "Save Layer to Assets",
|
||||
"cropLayerToBbox": "Crop Layer to Bbox",
|
||||
"savedToGalleryOk": "Saved to Gallery",
|
||||
"savedToGalleryError": "Error saving to gallery",
|
||||
"newGlobalReferenceImageOk": "Created Global Reference Image",
|
||||
@@ -1632,7 +1652,6 @@
|
||||
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
|
||||
"globalReferenceImages_withCount_visible": "Global Reference Images ({{count}})",
|
||||
"inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})",
|
||||
"layer": "Layer",
|
||||
"layer_one": "Layer",
|
||||
"layer_other": "Layers",
|
||||
"layer_withCount_one": "Layer ({{count}})",
|
||||
@@ -1658,6 +1677,10 @@
|
||||
"negativePrompt": "Negative Prompt",
|
||||
"beginEndStepPercentShort": "Begin/End %",
|
||||
"weight": "Weight",
|
||||
"newGallerySession": "New Gallery Session",
|
||||
"newGallerySessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be sent to the gallery.",
|
||||
"newCanvasSession": "New Canvas Session",
|
||||
"newCanvasSessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be staged on the canvas.",
|
||||
"controlMode": {
|
||||
"controlMode": "Control Mode",
|
||||
"balanced": "Balanced",
|
||||
@@ -1797,7 +1820,8 @@
|
||||
"isolatedStagingPreview": "Isolated Staging Preview",
|
||||
"isolatedFilteringPreview": "Isolated Filtering Preview",
|
||||
"isolatedTransformingPreview": "Isolated Transforming Preview",
|
||||
"invertBrushSizeScrollDirection": "Invert Scroll for Brush Size"
|
||||
"invertBrushSizeScrollDirection": "Invert Scroll for Brush Size",
|
||||
"pressureSensitivity": "Pressure Sensitivity"
|
||||
},
|
||||
"HUD": {
|
||||
"bbox": "Bbox",
|
||||
@@ -1812,6 +1836,7 @@
|
||||
}
|
||||
},
|
||||
"canvasContextMenu": {
|
||||
"canvasGroup": "Canvas",
|
||||
"saveToGalleryGroup": "Save To Gallery",
|
||||
"saveCanvasToGallery": "Save Canvas To Gallery",
|
||||
"saveBboxToGallery": "Save Bbox To Gallery",
|
||||
@@ -1819,7 +1844,8 @@
|
||||
"newGlobalReferenceImage": "New Global Reference Image",
|
||||
"newRegionalReferenceImage": "New Regional Reference Image",
|
||||
"newControlLayer": "New Control Layer",
|
||||
"newRasterLayer": "New Raster Layer"
|
||||
"newRasterLayer": "New Raster Layer",
|
||||
"cropCanvasToBbox": "Crop Canvas to Bbox"
|
||||
},
|
||||
"stagingArea": {
|
||||
"accept": "Accept",
|
||||
|
||||
@@ -219,9 +219,7 @@
|
||||
"uploadImage": "Cargar imagen",
|
||||
"previousImage": "Imagen anterior",
|
||||
"nextImage": "Siguiente imagen",
|
||||
"showOptionsPanel": "Mostrar el panel lateral",
|
||||
"menu": "Menú",
|
||||
"showGalleryPanel": "Mostrar panel de galería",
|
||||
"about": "Acerca de",
|
||||
"createIssue": "Crear un problema",
|
||||
"resetUI": "Interfaz de usuario $t(accessibility.reset)",
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"uploadImage": "Lataa kuva",
|
||||
"invokeProgressBar": "Invoken edistymispalkki",
|
||||
"nextImage": "Seuraava kuva",
|
||||
"previousImage": "Edellinen kuva",
|
||||
"showOptionsPanel": "Näytä asetukset"
|
||||
"previousImage": "Edellinen kuva"
|
||||
},
|
||||
"common": {
|
||||
"languagePickerLabel": "Kielen valinta",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -65,7 +65,7 @@
|
||||
"blue": "Blu",
|
||||
"alpha": "Alfa",
|
||||
"copy": "Copia",
|
||||
"on": "Attivato",
|
||||
"on": "Acceso",
|
||||
"checkpoint": "Checkpoint",
|
||||
"safetensors": "Safetensors",
|
||||
"ai": "ia",
|
||||
@@ -81,7 +81,17 @@
|
||||
"tab": "Scheda",
|
||||
"enabled": "Abilitato",
|
||||
"disabled": "Disabilitato",
|
||||
"dontShowMeThese": "Non mostrare più"
|
||||
"dontShowMeThese": "Non mostrare più",
|
||||
"openInViewer": "Apri nel visualizzatore",
|
||||
"apply": "Applica",
|
||||
"loadingImage": "Caricamento immagine",
|
||||
"off": "Spento",
|
||||
"edit": "Modifica",
|
||||
"placeholderSelectAModel": "Seleziona un modello",
|
||||
"reset": "Reimposta",
|
||||
"none": "Niente",
|
||||
"new": "Nuovo",
|
||||
"view": "Vista"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Dimensione dell'immagine",
|
||||
@@ -135,18 +145,329 @@
|
||||
"showStarredImagesFirst": "Mostra prima le immagini contrassegnate",
|
||||
"showArchivedBoards": "Mostra le bacheche archiviate",
|
||||
"searchImages": "Ricerca per metadati",
|
||||
"displayBoardSearch": "Mostra la ricerca nelle Bacheche",
|
||||
"displaySearch": "Mostra la ricerca",
|
||||
"displayBoardSearch": "Ricerca nella Bacheca",
|
||||
"displaySearch": "Ricerca immagine",
|
||||
"selectAllOnPage": "Seleziona tutto nella pagina",
|
||||
"exitBoardSearch": "Esci da Ricerca bacheca",
|
||||
"exitSearch": "Esci dalla ricerca",
|
||||
"exitSearch": "Esci dalla ricerca immagini",
|
||||
"go": "Vai",
|
||||
"jump": "Salta"
|
||||
"jump": "Salta",
|
||||
"move": "Sposta",
|
||||
"gallery": "Galleria",
|
||||
"openViewer": "Apri visualizzatore",
|
||||
"closeViewer": "Chiudi visualizzatore"
|
||||
},
|
||||
"hotkeys": {
|
||||
"searchHotkeys": "Cerca tasti di scelta rapida",
|
||||
"noHotkeysFound": "Nessun tasto di scelta rapida trovato",
|
||||
"clearSearch": "Cancella ricerca"
|
||||
"clearSearch": "Cancella ricerca",
|
||||
"app": {
|
||||
"selectCanvasTab": {
|
||||
"title": "Seleziona la scheda Tela",
|
||||
"desc": "Seleziona la scheda Tela."
|
||||
},
|
||||
"title": "Applicazione",
|
||||
"invoke": {
|
||||
"desc": "Metti in coda una generazione, aggiungendola alla fine della coda."
|
||||
},
|
||||
"invokeFront": {
|
||||
"title": "Invoke (Fronte)",
|
||||
"desc": "Metti in coda una generazione, aggiungendola all'inizio della coda."
|
||||
},
|
||||
"cancelQueueItem": {
|
||||
"desc": "Annulla l'elemento della coda in elaborazione.",
|
||||
"title": "Annulla"
|
||||
},
|
||||
"clearQueue": {
|
||||
"title": "Cancella la coda",
|
||||
"desc": "Annulla e cancella tutti gli elementi in coda."
|
||||
},
|
||||
"selectUpscalingTab": {
|
||||
"title": "Seleziona la scheda Amplia",
|
||||
"desc": "Seleziona la scheda Amplia."
|
||||
},
|
||||
"selectModelsTab": {
|
||||
"title": "Seleziona la scheda Modelli",
|
||||
"desc": "Seleziona la scheda Modelli."
|
||||
},
|
||||
"selectQueueTab": {
|
||||
"title": "Seleziona la scheda della Coda",
|
||||
"desc": "Seleziona la scheda della Coda."
|
||||
},
|
||||
"selectWorkflowsTab": {
|
||||
"desc": "Seleziona la scheda dei Flussi di lavoro.",
|
||||
"title": "Seleziona la scheda dei Flussi di lavoro"
|
||||
},
|
||||
"focusPrompt": {
|
||||
"title": "Seleziona il Prompt",
|
||||
"desc": "Sposta il cursore sul prompt positivo."
|
||||
},
|
||||
"toggleLeftPanel": {
|
||||
"title": "Attiva/disattiva il pannello sinistro",
|
||||
"desc": "Attiva/disattiva il pannello sinistro."
|
||||
},
|
||||
"toggleRightPanel": {
|
||||
"title": "Attiva/disattiva il pannello destro",
|
||||
"desc": "Attiva/disattiva il pannello destro."
|
||||
},
|
||||
"resetPanelLayout": {
|
||||
"title": "Ripristina il layout del pannello",
|
||||
"desc": "Ripristina le dimensioni e il layout predefiniti dei pannelli sinistro e destro."
|
||||
},
|
||||
"togglePanels": {
|
||||
"title": "Attiva/disattiva i pannelli",
|
||||
"desc": "Mostra o nascondi contemporaneamente i pannelli sinistro e destro."
|
||||
}
|
||||
},
|
||||
"hotkeys": "Tasti di scelta rapida",
|
||||
"canvas": {
|
||||
"transformSelected": {
|
||||
"desc": "Trasforma il livello selezionato.",
|
||||
"title": "Trasforma"
|
||||
},
|
||||
"fitBboxToCanvas": {
|
||||
"desc": "Scala e posiziona la vista per adattarla al riquadro di delimitazione.",
|
||||
"title": "Adatta il riquadro di delimitazione alla tela"
|
||||
},
|
||||
"redo": {
|
||||
"title": "Ripeti",
|
||||
"desc": "Ripeti l'ultima azione sulla tela."
|
||||
},
|
||||
"selectBrushTool": {
|
||||
"title": "Strumento pennello",
|
||||
"desc": "Seleziona lo strumento pennello."
|
||||
},
|
||||
"selectBboxTool": {
|
||||
"title": "Strumento di selezione riquadro",
|
||||
"desc": "Seleziona lo strumento riquadro di delimitazione."
|
||||
},
|
||||
"decrementToolWidth": {
|
||||
"title": "Diminuisci la larghezza dello strumento",
|
||||
"desc": "Diminuisce la larghezza dello strumento pennello o gomma, a seconda di quello selezionato."
|
||||
},
|
||||
"incrementToolWidth": {
|
||||
"title": "Aumenta la larghezza dello strumento",
|
||||
"desc": "Aumenta la larghezza dello strumento pennello o gomma, a seconda di quello selezionato."
|
||||
},
|
||||
"selectColorPickerTool": {
|
||||
"title": "Strumento di selezione del colore",
|
||||
"desc": "Seleziona lo strumento di selezione del colore."
|
||||
},
|
||||
"resetSelected": {
|
||||
"title": "Reimposta il Livello",
|
||||
"desc": "Reimposta il livello selezionato. Si applica solo alla Maschera Inpaint e alla Guida Regionale."
|
||||
},
|
||||
"undo": {
|
||||
"title": "Annulla",
|
||||
"desc": "Annulla l'ultima azione sulla tela."
|
||||
},
|
||||
"nextEntity": {
|
||||
"title": "Livello successivo",
|
||||
"desc": "Seleziona il livello successivo nell'elenco."
|
||||
},
|
||||
"filterSelected": {
|
||||
"title": "Filtro",
|
||||
"desc": "Filtra il livello selezionato. Applicabile solo ai livelli Raster e Controllo."
|
||||
},
|
||||
"setZoomTo100Percent": {
|
||||
"title": "Zoom al 100%",
|
||||
"desc": "Imposta l'ingrandimento della tela al 100%."
|
||||
},
|
||||
"setZoomTo200Percent": {
|
||||
"title": "Zoom al 200%",
|
||||
"desc": "Imposta l'ingrandimento della tela al 200%."
|
||||
},
|
||||
"setZoomTo400Percent": {
|
||||
"title": "Zoom al 400%",
|
||||
"desc": "Imposta l'ingrandimento della tela al 400%."
|
||||
},
|
||||
"setZoomTo800Percent": {
|
||||
"title": "Zoom al 800%",
|
||||
"desc": "Imposta l'ingrandimento della tela al 800%."
|
||||
},
|
||||
"quickSwitch": {
|
||||
"title": "Cambio rapido livello",
|
||||
"desc": "Passa tra gli ultimi due livelli selezionati. Se un livello è aggiunto ai segnalibri, passa sempre tra questo e l'ultimo livello non aggiunto ai segnalibri."
|
||||
},
|
||||
"deleteSelected": {
|
||||
"title": "Elimina livello",
|
||||
"desc": "Elimina il livello selezionato."
|
||||
},
|
||||
"prevEntity": {
|
||||
"title": "Livello precedente",
|
||||
"desc": "Seleziona il livello precedente nell'elenco."
|
||||
},
|
||||
"setFillToWhite": {
|
||||
"title": "Imposta il colore su bianco",
|
||||
"desc": "Imposta il colore dello strumento corrente su bianco."
|
||||
},
|
||||
"title": "Tela",
|
||||
"selectMoveTool": {
|
||||
"title": "Strumento Sposta",
|
||||
"desc": "Seleziona lo strumento sposta."
|
||||
},
|
||||
"fitLayersToCanvas": {
|
||||
"desc": "Scala e posiziona la vista per adattarla a tutti i livelli visibili.",
|
||||
"title": "Adatta i livelli alla tela"
|
||||
},
|
||||
"selectEraserTool": {
|
||||
"title": "Strumento gomma",
|
||||
"desc": "Selezionare lo strumento gomma."
|
||||
},
|
||||
"selectRectTool": {
|
||||
"title": "Strumento Rettangolo",
|
||||
"desc": "Seleziona lo strumento rettangolo."
|
||||
},
|
||||
"selectViewTool": {
|
||||
"title": "Strumento Visualizza",
|
||||
"desc": "Seleziona lo strumento Visualizza."
|
||||
},
|
||||
"applyFilter": {
|
||||
"title": "Applica filtro",
|
||||
"desc": "Applica il filtro in sospeso al livello selezionato."
|
||||
},
|
||||
"cancelFilter": {
|
||||
"title": "Annulla filtro",
|
||||
"desc": "Annulla il filtro in sospeso."
|
||||
},
|
||||
"cancelTransform": {
|
||||
"desc": "Annulla la trasformazione in sospeso.",
|
||||
"title": "Annulla Trasforma"
|
||||
},
|
||||
"applyTransform": {
|
||||
"title": "Applica trasformazione",
|
||||
"desc": "Applica la trasformazione in sospeso al livello selezionato."
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
"addNode": {
|
||||
"title": "Aggiungi nodo",
|
||||
"desc": "Apri il menu aggiungi nodo."
|
||||
},
|
||||
"pasteSelectionWithEdges": {
|
||||
"title": "Incolla con collegamenti",
|
||||
"desc": "Incolla i nodi copiati, i collegamenti e tutti i collegamenti connessi ai nodi copiati."
|
||||
},
|
||||
"copySelection": {
|
||||
"title": "Copia",
|
||||
"desc": "Copia i nodi ed i collegamenti selezionati."
|
||||
},
|
||||
"pasteSelection": {
|
||||
"title": "Incolla",
|
||||
"desc": "Incolla i nodi ed i collegamenti copiati."
|
||||
},
|
||||
"deleteSelection": {
|
||||
"title": "Elimina",
|
||||
"desc": "Elimina i nodi ed i collegamenti selezionati."
|
||||
},
|
||||
"redo": {
|
||||
"title": "Ripeti",
|
||||
"desc": "Ripeti l'ultima azione del flusso di lavoro."
|
||||
},
|
||||
"selectAll": {
|
||||
"desc": "Seleziona tutti i nodi ed i collegamenti.",
|
||||
"title": "Seleziona tutto"
|
||||
},
|
||||
"undo": {
|
||||
"desc": "Annulla l'ultima azione del flusso di lavoro.",
|
||||
"title": "Annulla"
|
||||
},
|
||||
"title": "Flussi di lavoro"
|
||||
},
|
||||
"viewer": {
|
||||
"nextComparisonMode": {
|
||||
"title": "Modalità di confronto successiva",
|
||||
"desc": "Scorri le modalità di confronto."
|
||||
},
|
||||
"recallPrompts": {
|
||||
"title": "Richiama i Prompt",
|
||||
"desc": "Richiama i prompt positivo e negativo per l'immagine corrente."
|
||||
},
|
||||
"remix": {
|
||||
"title": "Remixa",
|
||||
"desc": "Richiama tutti i metadati, ad eccezione del seme, per l'immagine corrente."
|
||||
},
|
||||
"useSize": {
|
||||
"desc": "Utilizza la dimensione dell'immagine corrente come dimensione del riquadro di delimitazione.",
|
||||
"title": "Usa Dimensioni"
|
||||
},
|
||||
"runPostprocessing": {
|
||||
"title": "Esegui Post-elaborazione",
|
||||
"desc": "Esegue la post-elaborazione selezionata sull'immagine corrente."
|
||||
},
|
||||
"title": "Visualizzatore immagini",
|
||||
"toggleViewer": {
|
||||
"title": "Mostra/Nascondi visualizzatore immagini",
|
||||
"desc": "Mostra o nascondi il visualizzatore di immagini. Disponibile solo nella scheda Tela."
|
||||
},
|
||||
"loadWorkflow": {
|
||||
"title": "Carica Flusso di lavoro",
|
||||
"desc": "Carica il flusso di lavoro salvato dell'immagine corrente (se presente)."
|
||||
},
|
||||
"recallAll": {
|
||||
"title": "Richiama tutti i metadati",
|
||||
"desc": "Richiama tutti i metadati dell'immagine corrente."
|
||||
},
|
||||
"swapImages": {
|
||||
"title": "Scambia le immagini di confronto",
|
||||
"desc": "Scambia le immagini da confrontare."
|
||||
},
|
||||
"recallSeed": {
|
||||
"title": "Richiama il seme",
|
||||
"desc": "Richiama il seme per l'immagine corrente."
|
||||
},
|
||||
"toggleMetadata": {
|
||||
"title": "Mostra/Nascondi metadati",
|
||||
"desc": "Mostra o nasconde la sovrapposizione dei metadati dell'immagine corrente."
|
||||
}
|
||||
},
|
||||
"gallery": {
|
||||
"selectAllOnPage": {
|
||||
"desc": "Seleziona tutte le immagini nella pagina corrente.",
|
||||
"title": "Seleziona tutto nella pagina"
|
||||
},
|
||||
"galleryNavUp": {
|
||||
"desc": "Naviga verso l'alto nella griglia della galleria, selezionando quell'immagine. Se sei in cima alla pagina, andrai alla pagina precedente.",
|
||||
"title": "Naviga verso l'alto"
|
||||
},
|
||||
"galleryNavRight": {
|
||||
"title": "Naviga a destra",
|
||||
"desc": "Naviga a destra nella griglia della galleria, selezionando quell'immagine. Se sei all'ultima immagine della riga, andrai alla riga successiva. Se sei all'ultima immagine della pagina, andrai alla pagina successiva."
|
||||
},
|
||||
"galleryNavLeftAlt": {
|
||||
"desc": "Uguale a Naviga a sinistra, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta.",
|
||||
"title": "Naviga a sinistra (Confronta immagine)"
|
||||
},
|
||||
"deleteSelection": {
|
||||
"title": "Elimina",
|
||||
"desc": "Elimina tutte le immagini selezionate. Per impostazione predefinita, ti verrà chiesto di confermare l'eliminazione. Se le immagini sono attualmente in uso nell'applicazione, verrai avvisato."
|
||||
},
|
||||
"clearSelection": {
|
||||
"title": "Cancella selezione",
|
||||
"desc": "Cancella la selezione corrente, se presente."
|
||||
},
|
||||
"galleryNavRightAlt": {
|
||||
"desc": "Uguale a Naviga a destra, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta.",
|
||||
"title": "Naviga a destra (Confronta immagine)"
|
||||
},
|
||||
"galleryNavDownAlt": {
|
||||
"title": "Naviga in basso (Confronta immagine)",
|
||||
"desc": "Uguale a Naviga in basso, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta."
|
||||
},
|
||||
"title": "Galleria",
|
||||
"galleryNavDown": {
|
||||
"desc": "Naviga verso il basso nella griglia della galleria, selezionando quell'immagine. Se sei in fondo alla pagina, andrai alla pagina successiva.",
|
||||
"title": "Naviga in basso"
|
||||
},
|
||||
"galleryNavLeft": {
|
||||
"title": "Naviga a sinistra",
|
||||
"desc": "Naviga a sinistra nella griglia della galleria, selezionando quell'immagine. Se sei alla prima immagine della riga, andrai alla riga precedente. Se sei alla prima immagine della pagina, andrai alla pagina precedente."
|
||||
},
|
||||
"galleryNavUpAlt": {
|
||||
"desc": "Uguale a Naviga verso l'alto, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta.",
|
||||
"title": "Naviga verso l'alto (Confronta immagine)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelManager": {
|
||||
"modelManager": "Gestione Modelli",
|
||||
@@ -250,7 +571,8 @@
|
||||
"ipAdapters": "Adattatori IP",
|
||||
"noMatchingModels": "Nessun modello corrispondente",
|
||||
"starterModelsInModelManager": "I modelli iniziali possono essere trovati in Gestione Modelli",
|
||||
"spandrelImageToImage": "Immagine a immagine (Spandrel)"
|
||||
"spandrelImageToImage": "Immagine a immagine (Spandrel)",
|
||||
"learnMoreAboutSupportedModels": "Scopri di più sui modelli che supportiamo"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Immagini",
|
||||
@@ -268,8 +590,8 @@
|
||||
"scale": "Scala",
|
||||
"imageFit": "Adatta l'immagine iniziale alle dimensioni di output",
|
||||
"scaleBeforeProcessing": "Scala prima dell'elaborazione",
|
||||
"scaledWidth": "Larghezza ridimensionata",
|
||||
"scaledHeight": "Altezza ridimensionata",
|
||||
"scaledWidth": "Larghezza scalata",
|
||||
"scaledHeight": "Altezza scalata",
|
||||
"infillMethod": "Metodo di riempimento",
|
||||
"tileSize": "Dimensione piastrella",
|
||||
"downloadImage": "Scarica l'immagine",
|
||||
@@ -285,8 +607,8 @@
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
"symmetry": "Simmetria",
|
||||
"seamlessXAxis": "Piastrella senza giunte Asse X",
|
||||
"seamlessYAxis": "Piastrella senza giunte Asse Y",
|
||||
"seamlessXAxis": "Asse X senza giunte",
|
||||
"seamlessYAxis": "Asse Y senza giunte",
|
||||
"scheduler": "Campionatore",
|
||||
"positivePromptPlaceholder": "Prompt Positivo",
|
||||
"negativePromptPlaceholder": "Prompt Negativo",
|
||||
@@ -311,8 +633,23 @@
|
||||
"ipAdapterIncompatibleBaseModel": "Il modello base dell'adattatore IP non è compatibile",
|
||||
"ipAdapterNoImageSelected": "Nessuna immagine dell'adattatore IP selezionata",
|
||||
"rgNoPromptsOrIPAdapters": "Nessun prompt o adattatore IP",
|
||||
"rgNoRegion": "Nessuna regione selezionata"
|
||||
}
|
||||
"rgNoRegion": "Nessuna regione selezionata",
|
||||
"t2iAdapterIncompatibleBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, larghezza riquadro è {{width}}",
|
||||
"t2iAdapterIncompatibleBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, altezza riquadro è {{height}}",
|
||||
"t2iAdapterIncompatibleScaledBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, larghezza del riquadro scalato {{width}}",
|
||||
"t2iAdapterIncompatibleScaledBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, altezza del riquadro scalato {{height}}"
|
||||
},
|
||||
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), altezza riquadro è {{height}}",
|
||||
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), larghezza riquadro è {{width}}",
|
||||
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), larghezza del riquadro scalato è {{width}}",
|
||||
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), altezza del riquadro scalato è {{height}}",
|
||||
"noT5EncoderModelSelected": "Nessun modello di encoder T5 selezionato per la generazione con FLUX",
|
||||
"noCLIPEmbedModelSelected": "Nessun modello CLIP Embed selezionato per la generazione con FLUX",
|
||||
"noFLUXVAEModelSelected": "Nessun modello VAE selezionato per la generazione con FLUX",
|
||||
"canvasIsTransforming": "La tela sta trasformando",
|
||||
"canvasIsRasterizing": "La tela sta rasterizzando",
|
||||
"canvasIsCompositing": "La tela è in fase di composizione",
|
||||
"canvasIsFiltering": "La tela sta filtrando"
|
||||
},
|
||||
"useCpuNoise": "Usa la CPU per generare rumore",
|
||||
"iterations": "Iterazioni",
|
||||
@@ -330,7 +667,13 @@
|
||||
"infillColorValue": "Colore di riempimento",
|
||||
"processImage": "Elabora Immagine",
|
||||
"sendToUpscale": "Invia a Amplia",
|
||||
"postProcessing": "Post-elaborazione (Shift + U)"
|
||||
"postProcessing": "Post-elaborazione (Shift + U)",
|
||||
"guidance": "Guida",
|
||||
"gaussianBlur": "Sfocatura Gaussiana",
|
||||
"boxBlur": "Sfocatura Box",
|
||||
"staged": "Maschera espansa",
|
||||
"optimizedImageToImage": "Immagine-a-immagine ottimizzata",
|
||||
"sendToCanvas": "Invia alla Tela"
|
||||
},
|
||||
"settings": {
|
||||
"models": "Modelli",
|
||||
@@ -364,7 +707,8 @@
|
||||
"enableInformationalPopovers": "Abilita testo informativo a comparsa",
|
||||
"reloadingIn": "Ricaricando in",
|
||||
"informationalPopoversDisabled": "Testo informativo a comparsa disabilitato",
|
||||
"informationalPopoversDisabledDesc": "I testi informativi a comparsa sono disabilitati. Attivali nelle impostazioni."
|
||||
"informationalPopoversDisabledDesc": "I testi informativi a comparsa sono disabilitati. Attivali nelle impostazioni.",
|
||||
"confirmOnNewSession": "Conferma su nuova sessione"
|
||||
},
|
||||
"toast": {
|
||||
"uploadFailed": "Caricamento fallito",
|
||||
@@ -407,7 +751,20 @@
|
||||
"somethingWentWrong": "Qualcosa è andato storto",
|
||||
"outOfMemoryErrorDesc": "Le impostazioni della generazione attuale superano la capacità del sistema. Modifica le impostazioni e riprova.",
|
||||
"importFailed": "Importazione non riuscita",
|
||||
"importSuccessful": "Importazione riuscita"
|
||||
"importSuccessful": "Importazione riuscita",
|
||||
"layerSavedToAssets": "Livello salvato nelle risorse",
|
||||
"problemSavingLayer": "Impossibile salvare il livello",
|
||||
"unableToLoadImage": "Impossibile caricare l'immagine",
|
||||
"problemCopyingLayer": "Impossibile copiare il livello",
|
||||
"sentToCanvas": "Inviato alla Tela",
|
||||
"sentToUpscale": "Inviato a Amplia",
|
||||
"unableToLoadStylePreset": "Impossibile caricare lo stile predefinito",
|
||||
"stylePresetLoaded": "Stile predefinito caricato",
|
||||
"unableToLoadImageMetadata": "Impossibile caricare i metadati dell'immagine",
|
||||
"imageSaved": "Immagine salvata",
|
||||
"imageSavingFailed": "Salvataggio dell'immagine non riuscito",
|
||||
"layerCopiedToClipboard": "Livello copiato negli appunti",
|
||||
"imageNotLoadedDesc": "Impossibile trovare l'immagine"
|
||||
},
|
||||
"accessibility": {
|
||||
"invokeProgressBar": "Barra di avanzamento generazione",
|
||||
@@ -415,14 +772,14 @@
|
||||
"previousImage": "Immagine precedente",
|
||||
"nextImage": "Immagine successiva",
|
||||
"reset": "Reimposta",
|
||||
"showOptionsPanel": "Mostra il pannello laterale",
|
||||
"menu": "Menu",
|
||||
"showGalleryPanel": "Mostra il pannello Galleria",
|
||||
"mode": "Modalità",
|
||||
"resetUI": "$t(accessibility.reset) l'Interfaccia Utente",
|
||||
"createIssue": "Segnala un problema",
|
||||
"about": "Informazioni",
|
||||
"submitSupportTicket": "Invia ticket di supporto"
|
||||
"submitSupportTicket": "Invia ticket di supporto",
|
||||
"toggleLeftPanel": "Attiva/disattiva il pannello sinistro (T)",
|
||||
"toggleRightPanel": "Attiva/disattiva il pannello destro (G)"
|
||||
},
|
||||
"nodes": {
|
||||
"zoomOutNodes": "Rimpicciolire",
|
||||
@@ -559,7 +916,8 @@
|
||||
"singleFieldType": "{{name}} (Singola)",
|
||||
"imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino ai valori predefiniti",
|
||||
"boardAccessError": "Impossibile trovare la bacheca {{board_id}}, ripristino ai valori predefiniti",
|
||||
"modelAccessError": "Impossibile trovare il modello {{key}}, ripristino ai valori predefiniti"
|
||||
"modelAccessError": "Impossibile trovare il modello {{key}}, ripristino ai valori predefiniti",
|
||||
"saveToGallery": "Salva nella Galleria"
|
||||
},
|
||||
"boards": {
|
||||
"autoAddBoard": "Aggiungi automaticamente bacheca",
|
||||
@@ -603,7 +961,8 @@
|
||||
"noBoards": "Nessuna bacheca {{boardType}}",
|
||||
"hideBoards": "Nascondi bacheche",
|
||||
"viewBoards": "Visualizza bacheche",
|
||||
"deletedPrivateBoardsCannotbeRestored": "Le bacheche cancellate non possono essere ripristinate. Selezionando 'Cancella solo bacheca', le immagini verranno spostate nella bacheca \"Non categorizzato\" privata dell'autore dell'immagine."
|
||||
"deletedPrivateBoardsCannotbeRestored": "Le bacheche cancellate non possono essere ripristinate. Selezionando 'Cancella solo bacheca', le immagini verranno spostate nella bacheca \"Non categorizzato\" privata dell'autore dell'immagine.",
|
||||
"updateBoardError": "Errore durante l'aggiornamento della bacheca"
|
||||
},
|
||||
"queue": {
|
||||
"queueFront": "Aggiungi all'inizio della coda",
|
||||
@@ -632,7 +991,7 @@
|
||||
"batchQueuedDesc_other": "Aggiunte {{count}} sessioni a {{direction}} della coda",
|
||||
"graphQueued": "Grafico in coda",
|
||||
"batch": "Lotto",
|
||||
"clearQueueAlertDialog": "Lo svuotamento della coda annulla immediatamente tutti gli elementi in elaborazione e cancella completamente la coda.",
|
||||
"clearQueueAlertDialog": "Lo svuotamento della coda annulla immediatamente tutti gli elementi in elaborazione e cancella completamente la coda. I filtri in sospeso verranno annullati.",
|
||||
"pending": "In attesa",
|
||||
"completedIn": "Completato in",
|
||||
"resumeFailed": "Problema nel riavvio dell'elaborazione",
|
||||
@@ -671,7 +1030,15 @@
|
||||
"prompts_other": "Prompt",
|
||||
"generations_one": "Generazione",
|
||||
"generations_many": "Generazioni",
|
||||
"generations_other": "Generazioni"
|
||||
"generations_other": "Generazioni",
|
||||
"origin": "Origine",
|
||||
"destination": "Destinazione",
|
||||
"upscaling": "Ampliamento",
|
||||
"canvas": "Tela",
|
||||
"workflows": "Flussi di lavoro",
|
||||
"generation": "Generazione",
|
||||
"other": "Altro",
|
||||
"gallery": "Galleria"
|
||||
},
|
||||
"models": {
|
||||
"noMatchingModels": "Nessun modello corrispondente",
|
||||
@@ -1080,6 +1447,25 @@
|
||||
"paragraphs": [
|
||||
"La struttura determina quanto l'immagine finale rispecchierà il layout dell'originale. Una struttura bassa permette cambiamenti significativi, mentre una struttura alta conserva la composizione e il layout originali."
|
||||
]
|
||||
},
|
||||
"fluxDevLicense": {
|
||||
"heading": "Licenza non commerciale",
|
||||
"paragraphs": [
|
||||
"I modelli FLUX.1 [dev] sono concessi in licenza con la licenza non commerciale FLUX [dev]. Per utilizzare questo tipo di modello per scopi commerciali in Invoke, visita il nostro sito Web per saperne di più."
|
||||
]
|
||||
},
|
||||
"optimizedDenoising": {
|
||||
"heading": "Immagine-a-immagine ottimizzata",
|
||||
"paragraphs": [
|
||||
"Abilita 'Immagine-a-immagine ottimizzata' per una scala di riduzione del rumore più graduale per le trasformazioni da immagine a immagine e di inpainting con modelli Flux. Questa impostazione migliora la capacità di controllare la quantità di modifica applicata a un'immagine, ma può essere disattivata se preferisci usare la scala di riduzione rumore standard. Questa impostazione è ancora in fase di messa a punto ed è in stato beta."
|
||||
]
|
||||
},
|
||||
"paramGuidance": {
|
||||
"heading": "Guida",
|
||||
"paragraphs": [
|
||||
"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."
|
||||
]
|
||||
}
|
||||
},
|
||||
"sdxl": {
|
||||
@@ -1127,7 +1513,8 @@
|
||||
"imageDimensions": "Dimensioni dell'immagine",
|
||||
"parameterSet": "Parametro {{parameter}} impostato",
|
||||
"parsingFailed": "Analisi non riuscita",
|
||||
"recallParameter": "Richiama {{label}}"
|
||||
"recallParameter": "Richiama {{label}}",
|
||||
"canvasV2Metadata": "Tela"
|
||||
},
|
||||
"hrf": {
|
||||
"enableHrf": "Abilita Correzione Alta Risoluzione",
|
||||
@@ -1172,7 +1559,10 @@
|
||||
"convertGraph": "Converti grafico",
|
||||
"loadWorkflow": "$t(common.load) Flusso di lavoro",
|
||||
"autoLayout": "Disposizione automatica",
|
||||
"loadFromGraph": "Carica il flusso di lavoro dal grafico"
|
||||
"loadFromGraph": "Carica il flusso di lavoro dal grafico",
|
||||
"userWorkflows": "Flussi di lavoro utente",
|
||||
"projectWorkflows": "Flussi di lavoro del progetto",
|
||||
"defaultWorkflows": "Flussi di lavoro predefiniti"
|
||||
},
|
||||
"accordions": {
|
||||
"compositing": {
|
||||
@@ -1208,10 +1598,303 @@
|
||||
"autoNegative": "Auto Negativo",
|
||||
"deletePrompt": "Cancella il prompt",
|
||||
"rectangle": "Rettangolo",
|
||||
"addPositivePrompt": "Aggiungi $t(common.positivePrompt)",
|
||||
"addNegativePrompt": "Aggiungi $t(common.negativePrompt)",
|
||||
"addPositivePrompt": "Aggiungi $t(controlLayers.prompt)",
|
||||
"addNegativePrompt": "Aggiungi $t(controlLayers.negativePrompt)",
|
||||
"regionalGuidance": "Guida regionale",
|
||||
"opacity": "Opacità"
|
||||
"opacity": "Opacità",
|
||||
"mergeVisible": "Fondi il visibile",
|
||||
"mergeVisibleOk": "Livelli visibili 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",
|
||||
"regionalReferenceImage": "Immagine di riferimento Regionale",
|
||||
"newLayerFromImage": "Nuovo livello da immagine",
|
||||
"newCanvasFromImage": "Nuova tela da immagine",
|
||||
"globalReferenceImage": "Immagine di riferimento Globale",
|
||||
"copyToClipboard": "Copia negli appunti",
|
||||
"sendingToCanvas": "Effettua le generazioni nella Tela",
|
||||
"clearHistory": "Cancella la cronologia",
|
||||
"inpaintMask": "Maschera Inpaint",
|
||||
"sendToGallery": "Invia alla Galleria",
|
||||
"controlLayer": "Livello di Controllo",
|
||||
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
|
||||
"rasterLayer_withCount_many": "Livelli Raster",
|
||||
"rasterLayer_withCount_other": "Livelli Raster",
|
||||
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
|
||||
"controlLayer_withCount_many": "Livelli di controllo",
|
||||
"controlLayer_withCount_other": "Livelli di controllo",
|
||||
"clipToBbox": "Ritaglia i tratti al riquadro",
|
||||
"duplicate": "Duplica",
|
||||
"width": "Larghezza",
|
||||
"addControlLayer": "Aggiungi $t(controlLayers.controlLayer)",
|
||||
"addInpaintMask": "Aggiungi $t(controlLayers.inpaintMask)",
|
||||
"addRegionalGuidance": "Aggiungi $t(controlLayers.regionalGuidance)",
|
||||
"sendToCanvasDesc": "Premendo Invoke il lavoro in corso viene visualizzato sulla tela.",
|
||||
"addRasterLayer": "Aggiungi $t(controlLayers.rasterLayer)",
|
||||
"clearCaches": "Svuota le cache",
|
||||
"regionIsEmpty": "La regione selezionata è vuota",
|
||||
"recalculateRects": "Ricalcola rettangoli",
|
||||
"removeBookmark": "Rimuovi segnalibro",
|
||||
"saveCanvasToGallery": "Salva la tela nella Galleria",
|
||||
"regional": "Regionale",
|
||||
"global": "Globale",
|
||||
"canvas": "Tela",
|
||||
"bookmark": "Segnalibro per cambio rapido",
|
||||
"newRegionalReferenceImageOk": "Immagine di riferimento regionale creata",
|
||||
"newRegionalReferenceImageError": "Problema nella creazione dell'immagine di riferimento regionale",
|
||||
"newControlLayerOk": "Livello di controllo creato",
|
||||
"bboxOverlay": "Mostra sovrapposizione riquadro",
|
||||
"resetCanvas": "Reimposta la tela",
|
||||
"outputOnlyMaskedRegions": "Solo regioni mascherate in uscita",
|
||||
"enableAutoNegative": "Abilita Auto Negativo",
|
||||
"disableAutoNegative": "Disabilita Auto Negativo",
|
||||
"showHUD": "Mostra HUD",
|
||||
"maskFill": "Riempimento maschera",
|
||||
"addReferenceImage": "Aggiungi $t(controlLayers.referenceImage)",
|
||||
"addGlobalReferenceImage": "Aggiungi $t(controlLayers.globalReferenceImage)",
|
||||
"sendingToGallery": "Inviare generazioni alla Galleria",
|
||||
"sendToGalleryDesc": "Premendo Invoke viene generata e salvata un'immagine unica nella tua galleria.",
|
||||
"sendToCanvas": "Invia alla Tela",
|
||||
"viewProgressInViewer": "Visualizza i progressi e i risultati nel <Btn>Visualizzatore immagini</Btn>.",
|
||||
"viewProgressOnCanvas": "Visualizza i progressi e i risultati nella <Btn>Tela</Btn>.",
|
||||
"saveBboxToGallery": "Salva il riquadro di delimitazione nella Galleria",
|
||||
"cropLayerToBbox": "Ritaglia il livello al riquadro di delimitazione",
|
||||
"savedToGalleryError": "Errore durante il salvataggio nella galleria",
|
||||
"rasterLayer": "Livello Raster",
|
||||
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
|
||||
"regionalGuidance_withCount_many": "Guide regionali",
|
||||
"regionalGuidance_withCount_other": "Guide regionali",
|
||||
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
|
||||
"inpaintMask_withCount_many": "Maschere Inpaint",
|
||||
"inpaintMask_withCount_other": "Maschere Inpaint",
|
||||
"savedToGalleryOk": "Salvato nella Galleria",
|
||||
"newGlobalReferenceImageOk": "Immagine di riferimento globale creata",
|
||||
"newGlobalReferenceImageError": "Problema nella creazione dell'immagine di riferimento globale",
|
||||
"newControlLayerError": "Problema nella creazione del livello di controllo",
|
||||
"newRasterLayerOk": "Livello raster creato",
|
||||
"newRasterLayerError": "Problema nella creazione del livello raster",
|
||||
"saveLayerToAssets": "Salva il livello nelle Risorse",
|
||||
"pullBboxIntoLayerError": "Problema nel caricare il riquadro nel livello",
|
||||
"pullBboxIntoReferenceImageOk": "Contenuto del riquadro inserito nell'immagine di riferimento",
|
||||
"pullBboxIntoLayerOk": "Riquadro caricato nel livello",
|
||||
"pullBboxIntoReferenceImageError": "Problema nell'inserimento del contenuto del riquadro nell'immagine di riferimento",
|
||||
"globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)",
|
||||
"globalReferenceImage_withCount_many": "Immagini di riferimento Globali",
|
||||
"globalReferenceImage_withCount_other": "Immagini di riferimento Globali",
|
||||
"controlMode": {
|
||||
"balanced": "Bilanciato",
|
||||
"controlMode": "Modalità di controllo",
|
||||
"prompt": "Prompt",
|
||||
"control": "Controllo",
|
||||
"megaControl": "Mega Controllo"
|
||||
},
|
||||
"negativePrompt": "Prompt Negativo",
|
||||
"prompt": "Prompt Positivo",
|
||||
"beginEndStepPercentShort": "Inizio/Fine %",
|
||||
"stagingOnCanvas": "Genera immagini nella",
|
||||
"ipAdapterMethod": {
|
||||
"full": "Completo",
|
||||
"style": "Solo Stile",
|
||||
"composition": "Solo Composizione",
|
||||
"ipAdapterMethod": "Metodo Adattatore IP"
|
||||
},
|
||||
"showingType": "Mostrare {{type}}",
|
||||
"dynamicGrid": "Griglia dinamica",
|
||||
"tool": {
|
||||
"view": "Muovi",
|
||||
"colorPicker": "Selettore Colore",
|
||||
"rectangle": "Rettangolo",
|
||||
"bbox": "Riquadro di delimitazione",
|
||||
"move": "Sposta",
|
||||
"brush": "Pennello",
|
||||
"eraser": "Cancellino"
|
||||
},
|
||||
"filter": {
|
||||
"apply": "Applica",
|
||||
"reset": "Reimposta",
|
||||
"process": "Elabora",
|
||||
"cancel": "Annulla",
|
||||
"autoProcess": "Processo automatico",
|
||||
"filterType": "Tipo Filtro",
|
||||
"filter": "Filtro",
|
||||
"filters": "Filtri",
|
||||
"mlsd_detection": {
|
||||
"score_threshold": "Soglia di punteggio",
|
||||
"distance_threshold": "Soglia di distanza",
|
||||
"description": "Genera una mappa dei segmenti di linea dal livello selezionato utilizzando il modello di rilevamento dei segmenti di linea MLSD."
|
||||
},
|
||||
"content_shuffle": {
|
||||
"label": "Mescola contenuto",
|
||||
"scale_factor": "Fattore di scala",
|
||||
"description": "Mescola il contenuto del livello selezionato, in modo simile all'effetto \"liquefa\"."
|
||||
},
|
||||
"mediapipe_face_detection": {
|
||||
"min_confidence": "Confidenza minima",
|
||||
"label": "Rilevamento del volto MediaPipe",
|
||||
"max_faces": "Max volti",
|
||||
"description": "Rileva i volti nel livello selezionato utilizzando il modello di rilevamento dei volti MediaPipe."
|
||||
},
|
||||
"dw_openpose_detection": {
|
||||
"draw_face": "Disegna il volto",
|
||||
"description": "Rileva le pose umane nel livello selezionato utilizzando il modello DW Openpose.",
|
||||
"label": "Rilevamento DW Openpose",
|
||||
"draw_hands": "Disegna le mani",
|
||||
"draw_body": "Disegna il corpo"
|
||||
},
|
||||
"normal_map": {
|
||||
"description": "Genera una mappa delle normali dal livello selezionato.",
|
||||
"label": "Mappa delle normali"
|
||||
},
|
||||
"lineart_edge_detection": {
|
||||
"label": "Rilevamento bordi Lineart",
|
||||
"coarse": "Grossolano",
|
||||
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi Lineart."
|
||||
},
|
||||
"depth_anything_depth_estimation": {
|
||||
"model_size_small": "Piccolo",
|
||||
"model_size_small_v2": "Piccolo v2",
|
||||
"model_size": "Dimensioni modello",
|
||||
"model_size_large": "Grande",
|
||||
"model_size_base": "Base",
|
||||
"description": "Genera una mappa di profondità dal livello selezionato utilizzando un modello Depth Anything."
|
||||
},
|
||||
"color_map": {
|
||||
"label": "Mappa colore",
|
||||
"description": "Crea una mappa dei colori dal livello selezionato.",
|
||||
"tile_size": "Dimens. Piastrella"
|
||||
},
|
||||
"canny_edge_detection": {
|
||||
"high_threshold": "Soglia superiore",
|
||||
"low_threshold": "Soglia inferiore",
|
||||
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando l'algoritmo di rilevamento dei bordi Canny.",
|
||||
"label": "Rilevamento bordi Canny"
|
||||
},
|
||||
"spandrel_filter": {
|
||||
"scale": "Scala di destinazione",
|
||||
"autoScaleDesc": "Il modello selezionato verrà eseguito fino al raggiungimento della scala di destinazione.",
|
||||
"description": "Esegue un modello immagine-a-immagine sul livello selezionato.",
|
||||
"label": "Modello Immagine-a-Immagine",
|
||||
"model": "Modello",
|
||||
"autoScale": "Auto Scala"
|
||||
},
|
||||
"pidi_edge_detection": {
|
||||
"quantize_edges": "Quantizza i bordi",
|
||||
"scribble": "Scarabocchio",
|
||||
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi PiDiNet.",
|
||||
"label": "Rilevamento bordi PiDiNet"
|
||||
},
|
||||
"hed_edge_detection": {
|
||||
"label": "Rilevamento bordi HED",
|
||||
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi HED.",
|
||||
"scribble": "Scarabocchio"
|
||||
},
|
||||
"lineart_anime_edge_detection": {
|
||||
"description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi Lineart Anime.",
|
||||
"label": "Rilevamento bordi Lineart Anime"
|
||||
}
|
||||
},
|
||||
"controlLayers_withCount_hidden": "Livelli di controllo ({{count}} nascosti)",
|
||||
"regionalGuidance_withCount_hidden": "Guida regionale ({{count}} nascosti)",
|
||||
"fill": {
|
||||
"grid": "Griglia",
|
||||
"crosshatch": "Tratteggio incrociato",
|
||||
"fillColor": "Colore di riempimento",
|
||||
"fillStyle": "Stile riempimento",
|
||||
"solid": "Solido",
|
||||
"vertical": "Verticale",
|
||||
"horizontal": "Orizzontale",
|
||||
"diagonal": "Diagonale"
|
||||
},
|
||||
"rasterLayers_withCount_hidden": "Livelli raster ({{count}} nascosti)",
|
||||
"inpaintMasks_withCount_hidden": "Maschere Inpaint ({{count}} nascoste)",
|
||||
"regionalGuidance_withCount_visible": "Guide regionali ({{count}})",
|
||||
"locked": "Bloccato",
|
||||
"hidingType": "Nascondere {{type}}",
|
||||
"logDebugInfo": "Registro Info Debug",
|
||||
"inpaintMasks_withCount_visible": "Maschere Inpaint ({{count}})",
|
||||
"layer": "Livello",
|
||||
"disableTransparencyEffect": "Disabilita l'effetto trasparenza",
|
||||
"controlLayers_withCount_visible": "Livelli di controllo ({{count}})",
|
||||
"transparency": "Trasparenza",
|
||||
"newCanvasSessionDesc": "Questo cancellerà la tela e tutte le impostazioni, eccetto la selezione del modello. Le generazioni saranno effettuate sulla tela.",
|
||||
"rasterLayers_withCount_visible": "Livelli raster ({{count}})",
|
||||
"globalReferenceImages_withCount_visible": "Immagini di riferimento Globali ({{count}})",
|
||||
"globalReferenceImages_withCount_hidden": "Immagini di riferimento globali ({{count}} nascoste)",
|
||||
"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",
|
||||
"pullBboxIntoLayer": "Carica l'immagine delimitata nel riquadro",
|
||||
"pullBboxIntoReferenceImage": "Carica l'immagine delimitata nel riquadro",
|
||||
"showProgressOnCanvas": "Mostra i progressi sulla Tela",
|
||||
"weight": "Peso",
|
||||
"newGallerySession": "Nuova sessione Galleria",
|
||||
"newGallerySessionDesc": "Questo cancellerà la tela e tutte le impostazioni, eccetto la selezione del modello. Le generazioni saranno inviate alla galleria.",
|
||||
"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": {
|
||||
"label": "Aggancia alla griglia",
|
||||
"on": "Acceso",
|
||||
"off": "Spento"
|
||||
},
|
||||
"pressureSensitivity": "Sensibilità alla pressione",
|
||||
"preserveMask": {
|
||||
"alert": "Preservare la regione mascherata",
|
||||
"label": "Preserva la regione mascherata"
|
||||
}
|
||||
},
|
||||
"transform": {
|
||||
"reset": "Reimposta",
|
||||
"fitToBbox": "Adatta al Riquadro",
|
||||
"transform": "Trasforma",
|
||||
"apply": "Applica",
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
"stagingArea": {
|
||||
"next": "Successiva",
|
||||
"discard": "Scarta",
|
||||
"discardAll": "Scarta tutto",
|
||||
"accept": "Accetta",
|
||||
"saveToGallery": "Salva nella Galleria",
|
||||
"previous": "Precedente",
|
||||
"showResultsOn": "Risultati visualizzati",
|
||||
"showResultsOff": "Risultati nascosti"
|
||||
},
|
||||
"HUD": {
|
||||
"bbox": "Riquadro di delimitazione",
|
||||
"entityStatus": {
|
||||
"isHidden": "{{title}} è nascosto",
|
||||
"isLocked": "{{title}} è bloccato",
|
||||
"isTransforming": "{{title}} sta trasformando",
|
||||
"isFiltering": "{{title}} sta filtrando",
|
||||
"isEmpty": "{{title}} è vuoto",
|
||||
"isDisabled": "{{title}} è disabilitato"
|
||||
},
|
||||
"scaledBbox": "Riquadro scalato"
|
||||
},
|
||||
"canvasContextMenu": {
|
||||
"newControlLayer": "Nuovo Livello di Controllo",
|
||||
"newRegionalReferenceImage": "Nuova immagine di riferimento Regionale",
|
||||
"newGlobalReferenceImage": "Nuova immagine di riferimento Globale",
|
||||
"bboxGroup": "Crea dal riquadro di delimitazione",
|
||||
"saveBboxToGallery": "Salva il riquadro nella Galleria",
|
||||
"cropCanvasToBbox": "Ritaglia la Tela al riquadro",
|
||||
"canvasGroup": "Tela",
|
||||
"newRasterLayer": "Nuovo Livello Raster",
|
||||
"saveCanvasToGallery": "Salva la Tela nella Galleria",
|
||||
"saveToGalleryGroup": "Salva nella Galleria"
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
"tabs": {
|
||||
@@ -1223,7 +1906,8 @@
|
||||
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
||||
"queue": "Coda",
|
||||
"upscaling": "Amplia",
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
|
||||
"gallery": "Galleria"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
@@ -1293,5 +1977,45 @@
|
||||
"noTemplates": "Nessun modello",
|
||||
"acceptedColumnsKeys": "Colonne/chiavi accettate:",
|
||||
"promptTemplateCleared": "Modello di prompt cancellato"
|
||||
},
|
||||
"newUserExperience": {
|
||||
"gettingStartedSeries": "Desideri maggiori informazioni? Consulta la nostra <LinkComponent>Getting Started Series</LinkComponent> per suggerimenti su come sfruttare appieno il potenziale di Invoke Studio.",
|
||||
"toGetStarted": "Per iniziare, inserisci un prompt nella casella e fai clic su <StrongComponent>Invoke</StrongComponent> per generare la tua prima immagine. 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"
|
||||
},
|
||||
"system": {
|
||||
"logLevel": {
|
||||
"info": "Info",
|
||||
"warn": "Avviso",
|
||||
"fatal": "Fatale",
|
||||
"error": "Errore",
|
||||
"debug": "Debug",
|
||||
"trace": "Traccia",
|
||||
"logLevel": "Livello di registro"
|
||||
},
|
||||
"logNamespaces": {
|
||||
"workflows": "Flussi di lavoro",
|
||||
"generation": "Generazione",
|
||||
"canvas": "Tela",
|
||||
"config": "Configurazione",
|
||||
"models": "Modelli",
|
||||
"gallery": "Galleria",
|
||||
"queue": "Coda",
|
||||
"events": "Eventi",
|
||||
"system": "Sistema",
|
||||
"metadata": "Metadati",
|
||||
"logNamespaces": "Elementi del registro"
|
||||
},
|
||||
"enableLogging": "Abilita la registrazione"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,8 +221,6 @@
|
||||
"uploadImage": "画像をアップロード",
|
||||
"previousImage": "前の画像",
|
||||
"nextImage": "次の画像",
|
||||
"showOptionsPanel": "サイドパネルを表示",
|
||||
"showGalleryPanel": "ギャラリーパネルを表示",
|
||||
"menu": "メニュー",
|
||||
"createIssue": "問題を報告",
|
||||
"resetUI": "$t(accessibility.reset) UI",
|
||||
|
||||
@@ -92,9 +92,7 @@
|
||||
"mode": "모드",
|
||||
"menu": "메뉴",
|
||||
"uploadImage": "이미지 업로드",
|
||||
"showGalleryPanel": "갤러리 패널 표시",
|
||||
"reset": "리셋",
|
||||
"showOptionsPanel": "사이드 패널 표시"
|
||||
"reset": "리셋"
|
||||
},
|
||||
"modelManager": {
|
||||
"availableModels": "사용 가능한 모델",
|
||||
|
||||
@@ -326,9 +326,7 @@
|
||||
"uploadImage": "Upload afbeelding",
|
||||
"previousImage": "Vorige afbeelding",
|
||||
"nextImage": "Volgende afbeelding",
|
||||
"showOptionsPanel": "Toon zijscherm",
|
||||
"menu": "Menu",
|
||||
"showGalleryPanel": "Toon deelscherm Galerij",
|
||||
"about": "Over",
|
||||
"mode": "Modus",
|
||||
"resetUI": "$t(accessibility.reset) UI",
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
"uploadImage": "Wgrywanie obrazu",
|
||||
"previousImage": "Poprzedni obraz",
|
||||
"nextImage": "Następny obraz",
|
||||
"showOptionsPanel": "Pokaż panel opcji",
|
||||
"menu": "Menu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,6 @@
|
||||
"invokeProgressBar": "Invocar barra de progresso",
|
||||
"reset": "Reiniciar",
|
||||
"nextImage": "Próxima imagem",
|
||||
"showOptionsPanel": "Mostrar painel de opções",
|
||||
"uploadImage": "Enviar imagem",
|
||||
"previousImage": "Imagem Anterior",
|
||||
"menu": "Menu",
|
||||
@@ -112,8 +111,7 @@
|
||||
"resetUI": "$t(accessibility.reset)UI",
|
||||
"createIssue": "Reportar Problema",
|
||||
"submitSupportTicket": "Submeter um ticket de Suporte",
|
||||
"mode": "Modo",
|
||||
"showGalleryPanel": "Mostrar Painel de Galeria"
|
||||
"mode": "Modo"
|
||||
},
|
||||
"boards": {
|
||||
"selectedForAutoAdd": "Selecionado para Auto-Adicionar",
|
||||
|
||||
@@ -83,7 +83,18 @@
|
||||
"tab": "Вкладка",
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено",
|
||||
"dontShowMeThese": "Не показывай мне это"
|
||||
"dontShowMeThese": "Не показывай мне это",
|
||||
"apply": "Применить",
|
||||
"loadingImage": "Загрузка изображения",
|
||||
"off": "Выкл",
|
||||
"openInViewer": "Открыть в просмотрщике",
|
||||
"edit": "Редактировать",
|
||||
"view": "Просмотреть",
|
||||
"placeholderSelectAModel": "Выбрать модель",
|
||||
"reset": "Сброс",
|
||||
"none": "Ничего",
|
||||
"new": "Новый",
|
||||
"ok": "Ok"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Размер изображений",
|
||||
@@ -138,17 +149,329 @@
|
||||
"selectAllOnPage": "Выбрать все на странице",
|
||||
"showArchivedBoards": "Показать архивированные доски",
|
||||
"searchImages": "Поиск по метаданным",
|
||||
"displayBoardSearch": "Отобразить поиск досок",
|
||||
"displaySearch": "Отобразить поиск",
|
||||
"displayBoardSearch": "Поиск доски",
|
||||
"displaySearch": "Поиск изображений",
|
||||
"exitBoardSearch": "Выйти из поиска досок",
|
||||
"go": "Перейти",
|
||||
"exitSearch": "Выйти из поиска",
|
||||
"jump": "Пыгнуть"
|
||||
"exitSearch": "Выйти из поиска изображений",
|
||||
"jump": "Пыгнуть",
|
||||
"move": "Двигать",
|
||||
"gallery": "Галерея",
|
||||
"openViewer": "Открыть просмотрщик",
|
||||
"closeViewer": "Закрыть просмотрщик"
|
||||
},
|
||||
"hotkeys": {
|
||||
"searchHotkeys": "Поиск горячих клавиш",
|
||||
"noHotkeysFound": "Горячие клавиши не найдены",
|
||||
"clearSearch": "Очистить поиск"
|
||||
"clearSearch": "Очистить поиск",
|
||||
"app": {
|
||||
"title": "Приложение",
|
||||
"invoke": {
|
||||
"desc": "Добавить генерацию в конец очереди.",
|
||||
"title": "Сгенерировать"
|
||||
},
|
||||
"clearQueue": {
|
||||
"title": "Очистить очередь",
|
||||
"desc": "Отмена и очистка всех элементов очереди."
|
||||
},
|
||||
"selectCanvasTab": {
|
||||
"title": "Выбрать вкладку Холст",
|
||||
"desc": "Выбирает вкладку Холст."
|
||||
},
|
||||
"selectUpscalingTab": {
|
||||
"title": "Выбрать вкладку Увеличение",
|
||||
"desc": "Выбирает вкладку увеличения."
|
||||
},
|
||||
"selectWorkflowsTab": {
|
||||
"title": "Выбрать вкладку Рабочие Процессы",
|
||||
"desc": "Выбирает вкладку рабочих процессов."
|
||||
},
|
||||
"focusPrompt": {
|
||||
"title": "Сфокусироваться на запросе",
|
||||
"desc": "Перемещает фокус курсора на положительный запрос."
|
||||
},
|
||||
"toggleLeftPanel": {
|
||||
"title": "Переключить левую панель",
|
||||
"desc": "Показывает или скрывает левую панель."
|
||||
},
|
||||
"resetPanelLayout": {
|
||||
"desc": "Верните левую и правую панели к размерам и расположению по умолчанию.",
|
||||
"title": "Сброс расположения панелей"
|
||||
},
|
||||
"invokeFront": {
|
||||
"title": "Сгенерировать (вперед)",
|
||||
"desc": "Добавьте генерацию вперед очереди."
|
||||
},
|
||||
"cancelQueueItem": {
|
||||
"title": "Отмена",
|
||||
"desc": "Отмена текущего обрабатываемого элемента очереди."
|
||||
},
|
||||
"selectModelsTab": {
|
||||
"desc": "Выбирает вкладку моделей.",
|
||||
"title": "Выбрать вкладку Модели"
|
||||
},
|
||||
"selectQueueTab": {
|
||||
"title": "Выбрать вкладку Очередь",
|
||||
"desc": "Выбирает вкладку очереди."
|
||||
},
|
||||
"togglePanels": {
|
||||
"title": "Переключить панели",
|
||||
"desc": "Показать или скрыть одновременно левую и правую панели."
|
||||
},
|
||||
"toggleRightPanel": {
|
||||
"title": "Переключить правую панель",
|
||||
"desc": "Показывает или скрывает правую панель."
|
||||
}
|
||||
},
|
||||
"canvas": {
|
||||
"title": "Холст",
|
||||
"selectBrushTool": {
|
||||
"title": "Инструмент кисть",
|
||||
"desc": "Выбирает кисть."
|
||||
},
|
||||
"selectBboxTool": {
|
||||
"title": "Инструмент рамка",
|
||||
"desc": "Выбрать инструмент «Ограничительная рамка»."
|
||||
},
|
||||
"incrementToolWidth": {
|
||||
"desc": "Increment the brush or eraser tool width, whichever is selected.",
|
||||
"title": "Increment Tool Width"
|
||||
},
|
||||
"selectColorPickerTool": {
|
||||
"title": "Color Picker Tool",
|
||||
"desc": "Select the color picker tool."
|
||||
},
|
||||
"prevEntity": {
|
||||
"title": "Prev Layer",
|
||||
"desc": "Select the previous layer in the list."
|
||||
},
|
||||
"filterSelected": {
|
||||
"title": "Filter",
|
||||
"desc": "Filter the selected layer. Only applies to Raster and Control layers."
|
||||
},
|
||||
"undo": {
|
||||
"desc": "Отменяет последнее действие на холсте.",
|
||||
"title": "Отменить"
|
||||
},
|
||||
"transformSelected": {
|
||||
"title": "Transform",
|
||||
"desc": "Transform the selected layer."
|
||||
},
|
||||
"setZoomTo400Percent": {
|
||||
"title": "Zoom to 400%",
|
||||
"desc": "Set the canvas zoom to 400%."
|
||||
},
|
||||
"setZoomTo200Percent": {
|
||||
"title": "Zoom to 200%",
|
||||
"desc": "Set the canvas zoom to 200%."
|
||||
},
|
||||
"deleteSelected": {
|
||||
"desc": "Delete the selected layer.",
|
||||
"title": "Delete Layer"
|
||||
},
|
||||
"resetSelected": {
|
||||
"title": "Reset Layer",
|
||||
"desc": "Reset the selected layer. Only applies to Inpaint Mask and Regional Guidance."
|
||||
},
|
||||
"redo": {
|
||||
"desc": "Возвращает последнее отмененное действие.",
|
||||
"title": "Вернуть"
|
||||
},
|
||||
"nextEntity": {
|
||||
"title": "Next Layer",
|
||||
"desc": "Select the next layer in the list."
|
||||
},
|
||||
"setFillToWhite": {
|
||||
"title": "Set Color to White",
|
||||
"desc": "Set the current tool color to white."
|
||||
},
|
||||
"applyFilter": {
|
||||
"title": "Apply Filter",
|
||||
"desc": "Apply the pending filter to the selected layer."
|
||||
},
|
||||
"cancelFilter": {
|
||||
"title": "Cancel Filter",
|
||||
"desc": "Cancel the pending filter."
|
||||
},
|
||||
"applyTransform": {
|
||||
"desc": "Apply the pending transform to the selected layer.",
|
||||
"title": "Apply Transform"
|
||||
},
|
||||
"cancelTransform": {
|
||||
"title": "Cancel Transform",
|
||||
"desc": "Cancel the pending transform."
|
||||
},
|
||||
"selectEraserTool": {
|
||||
"title": "Eraser Tool",
|
||||
"desc": "Select the eraser tool."
|
||||
},
|
||||
"fitLayersToCanvas": {
|
||||
"desc": "Scale and position the view to fit all visible layers.",
|
||||
"title": "Fit Layers to Canvas"
|
||||
},
|
||||
"decrementToolWidth": {
|
||||
"title": "Decrement Tool Width",
|
||||
"desc": "Decrement the brush or eraser tool width, whichever is selected."
|
||||
},
|
||||
"setZoomTo800Percent": {
|
||||
"title": "Zoom to 800%",
|
||||
"desc": "Set the canvas zoom to 800%."
|
||||
},
|
||||
"quickSwitch": {
|
||||
"title": "Layer Quick Switch",
|
||||
"desc": "Switch between the last two selected layers. If a layer is bookmarked, always switch between it and the last non-bookmarked layer."
|
||||
},
|
||||
"fitBboxToCanvas": {
|
||||
"title": "Fit Bbox to Canvas",
|
||||
"desc": "Scale and position the view to fit the bbox."
|
||||
},
|
||||
"setZoomTo100Percent": {
|
||||
"title": "Zoom to 100%",
|
||||
"desc": "Set the canvas zoom to 100%."
|
||||
},
|
||||
"selectMoveTool": {
|
||||
"desc": "Select the move tool.",
|
||||
"title": "Move Tool"
|
||||
},
|
||||
"selectRectTool": {
|
||||
"title": "Rect Tool",
|
||||
"desc": "Select the rect tool."
|
||||
},
|
||||
"selectViewTool": {
|
||||
"title": "View Tool",
|
||||
"desc": "Select the view tool."
|
||||
}
|
||||
},
|
||||
"hotkeys": "Горячие клавиши",
|
||||
"workflows": {
|
||||
"undo": {
|
||||
"title": "Отмена",
|
||||
"desc": "Отменить последнее действие в рабочем процессе."
|
||||
},
|
||||
"deleteSelection": {
|
||||
"desc": "Удалить выделенные узлы и ребра.",
|
||||
"title": "Delete"
|
||||
},
|
||||
"redo": {
|
||||
"title": "Вернуть",
|
||||
"desc": "Вернуть последнее действие в рабочем процессе."
|
||||
},
|
||||
"copySelection": {
|
||||
"title": "Copy",
|
||||
"desc": "Copy selected nodes and edges."
|
||||
},
|
||||
"pasteSelection": {
|
||||
"title": "Paste",
|
||||
"desc": "Paste copied nodes and edges."
|
||||
},
|
||||
"addNode": {
|
||||
"desc": "Open the add node menu.",
|
||||
"title": "Add Node"
|
||||
},
|
||||
"title": "Workflows",
|
||||
"pasteSelectionWithEdges": {
|
||||
"title": "Paste with Edges",
|
||||
"desc": "Paste copied nodes, edges, and all edges connected to copied nodes."
|
||||
},
|
||||
"selectAll": {
|
||||
"desc": "Select all nodes and edges.",
|
||||
"title": "Select All"
|
||||
}
|
||||
},
|
||||
"viewer": {
|
||||
"nextComparisonMode": {
|
||||
"title": "Следующий режим сравнения",
|
||||
"desc": "Циклическое переключение режимов сравнения."
|
||||
},
|
||||
"loadWorkflow": {
|
||||
"desc": "Загрузить сохраненный рабочий процесс текущего изображения (если он есть).",
|
||||
"title": "Загрузить рабочий процесс"
|
||||
},
|
||||
"recallAll": {
|
||||
"desc": "Восстановить все метаданные текущего изображения.",
|
||||
"title": "Восстановить все метаданные"
|
||||
},
|
||||
"swapImages": {
|
||||
"desc": "Поменять местами сравниваемые изображения.",
|
||||
"title": "Swap Comparison Images"
|
||||
},
|
||||
"title": "Просмотрщик изображений",
|
||||
"toggleViewer": {
|
||||
"title": "Открыть/закрыть просмотрщик",
|
||||
"desc": "Показать или скрыть просмотрщик изображений. Доступно только на вкладке «Холст»."
|
||||
},
|
||||
"recallSeed": {
|
||||
"title": "Recall Seed",
|
||||
"desc": "Recall the seed for the current image."
|
||||
},
|
||||
"recallPrompts": {
|
||||
"desc": "Recall the positive and negative prompts for the current image.",
|
||||
"title": "Recall Prompts"
|
||||
},
|
||||
"remix": {
|
||||
"title": "Remix",
|
||||
"desc": "Recall all metadata except for the seed for the current image."
|
||||
},
|
||||
"useSize": {
|
||||
"desc": "Use the current image's size as the bbox size.",
|
||||
"title": "Use Size"
|
||||
},
|
||||
"runPostprocessing": {
|
||||
"title": "Run Postprocessing",
|
||||
"desc": "Run the selected postprocessing on the current image."
|
||||
},
|
||||
"toggleMetadata": {
|
||||
"title": "Show/Hide Metadata",
|
||||
"desc": "Show or hide the current image's metadata overlay."
|
||||
}
|
||||
},
|
||||
"gallery": {
|
||||
"galleryNavRightAlt": {
|
||||
"desc": "Same as Navigate Right, but selects the compare image, opening compare mode if it isn't already open.",
|
||||
"title": "Navigate Right (Compare Image)"
|
||||
},
|
||||
"galleryNavRight": {
|
||||
"desc": "Navigate right in the gallery grid, selecting that image. If at the last image of the row, go to the next row. If at the last image of the page, go to the next page.",
|
||||
"title": "Navigate Right"
|
||||
},
|
||||
"galleryNavUp": {
|
||||
"desc": "Navigate up in the gallery grid, selecting that image. If at the top of the page, go to the previous page.",
|
||||
"title": "Navigate Up"
|
||||
},
|
||||
"galleryNavDown": {
|
||||
"title": "Navigate Down",
|
||||
"desc": "Navigate down in the gallery grid, selecting that image. If at the bottom of the page, go to the next page."
|
||||
},
|
||||
"galleryNavLeft": {
|
||||
"title": "Navigate Left",
|
||||
"desc": "Navigate left in the gallery grid, selecting that image. If at the first image of the row, go to the previous row. If at the first image of the page, go to the previous page."
|
||||
},
|
||||
"galleryNavDownAlt": {
|
||||
"title": "Navigate Down (Compare Image)",
|
||||
"desc": "Same as Navigate Down, but selects the compare image, opening compare mode if it isn't already open."
|
||||
},
|
||||
"galleryNavLeftAlt": {
|
||||
"desc": "Same as Navigate Left, but selects the compare image, opening compare mode if it isn't already open.",
|
||||
"title": "Navigate Left (Compare Image)"
|
||||
},
|
||||
"clearSelection": {
|
||||
"desc": "Clear the current selection, if any.",
|
||||
"title": "Clear Selection"
|
||||
},
|
||||
"deleteSelection": {
|
||||
"title": "Delete",
|
||||
"desc": "Delete all selected images. By default, you will be prompted to confirm deletion. If the images are currently in use in the app, you will be warned."
|
||||
},
|
||||
"galleryNavUpAlt": {
|
||||
"title": "Navigate Up (Compare Image)",
|
||||
"desc": "Same as Navigate Up, but selects the compare image, opening compare mode if it isn't already open."
|
||||
},
|
||||
"title": "Gallery",
|
||||
"selectAllOnPage": {
|
||||
"title": "Select All On Page",
|
||||
"desc": "Select all images on the current page."
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelManager": {
|
||||
"modelManager": "Менеджер моделей",
|
||||
@@ -254,7 +577,11 @@
|
||||
"noModelsInstalledDesc1": "Установите модели с помощью",
|
||||
"noMatchingModels": "Нет подходящих моделей",
|
||||
"ipAdapters": "IP адаптеры",
|
||||
"starterModelsInModelManager": "Стартовые модели можно найти в Менеджере моделей"
|
||||
"starterModelsInModelManager": "Стартовые модели можно найти в Менеджере моделей",
|
||||
"learnMoreAboutSupportedModels": "Подробнее о поддерживаемых моделях",
|
||||
"t5Encoder": "T5 энкодер",
|
||||
"spandrelImageToImage": "Image to Image (Spandrel)",
|
||||
"clipEmbed": "CLIP Embed"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Изображения",
|
||||
@@ -289,8 +616,8 @@
|
||||
"symmetry": "Симметрия",
|
||||
"denoisingStrength": "Сила зашумления",
|
||||
"copyImage": "Скопировать изображение",
|
||||
"seamlessXAxis": "Бесшовность по оси X",
|
||||
"seamlessYAxis": "Бесшовность по оси Y",
|
||||
"seamlessXAxis": "Бесшовная ось X",
|
||||
"seamlessYAxis": "Бесшовная ось Y",
|
||||
"scheduler": "Планировщик",
|
||||
"positivePromptPlaceholder": "Запрос",
|
||||
"negativePromptPlaceholder": "Исключающий запрос",
|
||||
@@ -314,8 +641,23 @@
|
||||
"rgNoRegion": "регион не выбран",
|
||||
"rgNoPromptsOrIPAdapters": "нет текстовых запросов или IP-адаптеров",
|
||||
"ipAdapterIncompatibleBaseModel": "несовместимая базовая модель IP-адаптера",
|
||||
"ipAdapterNoImageSelected": "изображение IP-адаптера не выбрано"
|
||||
}
|
||||
"ipAdapterNoImageSelected": "изображение IP-адаптера не выбрано",
|
||||
"t2iAdapterIncompatibleScaledBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, масштабированная ширина рамки {{width}}",
|
||||
"t2iAdapterIncompatibleBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, высота рамки {{height}}",
|
||||
"t2iAdapterIncompatibleBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, ширина рамки {{width}}",
|
||||
"t2iAdapterIncompatibleScaledBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, масштабированная высота рамки {{height}}"
|
||||
},
|
||||
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), ширина рамки {{width}}",
|
||||
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), высота рамки {{height}}",
|
||||
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), масштабированная высота рамки {{height}}",
|
||||
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16) масштабированная ширина рамки {{width}}",
|
||||
"noFLUXVAEModelSelected": "Для генерации FLUX не выбрана модель VAE",
|
||||
"noT5EncoderModelSelected": "Для генерации FLUX не выбрана модель T5 энкодера",
|
||||
"canvasIsFiltering": "Холст фильтруется",
|
||||
"canvasIsTransforming": "Холст трансформируется",
|
||||
"noCLIPEmbedModelSelected": "Для генерации FLUX не выбрана модель CLIP Embed",
|
||||
"canvasIsRasterizing": "Холст растрируется",
|
||||
"canvasIsCompositing": "Холст составляется"
|
||||
},
|
||||
"cfgRescaleMultiplier": "Множитель масштабирования CFG",
|
||||
"patchmatchDownScaleSize": "уменьшить",
|
||||
@@ -336,7 +678,13 @@
|
||||
"infillColorValue": "Цвет заливки",
|
||||
"postProcessing": "Постобработка (Shift + U)",
|
||||
"processImage": "Обработка изображения",
|
||||
"sendToUpscale": "Отправить на увеличение"
|
||||
"sendToUpscale": "Отправить на увеличение",
|
||||
"gaussianBlur": "Размытие по Гауссу",
|
||||
"staged": "Инсценировка",
|
||||
"optimizedImageToImage": "Оптимизированное img2img",
|
||||
"sendToCanvas": "Отправить на холст",
|
||||
"guidance": "Точность",
|
||||
"boxBlur": "Box Blur"
|
||||
},
|
||||
"settings": {
|
||||
"models": "Модели",
|
||||
@@ -370,7 +718,8 @@
|
||||
"intermediatesClearedFailed": "Проблема очистки промежуточных",
|
||||
"reloadingIn": "Перезагрузка через",
|
||||
"informationalPopoversDisabled": "Информационные всплывающие окна отключены",
|
||||
"informationalPopoversDisabledDesc": "Информационные всплывающие окна были отключены. Включите их в Настройках."
|
||||
"informationalPopoversDisabledDesc": "Информационные всплывающие окна были отключены. Включите их в Настройках.",
|
||||
"confirmOnNewSession": "Подтверждение нового сеанса"
|
||||
},
|
||||
"toast": {
|
||||
"uploadFailed": "Загрузка не удалась",
|
||||
@@ -384,8 +733,8 @@
|
||||
"parameterSet": "Параметр задан",
|
||||
"problemCopyingImage": "Не удается скопировать изображение",
|
||||
"baseModelChangedCleared_one": "Очищена или отключена {{count}} несовместимая подмодель",
|
||||
"baseModelChangedCleared_few": "Очищены или отключены {{count}} несовместимые подмодели",
|
||||
"baseModelChangedCleared_many": "Очищены или отключены {{count}} несовместимых подмоделей",
|
||||
"baseModelChangedCleared_few": "Очищено или отключено {{count}} несовместимых подмодели",
|
||||
"baseModelChangedCleared_many": "Очищено или отключено {{count}} несовместимых подмоделей",
|
||||
"loadedWithWarnings": "Рабочий процесс загружен с предупреждениями",
|
||||
"setControlImage": "Установить как контрольное изображение",
|
||||
"setNodeField": "Установить как поле узла",
|
||||
@@ -413,22 +762,35 @@
|
||||
"outOfMemoryErrorDesc": "Ваши текущие настройки генерации превышают возможности системы. Пожалуйста, измените настройки и повторите попытку.",
|
||||
"somethingWentWrong": "Что-то пошло не так",
|
||||
"importFailed": "Импорт неудачен",
|
||||
"importSuccessful": "Импорт успешен"
|
||||
"importSuccessful": "Импорт успешен",
|
||||
"problemSavingLayer": "Не удалось сохранить слой",
|
||||
"sentToCanvas": "Отправить на холст",
|
||||
"unableToLoadImage": "Невозможно загрузить изображение",
|
||||
"unableToLoadImageMetadata": "Невозможно загрузить метаданные изображения",
|
||||
"imageSaved": "Изображение сохранено",
|
||||
"stylePresetLoaded": "Предустановка стиля загружена",
|
||||
"imageNotLoadedDesc": "Не удалось найти изображение",
|
||||
"imageSavingFailed": "Не удалось сохранить изображение",
|
||||
"problemCopyingLayer": "Не удалось скопировать слой",
|
||||
"unableToLoadStylePreset": "Невозможно загрузить предустановку стиля",
|
||||
"layerCopiedToClipboard": "Слой скопирован в буфер обмена",
|
||||
"sentToUpscale": "Отправить на увеличение",
|
||||
"layerSavedToAssets": "Слой сохранен в активах"
|
||||
},
|
||||
"accessibility": {
|
||||
"uploadImage": "Загрузить изображение",
|
||||
"nextImage": "Следующее изображение",
|
||||
"previousImage": "Предыдущее изображение",
|
||||
"showOptionsPanel": "Показать боковую панель",
|
||||
"invokeProgressBar": "Индикатор выполнения",
|
||||
"reset": "Сброс",
|
||||
"menu": "Меню",
|
||||
"showGalleryPanel": "Показать панель галереи",
|
||||
"mode": "Режим",
|
||||
"resetUI": "$t(accessibility.reset) интерфейс",
|
||||
"createIssue": "Сообщить о проблеме",
|
||||
"about": "Об этом",
|
||||
"submitSupportTicket": "Отправить тикет в службу поддержки"
|
||||
"submitSupportTicket": "Отправить тикет в службу поддержки",
|
||||
"toggleRightPanel": "Переключить правую панель (G)",
|
||||
"toggleLeftPanel": "Переключить левую панель (T)"
|
||||
},
|
||||
"nodes": {
|
||||
"zoomInNodes": "Увеличьте масштаб",
|
||||
@@ -565,7 +927,8 @@
|
||||
"noGraph": "Нет графика",
|
||||
"imageAccessError": "Невозможно найти изображение {{image_name}}, сбрасываем на значение по умолчанию",
|
||||
"boardAccessError": "Невозможно найти доску {{board_id}}, сбрасываем на значение по умолчанию",
|
||||
"modelAccessError": "Невозможно найти модель {{key}}, сброс на модель по умолчанию"
|
||||
"modelAccessError": "Невозможно найти модель {{key}}, сброс на модель по умолчанию",
|
||||
"saveToGallery": "Сохранить в галерею"
|
||||
},
|
||||
"boards": {
|
||||
"autoAddBoard": "Авто добавление Доски",
|
||||
@@ -584,16 +947,16 @@
|
||||
"loading": "Загрузка...",
|
||||
"clearSearch": "Очистить поиск",
|
||||
"deleteBoardOnly": "Удалить только доску",
|
||||
"movingImagesToBoard_one": "Перемещаем {{count}} изображение на доску:",
|
||||
"movingImagesToBoard_few": "Перемещаем {{count}} изображения на доску:",
|
||||
"movingImagesToBoard_many": "Перемещаем {{count}} изображений на доску:",
|
||||
"movingImagesToBoard_one": "Перемещение {{count}} изображения на доску:",
|
||||
"movingImagesToBoard_few": "Перемещение {{count}} изображений на доску:",
|
||||
"movingImagesToBoard_many": "Перемещение {{count}} изображений на доску:",
|
||||
"downloadBoard": "Скачать доску",
|
||||
"deleteBoard": "Удалить доску",
|
||||
"deleteBoardAndImages": "Удалить доску и изображения",
|
||||
"deletedBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в состояние без категории.",
|
||||
"assetsWithCount_one": "{{count}} ассет",
|
||||
"assetsWithCount_few": "{{count}} ассета",
|
||||
"assetsWithCount_many": "{{count}} ассетов",
|
||||
"assetsWithCount_one": "{{count}} актив",
|
||||
"assetsWithCount_few": "{{count}} актива",
|
||||
"assetsWithCount_many": "{{count}} активов",
|
||||
"imagesWithCount_one": "{{count}} изображение",
|
||||
"imagesWithCount_few": "{{count}} изображения",
|
||||
"imagesWithCount_many": "{{count}} изображений",
|
||||
@@ -609,7 +972,8 @@
|
||||
"hideBoards": "Скрыть доски",
|
||||
"viewBoards": "Просмотреть доски",
|
||||
"noBoards": "Нет досок {{boardType}}",
|
||||
"deletedPrivateBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в приватное состояние без категории для создателя изображения."
|
||||
"deletedPrivateBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в приватное состояние без категории для создателя изображения.",
|
||||
"updateBoardError": "Ошибка обновления доски"
|
||||
},
|
||||
"dynamicPrompts": {
|
||||
"seedBehaviour": {
|
||||
@@ -990,6 +1354,25 @@
|
||||
"paragraphs": [
|
||||
"Модель увеличения масштаба масштабирует изображение до выходного размера перед добавлением деталей. Можно использовать любую поддерживаемую модель масштабирования, но некоторые из них специализированы для различных видов изображений, например фотографий или линейных рисунков."
|
||||
]
|
||||
},
|
||||
"fluxDevLicense": {
|
||||
"heading": "Некоммерческая лицензия",
|
||||
"paragraphs": [
|
||||
"Модели FLUX.1 [dev] лицензируются по некоммерческой лицензии FLUX [dev]. Чтобы использовать этот тип модели в коммерческих целях в Invoke, посетите наш веб-сайт, чтобы узнать больше."
|
||||
]
|
||||
},
|
||||
"optimizedDenoising": {
|
||||
"heading": "Оптимизированный img2img",
|
||||
"paragraphs": [
|
||||
"Включите опцию «Оптимизированный img2img», чтобы получить более плавную шкалу Denoise Strength для img2img и перерисовки с моделями Flux. Эта настройка улучшает возможность контролировать степень изменения изображения, но может быть отключена, если вы предпочитаете использовать стандартную шкалу Denoise Strength. Эта настройка все еще находится в стадии настройки и в настоящее время имеет статус бета-версии."
|
||||
]
|
||||
},
|
||||
"paramGuidance": {
|
||||
"paragraphs": [
|
||||
"Контролирует, насколько сильно запрос влияет на процесс генерации.",
|
||||
"Высокие значения точности могут привести к перенасыщению, а высокие или низкие значения точности могут привести к искажению результатов генерации. Точность применима только к моделям FLUX DEV."
|
||||
],
|
||||
"heading": "Точность"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
@@ -1020,7 +1403,8 @@
|
||||
"parsingFailed": "Не удалось выполнить синтаксический анализ",
|
||||
"recallParameter": "Отозвать {{label}}",
|
||||
"allPrompts": "Все запросы",
|
||||
"imageDimensions": "Размеры изображения"
|
||||
"imageDimensions": "Размеры изображения",
|
||||
"canvasV2Metadata": "Холст"
|
||||
},
|
||||
"queue": {
|
||||
"status": "Статус",
|
||||
@@ -1049,7 +1433,7 @@
|
||||
"graphQueued": "График поставлен в очередь",
|
||||
"queue": "Очередь",
|
||||
"batch": "Пакет",
|
||||
"clearQueueAlertDialog": "Очистка очереди немедленно отменяет все элементы обработки и полностью очищает очередь.",
|
||||
"clearQueueAlertDialog": "Очистка очереди немедленно отменяет все элементы обработки и полностью очищает очередь. Ожидающие фильтры будут отменены.",
|
||||
"pending": "В ожидании",
|
||||
"completedIn": "Завершено за",
|
||||
"resumeFailed": "Проблема с возобновлением рендеринга",
|
||||
@@ -1088,7 +1472,15 @@
|
||||
"iterations_many": "Итераций",
|
||||
"generations_one": "Генерация",
|
||||
"generations_few": "Генерации",
|
||||
"generations_many": "Генераций"
|
||||
"generations_many": "Генераций",
|
||||
"other": "Другое",
|
||||
"gallery": "Галерея",
|
||||
"upscaling": "Увеличение",
|
||||
"canvas": "Холст",
|
||||
"generation": "Генерация",
|
||||
"workflows": "Рабочие процессы",
|
||||
"origin": "Источник",
|
||||
"destination": "Назначение"
|
||||
},
|
||||
"sdxl": {
|
||||
"refinerStart": "Запуск доработчика",
|
||||
@@ -1158,7 +1550,10 @@
|
||||
"loadWorkflow": "Рабочий процесс $t(common.load)",
|
||||
"convertGraph": "Конвертировать график",
|
||||
"loadFromGraph": "Загрузка рабочего процесса из графика",
|
||||
"autoLayout": "Автоматическое расположение"
|
||||
"autoLayout": "Автоматическое расположение",
|
||||
"userWorkflows": "Пользовательские рабочие процессы",
|
||||
"projectWorkflows": "Рабочие процессы проекта",
|
||||
"defaultWorkflows": "Стандартные рабочие процессы"
|
||||
},
|
||||
"hrf": {
|
||||
"enableHrf": "Включить исправление высокого разрешения",
|
||||
@@ -1215,12 +1610,307 @@
|
||||
"autoNegative": "Авто негатив",
|
||||
"deletePrompt": "Удалить запрос",
|
||||
"rectangle": "Прямоугольник",
|
||||
"addNegativePrompt": "Добавить $t(common.negativePrompt)",
|
||||
"addNegativePrompt": "Добавить $t(controlLayers.negativePrompt)",
|
||||
"regionalGuidance": "Региональная точность",
|
||||
"opacity": "Непрозрачность",
|
||||
"addLayer": "Добавить слой",
|
||||
"moveToFront": "На передний план",
|
||||
"addPositivePrompt": "Добавить $t(common.positivePrompt)"
|
||||
"addPositivePrompt": "Добавить $t(controlLayers.prompt)",
|
||||
"regional": "Региональный",
|
||||
"bookmark": "Закладка для быстрого переключения",
|
||||
"fitBboxToLayers": "Подогнать рамку к слоям",
|
||||
"mergeVisibleOk": "Объединенные видимые слои",
|
||||
"mergeVisibleError": "Ошибка объединения видимых слоев",
|
||||
"clearHistory": "Очистить историю",
|
||||
"mergeVisible": "Объединить видимые",
|
||||
"removeBookmark": "Удалить закладку",
|
||||
"saveLayerToAssets": "Сохранить слой в активы",
|
||||
"clearCaches": "Очистить кэши",
|
||||
"recalculateRects": "Пересчитать прямоугольники",
|
||||
"saveBboxToGallery": "Сохранить рамку в галерею",
|
||||
"resetCanvas": "Сбросить холст",
|
||||
"canvas": "Холст",
|
||||
"global": "Глобальный",
|
||||
"newGlobalReferenceImageError": "Проблема с созданием глобального эталонного изображения",
|
||||
"newRegionalReferenceImageOk": "Создано региональное эталонное изображение",
|
||||
"newRegionalReferenceImageError": "Проблема создания регионального эталонного изображения",
|
||||
"newControlLayerOk": "Создан слой управления",
|
||||
"newControlLayerError": "Ошибка создания слоя управления",
|
||||
"newRasterLayerOk": "Создан растровый слой",
|
||||
"newRasterLayerError": "Ошибка создания растрового слоя",
|
||||
"newGlobalReferenceImageOk": "Создано глобальное эталонное изображение",
|
||||
"bboxOverlay": "Показать наложение ограничительной рамки",
|
||||
"saveCanvasToGallery": "Сохранить холст в галерею",
|
||||
"pullBboxIntoReferenceImageOk": "рамка перенесена в эталонное изображение",
|
||||
"pullBboxIntoReferenceImageError": "Ошибка переноса рамки в эталонное изображение",
|
||||
"regionIsEmpty": "Выбранный регион пуст",
|
||||
"savedToGalleryOk": "Сохранено в галерею",
|
||||
"savedToGalleryError": "Ошибка сохранения в галерею",
|
||||
"pullBboxIntoLayerOk": "Рамка перенесена в слой",
|
||||
"pullBboxIntoLayerError": "Проблема с переносом рамки в слой",
|
||||
"newLayerFromImage": "Новый слой из изображения",
|
||||
"filter": {
|
||||
"lineart_anime_edge_detection": {
|
||||
"label": "Обнаружение краев Lineart Anime",
|
||||
"description": "Создает карту краев выбранного слоя с помощью модели обнаружения краев Lineart Anime."
|
||||
},
|
||||
"hed_edge_detection": {
|
||||
"scribble": "Штрих",
|
||||
"label": "обнаружение границ HED",
|
||||
"description": "Создает карту границ из выбранного слоя с использованием модели обнаружения границ HED."
|
||||
},
|
||||
"mlsd_detection": {
|
||||
"description": "Генерирует карту сегментов линий из выбранного слоя с помощью модели обнаружения сегментов линий MLSD.",
|
||||
"score_threshold": "Пороговый балл",
|
||||
"distance_threshold": "Порог расстояния",
|
||||
"label": "Обнаружение сегментов линии"
|
||||
},
|
||||
"canny_edge_detection": {
|
||||
"low_threshold": "Низкий порог",
|
||||
"high_threshold": "Высокий порог",
|
||||
"label": "Обнаружение краев",
|
||||
"description": "Создает карту краев выбранного слоя с помощью алгоритма обнаружения краев Canny."
|
||||
},
|
||||
"color_map": {
|
||||
"description": "Создайте цветовую карту из выбранного слоя.",
|
||||
"label": "Цветная карта",
|
||||
"tile_size": "Размер плитки"
|
||||
},
|
||||
"depth_anything_depth_estimation": {
|
||||
"model_size_base": "Базовая",
|
||||
"model_size_large": "Большая",
|
||||
"label": "Анализ глубины",
|
||||
"model_size_small": "Маленькая",
|
||||
"model_size_small_v2": "Маленькая v2",
|
||||
"description": "Создает карту глубины из выбранного слоя с использованием модели Depth Anything.",
|
||||
"model_size": "Размер модели"
|
||||
},
|
||||
"mediapipe_face_detection": {
|
||||
"min_confidence": "Минимальная уверенность",
|
||||
"label": "Распознавание лиц MediaPipe",
|
||||
"description": "Обнаруживает лица в выбранном слое с помощью модели обнаружения лиц MediaPipe.",
|
||||
"max_faces": "Максимум лиц"
|
||||
},
|
||||
"lineart_edge_detection": {
|
||||
"label": "Обнаружение краев Lineart",
|
||||
"description": "Создает карту краев выбранного слоя с помощью модели обнаружения краев Lineart.",
|
||||
"coarse": "Грубый"
|
||||
},
|
||||
"filterType": "Тип фильтра",
|
||||
"autoProcess": "Автообработка",
|
||||
"reset": "Сбросить",
|
||||
"content_shuffle": {
|
||||
"scale_factor": "Коэффициент",
|
||||
"label": "Перетасовка контента",
|
||||
"description": "Перемешивает содержимое выбранного слоя, аналогично эффекту «сжижения»."
|
||||
},
|
||||
"dw_openpose_detection": {
|
||||
"label": "Обнаружение DW Openpose",
|
||||
"draw_hands": "Рисовать руки",
|
||||
"description": "Обнаруживает позы человека в выбранном слое с помощью модели DW Openpose.",
|
||||
"draw_face": "Рисовать лицо",
|
||||
"draw_body": "Рисовать тело"
|
||||
},
|
||||
"normal_map": {
|
||||
"label": "Карта нормалей",
|
||||
"description": "Создает карту нормалей для выбранного слоя."
|
||||
},
|
||||
"spandrel_filter": {
|
||||
"model": "Модель",
|
||||
"label": "Модель img2img",
|
||||
"autoScale": "Авто масштабирование",
|
||||
"scale": "Целевой масштаб",
|
||||
"description": "Запустить модель изображения к изображению на выбранном слое.",
|
||||
"autoScaleDesc": "Выбранная модель будет работать до тех пор, пока не будет достигнут целевой масштаб."
|
||||
},
|
||||
"pidi_edge_detection": {
|
||||
"scribble": "Штрих",
|
||||
"description": "Генерирует карту краев из выбранного слоя с помощью модели обнаружения краев PiDiNet.",
|
||||
"label": "Обнаружение краев PiDiNet",
|
||||
"quantize_edges": "Квантизация краев"
|
||||
},
|
||||
"process": "Обработать",
|
||||
"apply": "Применить",
|
||||
"cancel": "Отменить",
|
||||
"filter": "Фильтр",
|
||||
"filters": "Фильтры"
|
||||
},
|
||||
"HUD": {
|
||||
"entityStatus": {
|
||||
"isHidden": "{{title}} скрыт",
|
||||
"isLocked": "{{title}} заблокирован",
|
||||
"isDisabled": "{{title}} отключен",
|
||||
"isEmpty": "{{title}} пуст",
|
||||
"isFiltering": "{{title}} фильтруется",
|
||||
"isTransforming": "{{title}} трансформируется"
|
||||
},
|
||||
"scaledBbox": "Масштабированная рамка",
|
||||
"bbox": "Ограничительная рамка"
|
||||
},
|
||||
"canvasContextMenu": {
|
||||
"saveBboxToGallery": "Сохранить рамку в галерею",
|
||||
"newGlobalReferenceImage": "Новое глобальное эталонное изображение",
|
||||
"bboxGroup": "Сохдать из рамки",
|
||||
"canvasGroup": "Холст",
|
||||
"newControlLayer": "Новый контрольный слой",
|
||||
"newRasterLayer": "Новый растровый слой",
|
||||
"saveToGalleryGroup": "Сохранить в галерею",
|
||||
"saveCanvasToGallery": "Сохранить холст в галерею",
|
||||
"cropCanvasToBbox": "Обрезать холст по рамке",
|
||||
"newRegionalReferenceImage": "Новое региональное эталонное изображение"
|
||||
},
|
||||
"fill": {
|
||||
"solid": "Сплошной",
|
||||
"fillStyle": "Стиль заполнения",
|
||||
"fillColor": "Цвет заполнения",
|
||||
"grid": "Сетка",
|
||||
"horizontal": "Горизонтальная",
|
||||
"diagonal": "Диагональная",
|
||||
"crosshatch": "Штриховка",
|
||||
"vertical": "Вертикальная"
|
||||
},
|
||||
"showHUD": "Показать HUD",
|
||||
"copyToClipboard": "Копировать в буфер обмена",
|
||||
"ipAdapterMethod": {
|
||||
"composition": "Только композиция",
|
||||
"style": "Только стиль",
|
||||
"ipAdapterMethod": "Метод IP адаптера",
|
||||
"full": "Полный"
|
||||
},
|
||||
"addReferenceImage": "Добавить $t(controlLayers.referenceImage)",
|
||||
"inpaintMask": "Маска перерисовки",
|
||||
"sendToGalleryDesc": "При нажатии кнопки Invoke создается изображение и сохраняется в вашей галерее.",
|
||||
"sendToCanvas": "Отправить на холст",
|
||||
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
|
||||
"regionalGuidance_withCount_few": "Региональных точности",
|
||||
"regionalGuidance_withCount_many": "Региональных точностей",
|
||||
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
|
||||
"controlLayer_withCount_few": "Контрольных слоя",
|
||||
"controlLayer_withCount_many": "Контрольных слоев",
|
||||
"newCanvasFromImage": "Новый холст из изображения",
|
||||
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
|
||||
"inpaintMask_withCount_few": "Маски перерисовки",
|
||||
"inpaintMask_withCount_many": "Масок перерисовки",
|
||||
"globalReferenceImages_withCount_visible": "Глобальные эталонные изображения ({{count}})",
|
||||
"controlMode": {
|
||||
"prompt": "Запрос",
|
||||
"controlMode": "Режим контроля",
|
||||
"megaControl": "Мега контроль",
|
||||
"balanced": "Сбалансированный",
|
||||
"control": "Контроль"
|
||||
},
|
||||
"settings": {
|
||||
"isolatedPreview": "Изолированный предпросмотр",
|
||||
"isolatedTransformingPreview": "Изолированный предпросмотр преобразования",
|
||||
"invertBrushSizeScrollDirection": "Инвертировать прокрутку для размера кисти",
|
||||
"snapToGrid": {
|
||||
"label": "Привязка к сетке",
|
||||
"on": "Вкл",
|
||||
"off": "Выкл"
|
||||
},
|
||||
"isolatedFilteringPreview": "Изолированный предпросмотр фильтрации",
|
||||
"pressureSensitivity": "Чувствительность к давлению",
|
||||
"isolatedStagingPreview": "Изолированный предпросмотр на промежуточной стадии",
|
||||
"preserveMask": {
|
||||
"label": "Сохранить замаскированную область",
|
||||
"alert": "Сохранение замаскированной области"
|
||||
}
|
||||
},
|
||||
"stagingArea": {
|
||||
"discardAll": "Отбросить все",
|
||||
"discard": "Отбросить",
|
||||
"accept": "Принять",
|
||||
"previous": "Предыдущий",
|
||||
"next": "Следующий",
|
||||
"saveToGallery": "Сохранить в галерею",
|
||||
"showResultsOn": "Показать результаты",
|
||||
"showResultsOff": "Скрыть результаты"
|
||||
},
|
||||
"pullBboxIntoReferenceImage": "Поместить рамку в эталонное изображение",
|
||||
"enableAutoNegative": "Включить авто негатив",
|
||||
"maskFill": "Заполнение маски",
|
||||
"viewProgressInViewer": "Просматривайте прогресс и результаты в <Btn>Просмотрщике изображений</Btn>.",
|
||||
"convertToRasterLayer": "Конвертировать в растровый слой",
|
||||
"tool": {
|
||||
"move": "Двигать",
|
||||
"bbox": "Ограничительная рамка",
|
||||
"view": "Смотреть",
|
||||
"brush": "Кисть",
|
||||
"eraser": "Ластик",
|
||||
"rectangle": "Прямоугольник",
|
||||
"colorPicker": "Подборщик цветов"
|
||||
},
|
||||
"rasterLayer": "Растровый слой",
|
||||
"sendingToCanvas": "Постановка генераций на холст",
|
||||
"rasterLayers_withCount_visible": "Растровые слои ({{count}})",
|
||||
"regionalGuidance_withCount_hidden": "Региональная точность ({{count}} скрыто)",
|
||||
"enableTransparencyEffect": "Включить эффект прозрачности",
|
||||
"hidingType": "Скрыть {{type}}",
|
||||
"addRegionalGuidance": "Добавить $t(controlLayers.regionalGuidance)",
|
||||
"sendingToGallery": "Отправка генераций в галерею",
|
||||
"viewProgressOnCanvas": "Просматривайте прогресс и результаты этапов на <Btn>Холсте</Btn>.",
|
||||
"controlLayers_withCount_hidden": "Контрольные слои ({{count}} скрыто)",
|
||||
"rasterLayers_withCount_hidden": "Растровые слои ({{count}} скрыто)",
|
||||
"deleteSelected": "Удалить выбранное",
|
||||
"stagingOnCanvas": "Постановка изображений на",
|
||||
"pullBboxIntoLayer": "Поместить рамку в слой",
|
||||
"locked": "Заблокировано",
|
||||
"replaceLayer": "Заменить слой",
|
||||
"width": "Ширина",
|
||||
"controlLayer": "Слой управления",
|
||||
"addRasterLayer": "Добавить $t(controlLayers.rasterLayer)",
|
||||
"addControlLayer": "Добавить $t(controlLayers.controlLayer)",
|
||||
"addInpaintMask": "Добавить $t(controlLayers.inpaintMask)",
|
||||
"inpaintMasks_withCount_hidden": "Маски перерисовки ({{count}} скрыто)",
|
||||
"regionalGuidance_withCount_visible": "Региональная точность ({{count}})",
|
||||
"newGallerySessionDesc": "Это очистит холст и все настройки, кроме выбранной модели. Генерации будут отправлены в галерею.",
|
||||
"newCanvasSession": "Новая сессия холста",
|
||||
"newCanvasSessionDesc": "Это очистит холст и все настройки, кроме выбора модели. Генерации будут размещены на холсте.",
|
||||
"cropLayerToBbox": "Обрезать слой по ограничительной рамке",
|
||||
"clipToBbox": "Обрезка штрихов в рамке",
|
||||
"outputOnlyMaskedRegions": "Вывод только маскированных областей",
|
||||
"duplicate": "Дублировать",
|
||||
"inpaintMasks_withCount_visible": "Маски перерисовки ({{count}})",
|
||||
"layer": "Слой",
|
||||
"prompt": "Запрос",
|
||||
"negativePrompt": "Исключающий запрос",
|
||||
"beginEndStepPercentShort": "Начало/конец %",
|
||||
"transform": {
|
||||
"transform": "Трансформировать",
|
||||
"fitToBbox": "Вместить в рамку",
|
||||
"reset": "Сбросить",
|
||||
"apply": "Применить",
|
||||
"cancel": "Отменить"
|
||||
},
|
||||
"disableAutoNegative": "Отключить авто негатив",
|
||||
"deleteReferenceImage": "Удалить эталонное изображение",
|
||||
"controlLayers_withCount_visible": "Контрольные слои ({{count}})",
|
||||
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
|
||||
"rasterLayer_withCount_few": "Растровых слоя",
|
||||
"rasterLayer_withCount_many": "Растровых слоев",
|
||||
"transparency": "Прозрачность",
|
||||
"weight": "Вес",
|
||||
"newGallerySession": "Новая сессия галереи",
|
||||
"sendToCanvasDesc": "Нажатие кнопки Invoke отображает вашу текущую работу на холсте.",
|
||||
"globalReferenceImages_withCount_hidden": "Глобальные эталонные изображения ({{count}} скрыто)",
|
||||
"convertToControlLayer": "Конвертировать в контрольный слой",
|
||||
"layer_withCount_one": "Слой ({{count}})",
|
||||
"layer_withCount_few": "Слои ({{count}})",
|
||||
"layer_withCount_many": "Слои ({{count}})",
|
||||
"disableTransparencyEffect": "Отключить эффект прозрачности",
|
||||
"showingType": "Показать {{type}}",
|
||||
"dynamicGrid": "Динамическая сетка",
|
||||
"logDebugInfo": "Писать отладочную информацию",
|
||||
"unlocked": "Разблокировано",
|
||||
"showProgressOnCanvas": "Показать прогресс на холсте",
|
||||
"globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)",
|
||||
"globalReferenceImage_withCount_few": "Глобальных эталонных изображения",
|
||||
"globalReferenceImage_withCount_many": "Глобальных эталонных изображений",
|
||||
"regionalReferenceImage": "Региональное эталонное изображение",
|
||||
"globalReferenceImage": "Глобальное эталонное изображение",
|
||||
"sendToGallery": "Отправить в галерею",
|
||||
"referenceImage": "Эталонное изображение",
|
||||
"addGlobalReferenceImage": "Добавить $t(controlLayers.globalReferenceImage)"
|
||||
},
|
||||
"ui": {
|
||||
"tabs": {
|
||||
@@ -1232,7 +1922,8 @@
|
||||
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
||||
"queue": "Очередь",
|
||||
"upscaling": "Увеличение",
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)",
|
||||
"gallery": "Галерея"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
@@ -1304,5 +1995,45 @@
|
||||
"professional": "Профессионал",
|
||||
"professionalUpsell": "Доступно в профессиональной версии Invoke. Нажмите здесь или посетите invoke.com/pricing для получения более подробной информации.",
|
||||
"shareAccess": "Поделиться доступом"
|
||||
},
|
||||
"system": {
|
||||
"logNamespaces": {
|
||||
"canvas": "Холст",
|
||||
"config": "Конфигурация",
|
||||
"generation": "Генерация",
|
||||
"workflows": "Рабочие процессы",
|
||||
"gallery": "Галерея",
|
||||
"models": "Модели",
|
||||
"logNamespaces": "Пространства имен логов",
|
||||
"events": "События",
|
||||
"system": "Система",
|
||||
"queue": "Очередь",
|
||||
"metadata": "Метаданные"
|
||||
},
|
||||
"enableLogging": "Включить логи",
|
||||
"logLevel": {
|
||||
"logLevel": "Уровень логов",
|
||||
"fatal": "Фатальное",
|
||||
"debug": "Отладка",
|
||||
"info": "Инфо",
|
||||
"warn": "Предупреждение",
|
||||
"error": "Ошибки",
|
||||
"trace": "Трассировка"
|
||||
}
|
||||
},
|
||||
"whatsNew": {
|
||||
"canvasV2Announcement": {
|
||||
"newLayerTypes": "Новые типы слоев для еще большего контроля",
|
||||
"readReleaseNotes": "Прочитать информацию о выпуске",
|
||||
"watchReleaseVideo": "Смотреть видео о выпуске",
|
||||
"fluxSupport": "Поддержка семейства моделей Flux",
|
||||
"newCanvas": "Новый мощный холст управления",
|
||||
"watchUiUpdatesOverview": "Обзор обновлений пользовательского интерфейса"
|
||||
},
|
||||
"whatsNewInInvoke": "Что нового в Invoke"
|
||||
},
|
||||
"newUserExperience": {
|
||||
"toGetStarted": "Чтобы начать работу, введите в поле запрос и нажмите <StrongComponent>Invoke</StrongComponent>, чтобы сгенерировать первое изображение. Вы можете сохранить изображения непосредственно в <StrongComponent>Галерею</StrongComponent> или отредактировать их на <StrongComponent>Холсте</StrongComponent>.",
|
||||
"gettingStartedSeries": "Хотите получить больше рекомендаций? Ознакомьтесь с нашей серией <LinkComponent>Getting Started Series</LinkComponent> для получения советов по раскрытию всего потенциала Invoke Studio."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"invokeProgressBar": "Invoke förloppsmätare",
|
||||
"nextImage": "Nästa bild",
|
||||
"reset": "Starta om",
|
||||
"previousImage": "Föregående bild",
|
||||
"showOptionsPanel": "Visa inställningspanelen"
|
||||
"previousImage": "Föregående bild"
|
||||
},
|
||||
"common": {
|
||||
"hotkeysLabel": "Snabbtangenter",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"accessibility": {
|
||||
"invokeProgressBar": "Invoke durum çubuğu",
|
||||
"nextImage": "Sonraki Görsel",
|
||||
"showOptionsPanel": "Yan Paneli Göster",
|
||||
"reset": "Resetle",
|
||||
"uploadImage": "Görsel Yükle",
|
||||
"previousImage": "Önceki Görsel",
|
||||
@@ -10,7 +9,6 @@
|
||||
"about": "Hakkında",
|
||||
"mode": "Kip",
|
||||
"resetUI": "$t(accessibility.reset)Arayüz",
|
||||
"showGalleryPanel": "Galeri Panelini Göster",
|
||||
"createIssue": "Sorun Bildir"
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -114,7 +114,6 @@
|
||||
"reset": "Скинути",
|
||||
"uploadImage": "Завантажити зображення",
|
||||
"previousImage": "Попереднє зображення",
|
||||
"showOptionsPanel": "Показати опції",
|
||||
"menu": "Меню"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,14 +410,13 @@
|
||||
"nextImage": "下一张图片",
|
||||
"uploadImage": "上传图片",
|
||||
"previousImage": "上一张图片",
|
||||
"showOptionsPanel": "显示侧栏浮窗",
|
||||
"menu": "菜单",
|
||||
"showGalleryPanel": "显示图库浮窗",
|
||||
"mode": "模式",
|
||||
"resetUI": "$t(accessibility.reset) UI",
|
||||
"createIssue": "创建问题",
|
||||
"about": "关于",
|
||||
"submitSupportTicket": "提交支持工单"
|
||||
"submitSupportTicket": "提交支持工单",
|
||||
"toggleRightPanel": "切换右侧面板(G)"
|
||||
},
|
||||
"nodes": {
|
||||
"zoomInNodes": "放大",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box, useGlobalModifiersInit } from '@invoke-ai/ui-library';
|
||||
import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys';
|
||||
import type { StudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import { useStudioInitAction } from 'app/hooks/useStudioInitAction';
|
||||
import { useSyncQueueStatus } from 'app/hooks/useSyncQueueStatus';
|
||||
@@ -7,11 +8,15 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import ImageUploadOverlay from 'common/components/ImageUploadOverlay';
|
||||
import { useScopeFocusWatcher } from 'common/hooks/interactionScopes';
|
||||
import { useFocusRegionWatcher } from 'common/hooks/focus';
|
||||
import { useClearStorage } from 'common/hooks/useClearStorage';
|
||||
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
|
||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
||||
import {
|
||||
NewCanvasSessionDialog,
|
||||
NewGallerySessionDialog,
|
||||
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
|
||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||
@@ -77,14 +82,14 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
||||
useStudioInitAction(studioInitAction);
|
||||
useStarterModelsToast();
|
||||
useSyncQueueStatus();
|
||||
useScopeFocusWatcher();
|
||||
useFocusRegionWatcher();
|
||||
|
||||
return (
|
||||
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
|
||||
<Box
|
||||
id="invoke-app-wrapper"
|
||||
w="100vw"
|
||||
h="100vh"
|
||||
w="100dvw"
|
||||
h="100dvh"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
{...dropzone.getRootProps()}
|
||||
@@ -104,6 +109,9 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
||||
<ClearQueueConfirmationsAlertDialog />
|
||||
<RefreshAfterResetModal />
|
||||
<DeleteBoardModal />
|
||||
<GlobalImageHotkeys />
|
||||
<NewGallerySessionDialog />
|
||||
<NewCanvasSessionDialog />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
|
||||
}, [error.message, error.name, isLocal]);
|
||||
|
||||
return (
|
||||
<Flex layerStyle="body" w="100vw" h="100vh" alignItems="center" justifyContent="center" p={4}>
|
||||
<Flex layerStyle="body" w="100dvw" h="100dvh" alignItems="center" justifyContent="center" p={4}>
|
||||
<Flex layerStyle="first" flexDir="column" borderRadius="base" justifyContent="center" gap={8} p={16}>
|
||||
<Flex alignItems="center" gap="2">
|
||||
<Image src={InvokeLogoYellow} alt="invoke-logo" w="24px" h="24px" minW="24px" minH="24px" userSelect="none" />
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const GlobalImageHotkeys = memo(() => {
|
||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
||||
|
||||
if (!imageDTO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <GlobalImageHotkeysInternal imageDTO={imageDTO} />;
|
||||
});
|
||||
|
||||
GlobalImageHotkeys.displayName = 'GlobalImageHotkeys';
|
||||
|
||||
const GlobalImageHotkeysInternal = memo(({ imageDTO }: { imageDTO: ImageDTO }) => {
|
||||
const isGalleryFocused = useIsRegionFocused('gallery');
|
||||
const isViewerFocused = useIsRegionFocused('viewer');
|
||||
const imageActions = useImageActions(imageDTO);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'loadWorkflow',
|
||||
category: 'viewer',
|
||||
callback: imageActions.loadWorkflow,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.loadWorkflow, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallAll',
|
||||
category: 'viewer',
|
||||
callback: imageActions.recallAll,
|
||||
options: { enabled: !isStaging && (isGalleryFocused || isViewerFocused) },
|
||||
dependencies: [imageActions.recallAll, isStaging, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallSeed',
|
||||
category: 'viewer',
|
||||
callback: imageActions.recallSeed,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.recallSeed, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'recallPrompts',
|
||||
category: 'viewer',
|
||||
callback: imageActions.recallPrompts,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.recallPrompts, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'remix',
|
||||
category: 'viewer',
|
||||
callback: imageActions.remix,
|
||||
options: { enabled: isGalleryFocused || isViewerFocused },
|
||||
dependencies: [imageActions.remix, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'useSize',
|
||||
category: 'viewer',
|
||||
callback: imageActions.recallSize,
|
||||
options: { enabled: !isStaging && (isGalleryFocused || isViewerFocused) },
|
||||
dependencies: [imageActions.recallSize, isStaging, isGalleryFocused, isViewerFocused],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'runPostprocessing',
|
||||
category: 'viewer',
|
||||
callback: imageActions.upscale,
|
||||
options: { enabled: isUpscalingEnabled && isViewerFocused },
|
||||
dependencies: [isUpscalingEnabled, imageDTO, isViewerFocused],
|
||||
});
|
||||
return null;
|
||||
});
|
||||
|
||||
GlobalImageHotkeysInternal.displayName = 'GlobalImageHotkeysInternal';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import { canvasReset, newSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import { stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
@@ -9,7 +9,7 @@ import { queueApi } from 'services/api/endpoints/queue';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
const matchCanvasOrStagingAreaReset = isAnyOf(stagingAreaReset, canvasReset);
|
||||
const matchCanvasOrStagingAreaReset = isAnyOf(stagingAreaReset, canvasReset, newSessionRequested);
|
||||
|
||||
export const addStagingListeners = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaS
|
||||
import {
|
||||
setCfgRescaleMultiplier,
|
||||
setCfgScale,
|
||||
setGuidance,
|
||||
setScheduler,
|
||||
setSteps,
|
||||
vaePrecisionChanged,
|
||||
@@ -13,6 +14,7 @@ import { setDefaultSettings } from 'features/parameters/store/actions';
|
||||
import {
|
||||
isParameterCFGRescaleMultiplier,
|
||||
isParameterCFGScale,
|
||||
isParameterGuidance,
|
||||
isParameterHeight,
|
||||
isParameterPrecision,
|
||||
isParameterScheduler,
|
||||
@@ -49,7 +51,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
|
||||
}
|
||||
|
||||
if (isNonRefinerMainModelConfig(modelConfig) && modelConfig.default_settings) {
|
||||
const { vae, vae_precision, cfg_scale, cfg_rescale_multiplier, steps, scheduler, width, height } =
|
||||
const { vae, vae_precision, cfg_scale, cfg_rescale_multiplier, steps, scheduler, width, height, guidance } =
|
||||
modelConfig.default_settings;
|
||||
|
||||
if (vae) {
|
||||
@@ -73,6 +75,12 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
|
||||
}
|
||||
}
|
||||
|
||||
if (guidance) {
|
||||
if (isParameterGuidance(guidance)) {
|
||||
dispatch(setGuidance(guidance));
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg_scale) {
|
||||
if (isParameterCFGScale(cfg_scale)) {
|
||||
dispatch(setCfgScale(cfg_scale));
|
||||
|
||||
@@ -1,80 +1,62 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import { memo, useRef } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
type Props = {
|
||||
isOver: boolean;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const initial: AnimationProps['initial'] = {
|
||||
opacity: 0,
|
||||
};
|
||||
const animate: AnimationProps['animate'] = {
|
||||
opacity: 1,
|
||||
transition: { duration: 0.1 },
|
||||
};
|
||||
const exit: AnimationProps['exit'] = {
|
||||
opacity: 0,
|
||||
transition: { duration: 0.1 },
|
||||
};
|
||||
|
||||
const IAIDropOverlay = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOver, label = t('gallery.drop') } = props;
|
||||
const motionId = useRef(uuidv4());
|
||||
return (
|
||||
<motion.div key={motionId.current} initial={initial} animate={animate} exit={exit}>
|
||||
<Flex position="absolute" top={0} right={0} bottom={0} left={0}>
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
w="full"
|
||||
h="full"
|
||||
bg="base.900"
|
||||
opacity={0.7}
|
||||
borderRadius="base"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
transitionProperty="common"
|
||||
transitionDuration="0.1s"
|
||||
/>
|
||||
<Flex position="absolute" top={0} right={0} bottom={0} left={0}>
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
w="full"
|
||||
h="full"
|
||||
bg="base.900"
|
||||
opacity={0.7}
|
||||
borderRadius="base"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
transitionProperty="common"
|
||||
transitionDuration="0.1s"
|
||||
/>
|
||||
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={0.5}
|
||||
right={0.5}
|
||||
bottom={0.5}
|
||||
left={0.5}
|
||||
opacity={1}
|
||||
borderWidth={1.5}
|
||||
borderColor={isOver ? 'invokeYellow.300' : 'base.500'}
|
||||
borderRadius="base"
|
||||
borderStyle="dashed"
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={0.5}
|
||||
right={0.5}
|
||||
bottom={0.5}
|
||||
left={0.5}
|
||||
opacity={1}
|
||||
borderWidth={1.5}
|
||||
borderColor={isOver ? 'invokeYellow.300' : 'base.500'}
|
||||
borderRadius="base"
|
||||
borderStyle="dashed"
|
||||
transitionProperty="common"
|
||||
transitionDuration="0.1s"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontWeight="semibold"
|
||||
color={isOver ? 'invokeYellow.300' : 'base.500'}
|
||||
transitionProperty="common"
|
||||
transitionDuration="0.1s"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
p={4}
|
||||
textAlign="center"
|
||||
>
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontWeight="semibold"
|
||||
color={isOver ? 'invokeYellow.300' : 'base.500'}
|
||||
transitionProperty="common"
|
||||
transitionDuration="0.1s"
|
||||
textAlign="center"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
</motion.div>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
|
||||
position="absolute"
|
||||
top={0}
|
||||
insetInlineStart={0}
|
||||
width="100vw"
|
||||
height="100vh"
|
||||
width="100dvw"
|
||||
height="100dvh"
|
||||
zIndex={999}
|
||||
backdropFilter="blur(20px)"
|
||||
>
|
||||
|
||||
@@ -89,7 +89,7 @@ const Content = ({ data, feature, hideDisable }: ContentProps) => {
|
||||
|
||||
const paragraphs = useMemo<string[]>(
|
||||
() =>
|
||||
t(`popovers.${feature}.paragraphs`, {
|
||||
t<string, { returnObjects: true }, string[]>(`popovers.${feature}.paragraphs`, {
|
||||
returnObjects: true,
|
||||
}) ?? [],
|
||||
[feature, t]
|
||||
|
||||
@@ -6,7 +6,7 @@ import { memo } from 'react';
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<Flex position="relative" width="100vw" height="100vh" alignItems="center" justifyContent="center" bg="#151519">
|
||||
<Flex position="relative" width="100dvw" height="100dvh" alignItems="center" justifyContent="center" bg="#151519">
|
||||
<Image src={InvokeLogoWhite} w="8rem" h="8rem" />
|
||||
<Spinner
|
||||
label="Loading"
|
||||
|
||||
182
invokeai/frontend/web/src/common/hooks/focus.ts
Normal file
182
invokeai/frontend/web/src/common/hooks/focus.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import type { Atom } from 'nanostores';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { objectKeys } from 'tsafe';
|
||||
|
||||
/**
|
||||
* We need to manage focus regions to conditionally enable hotkeys:
|
||||
* - Some hotkeys should only be enabled when a specific region is focused.
|
||||
* - Some hotkeys may conflict with other regions, so we need to disable them when a specific region is focused. For
|
||||
* example, `esc` is used to clear the gallery selection, but it is also used to cancel a filter or transform on the
|
||||
* canvas.
|
||||
*
|
||||
* To manage focus regions, we use a system of hooks and stores:
|
||||
* - `useFocusRegion` is a hook that registers an element as part of a focus region. When that element is focused, by
|
||||
* click or any other action, that region is set as the focused region. Optionally, focus can be set on mount. This
|
||||
* is useful for components like the image viewer.
|
||||
* - `useIsRegionFocused` is a hook that returns a boolean indicating if a specific region is focused.
|
||||
* - `useFocusRegionWatcher` is a hook that listens for focus events on the window. When an element is focused, it
|
||||
* checks if it is part of a focus region and sets that region as the focused region.
|
||||
*/
|
||||
|
||||
//
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
/**
|
||||
* The names of the focus regions.
|
||||
*/
|
||||
type FocusRegionName = 'gallery' | 'layers' | 'canvas' | 'workflows' | 'viewer';
|
||||
|
||||
/**
|
||||
* A map of focus regions to the elements that are part of that region.
|
||||
*/
|
||||
const REGION_TARGETS: Record<FocusRegionName, Set<HTMLElement>> = {
|
||||
gallery: new Set<HTMLElement>(),
|
||||
layers: new Set<HTMLElement>(),
|
||||
canvas: new Set<HTMLElement>(),
|
||||
workflows: new Set<HTMLElement>(),
|
||||
viewer: new Set<HTMLElement>(),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* The currently-focused region or `null` if no region is focused.
|
||||
*/
|
||||
const $focusedRegion = atom<FocusRegionName | null>(null);
|
||||
|
||||
/**
|
||||
* A map of focus regions to atoms that indicate if that region is focused.
|
||||
*/
|
||||
const FOCUS_REGIONS = objectKeys(REGION_TARGETS).reduce(
|
||||
(acc, region) => {
|
||||
acc[`$${region}`] = computed($focusedRegion, (focusedRegion) => focusedRegion === region);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<`$${FocusRegionName}`, Atom<boolean>>
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the focused region, logging a trace level message.
|
||||
*/
|
||||
const setFocus = (region: FocusRegionName | null) => {
|
||||
$focusedRegion.set(region);
|
||||
log.trace(`Focus changed: ${region}`);
|
||||
};
|
||||
|
||||
type UseFocusRegionOptions = {
|
||||
focusOnMount?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers an element as part of a focus region. When that element is focused, by click or any other action, that
|
||||
* region is set as the focused region. Optionally, focus can be set on mount.
|
||||
*
|
||||
* On unmount, if the element is the last element in the region and the region is focused, the focused region is set to
|
||||
* `null`.
|
||||
*
|
||||
* @param region The focus region name.
|
||||
* @param ref The ref of the element to register.
|
||||
* @param options The options.
|
||||
*/
|
||||
export const useFocusRegion = (
|
||||
region: FocusRegionName,
|
||||
ref: RefObject<HTMLElement>,
|
||||
options?: UseFocusRegionOptions
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { focusOnMount = false } = { focusOnMount: false, ...options };
|
||||
|
||||
const element = ref.current;
|
||||
|
||||
REGION_TARGETS[region].add(element);
|
||||
|
||||
if (focusOnMount) {
|
||||
setFocus(region);
|
||||
}
|
||||
|
||||
return () => {
|
||||
REGION_TARGETS[region].delete(element);
|
||||
|
||||
if (REGION_TARGETS[region].size === 0 && $focusedRegion.get() === region) {
|
||||
setFocus(null);
|
||||
}
|
||||
};
|
||||
}, [options, ref, region]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating if a specific region is focused.
|
||||
* @param region The focus region name.
|
||||
*/
|
||||
export const useIsRegionFocused = (region: FocusRegionName) => {
|
||||
return useStore(FOCUS_REGIONS[`$${region}`]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Listens for focus events on the window. When an element is focused, it checks if it is part of a focus region and sets
|
||||
* that region as the focused region. The region corresponding to the deepest element is set.
|
||||
*/
|
||||
const onFocus = (_: FocusEvent) => {
|
||||
const activeElement = document.activeElement;
|
||||
if (!(activeElement instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const regionCandidates: { region: FocusRegionName; element: HTMLElement }[] = [];
|
||||
|
||||
for (const region of objectKeys(REGION_TARGETS)) {
|
||||
for (const element of REGION_TARGETS[region]) {
|
||||
if (element.contains(activeElement)) {
|
||||
regionCandidates.push({ region, element });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (regionCandidates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by the shallowest element
|
||||
regionCandidates.sort((a, b) => {
|
||||
if (b.element.contains(a.element)) {
|
||||
return -1;
|
||||
}
|
||||
if (a.element.contains(b.element)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Set the region of the deepest element
|
||||
const focusedRegion = regionCandidates[0]?.region;
|
||||
|
||||
if (!focusedRegion) {
|
||||
log.warn('No focused region found');
|
||||
return;
|
||||
}
|
||||
|
||||
setFocus(focusedRegion);
|
||||
};
|
||||
|
||||
/**
|
||||
* Listens for focus events on the window. When an element is focused, it checks if it is part of a focus region and sets
|
||||
* that region as the focused region. This is a singleton.
|
||||
*/
|
||||
export const useFocusRegionWatcher = () => {
|
||||
useAssertSingleton('useFocusRegionWatcher');
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', onFocus, { capture: true });
|
||||
return () => {
|
||||
window.removeEventListener('focus', onFocus, { capture: true });
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -1,158 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { objectKeys } from 'common/util/objectKeys';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { Atom } from 'nanostores';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
const _INTERACTION_SCOPES = ['gallery', 'canvas', 'stagingArea', 'workflows', 'imageViewer'] as const;
|
||||
|
||||
type InteractionScope = (typeof _INTERACTION_SCOPES)[number];
|
||||
|
||||
export const $activeScopes = atom<Set<InteractionScope>>(new Set());
|
||||
|
||||
type InteractionScopeData = {
|
||||
targets: Set<HTMLElement>;
|
||||
$isActive: Atom<boolean>;
|
||||
};
|
||||
|
||||
export const INTERACTION_SCOPES: Record<InteractionScope, InteractionScopeData> = _INTERACTION_SCOPES.reduce(
|
||||
(acc, region) => {
|
||||
acc[region] = {
|
||||
targets: new Set(),
|
||||
$isActive: computed($activeScopes, (activeScopes) => activeScopes.has(region)),
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<InteractionScope, InteractionScopeData>
|
||||
);
|
||||
|
||||
const formatScopes = (interactionScopes: Set<InteractionScope>) => {
|
||||
if (interactionScopes.size === 0) {
|
||||
return 'none';
|
||||
}
|
||||
return Array.from(interactionScopes).join(', ');
|
||||
};
|
||||
|
||||
export const addScope = (scope: InteractionScope) => {
|
||||
const currentScopes = $activeScopes.get();
|
||||
if (currentScopes.has(scope)) {
|
||||
return;
|
||||
}
|
||||
const newScopes = new Set(currentScopes);
|
||||
newScopes.add(scope);
|
||||
$activeScopes.set(newScopes);
|
||||
log.trace(`Added scope ${scope}: ${formatScopes($activeScopes.get())}`);
|
||||
};
|
||||
|
||||
export const removeScope = (scope: InteractionScope) => {
|
||||
const currentScopes = $activeScopes.get();
|
||||
if (!currentScopes.has(scope)) {
|
||||
return;
|
||||
}
|
||||
const newScopes = new Set(currentScopes);
|
||||
newScopes.delete(scope);
|
||||
$activeScopes.set(newScopes);
|
||||
log.trace(`Removed scope ${scope}: ${formatScopes($activeScopes.get())}`);
|
||||
};
|
||||
|
||||
export const setScopes = (scopes: InteractionScope[]) => {
|
||||
const newScopes = new Set(scopes);
|
||||
$activeScopes.set(newScopes);
|
||||
log.trace(`Set scopes: ${formatScopes($activeScopes.get())}`);
|
||||
};
|
||||
|
||||
export const useScopeOnFocus = (scope: InteractionScope, ref: RefObject<HTMLElement>) => {
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
INTERACTION_SCOPES[scope].targets.add(element);
|
||||
|
||||
return () => {
|
||||
INTERACTION_SCOPES[scope].targets.delete(element);
|
||||
};
|
||||
}, [ref, scope]);
|
||||
};
|
||||
|
||||
type UseScopeOnMountOptions = {
|
||||
mount?: boolean;
|
||||
unmount?: boolean;
|
||||
};
|
||||
|
||||
const defaultUseScopeOnMountOptions: UseScopeOnMountOptions = {
|
||||
mount: true,
|
||||
unmount: true,
|
||||
};
|
||||
|
||||
export const useScopeOnMount = (scope: InteractionScope, options?: UseScopeOnMountOptions) => {
|
||||
useEffect(() => {
|
||||
const { mount, unmount } = { ...defaultUseScopeOnMountOptions, ...options };
|
||||
|
||||
if (mount) {
|
||||
addScope(scope);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (unmount) {
|
||||
removeScope(scope);
|
||||
}
|
||||
};
|
||||
}, [options, scope]);
|
||||
};
|
||||
|
||||
export const useScopeImperativeApi = (scope: InteractionScope) => {
|
||||
const api = useMemo(() => {
|
||||
return {
|
||||
add: () => {
|
||||
addScope(scope);
|
||||
},
|
||||
remove: () => {
|
||||
removeScope(scope);
|
||||
},
|
||||
};
|
||||
}, [scope]);
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
const handleFocusEvent = (_event: FocusEvent) => {
|
||||
const activeElement = document.activeElement;
|
||||
if (!(activeElement instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newActiveScopes = new Set<InteractionScope>();
|
||||
|
||||
for (const scope of objectKeys(INTERACTION_SCOPES)) {
|
||||
for (const element of INTERACTION_SCOPES[scope].targets) {
|
||||
if (element.contains(activeElement)) {
|
||||
newActiveScopes.add(scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const oldActiveScopes = $activeScopes.get();
|
||||
if (!isEqual(oldActiveScopes, newActiveScopes)) {
|
||||
$activeScopes.set(newActiveScopes);
|
||||
log.trace(`Scopes changed: ${formatScopes($activeScopes.get())}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const useScopeFocusWatcher = () => {
|
||||
useAssertSingleton('useScopeFocusWatcher');
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', handleFocusEvent, true);
|
||||
return () => {
|
||||
window.removeEventListener('focus', handleFocusEvent, true);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
|
||||
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
@@ -71,8 +70,6 @@ export const useGlobalHotkeys = () => {
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('canvas'));
|
||||
addScope('canvas');
|
||||
removeScope('workflows');
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
@@ -82,8 +79,6 @@ export const useGlobalHotkeys = () => {
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('upscaling'));
|
||||
removeScope('canvas');
|
||||
removeScope('workflows');
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
@@ -93,8 +88,6 @@ export const useGlobalHotkeys = () => {
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('workflows'));
|
||||
removeScope('canvas');
|
||||
addScope('workflows');
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
@@ -104,7 +97,6 @@ export const useGlobalHotkeys = () => {
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('models'));
|
||||
setScopes([]);
|
||||
},
|
||||
options: {
|
||||
enabled: isModelManagerEnabled,
|
||||
@@ -117,7 +109,6 @@ export const useGlobalHotkeys = () => {
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('queue'));
|
||||
setScopes([]);
|
||||
},
|
||||
dependencies: [dispatch, isModelManagerEnabled],
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MenuGroup, MenuItem } from '@invoke-ai/ui-library';
|
||||
import { CanvasContextMenuItemsCropCanvasToBbox } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuItemsCropCanvasToBbox';
|
||||
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
|
||||
import {
|
||||
useNewControlLayerFromBbox,
|
||||
@@ -25,6 +26,9 @@ export const CanvasContextMenuGlobalMenuItems = memo(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuGroup title={t('controlLayers.canvasContextMenu.canvasGroup')}>
|
||||
<CanvasContextMenuItemsCropCanvasToBbox />
|
||||
</MenuGroup>
|
||||
<MenuGroup title={t('controlLayers.canvasContextMenu.saveToGalleryGroup')}>
|
||||
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveCanvasToGallery}>
|
||||
{t('controlLayers.canvasContextMenu.saveCanvasToGallery')}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCropBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasContextMenuItemsCropCanvasToBbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const canvasManager = useCanvasManager();
|
||||
const cropCanvasToBbox = useCallback(async () => {
|
||||
const adapters = canvasManager.getAllAdapters();
|
||||
for (const adapter of adapters) {
|
||||
await adapter.cropToBbox();
|
||||
}
|
||||
}, [canvasManager]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiCropBold />} isDisabled={isBusy} onClick={cropCanvasToBbox}>
|
||||
{t('controlLayers.canvasContextMenu.cropCanvasToBbox')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasContextMenuItemsCropCanvasToBbox.displayName = 'CanvasContextMenuItemsCropCanvasToBbox';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuGroup } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
|
||||
@@ -28,6 +29,7 @@ const CanvasContextMenuSelectedEntityMenuItemsContent = memo(() => {
|
||||
{isTransformableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsTransform />}
|
||||
{isSaveableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsCopyToClipboard />}
|
||||
{isSaveableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsSave />}
|
||||
{isTransformableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsCropToBbox />}
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</MenuGroup>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from 'features/dnd/types';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const addRasterLayerFromImageDropData: AddRasterLayerFromImageDropData = {
|
||||
id: 'add-raster-layer-from-image-drop-data',
|
||||
@@ -30,6 +31,7 @@ const addGlobalReferenceImageFromImageDropData: AddGlobalReferenceImageFromImage
|
||||
};
|
||||
|
||||
export const CanvasDropArea = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
if (imageViewer.isOpen) {
|
||||
@@ -49,16 +51,28 @@ export const CanvasDropArea = memo(() => {
|
||||
pointerEvents="none"
|
||||
>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable dropLabel="New Raster Layer" data={addRasterLayerFromImageDropData} />
|
||||
<IAIDroppable
|
||||
dropLabel={t('controlLayers.canvasContextMenu.newRasterLayer')}
|
||||
data={addRasterLayerFromImageDropData}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable dropLabel="New Control Layer" data={addControlLayerFromImageDropData} />
|
||||
<IAIDroppable
|
||||
dropLabel={t('controlLayers.canvasContextMenu.newControlLayer')}
|
||||
data={addControlLayerFromImageDropData}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable dropLabel="New Regional Reference Image" data={addRegionalReferenceImageFromImageDropData} />
|
||||
<IAIDroppable
|
||||
dropLabel={t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
|
||||
data={addRegionalReferenceImageFromImageDropData}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable dropLabel="New Global Reference Image" data={addGlobalReferenceImageFromImageDropData} />
|
||||
<IAIDroppable
|
||||
dropLabel={t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
|
||||
data={addGlobalReferenceImageFromImageDropData}
|
||||
/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFocusRegion } from 'common/hooks/focus';
|
||||
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
|
||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
||||
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||
import { memo } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
|
||||
export const CanvasLayersPanelContent = memo(() => {
|
||||
const hasEntities = useAppSelector(selectHasEntities);
|
||||
const layersPanelFocusRef = useRef<HTMLDivElement>(null);
|
||||
useFocusRegion('layers', layersPanelFocusRef);
|
||||
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||
<EntityListSelectedEntityActionBar />
|
||||
<Divider py={0} />
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
{hasEntities && <CanvasEntityList />}
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
<Flex ref={layersPanelFocusRef} flexDir="column" gap={2} w="full" h="full">
|
||||
<EntityListSelectedEntityActionBar />
|
||||
<Divider py={0} />
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
{hasEntities && <CanvasEntityList />}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ContextMenu, Flex, MenuList } from '@invoke-ai/ui-library';
|
||||
import { ContextMenu, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { useFocusRegion } from 'common/hooks/focus';
|
||||
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
|
||||
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
|
||||
import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
|
||||
@@ -18,6 +18,18 @@ import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/Canva
|
||||
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { GatedImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||
|
||||
const MenuContent = () => {
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<MenuList>
|
||||
<CanvasContextMenuGlobalMenuItems />
|
||||
<CanvasContextMenuSelectedEntityMenuItems />
|
||||
</MenuList>
|
||||
</CanvasManagerProviderGate>
|
||||
);
|
||||
};
|
||||
|
||||
export const CanvasMainPanelContent = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -25,17 +37,10 @@ export const CanvasMainPanelContent = memo(() => {
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
|
||||
const renderMenu = useCallback(() => {
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<MenuList>
|
||||
<CanvasContextMenuGlobalMenuItems />
|
||||
<CanvasContextMenuSelectedEntityMenuItems />
|
||||
</MenuList>
|
||||
</CanvasManagerProviderGate>
|
||||
);
|
||||
return <MenuContent />;
|
||||
}, []);
|
||||
|
||||
useScopeOnFocus('canvas', ref);
|
||||
useFocusRegion('canvas', ref);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -53,7 +58,7 @@ export const CanvasMainPanelContent = memo(() => {
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasToolbar />
|
||||
</CanvasManagerProviderGate>
|
||||
<ContextMenu<HTMLDivElement> renderMenu={renderMenu}>
|
||||
<ContextMenu<HTMLDivElement> renderMenu={renderMenu} withLongPress={false}>
|
||||
{(ref) => (
|
||||
<Flex
|
||||
ref={ref}
|
||||
@@ -75,6 +80,12 @@ export const CanvasMainPanelContent = memo(() => {
|
||||
<CanvasAlertsPreserveMask />
|
||||
<CanvasAlertsSendingToGallery />
|
||||
</Flex>
|
||||
<Flex position="absolute" top={1} insetInlineEnd={1}>
|
||||
<Menu>
|
||||
<MenuButton as={IconButton} icon={<PiDotsThreeOutlineVerticalFill />} colorScheme="base" />
|
||||
<MenuContent />
|
||||
</Menu>
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useDndContext } from '@dnd-kit/core';
|
||||
import { Box, Button, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import {
|
||||
$canvasRightPanelTabIndex,
|
||||
selectCanvasRightPanelGalleryTab,
|
||||
@@ -18,9 +18,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasRightPanel = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const tabIndex = useStore($canvasRightPanelTabIndex);
|
||||
useScopeOnFocus('gallery', ref);
|
||||
const imageViewer = useImageViewer();
|
||||
const onClickViewerToggleButton = useCallback(() => {
|
||||
if ($canvasRightPanelTabIndex.get() !== 1) {
|
||||
@@ -46,7 +44,9 @@ export const CanvasRightPanel = memo(() => {
|
||||
</TabList>
|
||||
<TabPanels w="full" h="full">
|
||||
<TabPanel w="full" h="full" p={0} pt={3}>
|
||||
<CanvasLayersPanelContent />
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasLayersPanelContent />
|
||||
</CanvasManagerProviderGate>
|
||||
</TabPanel>
|
||||
<TabPanel w="full" h="full" p={0} pt={3}>
|
||||
<GalleryPanelContent />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
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 { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
@@ -20,6 +21,7 @@ export const ControlLayerMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsCopyToClipboard />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, ButtonGroup, Flex, FormControl, FormLabel, Heading, Spacer, Switch } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { FilterSettings } from 'features/controlLayers/components/Filters/FilterSettings';
|
||||
import { FilterTypeSelect } from 'features/controlLayers/components/Filters/FilterTypeSelect';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
@@ -14,118 +15,142 @@ import {
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import type { FilterConfig } from 'features/controlLayers/store/filters';
|
||||
import { IMAGE_FILTERS } from 'features/controlLayers/store/filters';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiShootingStarBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
const FilterBox = memo(({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const config = useStore(adapter.filterer.$filterConfig);
|
||||
const isProcessing = useStore(adapter.filterer.$isProcessing);
|
||||
const hasProcessed = useStore(adapter.filterer.$hasProcessed);
|
||||
const autoProcessFilter = useAppSelector(selectAutoProcessFilter);
|
||||
const isolatedFilteringPreview = useAppSelector(selectIsolatedFilteringPreview);
|
||||
const onChangeIsolatedPreview = useCallback(() => {
|
||||
dispatch(settingsIsolatedFilteringPreviewToggled());
|
||||
}, [dispatch]);
|
||||
const FilterContent = memo(
|
||||
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useFocusRegion('canvas', ref, { focusOnMount: true });
|
||||
|
||||
const onChangeFilterConfig = useCallback(
|
||||
(filterConfig: FilterConfig) => {
|
||||
adapter.filterer.$filterConfig.set(filterConfig);
|
||||
},
|
||||
[adapter.filterer.$filterConfig]
|
||||
);
|
||||
const config = useStore(adapter.filterer.$filterConfig);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
const isProcessing = useStore(adapter.filterer.$isProcessing);
|
||||
const hasProcessed = useStore(adapter.filterer.$hasProcessed);
|
||||
const autoProcessFilter = useAppSelector(selectAutoProcessFilter);
|
||||
const isolatedFilteringPreview = useAppSelector(selectIsolatedFilteringPreview);
|
||||
const onChangeIsolatedPreview = useCallback(() => {
|
||||
dispatch(settingsIsolatedFilteringPreviewToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
const onChangeFilterType = useCallback(
|
||||
(filterType: FilterConfig['type']) => {
|
||||
adapter.filterer.$filterConfig.set(IMAGE_FILTERS[filterType].buildDefaults());
|
||||
},
|
||||
[adapter.filterer.$filterConfig]
|
||||
);
|
||||
const onChangeFilterConfig = useCallback(
|
||||
(filterConfig: FilterConfig) => {
|
||||
adapter.filterer.$filterConfig.set(filterConfig);
|
||||
},
|
||||
[adapter.filterer.$filterConfig]
|
||||
);
|
||||
|
||||
const onChangeAutoProcessFilter = useCallback(() => {
|
||||
dispatch(settingsAutoProcessFilterToggled());
|
||||
}, [dispatch]);
|
||||
const onChangeFilterType = useCallback(
|
||||
(filterType: FilterConfig['type']) => {
|
||||
adapter.filterer.$filterConfig.set(IMAGE_FILTERS[filterType].buildDefaults());
|
||||
},
|
||||
[adapter.filterer.$filterConfig]
|
||||
);
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
return IMAGE_FILTERS[config.type].validateConfig?.(config as never) ?? true;
|
||||
}, [config]);
|
||||
const onChangeAutoProcessFilter = useCallback(() => {
|
||||
dispatch(settingsAutoProcessFilterToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
bg="base.800"
|
||||
borderRadius="base"
|
||||
p={4}
|
||||
flexDir="column"
|
||||
gap={4}
|
||||
w={420}
|
||||
h="auto"
|
||||
shadow="dark-lg"
|
||||
transitionProperty="height"
|
||||
transitionDuration="normal"
|
||||
>
|
||||
<Flex w="full" gap={4}>
|
||||
<Heading size="md" color="base.300" userSelect="none">
|
||||
{t('controlLayers.filter.filter')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>{t('controlLayers.filter.autoProcess')}</FormLabel>
|
||||
<Switch size="sm" isChecked={autoProcessFilter} onChange={onChangeAutoProcessFilter} />
|
||||
</FormControl>
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>{t('controlLayers.settings.isolatedPreview')}</FormLabel>
|
||||
<Switch size="sm" isChecked={isolatedFilteringPreview} onChange={onChangeIsolatedPreview} />
|
||||
</FormControl>
|
||||
const isValid = useMemo(() => {
|
||||
return IMAGE_FILTERS[config.type].validateConfig?.(config as never) ?? true;
|
||||
}, [config]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'applyFilter',
|
||||
category: 'canvas',
|
||||
callback: adapter.filterer.apply,
|
||||
options: { enabled: !isProcessing && isCanvasFocused },
|
||||
dependencies: [adapter.filterer, isProcessing, isCanvasFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'cancelFilter',
|
||||
category: 'canvas',
|
||||
callback: adapter.filterer.cancel,
|
||||
options: { enabled: !isProcessing && isCanvasFocused },
|
||||
dependencies: [adapter.filterer, isProcessing, isCanvasFocused],
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={ref}
|
||||
bg="base.800"
|
||||
borderRadius="base"
|
||||
p={4}
|
||||
flexDir="column"
|
||||
gap={4}
|
||||
w={420}
|
||||
h="auto"
|
||||
shadow="dark-lg"
|
||||
transitionProperty="height"
|
||||
transitionDuration="normal"
|
||||
>
|
||||
<Flex w="full" gap={4}>
|
||||
<Heading size="md" color="base.300" userSelect="none">
|
||||
{t('controlLayers.filter.filter')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>{t('controlLayers.filter.autoProcess')}</FormLabel>
|
||||
<Switch size="sm" isChecked={autoProcessFilter} onChange={onChangeAutoProcessFilter} />
|
||||
</FormControl>
|
||||
<FormControl w="min-content">
|
||||
<FormLabel m={0}>{t('controlLayers.settings.isolatedPreview')}</FormLabel>
|
||||
<Switch size="sm" isChecked={isolatedFilteringPreview} onChange={onChangeIsolatedPreview} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<FilterTypeSelect filterType={config.type} onChange={onChangeFilterType} />
|
||||
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
|
||||
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiShootingStarBold />}
|
||||
onClick={adapter.filterer.processImmediate}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.process')}
|
||||
isDisabled={!isValid || autoProcessFilter}
|
||||
>
|
||||
{t('controlLayers.filter.process')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
leftIcon={<PiArrowsCounterClockwiseBold />}
|
||||
onClick={adapter.filterer.reset}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.reset')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('controlLayers.filter.reset')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiCheckBold />}
|
||||
onClick={adapter.filterer.apply}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.apply')}
|
||||
isDisabled={!isValid || !hasProcessed}
|
||||
>
|
||||
{t('controlLayers.filter.apply')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiXBold />}
|
||||
onClick={adapter.filterer.cancel}
|
||||
loadingText={t('controlLayers.filter.cancel')}
|
||||
>
|
||||
{t('controlLayers.filter.cancel')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
<FilterTypeSelect filterType={config.type} onChange={onChangeFilterType} />
|
||||
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
|
||||
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiShootingStarBold />}
|
||||
onClick={adapter.filterer.processImmediate}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.process')}
|
||||
isDisabled={!isValid || autoProcessFilter}
|
||||
>
|
||||
{t('controlLayers.filter.process')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
leftIcon={<PiArrowsCounterClockwiseBold />}
|
||||
onClick={adapter.filterer.reset}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.reset')}
|
||||
variant="ghost"
|
||||
>
|
||||
{t('controlLayers.filter.reset')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiCheckBold />}
|
||||
onClick={adapter.filterer.apply}
|
||||
isLoading={isProcessing}
|
||||
loadingText={t('controlLayers.filter.apply')}
|
||||
isDisabled={!isValid || !hasProcessed}
|
||||
>
|
||||
{t('controlLayers.filter.apply')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiXBold />}
|
||||
onClick={adapter.filterer.cancel}
|
||||
loadingText={t('controlLayers.filter.cancel')}
|
||||
>
|
||||
{t('controlLayers.filter.cancel')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FilterBox.displayName = 'FilterBox';
|
||||
FilterContent.displayName = 'FilterContent';
|
||||
|
||||
export const Filter = () => {
|
||||
const canvasManager = useCanvasManager();
|
||||
@@ -134,7 +159,7 @@ export const Filter = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <FilterBox adapter={adapter} />;
|
||||
return <FilterContent adapter={adapter} />;
|
||||
};
|
||||
|
||||
Filter.displayName = 'Filter';
|
||||
|
||||
@@ -49,7 +49,16 @@ export const IPAdapterImagePreview = memo(({ image, onChangeImage, droppableData
|
||||
}, [handleResetControlImage, isConnected, isErrorControlImage]);
|
||||
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" alignItems="center">
|
||||
<Flex
|
||||
position="relative"
|
||||
w="full"
|
||||
h="full"
|
||||
alignItems="center"
|
||||
borderColor="error.500"
|
||||
borderStyle="solid"
|
||||
borderWidth={controlImage ? 0 : 1}
|
||||
borderRadius="base"
|
||||
>
|
||||
<IAIDndImage
|
||||
draggableData={draggableData}
|
||||
droppableData={droppableData}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
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 { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
@@ -12,6 +13,7 @@ export const InpaintMaskMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
|
||||
import {
|
||||
selectCanvasRightPanelGalleryTab,
|
||||
selectCanvasRightPanelLayersTab,
|
||||
} from 'features/controlLayers/store/ephemeral';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import {
|
||||
selectSystemShouldConfirmOnNewSession,
|
||||
shouldConfirmOnNewSessionToggled,
|
||||
} from 'features/system/store/systemSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const [useNewGallerySessionDialog] = buildUseBoolean(false);
|
||||
const [useNewCanvasSessionDialog] = buildUseBoolean(false);
|
||||
|
||||
export const useNewGallerySession = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const imageViewer = useImageViewer();
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const newSessionDialog = useNewGallerySessionDialog();
|
||||
|
||||
const newGallerySessionImmediate = useCallback(() => {
|
||||
dispatch(newGallerySessionRequested());
|
||||
imageViewer.open();
|
||||
selectCanvasRightPanelGalleryTab();
|
||||
}, [dispatch, imageViewer]);
|
||||
|
||||
const newGallerySessionWithDialog = useCallback(() => {
|
||||
if (shouldConfirmOnNewSession) {
|
||||
newSessionDialog.setTrue();
|
||||
return;
|
||||
}
|
||||
newGallerySessionImmediate();
|
||||
}, [newGallerySessionImmediate, newSessionDialog, shouldConfirmOnNewSession]);
|
||||
|
||||
return { newGallerySessionImmediate, newGallerySessionWithDialog };
|
||||
};
|
||||
|
||||
export const useNewCanvasSession = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const imageViewer = useImageViewer();
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const newSessionDialog = useNewCanvasSessionDialog();
|
||||
|
||||
const newCanvasSessionImmediate = useCallback(() => {
|
||||
dispatch(newCanvasSessionRequested());
|
||||
imageViewer.close();
|
||||
selectCanvasRightPanelLayersTab();
|
||||
}, [dispatch, imageViewer]);
|
||||
|
||||
const newCanvasSessionWithDialog = useCallback(() => {
|
||||
if (shouldConfirmOnNewSession) {
|
||||
newSessionDialog.setTrue();
|
||||
return;
|
||||
}
|
||||
|
||||
newCanvasSessionImmediate();
|
||||
}, [newCanvasSessionImmediate, newSessionDialog, shouldConfirmOnNewSession]);
|
||||
|
||||
return { newCanvasSessionImmediate, newCanvasSessionWithDialog };
|
||||
};
|
||||
|
||||
export const NewGallerySessionDialog = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const dialog = useNewGallerySessionDialog();
|
||||
const { newGallerySessionImmediate } = useNewGallerySession();
|
||||
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const onToggleConfirm = useCallback(() => {
|
||||
dispatch(shouldConfirmOnNewSessionToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
isOpen={dialog.isTrue}
|
||||
onClose={dialog.setFalse}
|
||||
title={t('controlLayers.newGallerySession')}
|
||||
acceptCallback={newGallerySessionImmediate}
|
||||
acceptButtonText={t('common.ok')}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex direction="column" gap={3}>
|
||||
<Text>{t('controlLayers.newGallerySessionDesc')}</Text>
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
<FormControl>
|
||||
<FormLabel>{t('common.dontAskMeAgain')}</FormLabel>
|
||||
<Checkbox isChecked={!shouldConfirmOnNewSession} onChange={onToggleConfirm} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</ConfirmationAlertDialog>
|
||||
);
|
||||
});
|
||||
|
||||
NewGallerySessionDialog.displayName = 'NewGallerySessionDialog';
|
||||
|
||||
export const NewCanvasSessionDialog = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const dialog = useNewCanvasSessionDialog();
|
||||
const { newCanvasSessionImmediate } = useNewCanvasSession();
|
||||
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const onToggleConfirm = useCallback(() => {
|
||||
dispatch(shouldConfirmOnNewSessionToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
isOpen={dialog.isTrue}
|
||||
onClose={dialog.setFalse}
|
||||
title={t('controlLayers.newCanvasSession')}
|
||||
acceptCallback={newCanvasSessionImmediate}
|
||||
acceptButtonText={t('common.ok')}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex direction="column" gap={3}>
|
||||
<Text>{t('controlLayers.newCanvasSessionDesc')}</Text>
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
<FormControl>
|
||||
<FormLabel>{t('common.dontAskMeAgain')}</FormLabel>
|
||||
<Checkbox isChecked={!shouldConfirmOnNewSession} onChange={onToggleConfirm} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</ConfirmationAlertDialog>
|
||||
);
|
||||
});
|
||||
|
||||
NewCanvasSessionDialog.displayName = 'NewCanvasSessionDialog';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
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 { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
@@ -18,6 +19,7 @@ export const RasterLayerMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsCopyToClipboard />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
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 { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
@@ -17,6 +18,7 @@ export const RegionalGuidanceMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
|
||||
@@ -26,10 +26,16 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={addRegionalGuidancePositivePrompt} isDisabled={!validActions.canAddPositivePrompt || isBusy}>
|
||||
<MenuItem
|
||||
onPointerUp={addRegionalGuidancePositivePrompt}
|
||||
isDisabled={!validActions.canAddPositivePrompt || isBusy}
|
||||
>
|
||||
{t('controlLayers.addPositivePrompt')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={addRegionalGuidanceNegativePrompt} isDisabled={!validActions.canAddNegativePrompt || isBusy}>
|
||||
<MenuItem
|
||||
onPointerUp={addRegionalGuidanceNegativePrompt}
|
||||
isDisabled={!validActions.canAddNegativePrompt || isBusy}
|
||||
>
|
||||
{t('controlLayers.addNegativePrompt')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={addRegionalGuidanceIPAdapter} isDisabled={isBusy}>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { CanvasSettingsIsolatedTransformingPreviewSwitch } from 'features/contro
|
||||
import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo';
|
||||
import { CanvasSettingsOutputOnlyMaskedRegionsCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsOutputOnlyMaskedRegionsCheckbox';
|
||||
import { CanvasSettingsPreserveMaskCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsPreserveMaskCheckbox';
|
||||
import { CanvasSettingsPressureSensitivityCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsPressureSensitivity';
|
||||
import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton';
|
||||
import { CanvasSettingsShowHUDSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsShowHUDSwitch';
|
||||
import { CanvasSettingsShowProgressOnCanvas } from 'features/controlLayers/components/Settings/CanvasSettingsShowProgressOnCanvasSwitch';
|
||||
@@ -50,6 +51,7 @@ export const CanvasSettingsPopover = memo(() => {
|
||||
<CanvasSettingsClipToBboxCheckbox />
|
||||
<CanvasSettingsOutputOnlyMaskedRegionsCheckbox />
|
||||
<CanvasSettingsSnapToGridCheckbox />
|
||||
<CanvasSettingsPressureSensitivityCheckbox />
|
||||
<CanvasSettingsShowProgressOnCanvas />
|
||||
<CanvasSettingsIsolatedStagingPreviewSwitch />
|
||||
<CanvasSettingsIsolatedFilteringPreviewSwitch />
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
selectPressureSensitivity,
|
||||
settingsPressureSensitivityToggled,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasSettingsPressureSensitivityCheckbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const pressureSensitivity = useAppSelector(selectPressureSensitivity);
|
||||
const onChange = useCallback<ChangeEventHandler<HTMLInputElement>>(() => {
|
||||
dispatch(settingsPressureSensitivityToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FormControl w="full">
|
||||
<FormLabel flexGrow={1}>{t('controlLayers.settings.pressureSensitivity')}</FormLabel>
|
||||
<Checkbox isChecked={pressureSensitivity} onChange={onChange} />
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasSettingsPressureSensitivityCheckbox.displayName = 'CanvasSettingsPressureSensitivityCheckbox';
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ButtonGroup } from '@invoke-ai/ui-library';
|
||||
import { useScopeOnMount } from 'common/hooks/interactionScopes';
|
||||
import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton';
|
||||
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
|
||||
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
|
||||
@@ -11,8 +10,6 @@ import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayer
|
||||
import { memo } from 'react';
|
||||
|
||||
export const StagingAreaToolbar = memo(() => {
|
||||
useScopeOnMount('stagingArea');
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import {
|
||||
@@ -25,7 +25,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -50,9 +50,9 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
|
||||
acceptSelected,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
|
||||
},
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
[isCanvasFocused, shouldShowStagedImage, imageCount]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import {
|
||||
selectImageCount,
|
||||
@@ -17,7 +17,7 @@ export const StagingAreaToolbarNextButton = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -30,9 +30,9 @@ export const StagingAreaToolbarNextButton = memo(() => {
|
||||
selectNext,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
|
||||
},
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
[isCanvasFocused, shouldShowStagedImage, imageCount]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import {
|
||||
selectImageCount,
|
||||
@@ -17,7 +17,7 @@ export const StagingAreaToolbarPrevButton = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -30,9 +30,9 @@ export const StagingAreaToolbarPrevButton = memo(() => {
|
||||
selectPrev,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasFocused && shouldShowStagedImage && imageCount > 1,
|
||||
},
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
[isCanvasFocused, shouldShowStagedImage, imageCount]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolBboxButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.bbox')} (C)`}
|
||||
tooltip={`${t('controlLayers.tool.bbox')} (C)`}
|
||||
icon={<PiBoundingBoxBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectBbox}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.bbox')} (C)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.bbox')} (C)`}
|
||||
icon={<PiBoundingBoxBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectBbox}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolBrushButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.brush')} (B)`}
|
||||
tooltip={`${t('controlLayers.tool.brush')} (B)`}
|
||||
icon={<PiPaintBrushBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectBrush}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.brush')} (B)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.brush')} (B)`}
|
||||
icon={<PiPaintBrushBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectBrush}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ToolViewButton } from './ToolViewButton';
|
||||
export const ToolChooser: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup isAttached>
|
||||
<ButtonGroup isAttached orientation="vertical">
|
||||
<ToolBrushButton />
|
||||
<ToolEraserButton />
|
||||
<ToolRectButton />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolColorPickerButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.colorPicker')} (I)`}
|
||||
tooltip={`${t('controlLayers.tool.colorPicker')} (I)`}
|
||||
icon={<PiEyedropperBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectColorPicker}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.colorPicker')} (I)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.colorPicker')} (I)`}
|
||||
icon={<PiEyedropperBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectColorPicker}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolEraserButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.eraser')} (E)`}
|
||||
tooltip={`${t('controlLayers.tool.eraser')} (E)`}
|
||||
icon={<PiEraserBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectEraser}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.eraser')} (E)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.eraser')} (E)`}
|
||||
icon={<PiEraserBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectEraser}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolMoveButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.move')} (V)`}
|
||||
tooltip={`${t('controlLayers.tool.move')} (V)`}
|
||||
icon={<PiCursorBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectMove}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.move')} (V)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.move')} (V)`}
|
||||
icon={<PiCursorBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectMove}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolRectButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.rectangle')} (U)`}
|
||||
tooltip={`${t('controlLayers.tool.rectangle')} (U)`}
|
||||
icon={<PiRectangleBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectRect}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.rectangle')} (U)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.rectangle')} (U)`}
|
||||
icon={<PiRectangleBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectRect}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolViewButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.view')} (H)`}
|
||||
tooltip={`${t('controlLayers.tool.view')} (H)`}
|
||||
icon={<PiHandBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectView}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.view')} (H)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.view')} (H)`}
|
||||
icon={<PiHandBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectView}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Divider, Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
|
||||
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
||||
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
|
||||
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
|
||||
import { CanvasToolbarFitBboxToLayersButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton';
|
||||
@@ -31,7 +30,6 @@ export const CanvasToolbar = memo(() => {
|
||||
|
||||
return (
|
||||
<Flex w="full" gap={2} alignItems="center">
|
||||
<ToolChooser />
|
||||
<ToolColorPicker />
|
||||
<ToolSettings />
|
||||
<Spacer />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -11,50 +10,50 @@ import { PiArrowsOutBold } from 'react-icons/pi';
|
||||
export const CanvasToolbarResetViewButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const canvasManager = useCanvasManager();
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'fitLayersToCanvas',
|
||||
category: 'canvas',
|
||||
callback: canvasManager.stage.fitLayersToStage,
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'fitBboxToCanvas',
|
||||
category: 'canvas',
|
||||
callback: canvasManager.stage.fitBboxToStage,
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'setZoomTo100Percent',
|
||||
category: 'canvas',
|
||||
callback: () => canvasManager.stage.setScale(1),
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'setZoomTo200Percent',
|
||||
category: 'canvas',
|
||||
callback: () => canvasManager.stage.setScale(2),
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'setZoomTo400Percent',
|
||||
category: 'canvas',
|
||||
callback: () => canvasManager.stage.setScale(4),
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
id: 'setZoomTo800Percent',
|
||||
category: 'canvas',
|
||||
callback: () => canvasManager.stage.setScale(8),
|
||||
options: { enabled: isCanvasActive && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasActive, imageViewer.isOpen],
|
||||
options: { enabled: isCanvasFocused && !imageViewer.isOpen, preventDefault: true },
|
||||
dependencies: [isCanvasFocused, imageViewer.isOpen],
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,27 +1,49 @@
|
||||
import { Button, ButtonGroup, Flex, FormControl, FormLabel, Heading, Spacer, Switch } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
|
||||
import {
|
||||
selectIsolatedTransformingPreview,
|
||||
settingsIsolatedTransformingPreviewToggled,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsCounterClockwiseBold, PiArrowsOutBold, PiCheckBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
||||
const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useFocusRegion('canvas', ref, { focusOnMount: true });
|
||||
const isCanvasFocused = useIsRegionFocused('canvas');
|
||||
const isProcessing = useStore(adapter.transformer.$isProcessing);
|
||||
const isolatedTransformingPreview = useAppSelector(selectIsolatedTransformingPreview);
|
||||
const onChangeIsolatedPreview = useCallback(() => {
|
||||
dispatch(settingsIsolatedTransformingPreviewToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'applyTransform',
|
||||
category: 'canvas',
|
||||
callback: adapter.transformer.applyTransform,
|
||||
options: { enabled: !isProcessing && isCanvasFocused },
|
||||
dependencies: [adapter.transformer, isProcessing, isCanvasFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'cancelTransform',
|
||||
category: 'canvas',
|
||||
callback: adapter.transformer.stopTransform,
|
||||
options: { enabled: !isProcessing && isCanvasFocused },
|
||||
dependencies: [adapter.transformer, isProcessing, isCanvasFocused],
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={ref}
|
||||
bg="base.800"
|
||||
borderRadius="base"
|
||||
p={4}
|
||||
@@ -86,7 +108,7 @@ const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
||||
);
|
||||
});
|
||||
|
||||
TransformBox.displayName = 'Transform';
|
||||
TransformContent.displayName = 'TransformContent';
|
||||
|
||||
export const Transform = () => {
|
||||
const canvasManager = useCanvasManager();
|
||||
@@ -96,5 +118,5 @@ export const Transform = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TransformBox adapter={adapter} />;
|
||||
return <TransformContent adapter={adapter} />;
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export const CanvasEntityDeleteButton = memo(() => {
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
icon={<PiTrashSimpleFill />}
|
||||
onClick={onClick}
|
||||
onPointerUp={onClick}
|
||||
colorScheme="error"
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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 { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCropBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityMenuItemsCropToBbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const onClick = useCallback(() => {
|
||||
if (!adapter) {
|
||||
return;
|
||||
}
|
||||
adapter.cropToBbox();
|
||||
}, [adapter]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiCropBold />} isDisabled={!isInteractable}>
|
||||
{t('controlLayers.cropLayerToBbox')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityMenuItemsCropToBbox.displayName = 'CanvasEntityMenuItemsCropToBbox';
|
||||
@@ -18,6 +18,7 @@ import type { CanvasEntityIdentifier, CanvasRenderableEntityState, Rect } from '
|
||||
import Konva from 'konva';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import stableHash from 'stable-hash';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
@@ -295,6 +296,11 @@ export abstract class CanvasEntityAdapterBase<
|
||||
return stableHash(arg);
|
||||
};
|
||||
|
||||
cropToBbox = (): Promise<ImageDTO> => {
|
||||
const { rect } = this.manager.stateApi.getBbox();
|
||||
return this.renderer.rasterize({ rect, replaceObjects: true, attrs: { opacity: 1, filters: [] } });
|
||||
};
|
||||
|
||||
destroy = (): void => {
|
||||
this.log.debug('Destroying module');
|
||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEnt
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLine';
|
||||
import { CanvasObjectBrushLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLineWithPressure';
|
||||
import { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLine';
|
||||
import { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
|
||||
import type { AnyObjectRenderer, AnyObjectState } from 'features/controlLayers/konva/CanvasObject/types';
|
||||
@@ -113,6 +115,15 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'brush_line_with_pressure') {
|
||||
assert(this.renderer instanceof CanvasObjectBrushLineWithPressure || !this.renderer);
|
||||
|
||||
if (!this.renderer) {
|
||||
this.renderer = new CanvasObjectBrushLineWithPressure(this.state, this);
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'eraser_line') {
|
||||
assert(this.renderer instanceof CanvasObjectEraserLine || !this.renderer);
|
||||
@@ -122,6 +133,15 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'eraser_line_with_pressure') {
|
||||
assert(this.renderer instanceof CanvasObjectEraserLineWithPressure || !this.renderer);
|
||||
|
||||
if (!this.renderer) {
|
||||
this.renderer = new CanvasObjectEraserLineWithPressure(this.state, this);
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'rect') {
|
||||
assert(this.renderer instanceof CanvasObjectRect || !this.renderer);
|
||||
@@ -205,14 +225,18 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
|
||||
|
||||
if (pushToState) {
|
||||
const entityIdentifier = this.parent.entityIdentifier;
|
||||
if (this.state.type === 'brush_line') {
|
||||
this.manager.stateApi.addBrushLine({ entityIdentifier, brushLine: this.state });
|
||||
} else if (this.state.type === 'eraser_line') {
|
||||
this.manager.stateApi.addEraserLine({ entityIdentifier, eraserLine: this.state });
|
||||
} else if (this.state.type === 'rect') {
|
||||
this.manager.stateApi.addRect({ entityIdentifier, rect: this.state });
|
||||
} else {
|
||||
this.log.warn({ buffer: this.state }, 'Invalid buffer object type');
|
||||
switch (this.state.type) {
|
||||
case 'brush_line':
|
||||
case 'brush_line_with_pressure':
|
||||
this.manager.stateApi.addBrushLine({ entityIdentifier, brushLine: this.state });
|
||||
break;
|
||||
case 'eraser_line':
|
||||
case 'eraser_line_with_pressure':
|
||||
this.manager.stateApi.addEraserLine({ entityIdentifier, eraserLine: this.state });
|
||||
break;
|
||||
case 'rect':
|
||||
this.manager.stateApi.addRect({ entityIdentifier, rect: this.state });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEnt
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLine';
|
||||
import { CanvasObjectBrushLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLineWithPressure';
|
||||
import { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLine';
|
||||
import { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
|
||||
import type { AnyObjectRenderer, AnyObjectState } from 'features/controlLayers/konva/CanvasObject/types';
|
||||
@@ -285,6 +287,16 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'brush_line_with_pressure') {
|
||||
assert(renderer instanceof CanvasObjectBrushLineWithPressure || !renderer);
|
||||
|
||||
if (!renderer) {
|
||||
renderer = new CanvasObjectBrushLineWithPressure(objectState, this);
|
||||
this.renderers.set(renderer.id, renderer);
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'eraser_line') {
|
||||
assert(renderer instanceof CanvasObjectEraserLine || !renderer);
|
||||
@@ -295,6 +307,16 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'eraser_line_with_pressure') {
|
||||
assert(renderer instanceof CanvasObjectEraserLineWithPressure || !renderer);
|
||||
|
||||
if (!renderer) {
|
||||
renderer = new CanvasObjectEraserLineWithPressure(objectState, this);
|
||||
this.renderers.set(renderer.id, renderer);
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'rect') {
|
||||
assert(renderer instanceof CanvasObjectRect || !renderer);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer';
|
||||
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getSVGPathDataFromPoints } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasBrushLineWithPressureState } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
export class CanvasObjectBrushLineWithPressure extends CanvasModuleBase {
|
||||
readonly type = 'object_brush_line_with_pressure';
|
||||
readonly id: string;
|
||||
readonly path: string[];
|
||||
readonly parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer;
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
state: CanvasBrushLineWithPressureState;
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
line: Konva.Path;
|
||||
};
|
||||
|
||||
constructor(
|
||||
state: CanvasBrushLineWithPressureState,
|
||||
parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer
|
||||
) {
|
||||
super();
|
||||
const { id, clip } = state;
|
||||
this.id = id;
|
||||
this.parent = parent;
|
||||
this.manager = parent.manager;
|
||||
this.path = this.manager.buildPath(this);
|
||||
this.log = this.manager.buildLogger(this);
|
||||
|
||||
this.log.debug({ state }, 'Creating module');
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({
|
||||
name: `${this.type}:group`,
|
||||
clip,
|
||||
listening: false,
|
||||
}),
|
||||
line: new Konva.Path({
|
||||
name: `${this.type}:path`,
|
||||
listening: false,
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation: 'source-over',
|
||||
}),
|
||||
};
|
||||
this.konva.group.add(this.konva.line);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
update(state: CanvasBrushLineWithPressureState, force = false): boolean {
|
||||
if (force || this.state !== state) {
|
||||
this.log.trace({ state }, 'Updating brush line with pressure');
|
||||
const { points, color, strokeWidth } = state;
|
||||
this.konva.line.setAttrs({
|
||||
data: getSVGPathDataFromPoints(points, {
|
||||
size: strokeWidth / 2,
|
||||
simulatePressure: false,
|
||||
last: true,
|
||||
thinning: 1,
|
||||
}),
|
||||
fill: rgbaColorToString(color),
|
||||
});
|
||||
this.state = state;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setVisibility(isVisible: boolean): void {
|
||||
this.log.trace({ isVisible }, 'Setting brush line visibility');
|
||||
this.konva.group.visible(isVisible);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.log.debug('Destroying module');
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
|
||||
repr = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
path: this.path,
|
||||
parent: this.parent.id,
|
||||
state: deepClone(this.state),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer';
|
||||
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getSVGPathDataFromPoints } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasEraserLineWithPressureState } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
export class CanvasObjectEraserLineWithPressure extends CanvasModuleBase {
|
||||
readonly type = 'object_eraser_line_with_pressure';
|
||||
readonly id: string;
|
||||
readonly path: string[];
|
||||
readonly parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer;
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
state: CanvasEraserLineWithPressureState;
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
line: Konva.Path;
|
||||
};
|
||||
|
||||
constructor(
|
||||
state: CanvasEraserLineWithPressureState,
|
||||
parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer
|
||||
) {
|
||||
super();
|
||||
const { id, clip } = state;
|
||||
this.id = id;
|
||||
this.parent = parent;
|
||||
this.manager = parent.manager;
|
||||
this.path = this.manager.buildPath(this);
|
||||
this.log = this.manager.buildLogger(this);
|
||||
|
||||
this.log.debug({ state }, 'Creating module');
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({
|
||||
name: `${this.type}:group`,
|
||||
clip,
|
||||
listening: false,
|
||||
}),
|
||||
line: new Konva.Path({
|
||||
name: `${this.type}:path`,
|
||||
listening: false,
|
||||
fill: 'red', // Eraser lines use compositing, does not matter what color they have
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation: 'destination-out',
|
||||
}),
|
||||
};
|
||||
this.konva.group.add(this.konva.line);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
update(state: CanvasEraserLineWithPressureState, force = false): boolean {
|
||||
if (force || this.state !== state) {
|
||||
this.log.trace({ state }, 'Updating eraser line with pressure');
|
||||
const { points, strokeWidth } = state;
|
||||
this.konva.line.setAttrs({
|
||||
data: getSVGPathDataFromPoints(points, {
|
||||
size: strokeWidth / 2,
|
||||
simulatePressure: false,
|
||||
last: true,
|
||||
thinning: 1,
|
||||
}),
|
||||
});
|
||||
this.state = state;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setVisibility(isVisible: boolean): void {
|
||||
this.log.trace({ isVisible }, 'Setting eraser line visibility');
|
||||
this.konva.group.visible(isVisible);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.log.debug('Destroying module');
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
|
||||
repr = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
path: this.path,
|
||||
parent: this.parent.id,
|
||||
state: deepClone(this.state),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLine';
|
||||
import type { CanvasObjectBrushLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLineWithPressure';
|
||||
import type { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLine';
|
||||
import type { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
|
||||
import type { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import type { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
|
||||
import type {
|
||||
CanvasBrushLineState,
|
||||
CanvasBrushLineWithPressureState,
|
||||
CanvasEraserLineState,
|
||||
CanvasEraserLineWithPressureState,
|
||||
CanvasImageState,
|
||||
CanvasRectState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
@@ -15,9 +19,17 @@ import type {
|
||||
|
||||
export type AnyObjectRenderer =
|
||||
| CanvasObjectBrushLine
|
||||
| CanvasObjectBrushLineWithPressure
|
||||
| CanvasObjectEraserLine
|
||||
| CanvasObjectEraserLineWithPressure
|
||||
| CanvasObjectRect
|
||||
| CanvasObjectImage; /**
|
||||
* Union of all object states.
|
||||
*/
|
||||
export type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState;
|
||||
export type AnyObjectState =
|
||||
| CanvasBrushLineState
|
||||
| CanvasBrushLineWithPressureState
|
||||
| CanvasEraserLineState
|
||||
| CanvasEraserLineWithPressureState
|
||||
| CanvasImageState
|
||||
| CanvasRectState;
|
||||
|
||||
@@ -95,6 +95,9 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
this.container.style.touchAction = 'none';
|
||||
this.container.style.userSelect = 'none';
|
||||
this.container.style.webkitUserSelect = 'none';
|
||||
this.konva.stage.container(this.container);
|
||||
this.setResizeObserver();
|
||||
this.fitStageToContainer();
|
||||
@@ -103,9 +106,17 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
this.konva.stage.on('dragmove', this.onStageDragMove);
|
||||
this.konva.stage.on('dragend', this.onStageDragEnd);
|
||||
|
||||
// Start dragging the stage when the middle mouse button is clicked. We do not need to listen for 'pointerdown' to
|
||||
// do cleanup - that is done in onStageDragEnd.
|
||||
this.konva.stage.on('pointerdown', this.onStagePointerDown);
|
||||
|
||||
this.subscriptions.add(() => this.konva.stage.off('wheel', this.onStageMouseWheel));
|
||||
this.subscriptions.add(() => this.konva.stage.off('dragmove', this.onStageDragMove));
|
||||
this.subscriptions.add(() => this.konva.stage.off('dragend', this.onStageDragEnd));
|
||||
|
||||
// Whenever the tool changes, we should stop dragging the stage. For example, user is MMB-dragging the stage, then
|
||||
// switches to the brush tool, we should stop dragging the stage.
|
||||
this.subscriptions.add(this.manager.tool.$tool.listen(this.stopDragging));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -285,6 +296,46 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStagePointerDown = (e: KonvaEventObject<PointerEvent>) => {
|
||||
// If the middle mouse button is clicked and we are not already dragging, start dragging the stage
|
||||
if (e.evt.button === 1) {
|
||||
this.startDragging();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Forcibly starts dragging the stage. This is useful when you want to start dragging the stage programmatically.
|
||||
*/
|
||||
startDragging = () => {
|
||||
// First make sure the stage is draggable
|
||||
this.setIsDraggable(true);
|
||||
|
||||
// Then start dragging the stage if it's not already being dragged
|
||||
if (!this.konva.stage.isDragging()) {
|
||||
this.konva.stage.startDrag();
|
||||
}
|
||||
|
||||
// And render the tool to update the cursor
|
||||
this.manager.tool.render();
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops dragging the stage. This is useful when you want to stop dragging the stage programmatically.
|
||||
*/
|
||||
stopDragging = () => {
|
||||
// Now that we have stopped the current drag event, we may need to revert the stage's draggable status, depending
|
||||
// on the current tool
|
||||
this.setIsDraggable(this.manager.tool.$tool.get() === 'view');
|
||||
|
||||
// Stop dragging the stage if it's being dragged
|
||||
if (this.konva.stage.isDragging()) {
|
||||
this.konva.stage.stopDrag();
|
||||
}
|
||||
|
||||
// And render the tool to update the cursor
|
||||
this.manager.tool.render();
|
||||
};
|
||||
|
||||
onStageDragMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||
if (e.target !== this.konva.stage) {
|
||||
return;
|
||||
@@ -297,8 +348,8 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
if (e.target !== this.konva.stage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncStageAttrs();
|
||||
// Do some cleanup when the stage is no longer being dragged
|
||||
this.stopDragging();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,11 +15,16 @@ type CanvasToolBrushConfig = {
|
||||
* The outer border color for the brush tool preview.
|
||||
*/
|
||||
BORDER_OUTER_COLOR: string;
|
||||
/**
|
||||
* The number of milliseconds to wait before hiding the brush preview's fill circle after the mouse is released.
|
||||
*/
|
||||
HIDE_FILL_TIMEOUT_MS: number;
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: CanvasToolBrushConfig = {
|
||||
BORDER_INNER_COLOR: 'rgba(0,0,0,1)',
|
||||
BORDER_OUTER_COLOR: 'rgba(255,255,255,0.8)',
|
||||
HIDE_FILL_TIMEOUT_MS: 1500, // same as Affinity
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -34,6 +39,7 @@ export class CanvasToolBrush extends CanvasModuleBase {
|
||||
readonly log: Logger;
|
||||
|
||||
config: CanvasToolBrushConfig = DEFAULT_CONFIG;
|
||||
hideFillTimeoutId: number | null = null;
|
||||
|
||||
/**
|
||||
* The Konva objects that make up the brush tool preview:
|
||||
@@ -85,18 +91,40 @@ export class CanvasToolBrush extends CanvasModuleBase {
|
||||
};
|
||||
this.konva.group.add(this.konva.fillCircle, this.konva.innerBorder, this.konva.outerBorder);
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const cursorPos = this.manager.tool.$cursorPos.get();
|
||||
const tool = this.parent.$tool.get();
|
||||
|
||||
// If the cursor position is not available, do not update the brush preview. The tool module will handle visiblity.
|
||||
if (!cursorPos) {
|
||||
if (tool !== 'brush') {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.parent.$cursorPos.get();
|
||||
const canDraw = this.parent.getCanDraw();
|
||||
|
||||
if (!cursorPos || !canDraw) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isMouseDown = this.parent.$isMouseDown.get();
|
||||
const lastPointerType = this.parent.$lastPointerType.get();
|
||||
|
||||
if (lastPointerType !== 'mouse' && isMouseDown) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setVisibility(true);
|
||||
|
||||
if (this.hideFillTimeoutId !== null) {
|
||||
window.clearTimeout(this.hideFillTimeoutId);
|
||||
this.hideFillTimeoutId = null;
|
||||
}
|
||||
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
const brushPreviewFill = this.manager.stateApi.getBrushPreviewColor();
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, settings.brushWidth);
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos.relative, settings.brushWidth);
|
||||
const radius = settings.brushWidth / 2;
|
||||
|
||||
// The circle is scaled
|
||||
@@ -105,6 +133,7 @@ export class CanvasToolBrush extends CanvasModuleBase {
|
||||
y: alignedCursorPos.y,
|
||||
radius,
|
||||
fill: rgbaColorToString(brushPreviewFill),
|
||||
visible: !isMouseDown && lastPointerType === 'mouse',
|
||||
});
|
||||
|
||||
// But the borders are in screen-pixels
|
||||
@@ -112,17 +141,22 @@ export class CanvasToolBrush extends CanvasModuleBase {
|
||||
const twoPixels = this.manager.stage.unscale(2);
|
||||
|
||||
this.konva.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
x: cursorPos.relative.x,
|
||||
y: cursorPos.relative.y,
|
||||
innerRadius: radius,
|
||||
outerRadius: radius + onePixel,
|
||||
});
|
||||
this.konva.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
x: cursorPos.relative.x,
|
||||
y: cursorPos.relative.y,
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
|
||||
this.hideFillTimeoutId = window.setTimeout(() => {
|
||||
this.konva.fillCircle.visible(false);
|
||||
this.hideFillTimeoutId = null;
|
||||
}, this.config.HIDE_FILL_TIMEOUT_MS);
|
||||
};
|
||||
|
||||
setVisibility = (visible: boolean) => {
|
||||
|
||||
@@ -190,13 +190,25 @@ export class CanvasToolColorPicker extends CanvasModuleBase {
|
||||
* Renders the color picker tool preview on the canvas.
|
||||
*/
|
||||
render = () => {
|
||||
const cursorPos = this.manager.tool.$cursorPos.get();
|
||||
const tool = this.parent.$tool.get();
|
||||
|
||||
// If the cursor position is not available, do not render the preview. The tool module will handle visibility.
|
||||
if (!cursorPos) {
|
||||
if (tool !== 'colorPicker') {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.parent.$cursorPos.get();
|
||||
const canDraw = this.parent.getCanDraw();
|
||||
|
||||
if (!cursorPos || tool !== 'colorPicker' || !canDraw) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setVisibility(true);
|
||||
|
||||
const { x, y } = cursorPos.relative;
|
||||
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
const colorUnderCursor = this.parent.$colorUnderCursor.get();
|
||||
const colorPickerInnerRadius = this.manager.stage.unscale(this.config.RING_INNER_RADIUS);
|
||||
@@ -205,28 +217,28 @@ export class CanvasToolColorPicker extends CanvasModuleBase {
|
||||
const twoPixels = this.manager.stage.unscale(2);
|
||||
|
||||
this.konva.ringCandidateColor.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
x,
|
||||
y,
|
||||
fill: rgbColorToString(colorUnderCursor),
|
||||
innerRadius: colorPickerInnerRadius,
|
||||
outerRadius: colorPickerOuterRadius,
|
||||
});
|
||||
this.konva.ringCurrentColor.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
x,
|
||||
y,
|
||||
fill: rgbColorToString(settings.color),
|
||||
innerRadius: colorPickerInnerRadius,
|
||||
outerRadius: colorPickerOuterRadius,
|
||||
});
|
||||
this.konva.ringInnerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
x,
|
||||
y,
|
||||
innerRadius: colorPickerOuterRadius,
|
||||
outerRadius: colorPickerOuterRadius + onePixel,
|
||||
});
|
||||
this.konva.ringOuterBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
x,
|
||||
y,
|
||||
innerRadius: colorPickerOuterRadius + onePixel,
|
||||
outerRadius: colorPickerOuterRadius + twoPixels,
|
||||
});
|
||||
@@ -239,35 +251,35 @@ export class CanvasToolColorPicker extends CanvasModuleBase {
|
||||
);
|
||||
this.konva.crosshairNorthOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
|
||||
points: [x, y - size, x, y - space],
|
||||
});
|
||||
this.konva.crosshairNorthInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
|
||||
points: [x, y - size, x, y - space],
|
||||
});
|
||||
this.konva.crosshairEastOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
|
||||
points: [x + space, y, x + size, y],
|
||||
});
|
||||
this.konva.crosshairEastInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
|
||||
points: [x + space, y, x + size, y],
|
||||
});
|
||||
this.konva.crosshairSouthOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
|
||||
points: [x, y + space, x, y + size],
|
||||
});
|
||||
this.konva.crosshairSouthInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
|
||||
points: [x, y + space, x, y + size],
|
||||
});
|
||||
this.konva.crosshairWestOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
|
||||
points: [x - space, y, x - size, y],
|
||||
});
|
||||
this.konva.crosshairWestInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
|
||||
points: [x - space, y, x - size, y],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -78,14 +78,33 @@ export class CanvasToolEraser extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const cursorPos = this.manager.tool.$cursorPos.get();
|
||||
const tool = this.parent.$tool.get();
|
||||
|
||||
if (!cursorPos) {
|
||||
if (tool !== 'eraser') {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.parent.$cursorPos.get();
|
||||
const canDraw = this.parent.getCanDraw();
|
||||
|
||||
if (!cursorPos || !canDraw) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isMouseDown = this.parent.$isMouseDown.get();
|
||||
const lastPointerType = this.parent.$lastPointerType.get();
|
||||
|
||||
if (lastPointerType !== 'mouse' && isMouseDown) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setVisibility(true);
|
||||
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, settings.eraserWidth);
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos.relative, settings.eraserWidth);
|
||||
const radius = settings.eraserWidth / 2;
|
||||
|
||||
// The circle is scaled
|
||||
@@ -100,14 +119,14 @@ export class CanvasToolEraser extends CanvasModuleBase {
|
||||
const twoPixels = this.manager.stage.unscale(2);
|
||||
|
||||
this.konva.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
x: cursorPos.relative.x,
|
||||
y: cursorPos.relative.y,
|
||||
innerRadius: radius,
|
||||
outerRadius: radius + onePixel,
|
||||
});
|
||||
this.konva.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
x: cursorPos.relative.x,
|
||||
y: cursorPos.relative.y,
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
|
||||
@@ -7,11 +7,12 @@ import {
|
||||
alignCoordForTool,
|
||||
calculateNewBrushSizeFromWheelDelta,
|
||||
floorCoord,
|
||||
getColorAtCoordinate,
|
||||
getIsPrimaryMouseDown,
|
||||
getLastPointOfLastLine,
|
||||
getLastPointOfLastLineWithPressure,
|
||||
getLastPointOfLine,
|
||||
getPrefixedId,
|
||||
getScaledCursorPosition,
|
||||
isDistanceMoreThanMin,
|
||||
offsetCoord,
|
||||
} from 'features/controlLayers/konva/util';
|
||||
@@ -26,12 +27,23 @@ import type {
|
||||
RgbColor,
|
||||
Tool,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { isRenderableEntity, RGBA_BLACK } from 'features/controlLayers/store/types';
|
||||
import { RGBA_BLACK } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { atom } from 'nanostores';
|
||||
import rafThrottle from 'raf-throttle';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
// Konva's docs say the default drag buttons are [0], but it's actually [0,1]. We only want left-click to drag, so we
|
||||
// need to override the default. The stage handles middle-mouse dragging on its own with dedicated event listeners.
|
||||
// TODO(psyche): Fix the docs upstream!
|
||||
Konva.dragButtons = [0];
|
||||
|
||||
// Typo insurance
|
||||
const KEY_ESCAPE = 'Escape';
|
||||
const KEY_SPACE = ' ';
|
||||
const KEY_ALT = 'Alt';
|
||||
|
||||
type CanvasToolModuleConfig = {
|
||||
BRUSH_SPACING_TARGET_SCALE: number;
|
||||
};
|
||||
@@ -71,11 +83,16 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
/**
|
||||
* The last cursor position.
|
||||
*/
|
||||
$cursorPos = atom<Coordinate | null>(null);
|
||||
$cursorPos = atom<{ relative: Coordinate; absolute: Coordinate } | null>(null);
|
||||
/**
|
||||
* The color currently under the cursor. Only has a value when the color picker tool is active.
|
||||
*/
|
||||
$colorUnderCursor = atom<RgbColor>(RGBA_BLACK);
|
||||
/**
|
||||
* The last pointer type that was used on the stage. This is used to determine if we should show a tool preview. For
|
||||
* example, when using a pen, we should not show a brush preview.
|
||||
*/
|
||||
$lastPointerType = atom<string | null>(null);
|
||||
|
||||
konva: {
|
||||
stage: Konva.Stage;
|
||||
@@ -136,11 +153,13 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
|
||||
syncCursorStyle = () => {
|
||||
const stage = this.manager.stage;
|
||||
const isMouseDown = this.$isMouseDown.get();
|
||||
const tool = this.$tool.get();
|
||||
const isStageDragging = this.manager.stage.konva.stage.isDragging();
|
||||
|
||||
if (tool === 'view') {
|
||||
stage.setCursor(isMouseDown ? 'grabbing' : 'grab');
|
||||
if (tool === 'view' && !isStageDragging) {
|
||||
stage.setCursor('grab');
|
||||
} else if (this.manager.stage.konva.stage.isDragging()) {
|
||||
stage.setCursor('grabbing');
|
||||
} else if (this.manager.stateApi.$isTransforming.get()) {
|
||||
stage.setCursor('default');
|
||||
} else if (this.manager.stateApi.$isFiltering.get()) {
|
||||
@@ -165,69 +184,43 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const stage = this.manager.stage;
|
||||
const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
const cursorPos = this.$cursorPos.get();
|
||||
const tool = this.$tool.get();
|
||||
const isFiltering = this.manager.stateApi.$isFiltering.get();
|
||||
const isStaging = this.manager.stagingArea.$isStaging.get();
|
||||
|
||||
const isDrawable =
|
||||
!!selectedEntity &&
|
||||
selectedEntity.state.isEnabled &&
|
||||
!selectedEntity.state.isLocked &&
|
||||
isRenderableEntity(selectedEntity.state);
|
||||
const isStageDragging = this.manager.stage.konva.stage.isDragging();
|
||||
|
||||
this.syncCursorStyle();
|
||||
|
||||
stage.setIsDraggable(tool === 'view');
|
||||
|
||||
if (!cursorPos || renderedEntityCount === 0 || isFiltering || isStaging) {
|
||||
// We can bail early if the mouse isn't over the stage or there are no layers
|
||||
/**
|
||||
* The tool should not be rendered when:
|
||||
* - There is no cursor position (i.e. the cursor is outside of the stage)
|
||||
* - The user is filtering, in which case the user is not allowed to use the tools. Note that we do not disable
|
||||
* the group while transforming, bc that requires use of the move tool.
|
||||
* - The canvas is staging, in which case the user is not allowed to use the tools.
|
||||
* - There are no entities rendered on the canvas. Maybe we should allow the user to draw on an empty canvas,
|
||||
* creating a new layer when they start?
|
||||
* - The stage is being dragged, in which case the user is not allowed to use the tools.
|
||||
*/
|
||||
if (!cursorPos || isFiltering || isStaging || renderedEntityCount === 0 || isStageDragging) {
|
||||
this.konva.group.visible(false);
|
||||
} else {
|
||||
this.konva.group.visible(true);
|
||||
|
||||
// No need to render the brush preview if the cursor position or color is missing
|
||||
if (cursorPos && tool === 'brush') {
|
||||
this.brushToolPreview.render();
|
||||
} else if (cursorPos && tool === 'eraser') {
|
||||
this.eraserToolPreview.render();
|
||||
} else if (cursorPos && tool === 'colorPicker') {
|
||||
this.colorPickerToolPreview.render();
|
||||
}
|
||||
|
||||
this.setToolVisibility(tool, isDrawable);
|
||||
this.brushToolPreview.render();
|
||||
this.eraserToolPreview.render();
|
||||
this.colorPickerToolPreview.render();
|
||||
}
|
||||
};
|
||||
|
||||
syncLastCursorPos = (): Coordinate | null => {
|
||||
const pos = getScaledCursorPosition(this.konva.stage);
|
||||
this.$cursorPos.set(pos);
|
||||
return pos;
|
||||
};
|
||||
syncCursorPositions = () => {
|
||||
const relative = this.konva.stage.getRelativePointerPosition();
|
||||
const absolute = this.konva.stage.getPointerPosition();
|
||||
|
||||
getColorUnderCursor = (): RgbColor | null => {
|
||||
const pos = this.konva.stage.getPointerPosition();
|
||||
if (!pos) {
|
||||
return null;
|
||||
}
|
||||
const ctx = this.konva.stage
|
||||
.toCanvas({ x: pos.x, y: pos.y, width: 1, height: 1, imageSmoothingEnabled: false })
|
||||
.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
return null;
|
||||
if (!relative || !absolute) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [r, g, b, _a] = ctx.getImageData(0, 0, 1, 1).data;
|
||||
|
||||
if (r === undefined || g === undefined || b === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { r, g, b };
|
||||
this.$cursorPos.set({ relative, absolute });
|
||||
};
|
||||
|
||||
getClip = (
|
||||
@@ -257,11 +250,14 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
setEventListeners = (): (() => void) => {
|
||||
this.konva.stage.on('mouseenter', this.onStageMouseEnter);
|
||||
this.konva.stage.on('mousedown', this.onStageMouseDown);
|
||||
this.konva.stage.on('mouseup', this.onStageMouseUp);
|
||||
this.konva.stage.on('mousemove', this.onStageMouseMove);
|
||||
this.konva.stage.on('mouseleave', this.onStageMouseLeave);
|
||||
this.konva.stage.on('pointerenter', this.onStagePointerEnter);
|
||||
this.konva.stage.on('pointerdown', this.onStagePointerDown);
|
||||
this.konva.stage.on('pointerup', this.onStagePointerUp);
|
||||
this.konva.stage.on('pointermove', this.onStagePointerMove);
|
||||
|
||||
// The Konva stage doesn't appear to handle pointerleave events, so we need to listen to the container instead
|
||||
this.manager.stage.container.addEventListener('pointerleave', this.onStagePointerLeave);
|
||||
|
||||
this.konva.stage.on('wheel', this.onStageMouseWheel);
|
||||
|
||||
window.addEventListener('keydown', this.onKeyDown);
|
||||
@@ -270,13 +266,15 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
window.addEventListener('blur', this.onWindowBlur);
|
||||
|
||||
return () => {
|
||||
this.konva.stage.off('mouseenter', this.onStageMouseEnter);
|
||||
this.konva.stage.off('mousedown', this.onStageMouseDown);
|
||||
this.konva.stage.off('mouseup', this.onStageMouseUp);
|
||||
this.konva.stage.off('mousemove', this.onStageMouseMove);
|
||||
this.konva.stage.off('mouseleave', this.onStageMouseLeave);
|
||||
this.konva.stage.off('pointerenter', this.onStagePointerEnter);
|
||||
this.konva.stage.off('pointerdown', this.onStagePointerDown);
|
||||
this.konva.stage.off('pointerup', this.onStagePointerUp);
|
||||
this.konva.stage.off('pointermove', this.onStagePointerMove);
|
||||
|
||||
this.manager.stage.container.removeEventListener('pointerleave', this.onStagePointerLeave);
|
||||
|
||||
this.konva.stage.off('wheel', this.onStageMouseWheel);
|
||||
|
||||
window.removeEventListener('keydown', this.onKeyDown);
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
window.removeEventListener('pointerup', this.onWindowPointerUp);
|
||||
@@ -287,28 +285,42 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
getCanDraw = (): boolean => {
|
||||
if (this.manager.stateApi.getRenderedEntityCount() === 0) {
|
||||
return false;
|
||||
} else if (this.manager.$isBusy.get()) {
|
||||
return false;
|
||||
} else if (!this.manager.stateApi.getSelectedEntityAdapter()?.$isInteractable.get()) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.manager.$isBusy.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (!selectedEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!selectedEntity.$isInteractable.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
onStageMouseEnter = async (_: KonvaEventObject<MouseEvent>) => {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
onStagePointerEnter = async (e: KonvaEventObject<PointerEvent>) => {
|
||||
try {
|
||||
this.$lastPointerType.set(e.evt.pointerType);
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncCursorPositions();
|
||||
const cursorPos = this.$cursorPos.get();
|
||||
|
||||
const isMouseDown = this.$isMouseDown.get();
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
const tool = this.$tool.get();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (!cursorPos || !isMouseDown || !selectedEntity?.state.isEnabled || selectedEntity.state.isLocked) {
|
||||
if (!cursorPos || !isMouseDown || !selectedEntity?.$isInteractable.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -318,32 +330,53 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
if (tool === 'brush') {
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
const normalizedPoint = offsetCoord(cursorPos.relative, selectedEntity.state.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line_with_pressure'),
|
||||
type: 'brush_line_with_pressure',
|
||||
points: [alignedPoint.x, alignedPoint.y, e.evt.pressure],
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (tool === 'eraser') {
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
const normalizedPoint = offsetCoord(cursorPos.relative, selectedEntity.state.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
if (selectedEntity.bufferRenderer.state && selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line_with_pressure'),
|
||||
type: 'eraser_line_with_pressure',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
@@ -351,66 +384,80 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStageMouseDown = async (e: KonvaEventObject<MouseEvent>) => {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$isMouseDown.set(getIsPrimaryMouseDown(e));
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
|
||||
onStagePointerDown = async (e: KonvaEventObject<PointerEvent>) => {
|
||||
try {
|
||||
this.$lastPointerType.set(e.evt.pointerType);
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isMouseDown = getIsPrimaryMouseDown(e);
|
||||
this.$isMouseDown.set(isMouseDown);
|
||||
|
||||
const cursorPos = this.$cursorPos.get();
|
||||
const tool = this.$tool.get();
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
|
||||
if (tool === 'colorPicker') {
|
||||
const color = this.getColorUnderCursor();
|
||||
if (color) {
|
||||
this.manager.stateApi.setColor({ ...settings.color, ...color });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isMouseDown = this.$isMouseDown.get();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (!cursorPos || !isMouseDown || !selectedEntity?.state.isEnabled || selectedEntity?.state.isLocked) {
|
||||
if (!cursorPos || !isMouseDown || !selectedEntity?.$isInteractable.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
const normalizedPoint = offsetCoord(cursorPos.relative, selectedEntity.state.position);
|
||||
|
||||
if (tool === 'brush') {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'brush_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
const lastLinePoint = getLastPointOfLastLineWithPressure(
|
||||
selectedEntity.state.objects,
|
||||
'brush_line_with_pressure'
|
||||
);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [
|
||||
// The last point of the last line is already normalized to the entity's coordinates
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [
|
||||
lastLinePoint.x,
|
||||
lastLinePoint.y,
|
||||
lastLinePoint.pressure,
|
||||
alignedPoint.x,
|
||||
alignedPoint.y,
|
||||
],
|
||||
e.evt.pressure,
|
||||
];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y, e.evt.pressure];
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line_with_pressure'),
|
||||
type: 'brush_line_with_pressure',
|
||||
points,
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'brush_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [lastLinePoint.x, lastLinePoint.y, alignedPoint.x, alignedPoint.y];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y];
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
points,
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
@@ -419,34 +466,56 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
if (tool === 'eraser') {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'eraser_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
const lastLinePoint = getLastPointOfLastLineWithPressure(
|
||||
selectedEntity.state.objects,
|
||||
'eraser_line_with_pressure'
|
||||
);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [
|
||||
// The last point of the last line is already normalized to the entity's coordinates
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [
|
||||
lastLinePoint.x,
|
||||
lastLinePoint.y,
|
||||
lastLinePoint.pressure,
|
||||
alignedPoint.x,
|
||||
alignedPoint.y,
|
||||
],
|
||||
e.evt.pressure,
|
||||
];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y, e.evt.pressure];
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line_with_pressure'),
|
||||
type: 'eraser_line_with_pressure',
|
||||
points,
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'eraser_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [lastLinePoint.x, lastLinePoint.y, alignedPoint.x, alignedPoint.y];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y];
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
points,
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
@@ -469,26 +538,36 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStageMouseUp = (_: KonvaEventObject<MouseEvent>) => {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
onStagePointerUp = (e: KonvaEventObject<PointerEvent>) => {
|
||||
try {
|
||||
this.$isMouseDown.set(false);
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
if (!cursorPos) {
|
||||
return;
|
||||
}
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked;
|
||||
if (!isDrawable) {
|
||||
this.$lastPointerType.set(e.evt.pointerType);
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tool = this.$tool.get();
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
|
||||
if (tool === 'colorPicker') {
|
||||
const color = this.$colorUnderCursor.get();
|
||||
if (color) {
|
||||
this.manager.stateApi.setColor({ ...settings.color, ...color });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
if (!selectedEntity?.$isInteractable.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tool === 'brush') {
|
||||
if (selectedEntity.bufferRenderer.state?.type === 'brush_line' && selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
if (
|
||||
(selectedEntity.bufferRenderer.state?.type === 'brush_line' ||
|
||||
selectedEntity.bufferRenderer.state?.type === 'brush_line_with_pressure') &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
} else {
|
||||
selectedEntity.bufferRenderer.clearBuffer();
|
||||
@@ -496,7 +575,11 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
if (tool === 'eraser') {
|
||||
if (selectedEntity.bufferRenderer.state?.type === 'eraser_line' && selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
if (
|
||||
(selectedEntity.bufferRenderer.state?.type === 'eraser_line' ||
|
||||
selectedEntity.bufferRenderer.state?.type === 'eraser_line_with_pressure') &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
} else {
|
||||
selectedEntity.bufferRenderer.clearBuffer();
|
||||
@@ -515,28 +598,43 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStageMouseMove = async (_: KonvaEventObject<MouseEvent>) => {
|
||||
if (!this.getCanDraw()) {
|
||||
syncColorUnderCursor = rafThrottle(() => {
|
||||
const cursorPos = this.$cursorPos.get();
|
||||
if (!cursorPos) {
|
||||
return;
|
||||
}
|
||||
|
||||
const color = getColorAtCoordinate(this.konva.stage, cursorPos.absolute);
|
||||
if (color) {
|
||||
this.$colorUnderCursor.set(color);
|
||||
}
|
||||
});
|
||||
|
||||
onStagePointerMove = async (e: KonvaEventObject<PointerEvent>) => {
|
||||
try {
|
||||
this.$lastPointerType.set(e.evt.pointerType);
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncCursorPositions();
|
||||
const cursorPos = this.$cursorPos.get();
|
||||
|
||||
if (!cursorPos) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tool = this.$tool.get();
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
|
||||
if (tool === 'colorPicker') {
|
||||
const color = this.getColorUnderCursor();
|
||||
if (color) {
|
||||
this.$colorUnderCursor.set(color);
|
||||
}
|
||||
return;
|
||||
this.syncColorUnderCursor();
|
||||
}
|
||||
|
||||
const isMouseDown = this.$isMouseDown.get();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked && cursorPos && isMouseDown;
|
||||
|
||||
if (!isDrawable) {
|
||||
if (!isMouseDown || !selectedEntity?.$isInteractable.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -548,14 +646,14 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
|
||||
if (tool === 'brush' && bufferState.type === 'brush_line') {
|
||||
if (tool === 'brush' && (bufferState.type === 'brush_line' || bufferState.type === 'brush_line_with_pressure')) {
|
||||
const lastPoint = getLastPointOfLine(bufferState.points);
|
||||
const minDistance = settings.brushWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
|
||||
if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) {
|
||||
if (!lastPoint || !isDistanceMoreThanMin(cursorPos.relative, lastPoint, minDistance)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
const normalizedPoint = offsetCoord(cursorPos.relative, selectedEntity.state.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
|
||||
if (lastPoint.x === alignedPoint.x && lastPoint.y === alignedPoint.y) {
|
||||
@@ -564,15 +662,23 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
bufferState.points.push(alignedPoint.x, alignedPoint.y);
|
||||
|
||||
if (bufferState.type === 'brush_line_with_pressure') {
|
||||
bufferState.points.push(e.evt.pressure);
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer(bufferState);
|
||||
} else if (tool === 'eraser' && bufferState.type === 'eraser_line') {
|
||||
} else if (
|
||||
tool === 'eraser' &&
|
||||
(bufferState.type === 'eraser_line' || bufferState.type === 'eraser_line_with_pressure')
|
||||
) {
|
||||
const lastPoint = getLastPointOfLine(bufferState.points);
|
||||
const minDistance = settings.eraserWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
|
||||
if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) {
|
||||
if (!lastPoint || !isDistanceMoreThanMin(cursorPos.relative, lastPoint, minDistance)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
const normalizedPoint = offsetCoord(cursorPos.relative, selectedEntity.state.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
|
||||
if (lastPoint.x === alignedPoint.x && lastPoint.y === alignedPoint.y) {
|
||||
@@ -581,9 +687,14 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
bufferState.points.push(alignedPoint.x, alignedPoint.y);
|
||||
|
||||
if (bufferState.type === 'eraser_line_with_pressure') {
|
||||
bufferState.points.push(e.evt.pressure);
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer(bufferState);
|
||||
} else if (tool === 'rect' && bufferState.type === 'rect') {
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
const normalizedPoint = offsetCoord(cursorPos.relative, selectedEntity.state.position);
|
||||
const alignedPoint = floorCoord(normalizedPoint);
|
||||
bufferState.rect.width = Math.round(alignedPoint.x - bufferState.rect.x);
|
||||
bufferState.rect.height = Math.round(alignedPoint.y - bufferState.rect.y);
|
||||
@@ -596,23 +707,27 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStageMouseLeave = (_: KonvaEventObject<MouseEvent>) => {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
onStagePointerLeave = (e: PointerEvent) => {
|
||||
try {
|
||||
this.$lastPointerType.set(e.pointerType);
|
||||
this.$cursorPos.set(null);
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (
|
||||
selectedEntity &&
|
||||
selectedEntity.bufferRenderer.state?.type !== 'rect' &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
} finally {
|
||||
this.render();
|
||||
}
|
||||
|
||||
this.$cursorPos.set(null);
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (
|
||||
selectedEntity &&
|
||||
selectedEntity.bufferRenderer.state?.type !== 'rect' &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
this.render();
|
||||
};
|
||||
|
||||
onStageMouseWheel = (e: KonvaEventObject<WheelEvent>) => {
|
||||
@@ -652,12 +767,16 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
* whatever the user was drawing from being lost, or ending up with stale state, we need to commit the buffer
|
||||
* on window pointer up.
|
||||
*/
|
||||
onWindowPointerUp = () => {
|
||||
this.$isMouseDown.set(false);
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
onWindowPointerUp = (_: PointerEvent) => {
|
||||
try {
|
||||
this.$isMouseDown.set(false);
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (selectedEntity && selectedEntity.bufferRenderer.hasBuffer() && !this.manager.$isBusy.get()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
if (selectedEntity && selectedEntity.bufferRenderer.hasBuffer() && !this.manager.$isBusy.get()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
} finally {
|
||||
this.render();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -678,7 +797,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
if (e.key === KEY_ESCAPE) {
|
||||
// Cancel shape drawing on escape
|
||||
e.preventDefault();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
@@ -693,7 +812,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === ' ') {
|
||||
if (e.key === KEY_SPACE) {
|
||||
// Select the view tool on space key down
|
||||
e.preventDefault();
|
||||
this.$toolBuffer.set(this.$tool.get());
|
||||
@@ -703,7 +822,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Alt') {
|
||||
if (e.key === KEY_ALT) {
|
||||
// Select the color picker on alt key down
|
||||
e.preventDefault();
|
||||
this.$toolBuffer.set(this.$tool.get());
|
||||
@@ -720,7 +839,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === ' ') {
|
||||
if (e.key === KEY_SPACE) {
|
||||
// Revert the tool to the previous tool on space key up
|
||||
e.preventDefault();
|
||||
this.revertToolBuffer();
|
||||
@@ -728,7 +847,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Alt') {
|
||||
if (e.key === KEY_ALT) {
|
||||
// Revert the tool to the previous tool on alt key up
|
||||
e.preventDefault();
|
||||
this.revertToolBuffer();
|
||||
|
||||
@@ -6,6 +6,9 @@ import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import type { StrokeOptions } from 'perfect-freehand';
|
||||
import getStroke from 'perfect-freehand';
|
||||
import type { RgbColor } from 'react-colorful';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
@@ -148,14 +151,32 @@ export const getLastPointOfLine = (points: number[]): Coordinate | null => {
|
||||
if (points.length < 2) {
|
||||
return null;
|
||||
}
|
||||
const x = points[points.length - 2];
|
||||
const y = points[points.length - 1];
|
||||
const x = points.at(-2);
|
||||
const y = points.at(-1);
|
||||
if (x === undefined || y === undefined) {
|
||||
return null;
|
||||
}
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the last point of a line as a coordinate.
|
||||
* @param points An array of numbers representing points as [x1, y1, x2, y2, ...]
|
||||
* @returns The last point of the line as a coordinate, or null if the line has less than 1 point
|
||||
*/
|
||||
export const getLastPointOfLineWithPressure = (points: number[]): CoordinateWithPressure | null => {
|
||||
if (points.length < 3) {
|
||||
return null;
|
||||
}
|
||||
const x = points.at(-3);
|
||||
const y = points.at(-2);
|
||||
const pressure = points.at(-1);
|
||||
if (x === undefined || y === undefined || pressure === undefined) {
|
||||
return null;
|
||||
}
|
||||
return { x, y, pressure };
|
||||
};
|
||||
|
||||
export function getIsPrimaryMouseDown(e: KonvaEventObject<MouseEvent>) {
|
||||
return e.evt.buttons === 1;
|
||||
}
|
||||
@@ -436,7 +457,9 @@ export function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
*/
|
||||
export const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10);
|
||||
|
||||
export function getPrefixedId(prefix: CanvasEntityIdentifier['type'] | (string & Record<never, never>)): string {
|
||||
export function getPrefixedId(
|
||||
prefix: CanvasEntityIdentifier['type'] | CanvasObjectState['type'] | (string & Record<never, never>)
|
||||
): string {
|
||||
return `${prefix}:${nanoid()}`;
|
||||
}
|
||||
|
||||
@@ -492,11 +515,32 @@ export const exhaustiveCheck = (value: never): never => {
|
||||
assert(false, `Unhandled value: ${value}`);
|
||||
};
|
||||
|
||||
type CoordinateWithPressure = {
|
||||
x: number;
|
||||
y: number;
|
||||
pressure: number;
|
||||
};
|
||||
export const getLastPointOfLastLineWithPressure = (
|
||||
objects: CanvasObjectState[],
|
||||
type: 'brush_line_with_pressure' | 'eraser_line_with_pressure'
|
||||
): CoordinateWithPressure | null => {
|
||||
const lastObject = objects.at(-1);
|
||||
if (!lastObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lastObject.type === type) {
|
||||
return getLastPointOfLineWithPressure(lastObject.points);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getLastPointOfLastLine = (
|
||||
objects: CanvasObjectState[],
|
||||
type: 'brush_line' | 'eraser_line'
|
||||
): Coordinate | null => {
|
||||
const lastObject = objects[objects.length - 1];
|
||||
const lastObject = objects.at(-1);
|
||||
if (!lastObject) {
|
||||
return null;
|
||||
}
|
||||
@@ -540,3 +584,77 @@ export const getKonvaNodeDebugAttrs = (node: Konva.Node) => {
|
||||
rotation: node.rotation(),
|
||||
};
|
||||
};
|
||||
|
||||
const average = (a: number, b: number) => (a + b) / 2;
|
||||
|
||||
function getSvgPathFromStroke(points: number[][], closed = true) {
|
||||
const len = points.length;
|
||||
|
||||
if (len < 4) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let a = points[0] as number[];
|
||||
let b = points[1] as number[];
|
||||
const c = points[2] as number[];
|
||||
|
||||
let result = `M${a[0]!.toFixed(2)},${a[1]!.toFixed(2)} Q${b[0]!.toFixed(
|
||||
2
|
||||
)},${b[1]!.toFixed(2)} ${average(b[0]!, c[0]!).toFixed(2)},${average(b[1]!, c[1]!).toFixed(2)} T`;
|
||||
|
||||
for (let i = 2, max = len - 1; i < max; i++) {
|
||||
a = points[i]!;
|
||||
b = points[i + 1]!;
|
||||
result += `${average(a[0]!, b[0]!).toFixed(2)},${average(a[1]!, b[1]!).toFixed(2)} `;
|
||||
}
|
||||
|
||||
if (closed) {
|
||||
result += 'Z';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const getSVGPathDataFromPoints = (points: number[], options?: StrokeOptions): string => {
|
||||
const chunked: [number, number, number][] = [];
|
||||
for (let i = 0; i < points.length; i += 3) {
|
||||
chunked.push([points[i]!, points[i + 1]!, points[i + 2]!]);
|
||||
}
|
||||
return getSvgPathFromStroke(getStroke(chunked, options));
|
||||
};
|
||||
|
||||
export const getPointerType = (e: KonvaEventObject<PointerEvent>): 'mouse' | 'pen' | 'touch' => {
|
||||
if (e.evt.pointerType === 'mouse') {
|
||||
return 'mouse';
|
||||
}
|
||||
|
||||
if (e.evt.pointerType === 'pen') {
|
||||
return 'pen';
|
||||
}
|
||||
|
||||
return 'touch';
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the color at the given coordinate on the stage.
|
||||
* @param stage The konva stage.
|
||||
* @param coord The coordinate to get the color at. This must be the _absolute_ coordinate on the stage.
|
||||
* @returns The color under the coordinate, or null if there was a problem getting the color.
|
||||
*/
|
||||
export const getColorAtCoordinate = (stage: Konva.Stage, coord: Coordinate): RgbColor | null => {
|
||||
const ctx = stage
|
||||
.toCanvas({ x: coord.x, y: coord.y, width: 1, height: 1, imageSmoothingEnabled: false })
|
||||
.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [r, g, b, _a] = ctx.getImageData(0, 0, 1, 1).data;
|
||||
|
||||
if (r === undefined || g === undefined || b === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { r, g, b };
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { createAction, isAnyOf } from '@reduxjs/toolkit';
|
||||
|
||||
// Needed to split this from canvasSlice.ts to avoid circular dependencies
|
||||
export const canvasReset = createAction('canvas/canvasReset');
|
||||
export const newGallerySessionRequested = createAction('canvas/newGallerySessionRequested');
|
||||
export const newCanvasSessionRequested = createAction('canvas/newCanvasSessionRequested');
|
||||
export const newSessionRequested = isAnyOf(newGallerySessionRequested, newCanvasSessionRequested);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
|
||||
type CanvasSettingsState = {
|
||||
@@ -78,6 +79,10 @@ type CanvasSettingsState = {
|
||||
* Whether to show only the selected layer while transforming.
|
||||
*/
|
||||
isolatedTransformingPreview: boolean;
|
||||
/**
|
||||
* Whether to use pressure sensitivity for the brush and eraser tool when a pen device is used.
|
||||
*/
|
||||
pressureSensitivity: boolean;
|
||||
};
|
||||
|
||||
const initialState: CanvasSettingsState = {
|
||||
@@ -98,6 +103,7 @@ const initialState: CanvasSettingsState = {
|
||||
isolatedStagingPreview: true,
|
||||
isolatedFilteringPreview: true,
|
||||
isolatedTransformingPreview: true,
|
||||
pressureSensitivity: true,
|
||||
};
|
||||
|
||||
export const canvasSettingsSlice = createSlice({
|
||||
@@ -155,6 +161,17 @@ export const canvasSettingsSlice = createSlice({
|
||||
settingsIsolatedTransformingPreviewToggled: (state) => {
|
||||
state.isolatedTransformingPreview = !state.isolatedTransformingPreview;
|
||||
},
|
||||
settingsPressureSensitivityToggled: (state) => {
|
||||
state.pressureSensitivity = !state.pressureSensitivity;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(newGallerySessionRequested, (state) => {
|
||||
state.sendToCanvas = false;
|
||||
});
|
||||
builder.addCase(newCanvasSessionRequested, (state) => {
|
||||
state.sendToCanvas = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -176,6 +193,7 @@ export const {
|
||||
settingsIsolatedStagingPreviewToggled,
|
||||
settingsIsolatedFilteringPreviewToggled,
|
||||
settingsIsolatedTransformingPreviewToggled,
|
||||
settingsPressureSensitivityToggled,
|
||||
} = canvasSettingsSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
@@ -214,3 +232,4 @@ export const selectIsolatedFilteringPreview = createCanvasSettingsSelector(
|
||||
export const selectIsolatedTransformingPreview = createCanvasSettingsSelector(
|
||||
(settings) => settings.isolatedTransformingPreview
|
||||
);
|
||||
export const selectPressureSensitivity = createCanvasSettingsSelector((settings) => settings.pressureSensitivity);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/uti
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import { canvasReset, newSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
|
||||
import {
|
||||
selectAllEntities,
|
||||
@@ -857,6 +857,9 @@ export const canvasSlice = createSlice({
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
newEntity.id = getPrefixedId('regional_guidance');
|
||||
for (const refImage of newEntity.referenceImages) {
|
||||
refImage.id = getPrefixedId('regional_guidance_ip_adapter');
|
||||
}
|
||||
state.regionalGuidance.entities.push(newEntity);
|
||||
break;
|
||||
case 'reference_image':
|
||||
@@ -947,7 +950,11 @@ export const canvasSlice = createSlice({
|
||||
|
||||
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
|
||||
// re-render it (reference equality check). I don't like this behaviour.
|
||||
entity.objects.push({ ...brushLine, points: simplifyFlatNumbersArray(brushLine.points) });
|
||||
entity.objects.push({
|
||||
...brushLine,
|
||||
// If the brush line is not pressure sensitive, we simplify the points to reduce the size of the state
|
||||
points: brushLine.type === 'brush_line' ? simplifyFlatNumbersArray(brushLine.points) : brushLine.points,
|
||||
});
|
||||
},
|
||||
entityEraserLineAdded: (state, action: PayloadAction<EntityEraserLineAddedPayload>) => {
|
||||
const { entityIdentifier, eraserLine } = action.payload;
|
||||
@@ -962,7 +969,11 @@ export const canvasSlice = createSlice({
|
||||
|
||||
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
|
||||
// re-render it (reference equality check). I don't like this behaviour.
|
||||
entity.objects.push({ ...eraserLine, points: simplifyFlatNumbersArray(eraserLine.points) });
|
||||
entity.objects.push({
|
||||
...eraserLine,
|
||||
// If the brush line is not pressure sensitive, we simplify the points to reduce the size of the state
|
||||
points: eraserLine.type === 'eraser_line' ? simplifyFlatNumbersArray(eraserLine.points) : eraserLine.points,
|
||||
});
|
||||
},
|
||||
entityRectAdded: (state, action: PayloadAction<EntityRectAddedPayload>) => {
|
||||
const { entityIdentifier, rect } = action.payload;
|
||||
@@ -1122,6 +1133,9 @@ export const canvasSlice = createSlice({
|
||||
syncScaledSize(state);
|
||||
}
|
||||
});
|
||||
builder.addMatcher(newSessionRequested, (state) => {
|
||||
return resetState(state);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import type { StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
|
||||
|
||||
import { newSessionRequested } from './actions';
|
||||
|
||||
type CanvasStagingAreaState = {
|
||||
stagedImages: StagingAreaImage[];
|
||||
selectedStagedImageIndex: number;
|
||||
@@ -43,6 +45,7 @@ export const canvasStagingAreaSlice = createSlice({
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(canvasReset, () => deepClone(initialState));
|
||||
builder.addMatcher(newSessionRequested, () => deepClone(initialState));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { LoRA } from 'features/controlLayers/store/types';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import type { LoRAModelConfig } from 'services/api/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { newSessionRequested } from './actions';
|
||||
|
||||
type LoRAsState = {
|
||||
loras: LoRA[];
|
||||
};
|
||||
@@ -34,6 +37,7 @@ export const lorasSlice = createSlice({
|
||||
},
|
||||
loraRecalled: (state, action: PayloadAction<{ lora: LoRA }>) => {
|
||||
const { lora } = action.payload;
|
||||
state.loras = state.loras.filter((l) => l.model.key !== lora.model.key && l.id !== lora.id);
|
||||
state.loras.push(lora);
|
||||
},
|
||||
loraDeleted: (state, action: PayloadAction<{ id: string }>) => {
|
||||
@@ -60,6 +64,12 @@ export const lorasSlice = createSlice({
|
||||
state.loras = [];
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addMatcher(newSessionRequested, () => {
|
||||
// When a new session is requested, clear all LoRAs
|
||||
return deepClone(initialState);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { loraAdded, loraRecalled, loraDeleted, loraWeightChanged, loraIsEnabledChanged, loraAllDeleted } =
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user