Compare commits

...

93 Commits

Author SHA1 Message Date
psychedelicious
e0b63559d0 docs(ui): getColorAtCoordinate 2024-10-05 23:41:33 -04:00
psychedelicious
aa54c1f969 feat(ui): fix color picker wrong color, improved perf
The color picker take some time to sample the color from the canvas state. This could cause a race condition where the cursor position changes between the time sampling starts, resulting in the picker showing the wrong color. Sometimes it picks up the color picker tool preview!

To resolve this, the color picker's color syncing is now throttled to once per animation frame. Besides fixing the incorrect color issue, it improves the perf substantially by reducing number of samples we take.
2024-10-05 23:41:33 -04:00
psychedelicious
87fdea4cc6 feat(ui): updated cursor position tracking
- Record both absolute and relative positions
- Use simpler method to get relative position
- Generalize getColorUnderCursor to be getColorAtCoordinate
2024-10-05 23:41:33 -04:00
psychedelicious
53443084c5 tidy(ui): move getColorUnderCursor to utils 2024-10-05 23:41:33 -04:00
psychedelicious
8d2e5bfd77 tidy(ui): use constants for keys 2024-10-05 23:41:33 -04:00
psychedelicious
05e285c95a tidy(ui): getCanDraw code style 2024-10-05 23:41:33 -04:00
psychedelicious
25f19a35d7 tidy(ui): use entity isInteractable in tool module 2024-10-05 23:41:33 -04:00
psychedelicious
01bbd32598 fix(ui): board drop targets
We just changed all buttons to use `onPointerUp` events to fix Apple Pencil behaviour. This, plus the specific DOM layout of boards, resulted in the `onPointerUp` being triggered on a board before the drop triggered.

The app saw this as selecting the board, which then reset the gallery selection to the first image in the board. By the time you drop, the gallery selection had reset.

DOM layout slightly altered to work around this.
2024-10-06 08:15:53 +11:00
Thomas Bolteau
0e2761d5c6 translationBot(ui): update translation (French)
Currently translated at 54.1% (796 of 1470 strings)

Co-authored-by: Thomas Bolteau <thomas.bolteau50@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fr/
Translation: InvokeAI/Web UI
2024-10-05 15:12:51 +10:00
psychedelicious
d5b51cca56 chore: bump version to v5.1.0rc5 2024-10-04 22:17:41 -04:00
psychedelicious
a303777777 fix(ui): image context menu buttons don't close menu
Need to render as a `MenuItem` to trigger the close behaviour
2024-10-04 21:33:01 -04:00
psychedelicious
e90b3de706 feat(ui): error state for missing ip adapter image 2024-10-04 21:30:38 -04:00
psychedelicious
3ce94e5b84 feat(ui): improved node image drop target & error state 2024-10-04 21:30:38 -04:00
psychedelicious
42e5ec3916 fix(ui): fix wonky drop target layouts 2024-10-04 21:30:38 -04:00
psychedelicious
ffa00d1d9a chore(ui): lint 2024-10-05 09:47:22 +10:00
psychedelicious
1648a2af6e fix(ui): board title editable 2024-10-05 09:47:22 +10:00
psychedelicious
852e9e280a chore: bump version to v5.1.0rc4 2024-10-04 08:19:44 -04:00
Riku
af72412d3f translationBot(ui): update translation (German)
Currently translated at 66.0% (971 of 1470 strings)

Co-authored-by: Riku <riku.block@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
Translation: InvokeAI/Web UI
2024-10-04 21:51:59 +10:00
psychedelicious
72f715e688 fix(ui): disable long-press context menu on canvas, add menu button 2024-10-04 07:44:40 -04:00
psychedelicious
3b567bef3d chore(ui): bump @invoke-ai/ui-library
This brings in the ability to disable long-press on context menus and a threshold move distance that cancels a pending long-press.
2024-10-04 07:44:40 -04:00
psychedelicious
3d867db315 chore(ui): bump @invoke-ai/ui-library
This brings in long-press support for context menus.
2024-10-04 07:44:40 -04:00
psychedelicious
a8c7dd74d0 fix(ui): type stuff 2024-10-04 07:44:40 -04:00
psychedelicious
2dc069d759 chore(ui): lint 2024-10-04 07:44:40 -04:00
psychedelicious
2a90f4f59e fix(ui): Apple Pencil requires onPointerUp instead of onClick
With `onClick`, elements w/ a tooltip require a double-tap.
2024-10-04 07:44:40 -04:00
psychedelicious
af5f342347 chore(ui): bump @invoke-ai/ui-library
This brings in a fix for Apple Pencil.
2024-10-04 07:44:40 -04:00
psychedelicious
6dd53b6a32 fix(ui): viewport cut off on iPad
Need to use dynamic viewport units.
2024-10-04 07:44:40 -04:00
psychedelicious
0ca8351911 fix(ui): incorrect hotkeys on floating button tooltips 2024-10-04 07:27:30 -04:00
psychedelicious
b14cbfde13 chore: v5.1.0rc3 2024-10-04 09:32:54 +10:00
psychedelicious
46dc633df9 installer: update torch extra-index-url 2024-10-04 09:32:54 +10:00
jkbdco
d4a981fc1c Update docker-compose.yml
Changed image from local (which most people looking for a boilerplate compose file likely will not have) to latest.
2024-10-04 07:21:20 +10:00
Jonseed
e0474ce822 Update communityNodes.md add Ollama node
Added an Ollama Node to the community nodes
2024-10-04 07:19:00 +10:00
psychedelicious
9e5ce6b2d4 chore: bump version to v5.1.0rc2 2024-10-03 17:10:50 -04:00
Hosted Weblate
98fa946f77 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-10-04 04:58:03 +10:00
Thomas Bolteau
ef80d40b63 translationBot(ui): update translation (French)
Currently translated at 45.4% (668 of 1470 strings)

translationBot(ui): update translation (French)

Currently translated at 33.1% (488 of 1470 strings)

translationBot(ui): update translation (French)

Currently translated at 32.5% (479 of 1470 strings)

translationBot(ui): update translation (French)

Currently translated at 30.7% (449 of 1458 strings)

translationBot(ui): update translation (French)

Currently translated at 30.2% (442 of 1460 strings)

Co-authored-by: Thomas Bolteau <thomas.bolteau50@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fr/
Translation: InvokeAI/Web UI
2024-10-04 04:58:03 +10:00
Riku
7a9f923d35 translationBot(ui): update translation (German)
Currently translated at 65.4% (955 of 1460 strings)

Co-authored-by: Riku <riku.block@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
Translation: InvokeAI/Web UI
2024-10-04 04:58:03 +10:00
psychedelicious
fd982fa7c2 fix(ui): prevent unhandled promise rejections 2024-10-03 10:32:59 -04:00
Ryan Dick
df86ed653a Bump xformers for compatibility with torch (#7022)
## Summary

#6890 bumped torch, which caused an incompatibility with xformers when
installing with `pip install ".[xformers]"`. This PR bumps xformers.

## QA Instructions

I ran some smoke tests to confirm that generating with xformers still
works.

In my tests on an A100, there is a performance regression after bumping
xformers (2.7 it/s vs 3.2 it/s). I think it is ok to ignore this for
A100s, since users should be using torch-sdp, which is much faster (4.3
it/s). But, we should test for regression on older cards where xformers
is still recommended.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [x] _Tests added / updated (if applicable)_
- [x] _Documentation added / updated (if applicable)_
2024-10-03 10:22:47 -04:00
Ryan Dick
0be8aacee6 Bump xformers for compatibility with torch. 2024-10-03 14:13:42 +00:00
psychedelicious
4f993a4f32 fix(ui): TS issue with latest i18n deps 2024-10-03 09:54:30 -04:00
psychedelicious
0158320940 chore(ui): bump react-i18next to latest to match other i18n deps 2024-10-03 09:54:30 -04:00
psychedelicious
bb2dc6c78b chore(ui): bump deps
I've reviewed the release notes for each dependency and it's all minor stuff. App seems to be running fine.
2024-10-03 09:54:30 -04:00
psychedelicious
80d7d69c2f fix(ui): recall LoRAs may create duplicates
Closes #7004
2024-10-03 08:50:30 -04:00
psychedelicious
1010c9877c fix(ui): give unique ID to duplicated regional guidance layers' ref images
Closes #6995
2024-10-03 08:48:18 -04:00
psychedelicious
8fd8994ee8 chore(ui): knip 2024-10-03 08:33:54 -04:00
psychedelicious
262c2f1fc7 feat(ui): add crop canvas to bbox 2024-10-03 08:33:54 -04:00
psychedelicious
150d3239e3 feat(ui): add crop layer to bbox 2024-10-03 08:33:54 -04:00
psychedelicious
e49e5e9782 feat(ui): add confirmation to new session actions 2024-10-03 08:31:00 -04:00
psychedelicious
2d1e745594 feat(ui): add new gallery/canvas session buttons to queue actions menu
A new "session" just means to reset most settings to default values, excluding model.

There are a few things that need to be reset:
- Parameters state, except for models and things dependent on model selection (like VAE precision)
- Canvas state, except for the `modelBase`, which is dependent on the model selection
- Canvas staging area state
- LoRAs state
- HRF state
- Style presets state

We also select the canvas tab.

For new gallery sessions, we:
- Open the image viewer
- Set the right panel tab to `gallery`

And for new canvas sessions, we:
- Close the image viewer
- Set the right panel tab to `layers`
2024-10-03 08:31:00 -04:00
psychedelicious
b793328edd feat(ui): update queue actions menu (wip) 2024-10-03 08:31:00 -04:00
psychedelicious
e79b316645 feat(ui): mmb panning 2024-10-03 00:08:41 -04:00
psychedelicious
8297e7964c fix(ui): show color picker when using pen 2024-10-03 10:43:18 +10:00
Ryan Dick
26832c1a0e Add unit test to confirm that GGMLTensor sizes (bytes) are being calculated correctly. 2024-10-02 18:33:05 -04:00
Ryan Dick
c29259ccdb Update ui ModelFormatBadge to support GGUF. 2024-10-02 18:33:05 -04:00
Ryan Dick
3d4bd71098 Update test_probe_handles_state_dict_with_integer_keys() to make sure that it is still testing what it's intended to test. Previously, we were skipping an important part of the test by using a fake file path. 2024-10-02 18:33:05 -04:00
Brandon Rising
814be44cd7 Ignore paths that don't exist in probe for unit tests 2024-10-02 18:33:05 -04:00
Brandon Rising
d328eaf743 Remove no longer used dequantize_tensor function 2024-10-02 18:33:05 -04:00
Brandon Rising
b502c05009 Add __init__.py file to scripts dir for pytest 2024-10-02 18:33:05 -04:00
Brandon Rising
0f333388bb Add comment describing why we're not using the meta device during probing of gguf files 2024-10-02 18:33:05 -04:00
Ryan Dick
bc63e2acc5 Add workaround for FLUX GGUF models with incorrect img_in.weight shape. 2024-10-02 18:33:05 -04:00
Ryan Dick
ec7e771942 Add a compute_dtype field to GGMLTensor. 2024-10-02 18:33:05 -04:00
Ryan Dick
fe84013392 Add unit tests for GGMLTensor. 2024-10-02 18:33:05 -04:00
Ryan Dick
710f81266b Fix type errors in GGMLTensor. 2024-10-02 18:33:05 -04:00
Brandon Rising
446e2884bc Remove no longer used code paths, general cleanup of new dequantization code, update probe 2024-10-02 18:33:05 -04:00
Brandon Rising
7d9f125232 Run ruff and update imports 2024-10-02 18:33:05 -04:00
Brandon Rising
66bbd62758 Run ruff and fix typing in torch patcher 2024-10-02 18:33:05 -04:00
Brandon Rising
0875e861f5 Various updates to gguf performance 2024-10-02 18:33:05 -04:00
Brandon
0267d73dfc Update invokeai/backend/model_manager/load/model_loaders/flux.py
Co-authored-by: Ryan Dick <ryanjdick3@gmail.com>
2024-10-02 18:33:05 -04:00
Brandon Rising
c9ab7c5233 Add gguf as a pyproject dependency 2024-10-02 18:33:05 -04:00
Ryan Dick
f06765dfba Get alternative GGUF implementation working... barely. 2024-10-02 18:33:05 -04:00
Ryan Dick
f347b26999 Initial experimentation with Tensor-like extension for GGUF. 2024-10-02 18:33:05 -04:00
Lincoln Stein
c665cf3525 recognize .gguf files when scanning a folder for import 2024-10-02 18:33:05 -04:00
Brandon Rising
8cf19c4124 Run Ruff 2024-10-02 18:33:05 -04:00
Brandon Rising
f7112ae57b Add unit tests for torch patcher 2024-10-02 18:33:05 -04:00
Brandon Rising
2bfb0ddff5 Initial GGUF support for flux models 2024-10-02 18:33:05 -04:00
psychedelicious
950c9f5d0c chore: bump version to v5.1.0rc1 2024-10-02 08:02:30 -04:00
psychedelicious
db283d21f9 chore(ui): lint 2024-10-02 08:02:30 -04:00
psychedelicious
70cca7a431 fix(ui): floating button tooltip orientations 2024-10-02 08:02:30 -04:00
psychedelicious
3c3938cfc8 tweak(ui): left-hand panel buttons 2024-10-02 08:02:30 -04:00
psychedelicious
4455fc4092 fix(ui): next/prev image buttons layout 2024-10-02 08:02:30 -04:00
psychedelicious
4b7e920612 feat(ui): add canvas setting for pressure sens 2024-10-02 08:02:30 -04:00
psychedelicious
433146d08f tidy(ui): restore redux store checks 2024-10-02 08:02:30 -04:00
psychedelicious
324a46d0c8 fix(ui): edge cases with tool rendering 2024-10-02 08:02:30 -04:00
psychedelicious
c4421241f6 feat(ui): updated layout for small screens
- Move color picker to floating buttons
- Always show floating buttons
- Minor layout tweaks for floating buttons
2024-10-02 08:02:30 -04:00
psychedelicious
43b417be6b tidy(ui): remove unused perfect-freehand options from brush state 2024-10-02 08:02:30 -04:00
psychedelicious
4a135c1017 feat(ui): hide brush preview when drawing with pen 2024-10-02 08:02:30 -04:00
psychedelicious
dd591abc2b feat(ui): hide brush fill circle on timeout 2024-10-02 08:02:30 -04:00
psychedelicious
0e65f295ac feat(ui): initial pressure sensitivity implementation 2024-10-02 08:02:30 -04:00
psychedelicious
ab7fbb7b30 feat(ui): use touch-action: none instead of events to prevent pan/zoom 2024-10-02 08:02:30 -04:00
psychedelicious
92aed5e4fc chore(ui): add perfect-freehand dep for tablet support 2024-10-02 08:02:30 -04:00
psychedelicious
d9b0697d1f feat(ui): use pointer events instead of mouse events
This gets touch input and tablet input working for basic drawing functions.
2024-10-02 08:02:30 -04:00
psychedelicious
34a9409bc1 feat(ui): prevent app from scrolling on touch events 2024-10-02 08:02:30 -04:00
psychedelicious
319d82751a build(ui): vite dev server host: 0.0.0.0 2024-10-02 08:02:30 -04:00
Josh Corbett
9b90834248 feat(context menu): condense top row of image context menu 2024-10-01 22:06:42 -04:00
297 changed files with 5741 additions and 5677 deletions

View File

@@ -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

View File

@@ -40,6 +40,7 @@ To use a community workflow, download the `.json` node graph file and load it in
+ [Metadata-Linked](#metadata-linked-nodes)
+ [Negative Image](#negative-image)
+ [Nightmare Promptgen](#nightmare-promptgen)
+ [Ollama](#ollama-node)
+ [One Button Prompt](#one-button-prompt)
+ [Oobabooga](#oobabooga)
+ [Prompt Tools](#prompt-tools)
@@ -390,6 +391,19 @@ 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:**
![ollama node](https://raw.githubusercontent.com/Jonseed/Ollama-Node/a3e7cdc55e394cb89c1ea7ed54e106c212c85e8c/ollama-node-screenshot.png)
--------------------------------
### One Button Prompt

View File

@@ -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

View File

@@ -213,7 +213,11 @@ class FluxDenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
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(

View File

@@ -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):
@@ -197,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")
@@ -363,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."""
@@ -466,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()],

View File

@@ -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."""

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View 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

View 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

View 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]]

View File

@@ -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.40",
"@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",

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"reportBugLabel": "Fehler melden",
"settingsLabel": "Einstellungen",
"img2img": "Bild zu Bild",
"nodes": "Workflows",
"nodes": "Arbeitsabläufe",
"upload": "Hochladen",
"load": "Laden",
"statusDisconnected": "Getrennt",
@@ -263,6 +263,10 @@
"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": {
@@ -652,10 +656,8 @@
"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",
@@ -702,7 +704,8 @@
"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.",
"assetsWithCount_one": "{{count}} in der Sammlung",
"assetsWithCount_other": "{{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",
@@ -1127,7 +1130,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",
@@ -1249,6 +1262,69 @@
"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": {

View File

@@ -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",
@@ -1081,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",
@@ -1561,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",
@@ -1674,6 +1678,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",
@@ -1813,7 +1821,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",
@@ -1828,6 +1837,7 @@
}
},
"canvasContextMenu": {
"canvasGroup": "Canvas",
"saveToGalleryGroup": "Save To Gallery",
"saveCanvasToGallery": "Save Canvas To Gallery",
"saveBboxToGallery": "Save Bbox To Gallery",
@@ -1835,7 +1845,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",

View File

@@ -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)",

View File

@@ -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",

View File

@@ -181,7 +181,35 @@
"deleteModel": "Supprimer le modèle",
"deleteConfig": "Supprimer la configuration",
"deleteMsg1": "Voulez-vous vraiment supprimer cette entrée de modèle dans InvokeAI ?",
"deleteMsg2": "Cela n'effacera pas le fichier de point de contrôle du modèle de votre disque. Vous pouvez les réajouter si vous le souhaitez."
"deleteMsg2": "Cela n'effacera pas le fichier de point de contrôle du modèle de votre disque. Vous pouvez les réajouter si vous le souhaitez.",
"convert": "Convertir",
"convertToDiffusersHelpText2": "Ce processus remplacera votre entrée dans le gestionaire de modèles par la version Diffusers du même modèle.",
"convertToDiffusersHelpText1": "Ce modèle sera converti au format 🧨 Diffusers.",
"huggingFaceHelper": "Si plusieurs modèles sont trouvés dans ce dépôt, vous serez invité à en sélectionner un à installer.",
"convertToDiffusers": "Convertir en Diffusers",
"convertToDiffusersHelpText5": "Veuillez vous assurer que vous disposez de suffisamment d'espace disque. La taille des modèles varient généralement entre 2 Go et 7 Go.",
"convertToDiffusersHelpText4": "C'est un processus executé une unique fois. Cela peut prendre environ 30 à 60 secondes en fonction des spécifications de votre ordinateur.",
"alpha": "Alpha",
"modelConverted": "Modèle Converti",
"convertToDiffusersHelpText3": "Votre fichier de point de contrôle sur le disque SERA supprimé s'il se trouve dans le dossier racine d'InvokeAI. S'il est dans un emplacement personnalisé, alors il NE SERA PAS supprimé.",
"convertToDiffusersHelpText6": "Souhaitez-vous convertir ce modèle?",
"modelConversionFailed": "Échec de la conversion du modèle",
"none": "aucun",
"selectModel": "Sélectionner le modèle",
"modelDeleted": "Modèle supprimé",
"vae": "VAE",
"baseModel": "Modèle de Base",
"convertingModelBegin": "Conversion du modèle. Veuillez patienter.",
"modelDeleteFailed": "Échec de la suppression du modèle",
"modelUpdateFailed": "Échec de la mise à jour du modèle",
"variant": "Variante",
"syncModels": "Synchroniser les Modèles",
"settings": "Paramètres",
"predictionType": "Type de Prédiction",
"advanced": "Avancé",
"modelType": "Type de modèle",
"vaePrecision": "Précision VAE",
"noModelSelected": "Aucun modèle sélectionné"
},
"parameters": {
"images": "Images",
@@ -209,7 +237,49 @@
"useSeed": "Utiliser la graine",
"useAll": "Tout utiliser",
"info": "Info",
"showOptionsPanel": "Afficher le panneau d'options"
"showOptionsPanel": "Afficher le panneau d'options",
"invoke": {
"layer": {
"rgNoPromptsOrIPAdapters": "aucun prompts ou IP Adapters"
},
"noPrompts": "Aucun prompts généré",
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} entrée manquante",
"missingFieldTemplate": "Modèle de champ manquant",
"invoke": "Invoke",
"addingImagesTo": "Ajouter des images à",
"missingNodeTemplate": "Modèle de nœud manquant",
"noModelSelected": "Aucun modèle sélectionné",
"noNodesInGraph": "Aucun nœud dans le graphique",
"systemDisconnected": "Système déconnecté"
},
"negativePromptPlaceholder": "Prompt Négatif",
"positivePromptPlaceholder": "Prompt Positif",
"general": "Général",
"symmetry": "Symétrie",
"denoisingStrength": "Force de débruitage",
"scheduler": "Planificateur",
"clipSkip": "CLIP Skip",
"seamlessXAxis": "Axe X sans jointure",
"seamlessYAxis": "Axe Y sans jointure",
"controlNetControlMode": "Mode de Contrôle",
"patchmatchDownScaleSize": "Réduire",
"coherenceMode": "Mode",
"maskBlur": "Flou de masque",
"iterations": "Itérations",
"cancel": {
"cancel": "Annuler"
},
"useCpuNoise": "Utiliser le bruit du CPU",
"imageActions": "Actions d'image",
"setToOptimalSize": "Optimiser la taille pour le modèle",
"setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (peut être trop petit)",
"swapDimensions": "Échanger les dimensions",
"aspect": "Aspect",
"cfgRescaleMultiplier": "Multiplicateur de mise à l'échelle CFG",
"setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (peut être trop grand)",
"useSize": "Utiliser la taille",
"remixImage": "Remixer l'image",
"lockAspectRatio": "Verrouiller le rapport hauteur/largeur"
},
"settings": {
"models": "Modèles",
@@ -218,26 +288,70 @@
"resetWebUI": "Réinitialiser l'interface Web",
"resetWebUIDesc1": "Réinitialiser l'interface Web ne réinitialise que le cache local du navigateur de vos images et de vos paramètres enregistrés. Cela n'efface pas les images du disque.",
"resetWebUIDesc2": "Si les images ne s'affichent pas dans la galerie ou si quelque chose d'autre ne fonctionne pas, veuillez essayer de réinitialiser avant de soumettre une demande sur GitHub.",
"resetComplete": "L'interface Web a été réinitialisée. Rafraîchissez la page pour recharger."
"resetComplete": "L'interface Web a été réinitialisée. Rafraîchissez la page pour recharger.",
"general": "Général",
"showProgressInViewer": "Afficher les images progressivement dans le Visualiseur",
"antialiasProgressImages": "Anti Alisasing des Images progressives",
"beta": "Bêta",
"generation": "Génération",
"ui": "Interface Utilisateur",
"developer": "Développeur",
"enableNSFWChecker": "Activer le vérificateur NSFW",
"clearIntermediatesDesc2": "Les images intermédiaires sont des sous-produits de la génération, différentes des images de résultat dans la galerie. La suppression des intermédiaires libérera de l'espace disque.",
"clearIntermediatesDisabled": "La file d'attente doit être vide pour effacer les intermédiaires.",
"reloadingIn": "Rechargement dans",
"intermediatesClearedFailed": "Problème de suppression des intermédiaires",
"clearIntermediates": "Effacer les intermédiaires",
"enableInvisibleWatermark": "Activer le Filigrane Invisible",
"clearIntermediatesDesc1": "Effacer les intermédiaires réinitialisera votre Toile et votre ControlNet.",
"enableInformationalPopovers": "Activer les infobulles d'information",
"intermediatesCleared_one": "Effacé {{count}} Intermédiaire",
"intermediatesCleared_many": "Effacé {{count}} Intermédiaires",
"intermediatesCleared_other": "Effacé {{count}} Intermédiaires",
"clearIntermediatesDesc3": "Vos images de galerie ne seront pas supprimées.",
"clearIntermediatesWithCount_one": "Effacé {{count}} Intermédiaire",
"clearIntermediatesWithCount_many": "Effacé {{count}} Intermédiaires",
"clearIntermediatesWithCount_other": "Effacé {{count}} Intermédiaires"
},
"toast": {
"uploadFailed": "Téléchargement échoué",
"imageCopied": "Image copiée",
"parametersNotSet": "Paramètres non définis"
"parametersNotSet": "Paramètres non définis",
"serverError": "Erreur du serveur",
"uploadFailedInvalidUploadDesc": "Doit être une unique image PNG ou JPEG",
"problemCopyingImage": "Impossible de copier l'image",
"parameterSet": "Paramètre Rappelé",
"parameterNotSet": "Paramètre non Rappelé",
"canceled": "Traitement annulé",
"addedToBoard": "Ajouté à la planche",
"workflowLoaded": "Processus chargé",
"connected": "Connecté au serveur",
"setNodeField": "Définir comme champ de nœud",
"imageUploadFailed": "Échec de l'importation de l'image",
"loadedWithWarnings": "Processus chargé avec des avertissements",
"imageUploaded": "Image importée",
"modelAddedSimple": "Modèle ajouté à la file d'attente",
"setControlImage": "Définir comme image de contrôle",
"workflowDeleted": "Processus supprimé",
"baseModelChangedCleared_one": "Effacé ou désactivé {{count}} sous-modèle incompatible",
"baseModelChangedCleared_many": "Effacé ou désactivé {{count}} sous-modèles incompatibles",
"baseModelChangedCleared_other": "Effacé ou désactivé {{count}} sous-modèles incompatibles",
"invalidUpload": "Téléchargement invalide",
"problemDownloadingImage": "Impossible de télécharger l'image",
"problemRetrievingWorkflow": "Problème de récupération du processus",
"problemDeletingWorkflow": "Problème de suppression du processus"
},
"accessibility": {
"uploadImage": "Charger une image",
"reset": "Réinitialiser",
"nextImage": "Image suivante",
"previousImage": "Image précédente",
"showOptionsPanel": "Afficher le panneau latéral",
"invokeProgressBar": "Barre de Progression Invoke",
"menu": "Menu",
"about": "À propos",
"mode": "Mode",
"createIssue": "Créer un ticket",
"submitSupportTicket": "Envoyer un ticket de support",
"showGalleryPanel": "Afficher la galerie",
"resetUI": "$t(accessibility.reset) l'Interface Utilisateur"
},
"boards": {
@@ -413,6 +527,557 @@
"disableFailed": "Problème lors de la désactivation du Cache d'Invocation"
},
"hotkeys": {
"hotkeys": "Raccourci clavier"
"hotkeys": "Raccourci clavier",
"viewer": {
"recallPrompts": {
"desc": "Rappeler le prompt positif et négatif pour l'image actuelle.",
"title": "Rappeler les Prompts"
}
},
"searchHotkeys": "Recherche raccourci clavier",
"app": {
"selectQueueTab": {
"desc": "Selectionne l'onglet de file d'attente.",
"title": "Sélectionner l'onglet File d'Attente"
},
"title": "Application",
"invoke": {
"title": "Invoke",
"desc": "Ajouter une génération à la fin de la file d'attente."
},
"invokeFront": {
"title": "Invoke (Front)",
"desc": "Ajouter une génération au début de la file d'attente."
},
"cancelQueueItem": {
"title": "Annuler",
"desc": "Annuler l'élément en cours de traitement dans la file d'attente."
},
"clearQueue": {
"title": "Vider la file d'attente",
"desc": "Annuler et retirer tous les éléments de la file d'attente."
},
"selectCanvasTab": {
"title": "Séléctionner l'onglet Toile",
"desc": "Séléctionne l'onglet Toile."
},
"selectUpscalingTab": {
"title": "Séléctionner l'onglet Agrandissement",
"desc": "Séléctionne l'onglet Agrandissement."
},
"selectWorkflowsTab": {
"desc": "Sélectionne l'onglet Processus.",
"title": "Sélectionner l'onglet Processus"
},
"togglePanels": {
"desc": "Affiche ou masque les panneaux gauche et droit en même temps.",
"title": "Afficher/Masquer les panneaux"
},
"selectModelsTab": {
"desc": "Sélectionne l'onglet Modèles.",
"title": "Sélectionner l'onglet Modèles"
},
"focusPrompt": {
"title": "Selectionne le Prompt",
"desc": "Déplace le focus du curseur sur le prompt positif."
},
"toggleLeftPanel": {
"title": "Afficher/Masquer le panneau de gauche",
"desc": "Affiche ou masque le panneau de gauche."
},
"resetPanelLayout": {
"desc": "Réinitialise les panneaux gauche et droit à leur taille et disposition par défaut.",
"title": "Reinitialiser l'organisation des panneau"
},
"toggleRightPanel": {
"title": "Afficher/Masquer le panneau de droite",
"desc": "Affiche ou masque le panneau de droite."
}
},
"canvas": {
"title": "Toile",
"selectBrushTool": {
"title": "Outil Pinceau",
"desc": "Sélectionne l'outil pinceau."
},
"incrementToolWidth": {
"title": "Augmenter largeur de l'outil",
"desc": "Augmente la largeur du pinceau ou de la gomme, en fonction de la sélection."
},
"selectColorPickerTool": {
"title": "Outil Pipette",
"desc": "Sélectionne l'outil pipette pour la sélection de couleur."
},
"selectEraserTool": {
"title": "Outil Gomme",
"desc": "Sélectionne l'outil gomme."
},
"selectMoveTool": {
"title": "Outil Déplacer",
"desc": "Sélectionne l'outil déplacer."
},
"selectRectTool": {
"title": "Outil Rectangle",
"desc": "Sélectionne l'outil rectangle."
},
"selectViewTool": {
"title": "Outil Visualisation",
"desc": "Sélectionne l'outil visualisation."
},
"selectBboxTool": {
"title": "Outil Cadre de délimitation",
"desc": "Sélectionne l'outil cadre de délimitation."
},
"fitLayersToCanvas": {
"title": "Adapte les Couches à la Toile",
"desc": "Mettre à l'échelle et positionner la vue pour l'adapter à tous les couches visibles."
},
"fitBboxToCanvas": {
"desc": "Ajuster l'échelle et la position de la vue pour s'adapter au cadre de délimitation.",
"title": "Ajuster le cadre de délimitation à la Toile"
},
"decrementToolWidth": {
"title": "Réduire largeur de l'outil",
"desc": "Réduit la largeur du pinceau ou de la gomme, en fonction de la sélection."
}
},
"clearSearch": "Annuler la recherche",
"noHotkeysFound": "Aucun raccourci clavier trouvé",
"gallery": {
"deleteSelection": {
"desc": "Supprime toutes les images séléctionnées. Par défault une confirmation vous sera demandée. Si les images sont actuellement utilisées dans l'application vous serez mis en garde."
}
}
},
"popovers": {
"paramPositiveConditioning": {
"paragraphs": [
"Guide le processus de génération. Vous pouvez utiliser n'importe quels mots ou phrases.",
"Prend en charge les syntaxes et les embeddings de Compel et des Prompts dynamiques."
],
"heading": "Prompt Positif"
},
"paramNegativeConditioning": {
"paragraphs": [
"Le processus de génération évite les concepts dans le prompt négatif. Utilisez cela pour exclure des qualités ou des objets du résultat.",
"Prend en charge la syntaxe et les embeddings de Compel."
],
"heading": "Prompt Négatif"
},
"paramVAEPrecision": {
"heading": "Précision du VAE",
"paragraphs": [
"La précision utilisée lors de l'encodage et du décodage VAE."
]
},
"controlNetWeight": {
"heading": "Poids",
"paragraphs": [
"Poids du Control Adapter. Un poids plus élevé aura un impact plus important sur l'image finale."
]
},
"compositingMaskAdjustments": {
"heading": "Ajustements de masque",
"paragraphs": [
"Ajuste le masque."
]
},
"infillMethod": {
"heading": "Méthode de Remplissage",
"paragraphs": [
"Méthode de remplissage lors du processus d'Outpainting ou d'Inpainting."
]
},
"clipSkip": {
"paragraphs": [
"Combien de couches du modèle CLIP faut-il ignorer.",
"Certains modèles sont mieux adaptés à une utilisation avec CLIP Skip."
],
"heading": "CLIP Skip"
},
"paramScheduler": {
"heading": "Planificateur",
"paragraphs": [
"Planificateur utilisé pendant le processus de génération.",
"Chaque planificateur définit comment ajouter de manière itérative du bruit à une image ou comment mettre à jour un échantillon en fonction de la sortie d'un modèle."
]
},
"controlNet": {
"paragraphs": [
"Les ControlNets fournissent des indications au processus de génération, aidant à créer des images avec une composition, une structure ou un style contrôlés, en fonction du modèle sélectionné."
],
"heading": "ControlNet"
},
"paramSteps": {
"heading": "Étapes",
"paragraphs": [
"Nombre d'étapes qui seront effectuées à chaque génération.",
"Des nombres d'étapes plus élevés créeront généralement de meilleures images, mais nécessiteront plus de temps de génération."
]
},
"controlNetBeginEnd": {
"heading": "Pourcentage de début / de fin d'étape",
"paragraphs": [
"La partie du processus de débruitage à laquelle le Control Adapter sera appliqué.",
"En général, les Control Adapter appliqués au début du processus guident la composition, tandis que les Control Adapter appliqués à la fin guident les détails."
]
},
"controlNetControlMode": {
"paragraphs": [
"Accordez plus de poids soit au prompt, soit au ControlNet."
],
"heading": "Mode de Contrôle"
},
"dynamicPromptsSeedBehaviour": {
"heading": "Comportement de la graine",
"paragraphs": [
"Contrôle l'utilisation de la graine lors de la génération des prompts.",
"Une graine unique pour chaque itération. Utilisez ceci pour explorer les variations de prompt sur une seule graine.",
"Par exemple, si vous avez 5 prompts, chaque image utilisera la même graine.",
"Par image utilisera une graine unique pour chaque image. Cela offre plus de variation."
]
},
"paramVAE": {
"heading": "VAE",
"paragraphs": [
"Modèle utilisé pour convertir la sortie de l'IA en l'image finale."
]
},
"compositingCoherenceMode": {
"heading": "Mode",
"paragraphs": [
"Méthode utilisée pour créer une image cohérente avec la zone masquée nouvellement générée."
]
},
"paramIterations": {
"heading": "Itérations",
"paragraphs": [
"Le nombre d'images à générer.",
"Si les prompts dynamiques sont activées, chaque prompt sera généré autant de fois."
]
},
"dynamicPrompts": {
"paragraphs": [
"Les Prompts dynamiques divisent un seul prompt en plusieurs.",
"La syntaxe de base est \"une balle {rouge|verte|bleue}\". Cela produira trois prompts: \"une balle rouge\", \"une balle verte\" et \"une balle bleue\".",
"Vous pouvez utiliser la syntaxe autant de fois que vous le souhaitez dans un seul prompt, mais veillez à garder le nombre de prompts générées sous contrôle avec le paramètre Max Prompts."
],
"heading": "Prompts Dynamiques"
},
"paramModel": {
"heading": "Modèle",
"paragraphs": [
"Modèle utilisé pour la génération. Différents modèles sont entraînés pour se spécialiser dans la production de résultats esthétiques et de contenus variés."
]
},
"compositingCoherencePass": {
"heading": "Passe de cohérence",
"paragraphs": [
"Un deuxième tour de débruitage aide à composer l'image remplie/étendue."
]
},
"paramRatio": {
"heading": "Rapport hauteur/largeur",
"paragraphs": [
"Le rapport hauteur/largeur de l'image générée.",
"Une taille d'image (en nombre de pixels) équivalente à 512x512 est recommandée pour les modèles SD1.5 et une taille équivalente à 1024x1024 est recommandée pour les modèles SDXL."
]
},
"paramSeed": {
"heading": "Graine",
"paragraphs": [
"Contrôle le bruit de départ utilisé pour la génération.",
"Désactivez l'option \"Aléatoire\" pour produire des résultats identiques avec les mêmes paramètres de génération."
]
},
"scaleBeforeProcessing": {
"heading": "Échelle avant traitement",
"paragraphs": [
"\"Auto\" ajuste la zone sélectionnée à la taille la mieux adaptée au modèle avant le processus de génération d'image."
]
},
"compositingBlurMethod": {
"heading": "Méthode de flou",
"paragraphs": [
"La méthode de flou appliquée à la zone masquée."
]
},
"controlNetResizeMode": {
"heading": "Mode de Redimensionnement",
"paragraphs": [
"Méthode pour adapter la taille de l'image d'entrée du Control Adapter à la taille de l'image générée."
]
},
"dynamicPromptsMaxPrompts": {
"heading": "Max Prompts",
"paragraphs": [
"Limite le nombre de prompts pouvant être générés par les Prompts Dynamiques."
]
},
"paramDenoisingStrength": {
"heading": "Force de débruitage",
"paragraphs": [
"Intensité du bruit ajouté à l'image d'entrée.",
"0 produira une image identique, tandis que 1 produira une image complètement différente."
]
},
"lora": {
"heading": "LoRA",
"paragraphs": [
"Modèles légers utilisés en conjonction avec des modèles de base."
]
},
"noiseUseCPU": {
"heading": "Utiliser le bruit du CPU",
"paragraphs": [
"Contrôle si le bruit est généré sur le CPU ou le GPU.",
"Avec le bruit du CPU activé, une graine particulière produira la même image sur n'importe quelle machine.",
"Il n'y a aucun impact sur les performances à activer le bruit du CPU."
]
},
"paramCFGScale": {
"heading": "Échelle CFG",
"paragraphs": [
"Contrôle de l'influence du prompt sur le processus de génération."
]
},
"loraWeight": {
"heading": "Poids"
},
"imageFit": {
"heading": "Ajuster l'image initiale à la taille de sortie",
"paragraphs": [
"Redimensionne l'image initiale à la largeur et à la hauteur de l'image de sortie. Il est recommandé de l'activer."
]
},
"paramCFGRescaleMultiplier": {
"heading": "Multiplicateur de mise à l'échelle CFG",
"paragraphs": [
"Multiplicateur de mise à l'échelle pour le guidage CFG, utilisé pour les modèles entraînés en utilisant le zero-terminal SNR (ztsnr)."
]
},
"controlNetProcessor": {
"heading": "Processeur",
"paragraphs": [
"Méthode de traitement de l'image d'entrée pour guider le processus de génération. Différents processeurs fourniront différents effets ou styles dans vos images générées."
]
}
},
"dynamicPrompts": {
"seedBehaviour": {
"label": "Comportement de la graine",
"perPromptDesc": "Utiliser une graine différente pour chaque image",
"perIterationLabel": "Graine par Itération",
"perIterationDesc": "Utiliser une graine différente pour chaque itération",
"perPromptLabel": "Graine par Image"
},
"maxPrompts": "Nombre maximum de Prompts",
"showDynamicPrompts": "Afficher les Prompts dynamiques",
"dynamicPrompts": "Prompts Dynamiques",
"promptsPreview": "Prévisualisation des Prompts",
"loading": "Génération des Pompts Dynamiques..."
},
"metadata": {
"positivePrompt": "Prompt Positif",
"allPrompts": "Tous les Prompts",
"negativePrompt": "Prompt Négatif",
"seamless": "Sans jointure",
"metadata": "Métadonné",
"scheduler": "Planificateur",
"imageDetails": "Détails de l'Image",
"seed": "Graine",
"workflow": "Processus",
"width": "Largeur",
"Threshold": "Seuil de bruit",
"noMetaData": "Aucune métadonnée trouvée",
"model": "Modèle",
"noImageDetails": "Aucun détail d'image trouvé",
"steps": "Étapes",
"cfgScale": "Échelle CFG",
"generationMode": "Mode Génération",
"height": "Hauteur",
"createdBy": "Créé par",
"strength": "Force d'image à image",
"vae": "VAE",
"noRecallParameters": "Aucun paramètre à rappeler trouvé.",
"cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)",
"recallParameters": "Rappeler les paramètres"
},
"sdxl": {
"freePromptStyle": "Écriture de Prompt manuelle",
"concatPromptStyle": "Lier Prompt & Style",
"negStylePrompt": "Prompt Négatif",
"posStylePrompt": "Prompt Positif",
"refinerStart": "Démarrer le Refiner",
"denoisingStrength": "Force de débruitage",
"steps": "Étapes",
"refinermodel": "Modèle de Refiner",
"scheduler": "Planificateur",
"cfgScale": "Échelle CFG",
"noModelsAvailable": "Aucun modèle disponible",
"posAestheticScore": "Score esthétique positif",
"loading": "Chargement...",
"negAestheticScore": "Score esthétique négatif",
"refiner": "Refiner"
},
"nodes": {
"showMinimapnodes": "Afficher la MiniCarte",
"fitViewportNodes": "Ajuster la Vue",
"hideLegendNodes": "Masquer la légende du type de champ",
"showLegendNodes": "Afficher la légende du type de champ",
"hideMinimapnodes": "Masquer MiniCarte",
"zoomOutNodes": "Dézoomer",
"zoomInNodes": "Zoomer",
"downloadWorkflow": "Télécharger processus en JSON",
"loadWorkflow": "Charger le processus",
"reloadNodeTemplates": "Recharger les modèles de nœuds",
"animatedEdges": "Connexions animées",
"cannotConnectToSelf": "Impossible de se connecter à soi-même",
"edge": "Connexion",
"workflowAuthor": "Auteur",
"enum": "Énumération",
"integer": "Entier",
"inputMayOnlyHaveOneConnection": "L'entrée ne peut avoir qu'une seule connexion.",
"noNodeSelected": "Aucun nœud sélectionné",
"nodeOpacity": "Opacité du nœud",
"workflowDescription": "Courte description",
"executionStateError": "Erreur",
"version": "Version",
"boolean": "Booléens",
"executionStateCompleted": "Terminé",
"colorCodeEdges": "Code de couleur des connexions",
"colorCodeEdgesHelp": "Code couleur des connexions en fonction de leurs champs connectés.",
"currentImage": "Image actuelle",
"noFieldsLinearview": "Aucun champ ajouté à la vue linéaire",
"float": "Flottant",
"mismatchedVersion": "Nœud invalide : le nœud {{node}} de type {{type}} a une version incompatible (essayez de mettre à jour?)",
"missingTemplate": "Nœud invalide : le nœud {{node}} de type {{type}} modèle manquant (non installé?)",
"noWorkflow": "Pas de processus",
"validateConnectionsHelp": "Prévenir la création de connexions invalides et l'invocation de graphes invalides.",
"workflowSettings": "Paramètres de l'Éditeur de Processus",
"workflowValidation": "Erreur de validation du processus",
"executionStateInProgress": "En cours",
"node": "Noeud",
"scheduler": "Planificateur",
"notes": "Notes",
"notesDescription": "Ajouter des notes sur votre flux de travail.",
"unableToLoadWorkflow": "Impossible de charger le processus",
"addNode": "Ajouter un nœud",
"problemSettingTitle": "Problème lors de définition du Titre",
"connectionWouldCreateCycle": "La connexion créerait un cycle.",
"currentImageDescription": "Affiche l'image actuelle dans l'éditeur de nœuds.",
"versionUnknown": " Version inconnue",
"cannotConnectInputToInput": "Impossible de connecter l'entrée à l'entrée.",
"addNodeToolTip": "Ajouter un nœud (Shift+A, Espace)",
"fullyContainNodesHelp": "Les nœuds doivent être entièrement à l'intérieur de la zone de sélection pour être sélectionnés.",
"cannotConnectOutputToOutput": "Impossible de connecter la sortie à la sortie.",
"loadingNodes": "Chargement des nœuds...",
"unknownField": "Champ inconnu",
"workflowNotes": "Notes",
"workflowTags": "Tags",
"animatedEdgesHelp": "Animer les connexions sélectionnées et les connexions associées aux nœuds sélectionnés",
"nodeTemplate": "Modèle de nœud",
"fieldTypesMustMatch": "Les types de champs doivent correspondre.",
"fullyContainNodes": "Contient complètement les nœuds à sélectionner",
"nodeSearch": "Rechercher des nœuds",
"collection": "Collection",
"noOutputRecorded": "Aucun résultat enregistré",
"removeLinearView": "Retirer de la vue linéaire",
"snapToGrid": "Aligner sur la grille",
"workflow": "Processus",
"updateApp": "Mettre à jour l'application",
"updateNode": "Mettre à jour le nœud",
"nodeOutputs": "Sorties de nœud",
"noConnectionInProgress": "Aucune connexion en cours",
"nodeType": "Type de nœud",
"workflowContact": "Contact",
"unknownTemplate": "Modèle inconnu",
"unknownNode": "Nœud inconnu",
"workflowVersion": "Version",
"string": "Chaîne de caractères",
"workflowName": "Nom",
"snapToGridHelp": "Aligner les nœuds sur la grille lorsqu'ils sont déplacés.",
"unableToValidateWorkflow": "Impossible de valider le processus",
"validateConnections": "Valider les connexions et le graphique",
"unableToUpdateNodes_one": "Impossible de mettre à jour {{count}} nœud",
"unableToUpdateNodes_many": "Impossible de mettre à jour {{count}} nœuds",
"unableToUpdateNodes_other": "Impossible de mettre à jour {{count}} nœuds",
"cannotDuplicateConnection": "Impossible de créer des connexions en double.",
"resetToDefaultValue": "Réinitialiser à la valeur par défaut",
"unknownNodeType": "Type de nœud inconnu",
"unknownInput": "Entrée inconnue : {{name}}",
"prototypeDesc": "Cette invocation est un prototype. Elle peut subir des modifications majeures lors des mises à jour de l'application et peut être supprimée à tout moment.",
"nodePack": "Paquet de nœuds",
"sourceNodeDoesNotExist": "Connexion invalide : le nœud source/de sortie {{node}} n'existe pas.",
"sourceNodeFieldDoesNotExist": "Connexion invalide : {{node}}.{{field}} n'existe pas",
"unableToGetWorkflowVersion": "Impossible d'obtenir la version du schéma de processus",
"newWorkflowDesc2": "Votre processus actuel comporte des modifications non enregistrées.",
"deletedInvalidEdge": "Connexion invalide supprimé {{source}} -> {{target}}",
"targetNodeDoesNotExist": "Connexion invalide : le nœud cible/entrée {{node}} n'existe pas.",
"targetNodeFieldDoesNotExist": "Connexion invalide : le champ {{node}}.{{field}} n'existe pas.",
"nodeVersion": "Version du noeud",
"clearWorkflowDesc2": "Votre processus actuel comporte des modifications non enregistrées.",
"clearWorkflow": "Effacer le Processus",
"clearWorkflowDesc": "Effacer ce processus et en commencer un nouveau?",
"unsupportedArrayItemType": "type d'élément de tableau non pris en charge \"{{type}}\"",
"addLinearView": "Ajouter à la vue linéaire",
"collectionOrScalarFieldType": "{{name}} (Unique ou Collection)",
"unableToExtractEnumOptions": "impossible d'extraire les options d'énumération",
"unsupportedAnyOfLength": "trop de membres dans l'union ({{count}})",
"ipAdapter": "IP-Adapter",
"viewMode": "Utiliser en vue linéaire",
"collectionFieldType": "{{name}} (Collection)",
"newWorkflow": "Nouveau processus",
"reorderLinearView": "Réorganiser la vue linéaire",
"unknownOutput": "Sortie inconnue : {{name}}",
"outputFieldTypeParseError": "Impossible d'analyser le type du champ de sortie {{node}}.{{field}} ({{message}})",
"unsupportedMismatchedUnion": "type CollectionOrScalar non concordant avec les types de base {{firstType}} et {{secondType}}",
"unableToParseFieldType": "impossible d'analyser le type de champ",
"betaDesc": "Cette invocation est en version bêta. Tant qu'elle n'est pas stable, elle peut avoir des changements majeurs lors des mises à jour de l'application. Nous prévoyons de soutenir cette invocation à long terme.",
"unknownFieldType": "$t(nodes.unknownField) type : {{type}}",
"inputFieldTypeParseError": "Impossible d'analyser le type du champ d'entrée {{node}}.{{field}} ({{message}})",
"unableToExtractSchemaNameFromRef": "impossible d'extraire le nom du schéma à partir de la référence",
"editMode": "Modifier dans l'éditeur de processus",
"unknownErrorValidatingWorkflow": "Erreur inconnue lors de la validation du processus.",
"updateAllNodes": "Mettre à jour les nœuds",
"allNodesUpdated": "Tous les nœuds mis à jour",
"newWorkflowDesc": "Créer un nouveau processus?"
},
"models": {
"noMatchingModels": "Aucun modèle correspondant",
"noModelsAvailable": "Aucun modèle disponible",
"loading": "chargement",
"selectModel": "Sélectionner un modèle",
"noMatchingLoRAs": "Aucun LoRA correspondant",
"lora": "LoRA",
"noRefinerModelsInstalled": "Aucun modèle SDXL Refiner installé",
"noLoRAsInstalled": "Aucun LoRA installé",
"addLora": "Ajouter LoRA",
"defaultVAE": "VAE par défaut"
},
"workflows": {
"workflowLibrary": "Bibliothèque",
"loading": "Chargement des processus",
"searchWorkflows": "Rechercher des processus",
"workflowCleared": "Processus effacé",
"noDescription": "Aucune description",
"deleteWorkflow": "Supprimer le processus",
"openWorkflow": "Ouvrir le processus",
"uploadWorkflow": "Charger à partir du fichier",
"workflowName": "Nom du processus",
"unnamedWorkflow": "Processus sans nom",
"saveWorkflowAs": "Enregistrer le processus sous",
"workflows": "Processus",
"savingWorkflow": "Enregistrement du processus...",
"saveWorkflowToProject": "Enregistrer le processus dans le projet",
"downloadWorkflow": "Enregistrer dans le fichier",
"saveWorkflow": "Enregistrer le processus",
"problemSavingWorkflow": "Problème de sauvegarde du processus",
"workflowEditorMenu": "Menu de l'Éditeur de Processus",
"newWorkflowCreated": "Nouveau processus créé",
"clearWorkflowSearchFilter": "Réinitialiser le filtre de recherche de processus",
"problemLoading": "Problème de chargement des processus",
"workflowSaved": "Processus enregistré",
"noWorkflows": "Pas de processus"
}
}

View File

@@ -729,9 +729,7 @@
"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",

View File

@@ -221,8 +221,6 @@
"uploadImage": "画像をアップロード",
"previousImage": "前の画像",
"nextImage": "次の画像",
"showOptionsPanel": "サイドパネルを表示",
"showGalleryPanel": "ギャラリーパネルを表示",
"menu": "メニュー",
"createIssue": "問題を報告",
"resetUI": "$t(accessibility.reset) UI",

View File

@@ -92,9 +92,7 @@
"mode": "모드",
"menu": "메뉴",
"uploadImage": "이미지 업로드",
"showGalleryPanel": "갤러리 패널 표시",
"reset": "리셋",
"showOptionsPanel": "사이드 패널 표시"
"reset": "리셋"
},
"modelManager": {
"availableModels": "사용 가능한 모델",

View File

@@ -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",

View File

@@ -65,7 +65,6 @@
"uploadImage": "Wgrywanie obrazu",
"previousImage": "Poprzedni obraz",
"nextImage": "Następny obraz",
"showOptionsPanel": "Pokaż panel opcji",
"menu": "Menu"
}
}

View File

@@ -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",

View File

@@ -566,11 +566,9 @@
"uploadImage": "Загрузить изображение",
"nextImage": "Следующее изображение",
"previousImage": "Предыдущее изображение",
"showOptionsPanel": "Показать боковую панель",
"invokeProgressBar": "Индикатор выполнения",
"reset": "Сброс",
"menu": "Меню",
"showGalleryPanel": "Показать панель галереи",
"mode": "Режим",
"resetUI": "$t(accessibility.reset) интерфейс",
"createIssue": "Сообщить о проблеме",

View File

@@ -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",

View File

@@ -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": {

View File

@@ -114,7 +114,6 @@
"reset": "Скинути",
"uploadImage": "Завантажити зображення",
"previousImage": "Попереднє зображення",
"showOptionsPanel": "Показати опції",
"menu": "Меню"
}
}

View File

@@ -410,9 +410,7 @@
"nextImage": "下一张图片",
"uploadImage": "上传图片",
"previousImage": "上一张图片",
"showOptionsPanel": "显示侧栏浮窗",
"menu": "菜单",
"showGalleryPanel": "显示图库浮窗",
"mode": "模式",
"resetUI": "$t(accessibility.reset) UI",
"createIssue": "创建问题",

View File

@@ -13,6 +13,10 @@ 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';
@@ -84,8 +88,8 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
<Box
id="invoke-app-wrapper"
w="100vw"
h="100vh"
w="100dvw"
h="100dvh"
position="relative"
overflow="hidden"
{...dropzone.getRootProps()}
@@ -106,6 +110,8 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
<RefreshAfterResetModal />
<DeleteBoardModal />
<GlobalImageHotkeys />
<NewGallerySessionDialog />
<NewCanvasSessionDialog />
</ErrorBoundary>
);
};

View File

@@ -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" />
@@ -65,10 +65,10 @@ const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
</Text>
</Flex>
<Flex gap={4}>
<Button leftIcon={<PiArrowCounterClockwiseBold />} onClick={resetErrorBoundary}>
<Button leftIcon={<PiArrowCounterClockwiseBold />} onPointerUp={resetErrorBoundary}>
{t('accessibility.resetUI')}
</Button>
<Button leftIcon={<PiCopyBold />} onClick={handleCopy}>
<Button leftIcon={<PiCopyBold />} onPointerUp={handleCopy}>
{t('common.copyError')}
</Button>
<Link href={url} isExternal>

View File

@@ -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({

View File

@@ -98,8 +98,8 @@ const RgbColorPicker = (props: Props) => {
export default memo(RgbColorPicker);
const ColorSwatch = ({ color, onChange }: { color: RgbColor; onChange: (color: RgbColor) => void }) => {
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
onChange(color);
}, [color, onChange]);
return <Box role="button" onClick={onClick} h={8} w={8} bg={rgbColorToString(color)} borderRadius="base" />;
return <Box role="button" onPointerUp={onPointerUp} h={8} w={8} bg={rgbColorToString(color)} borderRadius="base" />;
};

View File

@@ -109,8 +109,8 @@ const RgbaColorPicker = (props: Props) => {
export default memo(RgbaColorPicker);
const ColorSwatch = ({ color, onChange }: { color: RgbaColor; onChange: (color: RgbaColor) => void }) => {
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
onChange(color);
}, [color, onChange]);
return <Box role="button" onClick={onClick} h={8} w={8} bg={rgbaColorToString(color)} borderRadius="base" />;
return <Box role="button" onPointerUp={onPointerUp} h={8} w={8} bg={rgbaColorToString(color)} borderRadius="base" />;
};

View File

@@ -55,7 +55,7 @@ type IAIDndImageProps = FlexProps & {
imageDTO: ImageDTO | undefined;
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
onPointerUp?: (event: MouseEvent<HTMLDivElement>) => void;
withMetadataOverlay?: boolean;
isDragDisabled?: boolean;
isDropDisabled?: boolean;
@@ -82,7 +82,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
const {
imageDTO,
onError,
onClick,
onPointerUp,
withMetadataOverlay = false,
isDropDisabled = false,
isDragDisabled = false,
@@ -228,7 +228,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
<IAIDraggable
data={draggableData}
disabled={isDragDisabled || !imageDTO}
onClick={onClick}
onPointerUp={onPointerUp}
onAuxClick={openInNewTab}
/>
)}

View File

@@ -16,17 +16,17 @@ const sx: SystemStyleObject = {
},
};
type Props = Omit<IconButtonProps, 'aria-label' | 'onClick' | 'tooltip'> & {
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
type Props = Omit<IconButtonProps, 'aria-label' | 'onPointerUp' | 'tooltip'> & {
onPointerUp: (event: MouseEvent<HTMLButtonElement>) => void;
tooltip: string;
};
const IAIDndImageIcon = (props: Props) => {
const { onClick, tooltip, icon, ...rest } = props;
const { onPointerUp, tooltip, icon, ...rest } = props;
return (
<IconButton
onClick={onClick}
onPointerUp={onPointerUp}
aria-label={tooltip}
icon={icon}
variant="link"

View File

@@ -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>
);
};

View File

@@ -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)"
>

View File

@@ -89,20 +89,20 @@ 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]
);
const onClickLearnMore = useCallback(() => {
const onPointerUpLearnMore = useCallback(() => {
if (!data?.href) {
return;
}
window.open(data.href);
}, [data?.href]);
const onClickDontShowMeThese = useCallback(() => {
const onPointerUpDontShowMeThese = useCallback(() => {
dispatch(setShouldEnableInformationalPopovers(false));
toast({
title: t('settings.informationalPopoversDisabled'),
@@ -135,13 +135,13 @@ const Content = ({ data, feature, hideDisable }: ContentProps) => {
<Divider />
<Flex alignItems="center" justifyContent="space-between" w="full">
{!hideDisable && (
<Button onClick={onClickDontShowMeThese} variant="link" size="sm">
<Button onPointerUp={onPointerUpDontShowMeThese} variant="link" size="sm">
{t('common.dontShowMeThese')}
</Button>
)}
<Spacer />
{data?.href && (
<Button onClick={onClickLearnMore} leftIcon={<PiArrowSquareOutBold />} variant="link" size="sm">
<Button onPointerUp={onPointerUpLearnMore} leftIcon={<PiArrowSquareOutBold />} variant="link" size="sm">
{t('common.learnMore') ?? heading}
</Button>
)}

View File

@@ -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"

View File

@@ -33,7 +33,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addGlobalReferenceImage}
onPointerUp={addGlobalReferenceImage}
isDisabled={isFLUX}
>
{t('controlLayers.globalReferenceImage')}
@@ -46,7 +46,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addInpaintMask}
onPointerUp={addInpaintMask}
>
{t('controlLayers.inpaintMask')}
</Button>
@@ -55,7 +55,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRegionalGuidance}
onPointerUp={addRegionalGuidance}
isDisabled={isFLUX}
>
{t('controlLayers.regionalGuidance')}
@@ -65,7 +65,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRegionalReferenceImage}
onPointerUp={addRegionalReferenceImage}
isDisabled={isFLUX}
>
{t('controlLayers.regionalReferenceImage')}
@@ -79,7 +79,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addControlLayer}
onPointerUp={addControlLayer}
isDisabled={isFLUX}
>
{t('controlLayers.controlLayer')}
@@ -89,7 +89,7 @@ export const CanvasAddEntityButtons = memo(() => {
variant="ghost"
justifyContent="flex-start"
leftIcon={<PiPlusBold />}
onClick={addRasterLayer}
onPointerUp={addRasterLayer}
>
{t('controlLayers.rasterLayer')}
</Button>

View File

@@ -17,12 +17,12 @@ import { Trans, useTranslation } from 'react-i18next';
const ActivateImageViewerButton = (props: PropsWithChildren) => {
const imageViewer = useImageViewer();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
imageViewer.open();
selectCanvasRightPanelGalleryTab();
}, [imageViewer]);
return (
<Button onClick={onClick} size="sm" variant="link" color="base.50">
<Button onPointerUp={onPointerUp} size="sm" variant="link" color="base.50">
{props.children}
</Button>
);
@@ -58,13 +58,13 @@ export const CanvasAlertsSendingToGallery = () => {
const ActivateCanvasButton = (props: PropsWithChildren) => {
const dispatch = useAppDispatch();
const imageViewer = useImageViewer();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(setActiveTab('canvas'));
selectCanvasRightPanelLayersTab();
imageViewer.close();
}, [dispatch, imageViewer]);
return (
<Button onClick={onClick} size="sm" variant="link" color="base.50">
<Button onPointerUp={onPointerUp} size="sm" variant="link" color="base.50">
{props.children}
</Button>
);

View File

@@ -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,25 +26,28 @@ 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}>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onPointerUp={saveCanvasToGallery}>
{t('controlLayers.canvasContextMenu.saveCanvasToGallery')}
</MenuItem>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveBboxToGallery}>
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onPointerUp={saveBboxToGallery}>
{t('controlLayers.canvasContextMenu.saveBboxToGallery')}
</MenuItem>
</MenuGroup>
<MenuGroup title={t('controlLayers.canvasContextMenu.bboxGroup')}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newGlobalReferenceImageFromBbox}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onPointerUp={newGlobalReferenceImageFromBbox}>
{t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newRegionalReferenceImageFromBbox}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onPointerUp={newRegionalReferenceImageFromBbox}>
{t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newControlLayerFromBbox}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onPointerUp={newControlLayerFromBbox}>
{t('controlLayers.canvasContextMenu.newControlLayer')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onClick={newRasterLayerFromBbox}>
<MenuItem icon={<NewLayerIcon />} isDisabled={isBusy} onPointerUp={newRasterLayerFromBbox}>
{t('controlLayers.canvasContextMenu.newRasterLayer')}
</MenuItem>
</MenuGroup>

View File

@@ -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} onPointerUp={cropCanvasToBbox}>
{t('controlLayers.canvasContextMenu.cropCanvasToBbox')}
</MenuItem>
);
});
CanvasContextMenuItemsCropCanvasToBbox.displayName = 'CanvasContextMenuItemsCropCanvasToBbox';

View File

@@ -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>
);

View File

@@ -40,26 +40,26 @@ export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
/>
<MenuList>
<MenuGroup title={t('controlLayers.global')}>
<MenuItem icon={<PiPlusBold />} onClick={addGlobalReferenceImage} isDisabled={isFLUX}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addGlobalReferenceImage} isDisabled={isFLUX}>
{t('controlLayers.globalReferenceImage')}
</MenuItem>
</MenuGroup>
<MenuGroup title={t('controlLayers.regional')}>
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addInpaintMask}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance} isDisabled={isFLUX}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addRegionalGuidance} isDisabled={isFLUX}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalReferenceImage} isDisabled={isFLUX}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addRegionalReferenceImage} isDisabled={isFLUX}>
{t('controlLayers.regionalReferenceImage')}
</MenuItem>
</MenuGroup>
<MenuGroup title={t('controlLayers.layer_other')}>
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer} isDisabled={isFLUX}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addControlLayer} isDisabled={isFLUX}>
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
<MenuItem icon={<PiPlusBold />} onPointerUp={addRasterLayer}>
{t('controlLayers.rasterLayer')}
</MenuItem>
</MenuGroup>

View File

@@ -12,7 +12,7 @@ export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => {
const dispatch = useAppDispatch();
const isBusy = useCanvasIsBusy();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
if (!selectedEntityIdentifier) {
return;
}
@@ -21,7 +21,7 @@ export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => {
return (
<IconButton
onClick={onClick}
onPointerUp={onPointerUp}
isDisabled={!selectedEntityIdentifier || isBusy}
size="sm"
variant="link"

View File

@@ -22,7 +22,7 @@ export const EntityListSelectedEntityActionBarFilterButton = memo(() => {
return (
<IconButton
onClick={filter.start}
onPointerUp={filter.start}
isDisabled={filter.isDisabled}
size="sm"
variant="link"

View File

@@ -15,7 +15,7 @@ export const EntityListSelectedEntityActionBarSaveToAssetsButton = memo(() => {
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const adapter = useEntityAdapterSafe(selectedEntityIdentifier);
const saveLayerToAssets = useSaveLayerToAssets();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
saveLayerToAssets(adapter);
}, [saveLayerToAssets, adapter]);
@@ -29,7 +29,7 @@ export const EntityListSelectedEntityActionBarSaveToAssetsButton = memo(() => {
return (
<IconButton
onClick={onClick}
onPointerUp={onPointerUp}
isDisabled={!selectedEntityIdentifier || isBusy}
size="sm"
variant="link"

View File

@@ -22,7 +22,7 @@ export const EntityListSelectedEntityActionBarTransformButton = memo(() => {
return (
<IconButton
onClick={transform.start}
onPointerUp={transform.start}
isDisabled={transform.isDisabled}
size="sm"
variant="link"

View File

@@ -1,4 +1,4 @@
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 { useFocusRegion } from 'common/hooks/focus';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
@@ -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,14 +37,7 @@ export const CanvasMainPanelContent = memo(() => {
const showHUD = useAppSelector(selectShowHUD);
const renderMenu = useCallback(() => {
return (
<CanvasManagerProviderGate>
<MenuList>
<CanvasContextMenuGlobalMenuItems />
<CanvasContextMenuSelectedEntityMenuItems />
</MenuList>
</CanvasManagerProviderGate>
);
return <MenuContent />;
}, []);
useFocusRegion('canvas', ref);
@@ -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>
)}

View File

@@ -20,7 +20,7 @@ export const CanvasRightPanel = memo(() => {
const { t } = useTranslation();
const tabIndex = useStore($canvasRightPanelTabIndex);
const imageViewer = useImageViewer();
const onClickViewerToggleButton = useCallback(() => {
const onPointerUpViewerToggleButton = useCallback(() => {
if ($canvasRightPanelTabIndex.get() !== 1) {
$canvasRightPanelTabIndex.set(1);
}
@@ -38,7 +38,7 @@ export const CanvasRightPanel = memo(() => {
<TabList alignItems="center">
<PanelTabs />
<Spacer />
<Button size="sm" variant="ghost" onClick={onClickViewerToggleButton}>
<Button size="sm" variant="ghost" onPointerUp={onPointerUpViewerToggleButton}>
{imageViewer.isOpen ? t('gallery.closeViewer') : t('gallery.openViewer')}
</Button>
</TabList>

View File

@@ -84,7 +84,7 @@ export const ControlLayerControlAdapter = memo(() => {
<Flex w="full" gap={2}>
<ControlLayerControlAdapterModel modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
<IconButton
onClick={filter.start}
onPointerUp={filter.start}
isDisabled={filter.isDisabled}
size="sm"
alignSelf="stretch"
@@ -94,7 +94,7 @@ export const ControlLayerControlAdapter = memo(() => {
icon={<PiShootingStarBold />}
/>
<IconButton
onClick={pullBboxIntoLayer}
onPointerUp={pullBboxIntoLayer}
isDisabled={isBusy}
size="sm"
alignSelf="stretch"

View File

@@ -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 />

View File

@@ -18,7 +18,7 @@ export const ControlLayerMenuItemsConvertControlToRaster = memo(() => {
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={convertControlLayerToRasterLayer} icon={<PiLightningBold />} isDisabled={!isInteractable}>
<MenuItem onPointerUp={convertControlLayerToRasterLayer} icon={<PiLightningBold />} isDisabled={!isInteractable}>
{t('controlLayers.convertToRasterLayer')}
</MenuItem>
);

View File

@@ -28,7 +28,7 @@ export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={onToggle} icon={<PiDropHalfBold />} isDisabled={!isInteractable}>
<MenuItem onPointerUp={onToggle} icon={<PiDropHalfBold />} isDisabled={!isInteractable}>
{withTransparencyEffect
? t('controlLayers.disableTransparencyEffect')
: t('controlLayers.enableTransparencyEffect')}

View File

@@ -109,7 +109,7 @@ const FilterContent = memo(
<Button
variant="ghost"
leftIcon={<PiShootingStarBold />}
onClick={adapter.filterer.processImmediate}
onPointerUp={adapter.filterer.processImmediate}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.process')}
isDisabled={!isValid || autoProcessFilter}
@@ -119,7 +119,7 @@ const FilterContent = memo(
<Spacer />
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.filterer.reset}
onPointerUp={adapter.filterer.reset}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.reset')}
variant="ghost"
@@ -129,7 +129,7 @@ const FilterContent = memo(
<Button
variant="ghost"
leftIcon={<PiCheckBold />}
onClick={adapter.filterer.apply}
onPointerUp={adapter.filterer.apply}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.apply')}
isDisabled={!isValid || !hasProcessed}
@@ -139,7 +139,7 @@ const FilterContent = memo(
<Button
variant="ghost"
leftIcon={<PiXBold />}
onClick={adapter.filterer.cancel}
onPointerUp={adapter.filterer.cancel}
loadingText={t('controlLayers.filter.cancel')}
>
{t('controlLayers.filter.cancel')}

View File

@@ -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}
@@ -60,7 +69,7 @@ export const IPAdapterImagePreview = memo(({ image, onChangeImage, droppableData
{controlImage && (
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
<IAIDndImageIcon
onClick={handleResetControlImage}
onPointerUp={handleResetControlImage}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('common.reset')}
/>

View File

@@ -103,7 +103,7 @@ export const IPAdapterSettings = memo(() => {
/>
</Box>
<IconButton
onClick={pullBboxIntoIPAdapter}
onPointerUp={pullBboxIntoIPAdapter}
isDisabled={isBusy}
variant="ghost"
aria-label={t('controlLayers.pullBboxIntoReferenceImage')}

View File

@@ -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 />
</>

View File

@@ -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';

View File

@@ -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 />

View File

@@ -15,7 +15,7 @@ export const RasterLayerMenuItemsConvertRasterToControl = memo(() => {
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(
rasterLayerConvertedToControlLayer({
entityIdentifier,
@@ -27,7 +27,7 @@ export const RasterLayerMenuItemsConvertRasterToControl = memo(() => {
}, [defaultControlAdapter, dispatch, entityIdentifier]);
return (
<MenuItem onClick={onClick} icon={<PiLightningBold />} isDisabled={!isInteractable}>
<MenuItem onPointerUp={onPointerUp} icon={<PiLightningBold />} isDisabled={!isInteractable}>
{t('controlLayers.convertToControlLayer')}
</MenuItem>
);

View File

@@ -30,7 +30,7 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
size="sm"
variant="ghost"
leftIcon={<PiPlusBold />}
onClick={addRegionalGuidancePositivePrompt}
onPointerUp={addRegionalGuidancePositivePrompt}
isDisabled={!validActions.canAddPositivePrompt}
>
{t('controlLayers.prompt')}
@@ -39,12 +39,12 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
size="sm"
variant="ghost"
leftIcon={<PiPlusBold />}
onClick={addRegionalGuidanceNegativePrompt}
onPointerUp={addRegionalGuidanceNegativePrompt}
isDisabled={!validActions.canAddNegativePrompt}
>
{t('controlLayers.negativePrompt')}
</Button>
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onClick={addRegionalGuidanceIPAdapter}>
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onPointerUp={addRegionalGuidanceIPAdapter}>
{t('controlLayers.referenceImage')}
</Button>
</Flex>

View File

@@ -15,7 +15,7 @@ export const RegionalGuidanceDeletePromptButton = memo(({ onDelete }: Props) =>
variant="link"
aria-label={t('controlLayers.deletePrompt')}
icon={<PiTrashSimpleFill />}
onClick={onDelete}
onPointerUp={onDelete}
flexGrow={0}
size="sm"
p={0}

View File

@@ -120,7 +120,7 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
icon={<PiTrashSimpleFill />}
tooltip={t('controlLayers.deleteReferenceImage')}
aria-label={t('controlLayers.deleteReferenceImage')}
onClick={onDeleteIPAdapter}
onPointerUp={onDeleteIPAdapter}
colorScheme="error"
/>
</Flex>
@@ -135,7 +135,7 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
/>
</Box>
<IconButton
onClick={pullBboxIntoIPAdapter}
onPointerUp={pullBboxIntoIPAdapter}
isDisabled={isBusy}
variant="ghost"
aria-label={t('controlLayers.pullBboxIntoReferenceImage')}

View File

@@ -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 />
</>

View File

@@ -26,13 +26,19 @@ 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}>
<MenuItem onPointerUp={addRegionalGuidanceIPAdapter} isDisabled={isBusy}>
{t('controlLayers.addReferenceImage')}
</MenuItem>
</>

View File

@@ -17,12 +17,12 @@ export const RegionalGuidanceMenuItemsAutoNegative = memo(() => {
[entityIdentifier]
);
const autoNegative = useAppSelector(selectAutoNegative);
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(rgAutoNegativeToggled({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem icon={<PiSelectionInverseBold />} onClick={onClick}>
<MenuItem icon={<PiSelectionInverseBold />} onPointerUp={onPointerUp}>
{autoNegative ? t('controlLayers.disableAutoNegative') : t('controlLayers.enableAutoNegative')}
</MenuItem>
);

View File

@@ -10,7 +10,7 @@ export const CanvasSettingsClearCachesButton = memo(() => {
canvasManager.cache.clearAll();
}, [canvasManager]);
return (
<Button onClick={clearCaches} size="sm" colorScheme="warning">
<Button onPointerUp={clearCaches} size="sm" colorScheme="warning">
{t('controlLayers.clearCaches')}
</Button>
);

View File

@@ -7,11 +7,11 @@ import { useTranslation } from 'react-i18next';
export const CanvasSettingsClearHistoryButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(canvasClearHistory());
}, [dispatch]);
return (
<Button onClick={onClick} size="sm">
<Button onPointerUp={onPointerUp} size="sm">
{t('controlLayers.clearHistory')}
</Button>
);

View File

@@ -6,11 +6,11 @@ import { useTranslation } from 'react-i18next';
export const CanvasSettingsLogDebugInfoButton = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
canvasManager.logDebugInfo();
}, [canvasManager]);
return (
<Button onClick={onClick} size="sm">
<Button onPointerUp={onPointerUp} size="sm">
{t('controlLayers.logDebugInfo')}
</Button>
);

View File

@@ -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 />

View File

@@ -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';

View File

@@ -6,14 +6,14 @@ import { useTranslation } from 'react-i18next';
export const CanvasSettingsRecalculateRectsButton = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
for (const adapter of canvasManager.getAllAdapters()) {
adapter.transformer.requestRectCalculation();
}
}, [canvasManager]);
return (
<Button onClick={onClick} size="sm" colorScheme="warning">
<Button onPointerUp={onPointerUp} size="sm" colorScheme="warning">
{t('controlLayers.recalculateRects')}
</Button>
);

View File

@@ -60,7 +60,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
tooltip={`${t('common.accept')} (Enter)`}
aria-label={`${t('common.accept')} (Enter)`}
icon={<PiCheckBold />}
onClick={acceptSelected}
onPointerUp={acceptSelected}
colorScheme="invokeBlue"
isDisabled={!selectedImage}
/>

View File

@@ -18,7 +18,7 @@ export const StagingAreaToolbarDiscardAllButton = memo(() => {
tooltip={`${t('controlLayers.stagingArea.discardAll')} (Esc)`}
aria-label={t('controlLayers.stagingArea.discardAll')}
icon={<PiTrashSimpleBold />}
onClick={discardAll}
onPointerUp={discardAll}
colorScheme="error"
fontSize={16}
/>

View File

@@ -35,7 +35,7 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
tooltip={t('controlLayers.stagingArea.discard')}
aria-label={t('controlLayers.stagingArea.discard')}
icon={<PiXBold />}
onClick={discardSelected}
onPointerUp={discardSelected}
colorScheme="invokeBlue"
fontSize={16}
isDisabled={!selectedImage}

View File

@@ -40,7 +40,7 @@ export const StagingAreaToolbarNextButton = memo(() => {
tooltip={`${t('controlLayers.stagingArea.next')} (Right)`}
aria-label={`${t('controlLayers.stagingArea.next')} (Right)`}
icon={<PiArrowRightBold />}
onClick={selectNext}
onPointerUp={selectNext}
colorScheme="invokeBlue"
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
/>

View File

@@ -40,7 +40,7 @@ export const StagingAreaToolbarPrevButton = memo(() => {
tooltip={`${t('controlLayers.stagingArea.previous')} (Left)`}
aria-label={`${t('controlLayers.stagingArea.previous')} (Left)`}
icon={<PiArrowLeftBold />}
onClick={selectPrev}
onPointerUp={selectPrev}
colorScheme="invokeBlue"
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
/>

View File

@@ -40,7 +40,7 @@ export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
tooltip={t('controlLayers.stagingArea.saveToGallery')}
aria-label={t('controlLayers.stagingArea.saveToGallery')}
icon={<PiFloppyDiskBold />}
onClick={saveSelectedImageToGallery}
onPointerUp={saveSelectedImageToGallery}
colorScheme="invokeBlue"
isDisabled={!selectedImage || !selectedImage.imageDTO.is_intermediate}
/>

View File

@@ -29,7 +29,7 @@ export const StagingAreaToolbarToggleShowResultsButton = memo(() => {
}
data-alert={!shouldShowStagedImage}
icon={shouldShowStagedImage ? <PiEyeBold /> : <PiEyeSlashBold />}
onClick={toggleShowResults}
onPointerUp={toggleShowResults}
colorScheme="invokeBlue"
/>
);

View File

@@ -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"
onPointerUp={selectBbox}
/>
</Tooltip>
);
});

View File

@@ -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"
onPointerUp={selectBrush}
/>
</Tooltip>
);
});

View File

@@ -11,7 +11,7 @@ import { ToolViewButton } from './ToolViewButton';
export const ToolChooser: React.FC = () => {
return (
<>
<ButtonGroup isAttached>
<ButtonGroup isAttached orientation="vertical">
<ToolBrushButton />
<ToolEraserButton />
<ToolRectButton />

View File

@@ -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"
onPointerUp={selectColorPicker}
/>
</Tooltip>
);
});

View File

@@ -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"
onPointerUp={selectEraser}
/>
</Tooltip>
);
});

View File

@@ -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"
onPointerUp={selectMove}
/>
</Tooltip>
);
});

View File

@@ -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"
onPointerUp={selectRect}
/>
</Tooltip>
);
});

View File

@@ -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"
onPointerUp={selectView}
/>
</Tooltip>
);
});

View File

@@ -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 />

View File

@@ -9,13 +9,13 @@ export const CanvasToolbarFitBboxToLayersButton = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const isBusy = useCanvasIsBusy();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
canvasManager.bbox.fitToLayers();
}, [canvasManager.bbox]);
return (
<IconButton
onClick={onClick}
onPointerUp={onPointerUp}
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.fitBboxToLayers')}

View File

@@ -12,7 +12,7 @@ export const CanvasToolbarRedoButton = memo(() => {
const dispatch = useAppDispatch();
const isBusy = useCanvasIsBusy();
const mayRedo = useAppSelector(selectCanvasMayRedo);
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(canvasRedo());
}, [dispatch]);
@@ -20,7 +20,7 @@ export const CanvasToolbarRedoButton = memo(() => {
<IconButton
aria-label={t('hotkeys.canvas.redo.title')}
tooltip={t('hotkeys.canvas.redo.title')}
onClick={onClick}
onPointerUp={onPointerUp}
icon={<PiArrowClockwiseBold />}
variant="link"
alignSelf="stretch"

View File

@@ -10,7 +10,7 @@ export const CanvasToolbarResetCanvasButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(canvasReset());
canvasManager.stage.fitLayersToStage();
}, [canvasManager.stage, dispatch]);
@@ -18,7 +18,7 @@ export const CanvasToolbarResetCanvasButton = memo(() => {
<IconButton
aria-label={t('controlLayers.resetCanvas')}
tooltip={t('controlLayers.resetCanvas')}
onClick={onClick}
onPointerUp={onPointerUp}
colorScheme="error"
icon={<PiTrashBold />}
variant="link"

View File

@@ -60,7 +60,7 @@ export const CanvasToolbarResetViewButton = memo(() => {
<IconButton
tooltip={t('hotkeys.canvas.fitLayersToCanvas.title')}
aria-label={t('hotkeys.canvas.fitLayersToCanvas.title')}
onClick={canvasManager.stage.fitLayersToStage}
onPointerUp={canvasManager.stage.fitLayersToStage}
icon={<PiArrowsOutBold />}
variant="link"
alignSelf="stretch"

View File

@@ -16,7 +16,7 @@ export const CanvasToolbarSaveToGalleryButton = memo(() => {
<IconButton
variant="link"
alignSelf="stretch"
onClick={shift ? saveBboxToGallery : saveCanvasToGallery}
onPointerUp={shift ? saveBboxToGallery : saveCanvasToGallery}
icon={<PiFloppyDiskBold />}
aria-label={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
tooltip={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}

View File

@@ -174,7 +174,7 @@ const SCALE_SNAPS = [0.1, 0.15, 0.2, 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 5, 7.5, 10
const ZoomOutButton = () => {
const canvasManager = useCanvasManager();
const scale = useStore(computed(canvasManager.stage.$stageAttrs, (attrs) => attrs.scale));
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
const nextScale =
SCALE_SNAPS.slice()
.reverse()
@@ -184,7 +184,7 @@ const ZoomOutButton = () => {
return (
<IconButton
onClick={onClick}
onPointerUp={onPointerUp}
icon={<PiMagnifyingGlassMinusBold />}
aria-label="Zoom out"
variant="link"
@@ -197,14 +197,14 @@ const ZoomOutButton = () => {
const ZoomInButton = () => {
const canvasManager = useCanvasManager();
const scale = useStore(computed(canvasManager.stage.$stageAttrs, (attrs) => attrs.scale));
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
const nextScale = SCALE_SNAPS.find((snap) => snap > scale) ?? canvasManager.stage.config.MAX_SCALE;
canvasManager.stage.setScale(Math.min(nextScale, canvasManager.stage.config.MAX_SCALE));
}, [canvasManager.stage, scale]);
return (
<IconButton
onClick={onClick}
onPointerUp={onPointerUp}
icon={<PiMagnifyingGlassPlusBold />}
aria-label="Zoom out"
variant="link"

View File

@@ -12,7 +12,7 @@ export const CanvasToolbarUndoButton = memo(() => {
const dispatch = useAppDispatch();
const isBusy = useCanvasIsBusy();
const mayUndo = useAppSelector(selectCanvasMayUndo);
const onClick = useCallback(() => {
const onPointerUp = useCallback(() => {
dispatch(canvasUndo());
}, [dispatch]);
@@ -20,7 +20,7 @@ export const CanvasToolbarUndoButton = memo(() => {
<IconButton
aria-label={t('hotkeys.canvas.undo.title')}
tooltip={t('hotkeys.canvas.undo.title')}
onClick={onClick}
onPointerUp={onPointerUp}
icon={<PiArrowCounterClockwiseBold />}
variant="link"
alignSelf="stretch"

View File

@@ -68,7 +68,7 @@ const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) =>
<ButtonGroup isAttached={false} size="sm" w="full">
<Button
leftIcon={<PiArrowsOutBold />}
onClick={adapter.transformer.fitProxyRectToBbox}
onPointerUp={adapter.transformer.fitProxyRectToBbox}
isLoading={isProcessing}
loadingText={t('controlLayers.transform.fitToBbox')}
variant="ghost"
@@ -78,7 +78,7 @@ const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) =>
<Spacer />
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.transformer.resetTransform}
onPointerUp={adapter.transformer.resetTransform}
isLoading={isProcessing}
loadingText={t('controlLayers.transform.reset')}
variant="ghost"
@@ -87,7 +87,7 @@ const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) =>
</Button>
<Button
leftIcon={<PiCheckBold />}
onClick={adapter.transformer.applyTransform}
onPointerUp={adapter.transformer.applyTransform}
isLoading={isProcessing}
loadingText={t('controlLayers.transform.apply')}
variant="ghost"
@@ -96,7 +96,7 @@ const TransformContent = memo(({ adapter }: { adapter: CanvasEntityAdapter }) =>
</Button>
<Button
leftIcon={<PiXBold />}
onClick={adapter.transformer.stopTransform}
onPointerUp={adapter.transformer.stopTransform}
isLoading={isProcessing}
loadingText={t('common.cancel')}
variant="ghost"

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