mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-21 17:28:02 -05:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e5ce6b2d4 | ||
|
|
98fa946f77 | ||
|
|
ef80d40b63 | ||
|
|
7a9f923d35 | ||
|
|
fd982fa7c2 | ||
|
|
df86ed653a | ||
|
|
0be8aacee6 | ||
|
|
4f993a4f32 | ||
|
|
0158320940 | ||
|
|
bb2dc6c78b | ||
|
|
80d7d69c2f | ||
|
|
1010c9877c | ||
|
|
8fd8994ee8 | ||
|
|
262c2f1fc7 | ||
|
|
150d3239e3 | ||
|
|
e49e5e9782 | ||
|
|
2d1e745594 | ||
|
|
b793328edd | ||
|
|
e79b316645 | ||
|
|
8297e7964c | ||
|
|
26832c1a0e | ||
|
|
c29259ccdb | ||
|
|
3d4bd71098 | ||
|
|
814be44cd7 | ||
|
|
d328eaf743 | ||
|
|
b502c05009 | ||
|
|
0f333388bb | ||
|
|
bc63e2acc5 | ||
|
|
ec7e771942 | ||
|
|
fe84013392 | ||
|
|
710f81266b | ||
|
|
446e2884bc | ||
|
|
7d9f125232 | ||
|
|
66bbd62758 | ||
|
|
0875e861f5 | ||
|
|
0267d73dfc | ||
|
|
c9ab7c5233 | ||
|
|
f06765dfba | ||
|
|
f347b26999 | ||
|
|
c665cf3525 | ||
|
|
8cf19c4124 | ||
|
|
f7112ae57b | ||
|
|
2bfb0ddff5 | ||
|
|
950c9f5d0c | ||
|
|
db283d21f9 | ||
|
|
70cca7a431 | ||
|
|
3c3938cfc8 | ||
|
|
4455fc4092 | ||
|
|
4b7e920612 | ||
|
|
433146d08f | ||
|
|
324a46d0c8 | ||
|
|
c4421241f6 | ||
|
|
43b417be6b | ||
|
|
4a135c1017 | ||
|
|
dd591abc2b | ||
|
|
0e65f295ac | ||
|
|
ab7fbb7b30 | ||
|
|
92aed5e4fc | ||
|
|
d9b0697d1f | ||
|
|
34a9409bc1 | ||
|
|
319d82751a | ||
|
|
9b90834248 |
@@ -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(
|
||||
|
||||
@@ -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()],
|
||||
|
||||
@@ -26,6 +26,7 @@ from invokeai.backend.model_manager.config import (
|
||||
CLIPEmbedDiffusersConfig,
|
||||
MainBnbQuantized4bCheckpointConfig,
|
||||
MainCheckpointConfig,
|
||||
MainGGUFCheckpointConfig,
|
||||
T5EncoderBnbQuantizedLlmInt8bConfig,
|
||||
T5EncoderConfig,
|
||||
VAECheckpointConfig,
|
||||
@@ -35,6 +36,8 @@ from invokeai.backend.model_manager.load.model_loader_registry import ModelLoade
|
||||
from invokeai.backend.model_manager.util.model_util import (
|
||||
convert_bundle_to_flux_transformer_checkpoint,
|
||||
)
|
||||
from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
|
||||
from invokeai.backend.quantization.gguf.utils import TORCH_COMPATIBLE_QTYPES
|
||||
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
||||
|
||||
try:
|
||||
@@ -204,6 +207,52 @@ class FluxCheckpointModel(ModelLoader):
|
||||
return model
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.Main, format=ModelFormat.GGUFQuantized)
|
||||
class FluxGGUFCheckpointModel(ModelLoader):
|
||||
"""Class to load GGUF main models."""
|
||||
|
||||
def _load_model(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> AnyModel:
|
||||
if not isinstance(config, CheckpointConfigBase):
|
||||
raise ValueError("Only CheckpointConfigBase models are currently supported here.")
|
||||
|
||||
match submodel_type:
|
||||
case SubModelType.Transformer:
|
||||
return self._load_from_singlefile(config)
|
||||
|
||||
raise ValueError(
|
||||
f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}"
|
||||
)
|
||||
|
||||
def _load_from_singlefile(
|
||||
self,
|
||||
config: AnyModelConfig,
|
||||
) -> AnyModel:
|
||||
assert isinstance(config, MainGGUFCheckpointConfig)
|
||||
model_path = Path(config.path)
|
||||
|
||||
with SilenceWarnings():
|
||||
model = Flux(params[config.config_path])
|
||||
|
||||
# HACK(ryand): We shouldn't be hard-coding the compute_dtype here.
|
||||
sd = gguf_sd_loader(model_path, compute_dtype=torch.bfloat16)
|
||||
|
||||
# HACK(ryand): There are some broken GGUF models in circulation that have the wrong shape for img_in.weight.
|
||||
# We override the shape here to fix the issue.
|
||||
# Example model with this issue (Q4_K_M): https://civitai.com/models/705823/ggufk-flux-unchained-km-quants
|
||||
img_in_weight = sd.get("img_in.weight", None)
|
||||
if img_in_weight is not None and img_in_weight._ggml_quantization_type in TORCH_COMPATIBLE_QTYPES:
|
||||
expected_img_in_weight_shape = model.img_in.weight.shape
|
||||
img_in_weight.quantized_data = img_in_weight.quantized_data.view(expected_img_in_weight_shape)
|
||||
img_in_weight.tensor_shape = expected_img_in_weight_shape
|
||||
|
||||
model.load_state_dict(sd, assign=True)
|
||||
return model
|
||||
|
||||
|
||||
@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.Main, format=ModelFormat.BnbQuantizednf4b)
|
||||
class FluxBnbQuantizednf4bCheckpointModel(ModelLoader):
|
||||
"""Class to load main models."""
|
||||
|
||||
@@ -30,6 +30,8 @@ from invokeai.backend.model_manager.config import (
|
||||
SchedulerPredictionType,
|
||||
)
|
||||
from invokeai.backend.model_manager.util.model_util import lora_token_vector_length, read_checkpoint_meta
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
|
||||
from invokeai.backend.spandrel_image_to_image_model import SpandrelImageToImageModel
|
||||
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
||||
|
||||
@@ -187,6 +189,7 @@ class ModelProbe(object):
|
||||
if fields["type"] in [ModelType.Main, ModelType.ControlNet, ModelType.VAE] and fields["format"] in [
|
||||
ModelFormat.Checkpoint,
|
||||
ModelFormat.BnbQuantizednf4b,
|
||||
ModelFormat.GGUFQuantized,
|
||||
]:
|
||||
ckpt_config_path = cls._get_checkpoint_config_path(
|
||||
model_path,
|
||||
@@ -220,7 +223,7 @@ class ModelProbe(object):
|
||||
|
||||
@classmethod
|
||||
def get_model_type_from_checkpoint(cls, model_path: Path, checkpoint: Optional[CkptType] = None) -> ModelType:
|
||||
if model_path.suffix not in (".bin", ".pt", ".ckpt", ".safetensors", ".pth"):
|
||||
if model_path.suffix not in (".bin", ".pt", ".ckpt", ".safetensors", ".pth", ".gguf"):
|
||||
raise InvalidModelConfigException(f"{model_path}: unrecognized suffix")
|
||||
|
||||
if model_path.name == "learned_embeds.bin":
|
||||
@@ -278,12 +281,10 @@ class ModelProbe(object):
|
||||
return ModelType.SpandrelImageToImage
|
||||
except spandrel.UnsupportedModelError:
|
||||
pass
|
||||
except RuntimeError as e:
|
||||
if "No such file or directory" in str(e):
|
||||
# This error is expected if the model_path does not exist (which is the case in some unit tests).
|
||||
pass
|
||||
else:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Encountered error while probing to determine if {model_path} is a Spandrel model. Ignoring. Error: {e}"
|
||||
)
|
||||
|
||||
raise InvalidModelConfigException(f"Unable to determine model type for {model_path}")
|
||||
|
||||
@@ -408,6 +409,8 @@ class ModelProbe(object):
|
||||
model = torch.load(model_path, map_location="cpu")
|
||||
assert isinstance(model, dict)
|
||||
return model
|
||||
elif model_path.suffix.endswith(".gguf"):
|
||||
return gguf_sd_loader(model_path, compute_dtype=torch.float32)
|
||||
else:
|
||||
return safetensors.torch.load_file(model_path)
|
||||
|
||||
@@ -477,6 +480,8 @@ class CheckpointProbeBase(ProbeBase):
|
||||
or "model.diffusion_model.double_blocks.0.img_attn.proj.weight.quant_state.bitsandbytes__nf4" in state_dict
|
||||
):
|
||||
return ModelFormat.BnbQuantizednf4b
|
||||
elif any(isinstance(v, GGMLTensor) for v in state_dict.values()):
|
||||
return ModelFormat.GGUFQuantized
|
||||
return ModelFormat("checkpoint")
|
||||
|
||||
def get_variant_type(self) -> ModelVariantType:
|
||||
|
||||
@@ -130,7 +130,7 @@ class ModelSearch:
|
||||
return
|
||||
|
||||
for n in file_names:
|
||||
if n.endswith((".ckpt", ".bin", ".pth", ".safetensors", ".pt")):
|
||||
if n.endswith((".ckpt", ".bin", ".pth", ".safetensors", ".pt", ".gguf")):
|
||||
try:
|
||||
self.model_found(absolute_path / n)
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -8,6 +8,8 @@ import safetensors
|
||||
import torch
|
||||
from picklescan.scanner import scan_file_path
|
||||
|
||||
from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader
|
||||
|
||||
|
||||
def _fast_safetensors_reader(path: str) -> Dict[str, torch.Tensor]:
|
||||
checkpoint = {}
|
||||
@@ -54,7 +56,11 @@ def read_checkpoint_meta(path: Union[str, Path], scan: bool = False) -> Dict[str
|
||||
scan_result = scan_file_path(path)
|
||||
if scan_result.infected_files != 0:
|
||||
raise Exception(f'The model file "{path}" is potentially infected by malware. Aborting import.')
|
||||
checkpoint = torch.load(path, map_location=torch.device("meta"))
|
||||
if str(path).endswith(".gguf"):
|
||||
# The GGUF reader used here uses numpy memmap, so these tensors are not loaded into memory during this function
|
||||
checkpoint = gguf_sd_loader(Path(path), compute_dtype=torch.float32)
|
||||
else:
|
||||
checkpoint = torch.load(path, map_location=torch.device("meta"))
|
||||
return checkpoint
|
||||
|
||||
|
||||
|
||||
152
invokeai/backend/quantization/gguf/ggml_tensor.py
Normal file
152
invokeai/backend/quantization/gguf/ggml_tensor.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from typing import overload
|
||||
|
||||
import gguf
|
||||
import torch
|
||||
|
||||
from invokeai.backend.quantization.gguf.utils import (
|
||||
DEQUANTIZE_FUNCTIONS,
|
||||
TORCH_COMPATIBLE_QTYPES,
|
||||
dequantize,
|
||||
)
|
||||
|
||||
|
||||
def dequantize_and_run(func, args, kwargs):
|
||||
"""A helper function for running math ops on GGMLTensor inputs.
|
||||
|
||||
Dequantizes the inputs, and runs the function.
|
||||
"""
|
||||
dequantized_args = [a.get_dequantized_tensor() if hasattr(a, "get_dequantized_tensor") else a for a in args]
|
||||
dequantized_kwargs = {
|
||||
k: v.get_dequantized_tensor() if hasattr(v, "get_dequantized_tensor") else v for k, v in kwargs.items()
|
||||
}
|
||||
return func(*dequantized_args, **dequantized_kwargs)
|
||||
|
||||
|
||||
def apply_to_quantized_tensor(func, args, kwargs):
|
||||
"""A helper function to apply a function to a quantized GGML tensor, and re-wrap the result in a GGMLTensor.
|
||||
|
||||
Assumes that the first argument is a GGMLTensor.
|
||||
"""
|
||||
# We expect the first argument to be a GGMLTensor, and all other arguments to be non-GGMLTensors.
|
||||
ggml_tensor = args[0]
|
||||
assert isinstance(ggml_tensor, GGMLTensor)
|
||||
assert all(not isinstance(a, GGMLTensor) for a in args[1:])
|
||||
assert all(not isinstance(v, GGMLTensor) for v in kwargs.values())
|
||||
|
||||
new_data = func(ggml_tensor.quantized_data, *args[1:], **kwargs)
|
||||
|
||||
if new_data.dtype != ggml_tensor.quantized_data.dtype:
|
||||
# This is intended to catch calls such as `.to(dtype-torch.float32)`, which are not supported on GGMLTensors.
|
||||
raise ValueError("Operation changed the dtype of GGMLTensor unexpectedly.")
|
||||
|
||||
return GGMLTensor(
|
||||
new_data, ggml_tensor._ggml_quantization_type, ggml_tensor.tensor_shape, ggml_tensor.compute_dtype
|
||||
)
|
||||
|
||||
|
||||
GGML_TENSOR_OP_TABLE = {
|
||||
# Ops to run on the quantized tensor.
|
||||
torch.ops.aten.detach.default: apply_to_quantized_tensor, # pyright: ignore
|
||||
torch.ops.aten._to_copy.default: apply_to_quantized_tensor, # pyright: ignore
|
||||
# Ops to run on dequantized tensors.
|
||||
torch.ops.aten.t.default: dequantize_and_run, # pyright: ignore
|
||||
torch.ops.aten.addmm.default: dequantize_and_run, # pyright: ignore
|
||||
torch.ops.aten.mul.Tensor: dequantize_and_run, # pyright: ignore
|
||||
}
|
||||
|
||||
|
||||
class GGMLTensor(torch.Tensor):
|
||||
"""A torch.Tensor sub-class holding a quantized GGML tensor.
|
||||
|
||||
The underlying tensor is quantized, but the GGMLTensor class provides a dequantized view of the tensor on-the-fly
|
||||
when it is used in operations.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def __new__(
|
||||
cls,
|
||||
data: torch.Tensor,
|
||||
ggml_quantization_type: gguf.GGMLQuantizationType,
|
||||
tensor_shape: torch.Size,
|
||||
compute_dtype: torch.dtype,
|
||||
):
|
||||
# Type hinting is not supported for torch.Tensor._make_wrapper_subclass, so we ignore the errors.
|
||||
return torch.Tensor._make_wrapper_subclass( # pyright: ignore
|
||||
cls,
|
||||
data.shape,
|
||||
dtype=data.dtype,
|
||||
layout=data.layout,
|
||||
device=data.device,
|
||||
strides=data.stride(),
|
||||
storage_offset=data.storage_offset(),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: torch.Tensor,
|
||||
ggml_quantization_type: gguf.GGMLQuantizationType,
|
||||
tensor_shape: torch.Size,
|
||||
compute_dtype: torch.dtype,
|
||||
):
|
||||
self.quantized_data = data
|
||||
self._ggml_quantization_type = ggml_quantization_type
|
||||
# The dequantized shape of the tensor.
|
||||
self.tensor_shape = tensor_shape
|
||||
self.compute_dtype = compute_dtype
|
||||
|
||||
def __repr__(self, *, tensor_contents=None):
|
||||
return f"GGMLTensor(type={self._ggml_quantization_type.name}, dequantized_shape=({self.tensor_shape})"
|
||||
|
||||
@overload
|
||||
def size(self, dim: None = None) -> torch.Size: ...
|
||||
|
||||
@overload
|
||||
def size(self, dim: int) -> int: ...
|
||||
|
||||
def size(self, dim: int | None = None):
|
||||
"""Return the size of the tensor after dequantization. I.e. the shape that will be used in any math ops."""
|
||||
if dim is not None:
|
||||
return self.tensor_shape[dim]
|
||||
return self.tensor_shape
|
||||
|
||||
@property
|
||||
def shape(self) -> torch.Size: # pyright: ignore[reportIncompatibleVariableOverride] pyright doesn't understand this for some reason.
|
||||
"""The shape of the tensor after dequantization. I.e. the shape that will be used in any math ops."""
|
||||
return self.size()
|
||||
|
||||
@property
|
||||
def quantized_shape(self) -> torch.Size:
|
||||
"""The shape of the quantized tensor."""
|
||||
return self.quantized_data.shape
|
||||
|
||||
def requires_grad_(self, mode: bool = True) -> torch.Tensor:
|
||||
"""The GGMLTensor class is currently only designed for inference (not training). Setting requires_grad to True
|
||||
is not supported. This method is a no-op.
|
||||
"""
|
||||
return self
|
||||
|
||||
def get_dequantized_tensor(self):
|
||||
"""Return the dequantized tensor.
|
||||
|
||||
Args:
|
||||
dtype: The dtype of the dequantized tensor.
|
||||
"""
|
||||
if self._ggml_quantization_type in TORCH_COMPATIBLE_QTYPES:
|
||||
return self.quantized_data.to(self.compute_dtype)
|
||||
elif self._ggml_quantization_type in DEQUANTIZE_FUNCTIONS:
|
||||
# TODO(ryand): Look into how the dtype param is intended to be used.
|
||||
return dequantize(
|
||||
data=self.quantized_data, qtype=self._ggml_quantization_type, oshape=self.tensor_shape, dtype=None
|
||||
).to(self.compute_dtype)
|
||||
else:
|
||||
# There is no GPU implementation for this quantization type, so fallback to the numpy implementation.
|
||||
new = gguf.quants.dequantize(self.quantized_data.cpu().numpy(), self._ggml_quantization_type)
|
||||
return torch.from_numpy(new).to(self.quantized_data.device, dtype=self.compute_dtype)
|
||||
|
||||
@classmethod
|
||||
def __torch_dispatch__(cls, func, types, args, kwargs):
|
||||
# We will likely hit cases here in the future where a new op is encountered that is not yet supported.
|
||||
# The new op simply needs to be added to the GGML_TENSOR_OP_TABLE.
|
||||
if func in GGML_TENSOR_OP_TABLE:
|
||||
return GGML_TENSOR_OP_TABLE[func](func, args, kwargs)
|
||||
return NotImplemented
|
||||
22
invokeai/backend/quantization/gguf/loaders.py
Normal file
22
invokeai/backend/quantization/gguf/loaders.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from pathlib import Path
|
||||
|
||||
import gguf
|
||||
import torch
|
||||
|
||||
from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor
|
||||
from invokeai.backend.quantization.gguf.utils import TORCH_COMPATIBLE_QTYPES
|
||||
|
||||
|
||||
def gguf_sd_loader(path: Path, compute_dtype: torch.dtype) -> dict[str, GGMLTensor]:
|
||||
reader = gguf.GGUFReader(path)
|
||||
|
||||
sd: dict[str, GGMLTensor] = {}
|
||||
for tensor in reader.tensors:
|
||||
torch_tensor = torch.from_numpy(tensor.data)
|
||||
shape = torch.Size(tuple(int(v) for v in reversed(tensor.shape)))
|
||||
if tensor.tensor_type in TORCH_COMPATIBLE_QTYPES:
|
||||
torch_tensor = torch_tensor.view(*shape)
|
||||
sd[tensor.name] = GGMLTensor(
|
||||
torch_tensor, ggml_quantization_type=tensor.tensor_type, tensor_shape=shape, compute_dtype=compute_dtype
|
||||
)
|
||||
return sd
|
||||
308
invokeai/backend/quantization/gguf/utils.py
Normal file
308
invokeai/backend/quantization/gguf/utils.py
Normal file
@@ -0,0 +1,308 @@
|
||||
# Largely based on https://github.com/city96/ComfyUI-GGUF
|
||||
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
import gguf
|
||||
import torch
|
||||
|
||||
TORCH_COMPATIBLE_QTYPES = {None, gguf.GGMLQuantizationType.F32, gguf.GGMLQuantizationType.F16}
|
||||
|
||||
# K Quants #
|
||||
QK_K = 256
|
||||
K_SCALE_SIZE = 12
|
||||
|
||||
|
||||
def get_scale_min(scales: torch.Tensor):
|
||||
n_blocks = scales.shape[0]
|
||||
scales = scales.view(torch.uint8)
|
||||
scales = scales.reshape((n_blocks, 3, 4))
|
||||
|
||||
d, m, m_d = torch.split(scales, scales.shape[-2] // 3, dim=-2)
|
||||
|
||||
sc = torch.cat([d & 0x3F, (m_d & 0x0F) | ((d >> 2) & 0x30)], dim=-1)
|
||||
min = torch.cat([m & 0x3F, (m_d >> 4) | ((m >> 2) & 0x30)], dim=-1)
|
||||
|
||||
return (sc.reshape((n_blocks, 8)), min.reshape((n_blocks, 8)))
|
||||
|
||||
|
||||
# Legacy Quants #
|
||||
def dequantize_blocks_Q8_0(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
d, x = split_block_dims(blocks, 2)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
x = x.view(torch.int8)
|
||||
return d * x
|
||||
|
||||
|
||||
def dequantize_blocks_Q5_1(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, m, qh, qs = split_block_dims(blocks, 2, 2, 4)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
m = m.view(torch.float16).to(dtype)
|
||||
qh = to_uint32(qh)
|
||||
|
||||
qh = qh.reshape((n_blocks, 1)) >> torch.arange(32, device=d.device, dtype=torch.int32).reshape(1, 32)
|
||||
ql = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor(
|
||||
[0, 4], device=d.device, dtype=torch.uint8
|
||||
).reshape(1, 1, 2, 1)
|
||||
qh = (qh & 1).to(torch.uint8)
|
||||
ql = (ql & 0x0F).reshape((n_blocks, -1))
|
||||
|
||||
qs = ql | (qh << 4)
|
||||
return (d * qs) + m
|
||||
|
||||
|
||||
def dequantize_blocks_Q5_0(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, qh, qs = split_block_dims(blocks, 2, 4)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
qh = to_uint32(qh)
|
||||
|
||||
qh = qh.reshape(n_blocks, 1) >> torch.arange(32, device=d.device, dtype=torch.int32).reshape(1, 32)
|
||||
ql = qs.reshape(n_blocks, -1, 1, block_size // 2) >> torch.tensor(
|
||||
[0, 4], device=d.device, dtype=torch.uint8
|
||||
).reshape(1, 1, 2, 1)
|
||||
|
||||
qh = (qh & 1).to(torch.uint8)
|
||||
ql = (ql & 0x0F).reshape(n_blocks, -1)
|
||||
|
||||
qs = (ql | (qh << 4)).to(torch.int8) - 16
|
||||
return d * qs
|
||||
|
||||
|
||||
def dequantize_blocks_Q4_1(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, m, qs = split_block_dims(blocks, 2, 2)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
m = m.view(torch.float16).to(dtype)
|
||||
|
||||
qs = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor(
|
||||
[0, 4], device=d.device, dtype=torch.uint8
|
||||
).reshape(1, 1, 2, 1)
|
||||
qs = (qs & 0x0F).reshape(n_blocks, -1)
|
||||
|
||||
return (d * qs) + m
|
||||
|
||||
|
||||
def dequantize_blocks_Q4_0(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, qs = split_block_dims(blocks, 2)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
|
||||
qs = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor(
|
||||
[0, 4], device=d.device, dtype=torch.uint8
|
||||
).reshape((1, 1, 2, 1))
|
||||
qs = (qs & 0x0F).reshape((n_blocks, -1)).to(torch.int8) - 8
|
||||
return d * qs
|
||||
|
||||
|
||||
def dequantize_blocks_BF16(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
return (blocks.view(torch.int16).to(torch.int32) << 16).view(torch.float32)
|
||||
|
||||
|
||||
def dequantize_blocks_Q6_K(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
(
|
||||
ql,
|
||||
qh,
|
||||
scales,
|
||||
d,
|
||||
) = split_block_dims(blocks, QK_K // 2, QK_K // 4, QK_K // 16)
|
||||
|
||||
scales = scales.view(torch.int8).to(dtype)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
d = (d * scales).reshape((n_blocks, QK_K // 16, 1))
|
||||
|
||||
ql = ql.reshape((n_blocks, -1, 1, 64)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 2, 1)
|
||||
)
|
||||
ql = (ql & 0x0F).reshape((n_blocks, -1, 32))
|
||||
qh = qh.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 4, 1)
|
||||
)
|
||||
qh = (qh & 0x03).reshape((n_blocks, -1, 32))
|
||||
q = (ql | (qh << 4)).to(torch.int8) - 32
|
||||
q = q.reshape((n_blocks, QK_K // 16, -1))
|
||||
|
||||
return (d * q).reshape((n_blocks, QK_K))
|
||||
|
||||
|
||||
def dequantize_blocks_Q5_K(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, dmin, scales, qh, qs = split_block_dims(blocks, 2, 2, K_SCALE_SIZE, QK_K // 8)
|
||||
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
dmin = dmin.view(torch.float16).to(dtype)
|
||||
|
||||
sc, m = get_scale_min(scales)
|
||||
|
||||
d = (d * sc).reshape((n_blocks, -1, 1))
|
||||
dm = (dmin * m).reshape((n_blocks, -1, 1))
|
||||
|
||||
ql = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 2, 1)
|
||||
)
|
||||
qh = qh.reshape((n_blocks, -1, 1, 32)) >> torch.tensor(list(range(8)), device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 8, 1)
|
||||
)
|
||||
ql = (ql & 0x0F).reshape((n_blocks, -1, 32))
|
||||
qh = (qh & 0x01).reshape((n_blocks, -1, 32))
|
||||
q = ql | (qh << 4)
|
||||
|
||||
return (d * q - dm).reshape((n_blocks, QK_K))
|
||||
|
||||
|
||||
def dequantize_blocks_Q4_K(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
d, dmin, scales, qs = split_block_dims(blocks, 2, 2, K_SCALE_SIZE)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
dmin = dmin.view(torch.float16).to(dtype)
|
||||
|
||||
sc, m = get_scale_min(scales)
|
||||
|
||||
d = (d * sc).reshape((n_blocks, -1, 1))
|
||||
dm = (dmin * m).reshape((n_blocks, -1, 1))
|
||||
|
||||
qs = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 2, 1)
|
||||
)
|
||||
qs = (qs & 0x0F).reshape((n_blocks, -1, 32))
|
||||
|
||||
return (d * qs - dm).reshape((n_blocks, QK_K))
|
||||
|
||||
|
||||
def dequantize_blocks_Q3_K(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
hmask, qs, scales, d = split_block_dims(blocks, QK_K // 8, QK_K // 4, 12)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
|
||||
lscales, hscales = scales[:, :8], scales[:, 8:]
|
||||
lscales = lscales.reshape((n_blocks, 1, 8)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 2, 1)
|
||||
)
|
||||
lscales = lscales.reshape((n_blocks, 16))
|
||||
hscales = hscales.reshape((n_blocks, 1, 4)) >> torch.tensor(
|
||||
[0, 2, 4, 6], device=d.device, dtype=torch.uint8
|
||||
).reshape((1, 4, 1))
|
||||
hscales = hscales.reshape((n_blocks, 16))
|
||||
scales = (lscales & 0x0F) | ((hscales & 0x03) << 4)
|
||||
scales = scales.to(torch.int8) - 32
|
||||
|
||||
dl = (d * scales).reshape((n_blocks, 16, 1))
|
||||
|
||||
ql = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 4, 1)
|
||||
)
|
||||
qh = hmask.reshape(n_blocks, -1, 1, 32) >> torch.tensor(list(range(8)), device=d.device, dtype=torch.uint8).reshape(
|
||||
(1, 1, 8, 1)
|
||||
)
|
||||
ql = ql.reshape((n_blocks, 16, QK_K // 16)) & 3
|
||||
qh = (qh.reshape((n_blocks, 16, QK_K // 16)) & 1) ^ 1
|
||||
q = ql.to(torch.int8) - (qh << 2).to(torch.int8)
|
||||
|
||||
return (dl * q).reshape((n_blocks, QK_K))
|
||||
|
||||
|
||||
def dequantize_blocks_Q2_K(
|
||||
blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None
|
||||
) -> torch.Tensor:
|
||||
n_blocks = blocks.shape[0]
|
||||
|
||||
scales, qs, d, dmin = split_block_dims(blocks, QK_K // 16, QK_K // 4, 2)
|
||||
d = d.view(torch.float16).to(dtype)
|
||||
dmin = dmin.view(torch.float16).to(dtype)
|
||||
|
||||
# (n_blocks, 16, 1)
|
||||
dl = (d * (scales & 0xF)).reshape((n_blocks, QK_K // 16, 1))
|
||||
ml = (dmin * (scales >> 4)).reshape((n_blocks, QK_K // 16, 1))
|
||||
|
||||
shift = torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 1, 4, 1))
|
||||
|
||||
qs = (qs.reshape((n_blocks, -1, 1, 32)) >> shift) & 3
|
||||
qs = qs.reshape((n_blocks, QK_K // 16, 16))
|
||||
qs = dl * qs - ml
|
||||
|
||||
return qs.reshape((n_blocks, -1))
|
||||
|
||||
|
||||
DEQUANTIZE_FUNCTIONS: dict[
|
||||
gguf.GGMLQuantizationType, Callable[[torch.Tensor, int, int, Optional[torch.dtype]], torch.Tensor]
|
||||
] = {
|
||||
gguf.GGMLQuantizationType.BF16: dequantize_blocks_BF16,
|
||||
gguf.GGMLQuantizationType.Q8_0: dequantize_blocks_Q8_0,
|
||||
gguf.GGMLQuantizationType.Q5_1: dequantize_blocks_Q5_1,
|
||||
gguf.GGMLQuantizationType.Q5_0: dequantize_blocks_Q5_0,
|
||||
gguf.GGMLQuantizationType.Q4_1: dequantize_blocks_Q4_1,
|
||||
gguf.GGMLQuantizationType.Q4_0: dequantize_blocks_Q4_0,
|
||||
gguf.GGMLQuantizationType.Q6_K: dequantize_blocks_Q6_K,
|
||||
gguf.GGMLQuantizationType.Q5_K: dequantize_blocks_Q5_K,
|
||||
gguf.GGMLQuantizationType.Q4_K: dequantize_blocks_Q4_K,
|
||||
gguf.GGMLQuantizationType.Q3_K: dequantize_blocks_Q3_K,
|
||||
gguf.GGMLQuantizationType.Q2_K: dequantize_blocks_Q2_K,
|
||||
}
|
||||
|
||||
|
||||
def is_torch_compatible(tensor: Optional[torch.Tensor]):
|
||||
return getattr(tensor, "tensor_type", None) in TORCH_COMPATIBLE_QTYPES
|
||||
|
||||
|
||||
def is_quantized(tensor: torch.Tensor):
|
||||
return not is_torch_compatible(tensor)
|
||||
|
||||
|
||||
def dequantize(
|
||||
data: torch.Tensor, qtype: gguf.GGMLQuantizationType, oshape: torch.Size, dtype: Optional[torch.dtype] = None
|
||||
):
|
||||
"""
|
||||
Dequantize tensor back to usable shape/dtype
|
||||
"""
|
||||
block_size, type_size = gguf.GGML_QUANT_SIZES[qtype]
|
||||
dequantize_blocks = DEQUANTIZE_FUNCTIONS[qtype]
|
||||
|
||||
rows = data.reshape((-1, data.shape[-1])).view(torch.uint8)
|
||||
|
||||
n_blocks = rows.numel() // type_size
|
||||
blocks = rows.reshape((n_blocks, type_size))
|
||||
blocks = dequantize_blocks(blocks, block_size, type_size, dtype)
|
||||
return blocks.reshape(oshape)
|
||||
|
||||
|
||||
def to_uint32(x: torch.Tensor) -> torch.Tensor:
|
||||
x = x.view(torch.uint8).to(torch.int32)
|
||||
return (x[:, 0] | x[:, 1] << 8 | x[:, 2] << 16 | x[:, 3] << 24).unsqueeze(1)
|
||||
|
||||
|
||||
def split_block_dims(blocks: torch.Tensor, *args):
|
||||
n_max = blocks.shape[1]
|
||||
dims = list(args) + [n_max - sum(args)]
|
||||
return torch.split(blocks, dims, dim=1)
|
||||
|
||||
|
||||
PATCH_TYPES = Union[torch.Tensor, list[torch.Tensor], tuple[torch.Tensor]]
|
||||
@@ -52,49 +52,50 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"@fontsource-variable/inter": "^5.1.0",
|
||||
"@invoke-ai/ui-library": "^0.0.37",
|
||||
"@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",
|
||||
"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 +103,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 +119,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",
|
||||
|
||||
6092
invokeai/frontend/web/pnpm-lock.yaml
generated
6092
invokeai/frontend/web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"reportBugLabel": "Fehler melden",
|
||||
"settingsLabel": "Einstellungen",
|
||||
"img2img": "Bild zu Bild",
|
||||
"nodes": "Workflows",
|
||||
"nodes": "Arbeitsabläufe",
|
||||
"upload": "Hochladen",
|
||||
"load": "Laden",
|
||||
"statusDisconnected": "Getrennt",
|
||||
@@ -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,7 +1262,49 @@
|
||||
"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"
|
||||
},
|
||||
"upsell": {
|
||||
"shareAccess": "Zugang teilen",
|
||||
|
||||
@@ -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 (T)",
|
||||
"toggleLeftPanel": "Toggle Left Panel (G)",
|
||||
"uploadImage": "Upload Image"
|
||||
},
|
||||
"boards": {
|
||||
@@ -94,6 +94,7 @@
|
||||
"on": "On",
|
||||
"off": "Off",
|
||||
"or": "or",
|
||||
"ok": "Ok",
|
||||
"checkpoint": "Checkpoint",
|
||||
"communityLabel": "Community",
|
||||
"controlNet": "ControlNet",
|
||||
@@ -1081,6 +1082,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 +1563,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 +1677,10 @@
|
||||
"negativePrompt": "Negative Prompt",
|
||||
"beginEndStepPercentShort": "Begin/End %",
|
||||
"weight": "Weight",
|
||||
"newGallerySession": "New Gallery Session",
|
||||
"newGallerySessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be sent to the gallery.",
|
||||
"newCanvasSession": "New Canvas Session",
|
||||
"newCanvasSessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be staged on the canvas.",
|
||||
"controlMode": {
|
||||
"controlMode": "Control Mode",
|
||||
"balanced": "Balanced",
|
||||
@@ -1813,7 +1820,8 @@
|
||||
"isolatedStagingPreview": "Isolated Staging Preview",
|
||||
"isolatedFilteringPreview": "Isolated Filtering Preview",
|
||||
"isolatedTransformingPreview": "Isolated Transforming Preview",
|
||||
"invertBrushSizeScrollDirection": "Invert Scroll for Brush Size"
|
||||
"invertBrushSizeScrollDirection": "Invert Scroll for Brush Size",
|
||||
"pressureSensitivity": "Pressure Sensitivity"
|
||||
},
|
||||
"HUD": {
|
||||
"bbox": "Bbox",
|
||||
@@ -1828,6 +1836,7 @@
|
||||
}
|
||||
},
|
||||
"canvasContextMenu": {
|
||||
"canvasGroup": "Canvas",
|
||||
"saveToGalleryGroup": "Save To Gallery",
|
||||
"saveCanvasToGallery": "Save Canvas To Gallery",
|
||||
"saveBboxToGallery": "Save Bbox To Gallery",
|
||||
@@ -1835,7 +1844,8 @@
|
||||
"newGlobalReferenceImage": "New Global Reference Image",
|
||||
"newRegionalReferenceImage": "New Regional Reference Image",
|
||||
"newControlLayer": "New Control Layer",
|
||||
"newRasterLayer": "New Raster Layer"
|
||||
"newRasterLayer": "New Raster Layer",
|
||||
"cropCanvasToBbox": "Crop Canvas to Bbox"
|
||||
},
|
||||
"stagingArea": {
|
||||
"accept": "Accept",
|
||||
|
||||
@@ -219,9 +219,7 @@
|
||||
"uploadImage": "Cargar imagen",
|
||||
"previousImage": "Imagen anterior",
|
||||
"nextImage": "Siguiente imagen",
|
||||
"showOptionsPanel": "Mostrar el panel lateral",
|
||||
"menu": "Menú",
|
||||
"showGalleryPanel": "Mostrar panel de galería",
|
||||
"about": "Acerca de",
|
||||
"createIssue": "Crear un problema",
|
||||
"resetUI": "Interfaz de usuario $t(accessibility.reset)",
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"uploadImage": "Lataa kuva",
|
||||
"invokeProgressBar": "Invoken edistymispalkki",
|
||||
"nextImage": "Seuraava kuva",
|
||||
"previousImage": "Edellinen kuva",
|
||||
"showOptionsPanel": "Näytä asetukset"
|
||||
"previousImage": "Edellinen kuva"
|
||||
},
|
||||
"common": {
|
||||
"languagePickerLabel": "Kielen valinta",
|
||||
|
||||
@@ -181,7 +181,33 @@
|
||||
"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"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Images",
|
||||
@@ -209,7 +235,40 @@
|
||||
"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"
|
||||
},
|
||||
"settings": {
|
||||
"models": "Modèles",
|
||||
@@ -218,26 +277,46 @@
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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 +492,418 @@
|
||||
"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"
|
||||
},
|
||||
"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."
|
||||
]
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"paramSeed": {
|
||||
"heading": "Graine"
|
||||
},
|
||||
"scaleBeforeProcessing": {
|
||||
"heading": "Échelle avant traitement"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"paramCFGScale": {
|
||||
"heading": "Échelle CFG"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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": "Bords animés",
|
||||
"cannotConnectToSelf": "Impossible de se connecter à soi-même",
|
||||
"edge": "Bord",
|
||||
"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 bords",
|
||||
"colorCodeEdgesHelp": "Code couleur des arêtes 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 arêtes sélectionnées et les arêtes connecté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"
|
||||
},
|
||||
"models": {
|
||||
"noMatchingModels": "Aucun modèle correspondant",
|
||||
"noModelsAvailable": "Aucun modèle disponible",
|
||||
"loading": "chargement",
|
||||
"selectModel": "Sélectionner un modèle",
|
||||
"noMatchingLoRAs": "Aucun LoRA correspondant"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -221,8 +221,6 @@
|
||||
"uploadImage": "画像をアップロード",
|
||||
"previousImage": "前の画像",
|
||||
"nextImage": "次の画像",
|
||||
"showOptionsPanel": "サイドパネルを表示",
|
||||
"showGalleryPanel": "ギャラリーパネルを表示",
|
||||
"menu": "メニュー",
|
||||
"createIssue": "問題を報告",
|
||||
"resetUI": "$t(accessibility.reset) UI",
|
||||
|
||||
@@ -92,9 +92,7 @@
|
||||
"mode": "모드",
|
||||
"menu": "메뉴",
|
||||
"uploadImage": "이미지 업로드",
|
||||
"showGalleryPanel": "갤러리 패널 표시",
|
||||
"reset": "리셋",
|
||||
"showOptionsPanel": "사이드 패널 표시"
|
||||
"reset": "리셋"
|
||||
},
|
||||
"modelManager": {
|
||||
"availableModels": "사용 가능한 모델",
|
||||
|
||||
@@ -326,9 +326,7 @@
|
||||
"uploadImage": "Upload afbeelding",
|
||||
"previousImage": "Vorige afbeelding",
|
||||
"nextImage": "Volgende afbeelding",
|
||||
"showOptionsPanel": "Toon zijscherm",
|
||||
"menu": "Menu",
|
||||
"showGalleryPanel": "Toon deelscherm Galerij",
|
||||
"about": "Over",
|
||||
"mode": "Modus",
|
||||
"resetUI": "$t(accessibility.reset) UI",
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
"uploadImage": "Wgrywanie obrazu",
|
||||
"previousImage": "Poprzedni obraz",
|
||||
"nextImage": "Następny obraz",
|
||||
"showOptionsPanel": "Pokaż panel opcji",
|
||||
"menu": "Menu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,6 @@
|
||||
"invokeProgressBar": "Invocar barra de progresso",
|
||||
"reset": "Reiniciar",
|
||||
"nextImage": "Próxima imagem",
|
||||
"showOptionsPanel": "Mostrar painel de opções",
|
||||
"uploadImage": "Enviar imagem",
|
||||
"previousImage": "Imagem Anterior",
|
||||
"menu": "Menu",
|
||||
@@ -112,8 +111,7 @@
|
||||
"resetUI": "$t(accessibility.reset)UI",
|
||||
"createIssue": "Reportar Problema",
|
||||
"submitSupportTicket": "Submeter um ticket de Suporte",
|
||||
"mode": "Modo",
|
||||
"showGalleryPanel": "Mostrar Painel de Galeria"
|
||||
"mode": "Modo"
|
||||
},
|
||||
"boards": {
|
||||
"selectedForAutoAdd": "Selecionado para Auto-Adicionar",
|
||||
|
||||
@@ -566,11 +566,9 @@
|
||||
"uploadImage": "Загрузить изображение",
|
||||
"nextImage": "Следующее изображение",
|
||||
"previousImage": "Предыдущее изображение",
|
||||
"showOptionsPanel": "Показать боковую панель",
|
||||
"invokeProgressBar": "Индикатор выполнения",
|
||||
"reset": "Сброс",
|
||||
"menu": "Меню",
|
||||
"showGalleryPanel": "Показать панель галереи",
|
||||
"mode": "Режим",
|
||||
"resetUI": "$t(accessibility.reset) интерфейс",
|
||||
"createIssue": "Сообщить о проблеме",
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"invokeProgressBar": "Invoke förloppsmätare",
|
||||
"nextImage": "Nästa bild",
|
||||
"reset": "Starta om",
|
||||
"previousImage": "Föregående bild",
|
||||
"showOptionsPanel": "Visa inställningspanelen"
|
||||
"previousImage": "Föregående bild"
|
||||
},
|
||||
"common": {
|
||||
"hotkeysLabel": "Snabbtangenter",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"accessibility": {
|
||||
"invokeProgressBar": "Invoke durum çubuğu",
|
||||
"nextImage": "Sonraki Görsel",
|
||||
"showOptionsPanel": "Yan Paneli Göster",
|
||||
"reset": "Resetle",
|
||||
"uploadImage": "Görsel Yükle",
|
||||
"previousImage": "Önceki Görsel",
|
||||
@@ -10,7 +9,6 @@
|
||||
"about": "Hakkında",
|
||||
"mode": "Kip",
|
||||
"resetUI": "$t(accessibility.reset)Arayüz",
|
||||
"showGalleryPanel": "Galeri Panelini Göster",
|
||||
"createIssue": "Sorun Bildir"
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -114,7 +114,6 @@
|
||||
"reset": "Скинути",
|
||||
"uploadImage": "Завантажити зображення",
|
||||
"previousImage": "Попереднє зображення",
|
||||
"showOptionsPanel": "Показати опції",
|
||||
"menu": "Меню"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,9 +410,7 @@
|
||||
"nextImage": "下一张图片",
|
||||
"uploadImage": "上传图片",
|
||||
"previousImage": "上一张图片",
|
||||
"showOptionsPanel": "显示侧栏浮窗",
|
||||
"menu": "菜单",
|
||||
"showGalleryPanel": "显示图库浮窗",
|
||||
"mode": "模式",
|
||||
"resetUI": "$t(accessibility.reset) UI",
|
||||
"createIssue": "创建问题",
|
||||
|
||||
@@ -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';
|
||||
@@ -106,6 +110,8 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
|
||||
<RefreshAfterResetModal />
|
||||
<DeleteBoardModal />
|
||||
<GlobalImageHotkeys />
|
||||
<NewGallerySessionDialog />
|
||||
<NewCanvasSessionDialog />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -89,7 +89,7 @@ const Content = ({ data, feature, hideDisable }: ContentProps) => {
|
||||
|
||||
const paragraphs = useMemo<string[]>(
|
||||
() =>
|
||||
t(`popovers.${feature}.paragraphs`, {
|
||||
t<string, { returnObjects: true }, string[]>(`popovers.${feature}.paragraphs`, {
|
||||
returnObjects: true,
|
||||
}) ?? [],
|
||||
[feature, t]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MenuGroup, MenuItem } from '@invoke-ai/ui-library';
|
||||
import { CanvasContextMenuItemsCropCanvasToBbox } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuItemsCropCanvasToBbox';
|
||||
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
|
||||
import {
|
||||
useNewControlLayerFromBbox,
|
||||
@@ -25,6 +26,9 @@ export const CanvasContextMenuGlobalMenuItems = memo(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuGroup title={t('controlLayers.canvasContextMenu.canvasGroup')}>
|
||||
<CanvasContextMenuItemsCropCanvasToBbox />
|
||||
</MenuGroup>
|
||||
<MenuGroup title={t('controlLayers.canvasContextMenu.saveToGalleryGroup')}>
|
||||
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveCanvasToGallery}>
|
||||
{t('controlLayers.canvasContextMenu.saveCanvasToGallery')}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCropBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasContextMenuItemsCropCanvasToBbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const canvasManager = useCanvasManager();
|
||||
const cropCanvasToBbox = useCallback(async () => {
|
||||
const adapters = canvasManager.getAllAdapters();
|
||||
for (const adapter of adapters) {
|
||||
await adapter.cropToBbox();
|
||||
}
|
||||
}, [canvasManager]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiCropBold />} isDisabled={isBusy} onClick={cropCanvasToBbox}>
|
||||
{t('controlLayers.canvasContextMenu.cropCanvasToBbox')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasContextMenuItemsCropCanvasToBbox.displayName = 'CanvasContextMenuItemsCropCanvasToBbox';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuGroup } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
|
||||
@@ -28,6 +29,7 @@ const CanvasContextMenuSelectedEntityMenuItemsContent = memo(() => {
|
||||
{isTransformableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsTransform />}
|
||||
{isSaveableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsCopyToClipboard />}
|
||||
{isSaveableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsSave />}
|
||||
{isTransformableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsCropToBbox />}
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</MenuGroup>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
@@ -20,6 +21,7 @@ export const ControlLayerMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsCopyToClipboard />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
@@ -12,6 +13,7 @@ export const InpaintMaskMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
|
||||
import {
|
||||
selectCanvasRightPanelGalleryTab,
|
||||
selectCanvasRightPanelLayersTab,
|
||||
} from 'features/controlLayers/store/ephemeral';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import {
|
||||
selectSystemShouldConfirmOnNewSession,
|
||||
shouldConfirmOnNewSessionToggled,
|
||||
} from 'features/system/store/systemSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const [useNewGallerySessionDialog] = buildUseBoolean(false);
|
||||
const [useNewCanvasSessionDialog] = buildUseBoolean(false);
|
||||
|
||||
export const useNewGallerySession = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const imageViewer = useImageViewer();
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const newSessionDialog = useNewGallerySessionDialog();
|
||||
|
||||
const newGallerySessionImmediate = useCallback(() => {
|
||||
dispatch(newGallerySessionRequested());
|
||||
imageViewer.open();
|
||||
selectCanvasRightPanelGalleryTab();
|
||||
}, [dispatch, imageViewer]);
|
||||
|
||||
const newGallerySessionWithDialog = useCallback(() => {
|
||||
if (shouldConfirmOnNewSession) {
|
||||
newSessionDialog.setTrue();
|
||||
return;
|
||||
}
|
||||
newGallerySessionImmediate();
|
||||
}, [newGallerySessionImmediate, newSessionDialog, shouldConfirmOnNewSession]);
|
||||
|
||||
return { newGallerySessionImmediate, newGallerySessionWithDialog };
|
||||
};
|
||||
|
||||
export const useNewCanvasSession = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const imageViewer = useImageViewer();
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const newSessionDialog = useNewCanvasSessionDialog();
|
||||
|
||||
const newCanvasSessionImmediate = useCallback(() => {
|
||||
dispatch(newCanvasSessionRequested());
|
||||
imageViewer.close();
|
||||
selectCanvasRightPanelLayersTab();
|
||||
}, [dispatch, imageViewer]);
|
||||
|
||||
const newCanvasSessionWithDialog = useCallback(() => {
|
||||
if (shouldConfirmOnNewSession) {
|
||||
newSessionDialog.setTrue();
|
||||
return;
|
||||
}
|
||||
|
||||
newCanvasSessionImmediate();
|
||||
}, [newCanvasSessionImmediate, newSessionDialog, shouldConfirmOnNewSession]);
|
||||
|
||||
return { newCanvasSessionImmediate, newCanvasSessionWithDialog };
|
||||
};
|
||||
|
||||
export const NewGallerySessionDialog = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const dialog = useNewGallerySessionDialog();
|
||||
const { newGallerySessionImmediate } = useNewGallerySession();
|
||||
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const onToggleConfirm = useCallback(() => {
|
||||
dispatch(shouldConfirmOnNewSessionToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
isOpen={dialog.isTrue}
|
||||
onClose={dialog.setFalse}
|
||||
title={t('controlLayers.newGallerySession')}
|
||||
acceptCallback={newGallerySessionImmediate}
|
||||
acceptButtonText={t('common.ok')}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex direction="column" gap={3}>
|
||||
<Text>{t('controlLayers.newGallerySessionDesc')}</Text>
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
<FormControl>
|
||||
<FormLabel>{t('common.dontAskMeAgain')}</FormLabel>
|
||||
<Checkbox isChecked={!shouldConfirmOnNewSession} onChange={onToggleConfirm} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</ConfirmationAlertDialog>
|
||||
);
|
||||
});
|
||||
|
||||
NewGallerySessionDialog.displayName = 'NewGallerySessionDialog';
|
||||
|
||||
export const NewCanvasSessionDialog = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const dialog = useNewCanvasSessionDialog();
|
||||
const { newCanvasSessionImmediate } = useNewCanvasSession();
|
||||
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const onToggleConfirm = useCallback(() => {
|
||||
dispatch(shouldConfirmOnNewSessionToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
isOpen={dialog.isTrue}
|
||||
onClose={dialog.setFalse}
|
||||
title={t('controlLayers.newCanvasSession')}
|
||||
acceptCallback={newCanvasSessionImmediate}
|
||||
acceptButtonText={t('common.ok')}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex direction="column" gap={3}>
|
||||
<Text>{t('controlLayers.newCanvasSessionDesc')}</Text>
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
<FormControl>
|
||||
<FormLabel>{t('common.dontAskMeAgain')}</FormLabel>
|
||||
<Checkbox isChecked={!shouldConfirmOnNewSession} onChange={onToggleConfirm} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</ConfirmationAlertDialog>
|
||||
);
|
||||
});
|
||||
|
||||
NewCanvasSessionDialog.displayName = 'NewCanvasSessionDialog';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
@@ -18,6 +19,7 @@ export const RasterLayerMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsCopyToClipboard />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsCropToBbox } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCropToBbox';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
@@ -17,6 +18,7 @@ export const RegionalGuidanceMenuItems = memo(() => {
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsCropToBbox />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { CanvasSettingsIsolatedTransformingPreviewSwitch } from 'features/contro
|
||||
import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo';
|
||||
import { CanvasSettingsOutputOnlyMaskedRegionsCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsOutputOnlyMaskedRegionsCheckbox';
|
||||
import { CanvasSettingsPreserveMaskCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsPreserveMaskCheckbox';
|
||||
import { CanvasSettingsPressureSensitivityCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsPressureSensitivity';
|
||||
import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton';
|
||||
import { CanvasSettingsShowHUDSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsShowHUDSwitch';
|
||||
import { CanvasSettingsShowProgressOnCanvas } from 'features/controlLayers/components/Settings/CanvasSettingsShowProgressOnCanvasSwitch';
|
||||
@@ -50,6 +51,7 @@ export const CanvasSettingsPopover = memo(() => {
|
||||
<CanvasSettingsClipToBboxCheckbox />
|
||||
<CanvasSettingsOutputOnlyMaskedRegionsCheckbox />
|
||||
<CanvasSettingsSnapToGridCheckbox />
|
||||
<CanvasSettingsPressureSensitivityCheckbox />
|
||||
<CanvasSettingsShowProgressOnCanvas />
|
||||
<CanvasSettingsIsolatedStagingPreviewSwitch />
|
||||
<CanvasSettingsIsolatedFilteringPreviewSwitch />
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
selectPressureSensitivity,
|
||||
settingsPressureSensitivityToggled,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const CanvasSettingsPressureSensitivityCheckbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const pressureSensitivity = useAppSelector(selectPressureSensitivity);
|
||||
const onChange = useCallback<ChangeEventHandler<HTMLInputElement>>(() => {
|
||||
dispatch(settingsPressureSensitivityToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FormControl w="full">
|
||||
<FormLabel flexGrow={1}>{t('controlLayers.settings.pressureSensitivity')}</FormLabel>
|
||||
<Checkbox isChecked={pressureSensitivity} onChange={onChange} />
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasSettingsPressureSensitivityCheckbox.displayName = 'CanvasSettingsPressureSensitivityCheckbox';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolBboxButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.bbox')} (C)`}
|
||||
tooltip={`${t('controlLayers.tool.bbox')} (C)`}
|
||||
icon={<PiBoundingBoxBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectBbox}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.bbox')} (C)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.bbox')} (C)`}
|
||||
icon={<PiBoundingBoxBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectBbox}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolBrushButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.brush')} (B)`}
|
||||
tooltip={`${t('controlLayers.tool.brush')} (B)`}
|
||||
icon={<PiPaintBrushBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectBrush}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.brush')} (B)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.brush')} (B)`}
|
||||
icon={<PiPaintBrushBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectBrush}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ToolViewButton } from './ToolViewButton';
|
||||
export const ToolChooser: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup isAttached>
|
||||
<ButtonGroup isAttached orientation="vertical">
|
||||
<ToolBrushButton />
|
||||
<ToolEraserButton />
|
||||
<ToolRectButton />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolColorPickerButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.colorPicker')} (I)`}
|
||||
tooltip={`${t('controlLayers.tool.colorPicker')} (I)`}
|
||||
icon={<PiEyedropperBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectColorPicker}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.colorPicker')} (I)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.colorPicker')} (I)`}
|
||||
icon={<PiEyedropperBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectColorPicker}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolEraserButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.eraser')} (E)`}
|
||||
tooltip={`${t('controlLayers.tool.eraser')} (E)`}
|
||||
icon={<PiEraserBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectEraser}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.eraser')} (E)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.eraser')} (E)`}
|
||||
icon={<PiEraserBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectEraser}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolMoveButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.move')} (V)`}
|
||||
tooltip={`${t('controlLayers.tool.move')} (V)`}
|
||||
icon={<PiCursorBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectMove}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.move')} (V)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.move')} (V)`}
|
||||
icon={<PiCursorBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectMove}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolRectButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.rectangle')} (U)`}
|
||||
tooltip={`${t('controlLayers.tool.rectangle')} (U)`}
|
||||
icon={<PiRectangleBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectRect}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.rectangle')} (U)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.rectangle')} (U)`}
|
||||
icon={<PiRectangleBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectRect}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
@@ -21,14 +21,15 @@ export const ToolViewButton = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.view')} (H)`}
|
||||
tooltip={`${t('controlLayers.tool.view')} (H)`}
|
||||
icon={<PiHandBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectView}
|
||||
/>
|
||||
<Tooltip label={`${t('controlLayers.tool.view')} (H)`} placement="end">
|
||||
<IconButton
|
||||
aria-label={`${t('controlLayers.tool.view')} (H)`}
|
||||
icon={<PiHandBold />}
|
||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||
variant="solid"
|
||||
onClick={selectView}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Divider, Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
|
||||
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
||||
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
|
||||
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
|
||||
import { CanvasToolbarFitBboxToLayersButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton';
|
||||
@@ -31,7 +30,6 @@ export const CanvasToolbar = memo(() => {
|
||||
|
||||
return (
|
||||
<Flex w="full" gap={2} alignItems="center">
|
||||
<ToolChooser />
|
||||
<ToolColorPicker />
|
||||
<ToolSettings />
|
||||
<Spacer />
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCropBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityMenuItemsCropToBbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const onClick = useCallback(() => {
|
||||
if (!adapter) {
|
||||
return;
|
||||
}
|
||||
adapter.cropToBbox();
|
||||
}, [adapter]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiCropBold />} isDisabled={!isInteractable}>
|
||||
{t('controlLayers.cropLayerToBbox')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityMenuItemsCropToBbox.displayName = 'CanvasEntityMenuItemsCropToBbox';
|
||||
@@ -18,6 +18,7 @@ import type { CanvasEntityIdentifier, CanvasRenderableEntityState, Rect } from '
|
||||
import Konva from 'konva';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import stableHash from 'stable-hash';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
@@ -295,6 +296,11 @@ export abstract class CanvasEntityAdapterBase<
|
||||
return stableHash(arg);
|
||||
};
|
||||
|
||||
cropToBbox = (): Promise<ImageDTO> => {
|
||||
const { rect } = this.manager.stateApi.getBbox();
|
||||
return this.renderer.rasterize({ rect, replaceObjects: true, attrs: { opacity: 1, filters: [] } });
|
||||
};
|
||||
|
||||
destroy = (): void => {
|
||||
this.log.debug('Destroying module');
|
||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEnt
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLine';
|
||||
import { CanvasObjectBrushLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLineWithPressure';
|
||||
import { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLine';
|
||||
import { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
|
||||
import type { AnyObjectRenderer, AnyObjectState } from 'features/controlLayers/konva/CanvasObject/types';
|
||||
@@ -113,6 +115,15 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'brush_line_with_pressure') {
|
||||
assert(this.renderer instanceof CanvasObjectBrushLineWithPressure || !this.renderer);
|
||||
|
||||
if (!this.renderer) {
|
||||
this.renderer = new CanvasObjectBrushLineWithPressure(this.state, this);
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'eraser_line') {
|
||||
assert(this.renderer instanceof CanvasObjectEraserLine || !this.renderer);
|
||||
@@ -122,6 +133,15 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'eraser_line_with_pressure') {
|
||||
assert(this.renderer instanceof CanvasObjectEraserLineWithPressure || !this.renderer);
|
||||
|
||||
if (!this.renderer) {
|
||||
this.renderer = new CanvasObjectEraserLineWithPressure(this.state, this);
|
||||
this.konva.group.add(this.renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = this.renderer.update(this.state, true);
|
||||
} else if (this.state.type === 'rect') {
|
||||
assert(this.renderer instanceof CanvasObjectRect || !this.renderer);
|
||||
@@ -205,14 +225,18 @@ export class CanvasEntityBufferObjectRenderer extends CanvasModuleBase {
|
||||
|
||||
if (pushToState) {
|
||||
const entityIdentifier = this.parent.entityIdentifier;
|
||||
if (this.state.type === 'brush_line') {
|
||||
this.manager.stateApi.addBrushLine({ entityIdentifier, brushLine: this.state });
|
||||
} else if (this.state.type === 'eraser_line') {
|
||||
this.manager.stateApi.addEraserLine({ entityIdentifier, eraserLine: this.state });
|
||||
} else if (this.state.type === 'rect') {
|
||||
this.manager.stateApi.addRect({ entityIdentifier, rect: this.state });
|
||||
} else {
|
||||
this.log.warn({ buffer: this.state }, 'Invalid buffer object type');
|
||||
switch (this.state.type) {
|
||||
case 'brush_line':
|
||||
case 'brush_line_with_pressure':
|
||||
this.manager.stateApi.addBrushLine({ entityIdentifier, brushLine: this.state });
|
||||
break;
|
||||
case 'eraser_line':
|
||||
case 'eraser_line_with_pressure':
|
||||
this.manager.stateApi.addEraserLine({ entityIdentifier, eraserLine: this.state });
|
||||
break;
|
||||
case 'rect':
|
||||
this.manager.stateApi.addRect({ entityIdentifier, rect: this.state });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEnt
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLine';
|
||||
import { CanvasObjectBrushLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLineWithPressure';
|
||||
import { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLine';
|
||||
import { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
|
||||
import type { AnyObjectRenderer, AnyObjectState } from 'features/controlLayers/konva/CanvasObject/types';
|
||||
@@ -285,6 +287,16 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'brush_line_with_pressure') {
|
||||
assert(renderer instanceof CanvasObjectBrushLineWithPressure || !renderer);
|
||||
|
||||
if (!renderer) {
|
||||
renderer = new CanvasObjectBrushLineWithPressure(objectState, this);
|
||||
this.renderers.set(renderer.id, renderer);
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'eraser_line') {
|
||||
assert(renderer instanceof CanvasObjectEraserLine || !renderer);
|
||||
@@ -295,6 +307,16 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'eraser_line_with_pressure') {
|
||||
assert(renderer instanceof CanvasObjectEraserLineWithPressure || !renderer);
|
||||
|
||||
if (!renderer) {
|
||||
renderer = new CanvasObjectEraserLineWithPressure(objectState, this);
|
||||
this.renderers.set(renderer.id, renderer);
|
||||
this.konva.objectGroup.add(renderer.konva.group);
|
||||
}
|
||||
|
||||
didRender = renderer.update(objectState, force || isFirstRender);
|
||||
} else if (objectState.type === 'rect') {
|
||||
assert(renderer instanceof CanvasObjectRect || !renderer);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer';
|
||||
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getSVGPathDataFromPoints } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasBrushLineWithPressureState } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
export class CanvasObjectBrushLineWithPressure extends CanvasModuleBase {
|
||||
readonly type = 'object_brush_line_with_pressure';
|
||||
readonly id: string;
|
||||
readonly path: string[];
|
||||
readonly parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer;
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
state: CanvasBrushLineWithPressureState;
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
line: Konva.Path;
|
||||
};
|
||||
|
||||
constructor(
|
||||
state: CanvasBrushLineWithPressureState,
|
||||
parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer
|
||||
) {
|
||||
super();
|
||||
const { id, clip } = state;
|
||||
this.id = id;
|
||||
this.parent = parent;
|
||||
this.manager = parent.manager;
|
||||
this.path = this.manager.buildPath(this);
|
||||
this.log = this.manager.buildLogger(this);
|
||||
|
||||
this.log.debug({ state }, 'Creating module');
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({
|
||||
name: `${this.type}:group`,
|
||||
clip,
|
||||
listening: false,
|
||||
}),
|
||||
line: new Konva.Path({
|
||||
name: `${this.type}:path`,
|
||||
listening: false,
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation: 'source-over',
|
||||
}),
|
||||
};
|
||||
this.konva.group.add(this.konva.line);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
update(state: CanvasBrushLineWithPressureState, force = false): boolean {
|
||||
if (force || this.state !== state) {
|
||||
this.log.trace({ state }, 'Updating brush line with pressure');
|
||||
const { points, color, strokeWidth } = state;
|
||||
this.konva.line.setAttrs({
|
||||
data: getSVGPathDataFromPoints(points, {
|
||||
size: strokeWidth / 2,
|
||||
simulatePressure: false,
|
||||
last: true,
|
||||
thinning: 1,
|
||||
}),
|
||||
fill: rgbaColorToString(color),
|
||||
});
|
||||
this.state = state;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setVisibility(isVisible: boolean): void {
|
||||
this.log.trace({ isVisible }, 'Setting brush line visibility');
|
||||
this.konva.group.visible(isVisible);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.log.debug('Destroying module');
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
|
||||
repr = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
path: this.path,
|
||||
parent: this.parent.id,
|
||||
state: deepClone(this.state),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { CanvasEntityBufferObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer';
|
||||
import type { CanvasEntityObjectRenderer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getSVGPathDataFromPoints } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasEraserLineWithPressureState } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
export class CanvasObjectEraserLineWithPressure extends CanvasModuleBase {
|
||||
readonly type = 'object_eraser_line_with_pressure';
|
||||
readonly id: string;
|
||||
readonly path: string[];
|
||||
readonly parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer;
|
||||
readonly manager: CanvasManager;
|
||||
readonly log: Logger;
|
||||
|
||||
state: CanvasEraserLineWithPressureState;
|
||||
konva: {
|
||||
group: Konva.Group;
|
||||
line: Konva.Path;
|
||||
};
|
||||
|
||||
constructor(
|
||||
state: CanvasEraserLineWithPressureState,
|
||||
parent: CanvasEntityObjectRenderer | CanvasEntityBufferObjectRenderer
|
||||
) {
|
||||
super();
|
||||
const { id, clip } = state;
|
||||
this.id = id;
|
||||
this.parent = parent;
|
||||
this.manager = parent.manager;
|
||||
this.path = this.manager.buildPath(this);
|
||||
this.log = this.manager.buildLogger(this);
|
||||
|
||||
this.log.debug({ state }, 'Creating module');
|
||||
|
||||
this.konva = {
|
||||
group: new Konva.Group({
|
||||
name: `${this.type}:group`,
|
||||
clip,
|
||||
listening: false,
|
||||
}),
|
||||
line: new Konva.Path({
|
||||
name: `${this.type}:path`,
|
||||
listening: false,
|
||||
fill: 'red', // Eraser lines use compositing, does not matter what color they have
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation: 'destination-out',
|
||||
}),
|
||||
};
|
||||
this.konva.group.add(this.konva.line);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
update(state: CanvasEraserLineWithPressureState, force = false): boolean {
|
||||
if (force || this.state !== state) {
|
||||
this.log.trace({ state }, 'Updating eraser line with pressure');
|
||||
const { points, strokeWidth } = state;
|
||||
this.konva.line.setAttrs({
|
||||
data: getSVGPathDataFromPoints(points, {
|
||||
size: strokeWidth / 2,
|
||||
simulatePressure: false,
|
||||
last: true,
|
||||
thinning: 1,
|
||||
}),
|
||||
});
|
||||
this.state = state;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setVisibility(isVisible: boolean): void {
|
||||
this.log.trace({ isVisible }, 'Setting eraser line visibility');
|
||||
this.konva.group.visible(isVisible);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.log.debug('Destroying module');
|
||||
this.konva.group.destroy();
|
||||
};
|
||||
|
||||
repr = () => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
path: this.path,
|
||||
parent: this.parent.id,
|
||||
state: deepClone(this.state),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { CanvasObjectBrushLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLine';
|
||||
import type { CanvasObjectBrushLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectBrushLineWithPressure';
|
||||
import type { CanvasObjectEraserLine } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLine';
|
||||
import type { CanvasObjectEraserLineWithPressure } from 'features/controlLayers/konva/CanvasObject/CanvasObjectEraserLineWithPressure';
|
||||
import type { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import type { CanvasObjectRect } from 'features/controlLayers/konva/CanvasObject/CanvasObjectRect';
|
||||
import type {
|
||||
CanvasBrushLineState,
|
||||
CanvasBrushLineWithPressureState,
|
||||
CanvasEraserLineState,
|
||||
CanvasEraserLineWithPressureState,
|
||||
CanvasImageState,
|
||||
CanvasRectState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
@@ -15,9 +19,17 @@ import type {
|
||||
|
||||
export type AnyObjectRenderer =
|
||||
| CanvasObjectBrushLine
|
||||
| CanvasObjectBrushLineWithPressure
|
||||
| CanvasObjectEraserLine
|
||||
| CanvasObjectEraserLineWithPressure
|
||||
| CanvasObjectRect
|
||||
| CanvasObjectImage; /**
|
||||
* Union of all object states.
|
||||
*/
|
||||
export type AnyObjectState = CanvasBrushLineState | CanvasEraserLineState | CanvasImageState | CanvasRectState;
|
||||
export type AnyObjectState =
|
||||
| CanvasBrushLineState
|
||||
| CanvasBrushLineWithPressureState
|
||||
| CanvasEraserLineState
|
||||
| CanvasEraserLineWithPressureState
|
||||
| CanvasImageState
|
||||
| CanvasRectState;
|
||||
|
||||
@@ -95,6 +95,7 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
|
||||
initialize = () => {
|
||||
this.log.debug('Initializing module');
|
||||
this.container.style.touchAction = 'none';
|
||||
this.konva.stage.container(this.container);
|
||||
this.setResizeObserver();
|
||||
this.fitStageToContainer();
|
||||
@@ -103,9 +104,17 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
this.konva.stage.on('dragmove', this.onStageDragMove);
|
||||
this.konva.stage.on('dragend', this.onStageDragEnd);
|
||||
|
||||
// Start dragging the stage when the middle mouse button is clicked. We do not need to listen for 'pointerdown' to
|
||||
// do cleanup - that is done in onStageDragEnd.
|
||||
this.konva.stage.on('pointerdown', this.onStagePointerDown);
|
||||
|
||||
this.subscriptions.add(() => this.konva.stage.off('wheel', this.onStageMouseWheel));
|
||||
this.subscriptions.add(() => this.konva.stage.off('dragmove', this.onStageDragMove));
|
||||
this.subscriptions.add(() => this.konva.stage.off('dragend', this.onStageDragEnd));
|
||||
|
||||
// Whenever the tool changes, we should stop dragging the stage. For example, user is MMB-dragging the stage, then
|
||||
// switches to the brush tool, we should stop dragging the stage.
|
||||
this.subscriptions.add(this.manager.tool.$tool.listen(this.stopDragging));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -285,6 +294,46 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStagePointerDown = (e: KonvaEventObject<PointerEvent>) => {
|
||||
// If the middle mouse button is clicked and we are not already dragging, start dragging the stage
|
||||
if (e.evt.button === 1) {
|
||||
this.startDragging();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Forcibly starts dragging the stage. This is useful when you want to start dragging the stage programmatically.
|
||||
*/
|
||||
startDragging = () => {
|
||||
// First make sure the stage is draggable
|
||||
this.setIsDraggable(true);
|
||||
|
||||
// Then start dragging the stage if it's not already being dragged
|
||||
if (!this.konva.stage.isDragging()) {
|
||||
this.konva.stage.startDrag();
|
||||
}
|
||||
|
||||
// And render the tool to update the cursor
|
||||
this.manager.tool.render();
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops dragging the stage. This is useful when you want to stop dragging the stage programmatically.
|
||||
*/
|
||||
stopDragging = () => {
|
||||
// Now that we have stopped the current drag event, we may need to revert the stage's draggable status, depending
|
||||
// on the current tool
|
||||
this.setIsDraggable(this.manager.tool.$tool.get() === 'view');
|
||||
|
||||
// Stop dragging the stage if it's being dragged
|
||||
if (this.konva.stage.isDragging()) {
|
||||
this.konva.stage.stopDrag();
|
||||
}
|
||||
|
||||
// And render the tool to update the cursor
|
||||
this.manager.tool.render();
|
||||
};
|
||||
|
||||
onStageDragMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||
if (e.target !== this.konva.stage) {
|
||||
return;
|
||||
@@ -297,8 +346,8 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
if (e.target !== this.konva.stage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncStageAttrs();
|
||||
// Do some cleanup when the stage is no longer being dragged
|
||||
this.stopDragging();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,11 +15,16 @@ type CanvasToolBrushConfig = {
|
||||
* The outer border color for the brush tool preview.
|
||||
*/
|
||||
BORDER_OUTER_COLOR: string;
|
||||
/**
|
||||
* The number of milliseconds to wait before hiding the brush preview's fill circle after the mouse is released.
|
||||
*/
|
||||
HIDE_FILL_TIMEOUT_MS: number;
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: CanvasToolBrushConfig = {
|
||||
BORDER_INNER_COLOR: 'rgba(0,0,0,1)',
|
||||
BORDER_OUTER_COLOR: 'rgba(255,255,255,0.8)',
|
||||
HIDE_FILL_TIMEOUT_MS: 1500, // same as Affinity
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -34,6 +39,7 @@ export class CanvasToolBrush extends CanvasModuleBase {
|
||||
readonly log: Logger;
|
||||
|
||||
config: CanvasToolBrushConfig = DEFAULT_CONFIG;
|
||||
hideFillTimeoutId: number | null = null;
|
||||
|
||||
/**
|
||||
* The Konva objects that make up the brush tool preview:
|
||||
@@ -85,15 +91,37 @@ export class CanvasToolBrush extends CanvasModuleBase {
|
||||
};
|
||||
this.konva.group.add(this.konva.fillCircle, this.konva.innerBorder, this.konva.outerBorder);
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const cursorPos = this.manager.tool.$cursorPos.get();
|
||||
const tool = this.parent.$tool.get();
|
||||
|
||||
// If the cursor position is not available, do not update the brush preview. The tool module will handle visiblity.
|
||||
if (!cursorPos) {
|
||||
if (tool !== 'brush') {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.parent.$cursorPos.get();
|
||||
const canDraw = this.parent.getCanDraw();
|
||||
|
||||
if (!cursorPos || !canDraw) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isMouseDown = this.parent.$isMouseDown.get();
|
||||
const lastPointerType = this.parent.$lastPointerType.get();
|
||||
|
||||
if (lastPointerType !== 'mouse' && isMouseDown) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setVisibility(true);
|
||||
|
||||
if (this.hideFillTimeoutId !== null) {
|
||||
window.clearTimeout(this.hideFillTimeoutId);
|
||||
this.hideFillTimeoutId = null;
|
||||
}
|
||||
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
const brushPreviewFill = this.manager.stateApi.getBrushPreviewColor();
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, settings.brushWidth);
|
||||
@@ -105,6 +133,7 @@ export class CanvasToolBrush extends CanvasModuleBase {
|
||||
y: alignedCursorPos.y,
|
||||
radius,
|
||||
fill: rgbaColorToString(brushPreviewFill),
|
||||
visible: !isMouseDown && lastPointerType === 'mouse',
|
||||
});
|
||||
|
||||
// But the borders are in screen-pixels
|
||||
@@ -123,6 +152,11 @@ export class CanvasToolBrush extends CanvasModuleBase {
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
|
||||
this.hideFillTimeoutId = window.setTimeout(() => {
|
||||
this.konva.fillCircle.visible(false);
|
||||
this.hideFillTimeoutId = null;
|
||||
}, this.config.HIDE_FILL_TIMEOUT_MS);
|
||||
};
|
||||
|
||||
setVisibility = (visible: boolean) => {
|
||||
|
||||
@@ -190,13 +190,23 @@ export class CanvasToolColorPicker extends CanvasModuleBase {
|
||||
* Renders the color picker tool preview on the canvas.
|
||||
*/
|
||||
render = () => {
|
||||
const cursorPos = this.manager.tool.$cursorPos.get();
|
||||
const tool = this.parent.$tool.get();
|
||||
|
||||
// If the cursor position is not available, do not render the preview. The tool module will handle visibility.
|
||||
if (!cursorPos) {
|
||||
if (tool !== 'colorPicker') {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.parent.$cursorPos.get();
|
||||
const canDraw = this.parent.getCanDraw();
|
||||
|
||||
if (!cursorPos || tool !== 'colorPicker' || !canDraw) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setVisibility(true);
|
||||
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
const colorUnderCursor = this.parent.$colorUnderCursor.get();
|
||||
const colorPickerInnerRadius = this.manager.stage.unscale(this.config.RING_INNER_RADIUS);
|
||||
|
||||
@@ -78,12 +78,31 @@ export class CanvasToolEraser extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const cursorPos = this.manager.tool.$cursorPos.get();
|
||||
const tool = this.parent.$tool.get();
|
||||
|
||||
if (!cursorPos) {
|
||||
if (tool !== 'eraser') {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.parent.$cursorPos.get();
|
||||
const canDraw = this.parent.getCanDraw();
|
||||
|
||||
if (!cursorPos || !canDraw) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isMouseDown = this.parent.$isMouseDown.get();
|
||||
const lastPointerType = this.parent.$lastPointerType.get();
|
||||
|
||||
if (lastPointerType !== 'mouse' && isMouseDown) {
|
||||
this.setVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setVisibility(true);
|
||||
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, settings.eraserWidth);
|
||||
const radius = settings.eraserWidth / 2;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
floorCoord,
|
||||
getIsPrimaryMouseDown,
|
||||
getLastPointOfLastLine,
|
||||
getLastPointOfLastLineWithPressure,
|
||||
getLastPointOfLine,
|
||||
getPrefixedId,
|
||||
getScaledCursorPosition,
|
||||
@@ -26,12 +27,17 @@ import type {
|
||||
RgbColor,
|
||||
Tool,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { isRenderableEntity, RGBA_BLACK } from 'features/controlLayers/store/types';
|
||||
import { RGBA_BLACK } from 'features/controlLayers/store/types';
|
||||
import Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
// Konva's docs say the default drag buttons are [0], but it's actually [0,1]. We only want left-click to drag, so we
|
||||
// need to override the default. The stage handles middle-mouse dragging on its own with dedicated event listeners.
|
||||
// TODO(psyche): Fix the docs upstream!
|
||||
Konva.dragButtons = [0];
|
||||
|
||||
type CanvasToolModuleConfig = {
|
||||
BRUSH_SPACING_TARGET_SCALE: number;
|
||||
};
|
||||
@@ -76,6 +82,11 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
* The color currently under the cursor. Only has a value when the color picker tool is active.
|
||||
*/
|
||||
$colorUnderCursor = atom<RgbColor>(RGBA_BLACK);
|
||||
/**
|
||||
* The last pointer type that was used on the stage. This is used to determine if we should show a tool preview. For
|
||||
* example, when using a pen, we should not show a brush preview.
|
||||
*/
|
||||
$lastPointerType = atom<string | null>(null);
|
||||
|
||||
konva: {
|
||||
stage: Konva.Stage;
|
||||
@@ -136,11 +147,13 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
|
||||
syncCursorStyle = () => {
|
||||
const stage = this.manager.stage;
|
||||
const isMouseDown = this.$isMouseDown.get();
|
||||
const tool = this.$tool.get();
|
||||
const isStageDragging = this.manager.stage.konva.stage.isDragging();
|
||||
|
||||
if (tool === 'view') {
|
||||
stage.setCursor(isMouseDown ? 'grabbing' : 'grab');
|
||||
if (tool === 'view' && !isStageDragging) {
|
||||
stage.setCursor('grab');
|
||||
} else if (this.manager.stage.konva.stage.isDragging()) {
|
||||
stage.setCursor('grabbing');
|
||||
} else if (this.manager.stateApi.$isTransforming.get()) {
|
||||
stage.setCursor('default');
|
||||
} else if (this.manager.stateApi.$isFiltering.get()) {
|
||||
@@ -165,40 +178,31 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const stage = this.manager.stage;
|
||||
const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
const cursorPos = this.$cursorPos.get();
|
||||
const tool = this.$tool.get();
|
||||
const isFiltering = this.manager.stateApi.$isFiltering.get();
|
||||
const isStaging = this.manager.stagingArea.$isStaging.get();
|
||||
|
||||
const isDrawable =
|
||||
!!selectedEntity &&
|
||||
selectedEntity.state.isEnabled &&
|
||||
!selectedEntity.state.isLocked &&
|
||||
isRenderableEntity(selectedEntity.state);
|
||||
const isStageDragging = this.manager.stage.konva.stage.isDragging();
|
||||
|
||||
this.syncCursorStyle();
|
||||
|
||||
stage.setIsDraggable(tool === 'view');
|
||||
|
||||
if (!cursorPos || renderedEntityCount === 0 || isFiltering || isStaging) {
|
||||
// We can bail early if the mouse isn't over the stage or there are no layers
|
||||
/**
|
||||
* The tool should not be rendered when:
|
||||
* - There is no cursor position (i.e. the cursor is outside of the stage)
|
||||
* - The user is filtering, in which case the user is not allowed to use the tools. Note that we do not disable
|
||||
* the group while transforming, bc that requires use of the move tool.
|
||||
* - The canvas is staging, in which case the user is not allowed to use the tools.
|
||||
* - There are no entities rendered on the canvas. Maybe we should allow the user to draw on an empty canvas,
|
||||
* creating a new layer when they start?
|
||||
* - The stage is being dragged, in which case the user is not allowed to use the tools.
|
||||
*/
|
||||
if (!cursorPos || isFiltering || isStaging || renderedEntityCount === 0 || isStageDragging) {
|
||||
this.konva.group.visible(false);
|
||||
} else {
|
||||
this.konva.group.visible(true);
|
||||
|
||||
// No need to render the brush preview if the cursor position or color is missing
|
||||
if (cursorPos && tool === 'brush') {
|
||||
this.brushToolPreview.render();
|
||||
} else if (cursorPos && tool === 'eraser') {
|
||||
this.eraserToolPreview.render();
|
||||
} else if (cursorPos && tool === 'colorPicker') {
|
||||
this.colorPickerToolPreview.render();
|
||||
}
|
||||
|
||||
this.setToolVisibility(tool, isDrawable);
|
||||
this.brushToolPreview.render();
|
||||
this.eraserToolPreview.render();
|
||||
this.colorPickerToolPreview.render();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -257,11 +261,14 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
setEventListeners = (): (() => void) => {
|
||||
this.konva.stage.on('mouseenter', this.onStageMouseEnter);
|
||||
this.konva.stage.on('mousedown', this.onStageMouseDown);
|
||||
this.konva.stage.on('mouseup', this.onStageMouseUp);
|
||||
this.konva.stage.on('mousemove', this.onStageMouseMove);
|
||||
this.konva.stage.on('mouseleave', this.onStageMouseLeave);
|
||||
this.konva.stage.on('pointerenter', this.onStagePointerEnter);
|
||||
this.konva.stage.on('pointerdown', this.onStagePointerDown);
|
||||
this.konva.stage.on('pointerup', this.onStagePointerUp);
|
||||
this.konva.stage.on('pointermove', this.onStagePointerMove);
|
||||
|
||||
// The Konva stage doesn't appear to handle pointerleave events, so we need to listen to the container instead
|
||||
this.manager.stage.container.addEventListener('pointerleave', this.onStagePointerLeave);
|
||||
|
||||
this.konva.stage.on('wheel', this.onStageMouseWheel);
|
||||
|
||||
window.addEventListener('keydown', this.onKeyDown);
|
||||
@@ -270,13 +277,15 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
window.addEventListener('blur', this.onWindowBlur);
|
||||
|
||||
return () => {
|
||||
this.konva.stage.off('mouseenter', this.onStageMouseEnter);
|
||||
this.konva.stage.off('mousedown', this.onStageMouseDown);
|
||||
this.konva.stage.off('mouseup', this.onStageMouseUp);
|
||||
this.konva.stage.off('mousemove', this.onStageMouseMove);
|
||||
this.konva.stage.off('mouseleave', this.onStageMouseLeave);
|
||||
this.konva.stage.off('pointerenter', this.onStagePointerEnter);
|
||||
this.konva.stage.off('pointerdown', this.onStagePointerDown);
|
||||
this.konva.stage.off('pointerup', this.onStagePointerUp);
|
||||
this.konva.stage.off('pointermove', this.onStagePointerMove);
|
||||
|
||||
this.manager.stage.container.removeEventListener('pointerleave', this.onStagePointerLeave);
|
||||
|
||||
this.konva.stage.off('wheel', this.onStageMouseWheel);
|
||||
|
||||
window.removeEventListener('keydown', this.onKeyDown);
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
window.removeEventListener('pointerup', this.onWindowPointerUp);
|
||||
@@ -296,13 +305,14 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStageMouseEnter = async (_: KonvaEventObject<MouseEvent>) => {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
onStagePointerEnter = async (e: KonvaEventObject<PointerEvent>) => {
|
||||
try {
|
||||
this.$lastPointerType.set(e.evt.pointerType);
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
const isMouseDown = this.$isMouseDown.get();
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
const tool = this.$tool.get();
|
||||
@@ -320,14 +330,25 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
if (tool === 'brush') {
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line_with_pressure'),
|
||||
type: 'brush_line_with_pressure',
|
||||
points: [alignedPoint.x, alignedPoint.y, e.evt.pressure],
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -337,13 +358,23 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
if (selectedEntity.bufferRenderer.state && selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line_with_pressure'),
|
||||
type: 'eraser_line_with_pressure',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
@@ -351,26 +382,19 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStageMouseDown = async (e: KonvaEventObject<MouseEvent>) => {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$isMouseDown.set(getIsPrimaryMouseDown(e));
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
|
||||
onStagePointerDown = async (e: KonvaEventObject<PointerEvent>) => {
|
||||
try {
|
||||
const tool = this.$tool.get();
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
this.$lastPointerType.set(e.evt.pointerType);
|
||||
|
||||
if (tool === 'colorPicker') {
|
||||
const color = this.getColorUnderCursor();
|
||||
if (color) {
|
||||
this.manager.stateApi.setColor({ ...settings.color, ...color });
|
||||
}
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$isMouseDown.set(getIsPrimaryMouseDown(e));
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
const tool = this.$tool.get();
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
|
||||
const isMouseDown = this.$isMouseDown.get();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
@@ -381,36 +405,57 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
|
||||
if (tool === 'brush') {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'brush_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
const lastLinePoint = getLastPointOfLastLineWithPressure(
|
||||
selectedEntity.state.objects,
|
||||
'brush_line_with_pressure'
|
||||
);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [
|
||||
// The last point of the last line is already normalized to the entity's coordinates
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [
|
||||
lastLinePoint.x,
|
||||
lastLinePoint.y,
|
||||
lastLinePoint.pressure,
|
||||
alignedPoint.x,
|
||||
alignedPoint.y,
|
||||
],
|
||||
e.evt.pressure,
|
||||
];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y, e.evt.pressure];
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line_with_pressure'),
|
||||
type: 'brush_line_with_pressure',
|
||||
points,
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'brush_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.brushWidth);
|
||||
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [lastLinePoint.x, lastLinePoint.y, alignedPoint.x, alignedPoint.y];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y];
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('brush_line'),
|
||||
type: 'brush_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
points,
|
||||
strokeWidth: settings.brushWidth,
|
||||
color: this.manager.stateApi.getCurrentColor(),
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
@@ -419,34 +464,56 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
if (tool === 'eraser') {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'eraser_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
if (e.evt.pointerType === 'pen' && settings.pressureSensitivity) {
|
||||
const lastLinePoint = getLastPointOfLastLineWithPressure(
|
||||
selectedEntity.state.objects,
|
||||
'eraser_line_with_pressure'
|
||||
);
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [
|
||||
// The last point of the last line is already normalized to the entity's coordinates
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [
|
||||
lastLinePoint.x,
|
||||
lastLinePoint.y,
|
||||
lastLinePoint.pressure,
|
||||
alignedPoint.x,
|
||||
alignedPoint.y,
|
||||
],
|
||||
e.evt.pressure,
|
||||
];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y, e.evt.pressure];
|
||||
}
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line_with_pressure'),
|
||||
type: 'eraser_line_with_pressure',
|
||||
points,
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
} else {
|
||||
const lastLinePoint = getLastPointOfLastLine(selectedEntity.state.objects, 'eraser_line');
|
||||
const alignedPoint = alignCoordForTool(normalizedPoint, settings.eraserWidth);
|
||||
|
||||
if (selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
let points: number[];
|
||||
if (e.evt.shiftKey && lastLinePoint) {
|
||||
// Create a straight line from the last line point
|
||||
points = [lastLinePoint.x, lastLinePoint.y, alignedPoint.x, alignedPoint.y];
|
||||
} else {
|
||||
points = [alignedPoint.x, alignedPoint.y];
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer({
|
||||
id: getPrefixedId('eraser_line'),
|
||||
type: 'eraser_line',
|
||||
points: [alignedPoint.x, alignedPoint.y],
|
||||
points,
|
||||
strokeWidth: settings.eraserWidth,
|
||||
clip: this.getClip(selectedEntity.state),
|
||||
});
|
||||
@@ -469,26 +536,37 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStageMouseUp = (_: KonvaEventObject<MouseEvent>) => {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
onStagePointerUp = (e: KonvaEventObject<PointerEvent>) => {
|
||||
try {
|
||||
this.$isMouseDown.set(false);
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
if (!cursorPos) {
|
||||
this.$lastPointerType.set(e.evt.pointerType);
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tool = this.$tool.get();
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
|
||||
if (tool === 'colorPicker') {
|
||||
const color = this.getColorUnderCursor();
|
||||
if (color) {
|
||||
this.manager.stateApi.setColor({ ...settings.color, ...color });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
const isDrawable = selectedEntity?.state.isEnabled && !selectedEntity.state.isLocked;
|
||||
if (!isDrawable) {
|
||||
return;
|
||||
}
|
||||
const tool = this.$tool.get();
|
||||
|
||||
if (tool === 'brush') {
|
||||
if (selectedEntity.bufferRenderer.state?.type === 'brush_line' && selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
if (
|
||||
(selectedEntity.bufferRenderer.state?.type === 'brush_line' ||
|
||||
selectedEntity.bufferRenderer.state?.type === 'brush_line_with_pressure') &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
} else {
|
||||
selectedEntity.bufferRenderer.clearBuffer();
|
||||
@@ -496,7 +574,11 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
if (tool === 'eraser') {
|
||||
if (selectedEntity.bufferRenderer.state?.type === 'eraser_line' && selectedEntity.bufferRenderer.hasBuffer()) {
|
||||
if (
|
||||
(selectedEntity.bufferRenderer.state?.type === 'eraser_line' ||
|
||||
selectedEntity.bufferRenderer.state?.type === 'eraser_line_with_pressure') &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
} else {
|
||||
selectedEntity.bufferRenderer.clearBuffer();
|
||||
@@ -515,12 +597,14 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStageMouseMove = async (_: KonvaEventObject<MouseEvent>) => {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
onStagePointerMove = async (e: KonvaEventObject<PointerEvent>) => {
|
||||
try {
|
||||
this.$lastPointerType.set(e.evt.pointerType);
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tool = this.$tool.get();
|
||||
const cursorPos = this.syncLastCursorPos();
|
||||
|
||||
@@ -548,7 +632,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
|
||||
const settings = this.manager.stateApi.getSettings();
|
||||
|
||||
if (tool === 'brush' && bufferState.type === 'brush_line') {
|
||||
if (tool === 'brush' && (bufferState.type === 'brush_line' || bufferState.type === 'brush_line_with_pressure')) {
|
||||
const lastPoint = getLastPointOfLine(bufferState.points);
|
||||
const minDistance = settings.brushWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
|
||||
if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) {
|
||||
@@ -564,8 +648,16 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
bufferState.points.push(alignedPoint.x, alignedPoint.y);
|
||||
|
||||
if (bufferState.type === 'brush_line_with_pressure') {
|
||||
bufferState.points.push(e.evt.pressure);
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer(bufferState);
|
||||
} else if (tool === 'eraser' && bufferState.type === 'eraser_line') {
|
||||
} else if (
|
||||
tool === 'eraser' &&
|
||||
(bufferState.type === 'eraser_line' || bufferState.type === 'eraser_line_with_pressure')
|
||||
) {
|
||||
const lastPoint = getLastPointOfLine(bufferState.points);
|
||||
const minDistance = settings.eraserWidth * this.config.BRUSH_SPACING_TARGET_SCALE;
|
||||
if (!lastPoint || !isDistanceMoreThanMin(cursorPos, lastPoint, minDistance)) {
|
||||
@@ -581,6 +673,11 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
bufferState.points.push(alignedPoint.x, alignedPoint.y);
|
||||
|
||||
if (bufferState.type === 'eraser_line_with_pressure') {
|
||||
bufferState.points.push(e.evt.pressure);
|
||||
}
|
||||
|
||||
await selectedEntity.bufferRenderer.setBuffer(bufferState);
|
||||
} else if (tool === 'rect' && bufferState.type === 'rect') {
|
||||
const normalizedPoint = offsetCoord(cursorPos, selectedEntity.state.position);
|
||||
@@ -596,23 +693,27 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
onStageMouseLeave = (_: KonvaEventObject<MouseEvent>) => {
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
onStagePointerLeave = (e: PointerEvent) => {
|
||||
try {
|
||||
this.$lastPointerType.set(e.pointerType);
|
||||
this.$cursorPos.set(null);
|
||||
|
||||
if (!this.getCanDraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (
|
||||
selectedEntity &&
|
||||
selectedEntity.bufferRenderer.state?.type !== 'rect' &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
} finally {
|
||||
this.render();
|
||||
}
|
||||
|
||||
this.$cursorPos.set(null);
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (
|
||||
selectedEntity &&
|
||||
selectedEntity.bufferRenderer.state?.type !== 'rect' &&
|
||||
selectedEntity.bufferRenderer.hasBuffer()
|
||||
) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
|
||||
this.render();
|
||||
};
|
||||
|
||||
onStageMouseWheel = (e: KonvaEventObject<WheelEvent>) => {
|
||||
@@ -652,12 +753,16 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
* whatever the user was drawing from being lost, or ending up with stale state, we need to commit the buffer
|
||||
* on window pointer up.
|
||||
*/
|
||||
onWindowPointerUp = () => {
|
||||
this.$isMouseDown.set(false);
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
onWindowPointerUp = (_: PointerEvent) => {
|
||||
try {
|
||||
this.$isMouseDown.set(false);
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (selectedEntity && selectedEntity.bufferRenderer.hasBuffer() && !this.manager.$isBusy.get()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
if (selectedEntity && selectedEntity.bufferRenderer.hasBuffer() && !this.manager.$isBusy.get()) {
|
||||
selectedEntity.bufferRenderer.commitBuffer();
|
||||
}
|
||||
} finally {
|
||||
this.render();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Vector2d } from 'konva/lib/types';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import type { StrokeOptions } from 'perfect-freehand';
|
||||
import getStroke from 'perfect-freehand';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
@@ -148,14 +150,32 @@ export const getLastPointOfLine = (points: number[]): Coordinate | null => {
|
||||
if (points.length < 2) {
|
||||
return null;
|
||||
}
|
||||
const x = points[points.length - 2];
|
||||
const y = points[points.length - 1];
|
||||
const x = points.at(-2);
|
||||
const y = points.at(-1);
|
||||
if (x === undefined || y === undefined) {
|
||||
return null;
|
||||
}
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the last point of a line as a coordinate.
|
||||
* @param points An array of numbers representing points as [x1, y1, x2, y2, ...]
|
||||
* @returns The last point of the line as a coordinate, or null if the line has less than 1 point
|
||||
*/
|
||||
export const getLastPointOfLineWithPressure = (points: number[]): CoordinateWithPressure | null => {
|
||||
if (points.length < 3) {
|
||||
return null;
|
||||
}
|
||||
const x = points.at(-3);
|
||||
const y = points.at(-2);
|
||||
const pressure = points.at(-1);
|
||||
if (x === undefined || y === undefined || pressure === undefined) {
|
||||
return null;
|
||||
}
|
||||
return { x, y, pressure };
|
||||
};
|
||||
|
||||
export function getIsPrimaryMouseDown(e: KonvaEventObject<MouseEvent>) {
|
||||
return e.evt.buttons === 1;
|
||||
}
|
||||
@@ -436,7 +456,9 @@ export function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
*/
|
||||
export const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10);
|
||||
|
||||
export function getPrefixedId(prefix: CanvasEntityIdentifier['type'] | (string & Record<never, never>)): string {
|
||||
export function getPrefixedId(
|
||||
prefix: CanvasEntityIdentifier['type'] | CanvasObjectState['type'] | (string & Record<never, never>)
|
||||
): string {
|
||||
return `${prefix}:${nanoid()}`;
|
||||
}
|
||||
|
||||
@@ -492,11 +514,32 @@ export const exhaustiveCheck = (value: never): never => {
|
||||
assert(false, `Unhandled value: ${value}`);
|
||||
};
|
||||
|
||||
type CoordinateWithPressure = {
|
||||
x: number;
|
||||
y: number;
|
||||
pressure: number;
|
||||
};
|
||||
export const getLastPointOfLastLineWithPressure = (
|
||||
objects: CanvasObjectState[],
|
||||
type: 'brush_line_with_pressure' | 'eraser_line_with_pressure'
|
||||
): CoordinateWithPressure | null => {
|
||||
const lastObject = objects.at(-1);
|
||||
if (!lastObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lastObject.type === type) {
|
||||
return getLastPointOfLineWithPressure(lastObject.points);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getLastPointOfLastLine = (
|
||||
objects: CanvasObjectState[],
|
||||
type: 'brush_line' | 'eraser_line'
|
||||
): Coordinate | null => {
|
||||
const lastObject = objects[objects.length - 1];
|
||||
const lastObject = objects.at(-1);
|
||||
if (!lastObject) {
|
||||
return null;
|
||||
}
|
||||
@@ -540,3 +583,53 @@ export const getKonvaNodeDebugAttrs = (node: Konva.Node) => {
|
||||
rotation: node.rotation(),
|
||||
};
|
||||
};
|
||||
|
||||
const average = (a: number, b: number) => (a + b) / 2;
|
||||
|
||||
function getSvgPathFromStroke(points: number[][], closed = true) {
|
||||
const len = points.length;
|
||||
|
||||
if (len < 4) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let a = points[0] as number[];
|
||||
let b = points[1] as number[];
|
||||
const c = points[2] as number[];
|
||||
|
||||
let result = `M${a[0]!.toFixed(2)},${a[1]!.toFixed(2)} Q${b[0]!.toFixed(
|
||||
2
|
||||
)},${b[1]!.toFixed(2)} ${average(b[0]!, c[0]!).toFixed(2)},${average(b[1]!, c[1]!).toFixed(2)} T`;
|
||||
|
||||
for (let i = 2, max = len - 1; i < max; i++) {
|
||||
a = points[i]!;
|
||||
b = points[i + 1]!;
|
||||
result += `${average(a[0]!, b[0]!).toFixed(2)},${average(a[1]!, b[1]!).toFixed(2)} `;
|
||||
}
|
||||
|
||||
if (closed) {
|
||||
result += 'Z';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const getSVGPathDataFromPoints = (points: number[], options?: StrokeOptions): string => {
|
||||
const chunked: [number, number, number][] = [];
|
||||
for (let i = 0; i < points.length; i += 3) {
|
||||
chunked.push([points[i]!, points[i + 1]!, points[i + 2]!]);
|
||||
}
|
||||
return getSvgPathFromStroke(getStroke(chunked, options));
|
||||
};
|
||||
|
||||
export const getPointerType = (e: KonvaEventObject<PointerEvent>): 'mouse' | 'pen' | 'touch' => {
|
||||
if (e.evt.pointerType === 'mouse') {
|
||||
return 'mouse';
|
||||
}
|
||||
|
||||
if (e.evt.pointerType === 'pen') {
|
||||
return 'pen';
|
||||
}
|
||||
|
||||
return 'touch';
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { createAction, isAnyOf } from '@reduxjs/toolkit';
|
||||
|
||||
// Needed to split this from canvasSlice.ts to avoid circular dependencies
|
||||
export const canvasReset = createAction('canvas/canvasReset');
|
||||
export const newGallerySessionRequested = createAction('canvas/newGallerySessionRequested');
|
||||
export const newCanvasSessionRequested = createAction('canvas/newCanvasSessionRequested');
|
||||
export const newSessionRequested = isAnyOf(newGallerySessionRequested, newCanvasSessionRequested);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
|
||||
type CanvasSettingsState = {
|
||||
@@ -78,6 +79,10 @@ type CanvasSettingsState = {
|
||||
* Whether to show only the selected layer while transforming.
|
||||
*/
|
||||
isolatedTransformingPreview: boolean;
|
||||
/**
|
||||
* Whether to use pressure sensitivity for the brush and eraser tool when a pen device is used.
|
||||
*/
|
||||
pressureSensitivity: boolean;
|
||||
};
|
||||
|
||||
const initialState: CanvasSettingsState = {
|
||||
@@ -98,6 +103,7 @@ const initialState: CanvasSettingsState = {
|
||||
isolatedStagingPreview: true,
|
||||
isolatedFilteringPreview: true,
|
||||
isolatedTransformingPreview: true,
|
||||
pressureSensitivity: true,
|
||||
};
|
||||
|
||||
export const canvasSettingsSlice = createSlice({
|
||||
@@ -155,6 +161,17 @@ export const canvasSettingsSlice = createSlice({
|
||||
settingsIsolatedTransformingPreviewToggled: (state) => {
|
||||
state.isolatedTransformingPreview = !state.isolatedTransformingPreview;
|
||||
},
|
||||
settingsPressureSensitivityToggled: (state) => {
|
||||
state.pressureSensitivity = !state.pressureSensitivity;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(newGallerySessionRequested, (state) => {
|
||||
state.sendToCanvas = false;
|
||||
});
|
||||
builder.addCase(newCanvasSessionRequested, (state) => {
|
||||
state.sendToCanvas = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -176,6 +193,7 @@ export const {
|
||||
settingsIsolatedStagingPreviewToggled,
|
||||
settingsIsolatedFilteringPreviewToggled,
|
||||
settingsIsolatedTransformingPreviewToggled,
|
||||
settingsPressureSensitivityToggled,
|
||||
} = canvasSettingsSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
@@ -214,3 +232,4 @@ export const selectIsolatedFilteringPreview = createCanvasSettingsSelector(
|
||||
export const selectIsolatedTransformingPreview = createCanvasSettingsSelector(
|
||||
(settings) => settings.isolatedTransformingPreview
|
||||
);
|
||||
export const selectPressureSensitivity = createCanvasSettingsSelector((settings) => settings.pressureSensitivity);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/uti
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import { canvasReset, newSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
|
||||
import {
|
||||
selectAllEntities,
|
||||
@@ -857,6 +857,9 @@ export const canvasSlice = createSlice({
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
newEntity.id = getPrefixedId('regional_guidance');
|
||||
for (const refImage of newEntity.referenceImages) {
|
||||
refImage.id = getPrefixedId('regional_guidance_ip_adapter');
|
||||
}
|
||||
state.regionalGuidance.entities.push(newEntity);
|
||||
break;
|
||||
case 'reference_image':
|
||||
@@ -947,7 +950,11 @@ export const canvasSlice = createSlice({
|
||||
|
||||
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
|
||||
// re-render it (reference equality check). I don't like this behaviour.
|
||||
entity.objects.push({ ...brushLine, points: simplifyFlatNumbersArray(brushLine.points) });
|
||||
entity.objects.push({
|
||||
...brushLine,
|
||||
// If the brush line is not pressure sensitive, we simplify the points to reduce the size of the state
|
||||
points: brushLine.type === 'brush_line' ? simplifyFlatNumbersArray(brushLine.points) : brushLine.points,
|
||||
});
|
||||
},
|
||||
entityEraserLineAdded: (state, action: PayloadAction<EntityEraserLineAddedPayload>) => {
|
||||
const { entityIdentifier, eraserLine } = action.payload;
|
||||
@@ -962,7 +969,11 @@ export const canvasSlice = createSlice({
|
||||
|
||||
// TODO(psyche): If we add the object without splatting, the renderer will see it as the same object and not
|
||||
// re-render it (reference equality check). I don't like this behaviour.
|
||||
entity.objects.push({ ...eraserLine, points: simplifyFlatNumbersArray(eraserLine.points) });
|
||||
entity.objects.push({
|
||||
...eraserLine,
|
||||
// If the brush line is not pressure sensitive, we simplify the points to reduce the size of the state
|
||||
points: eraserLine.type === 'eraser_line' ? simplifyFlatNumbersArray(eraserLine.points) : eraserLine.points,
|
||||
});
|
||||
},
|
||||
entityRectAdded: (state, action: PayloadAction<EntityRectAddedPayload>) => {
|
||||
const { entityIdentifier, rect } = action.payload;
|
||||
@@ -1122,6 +1133,9 @@ export const canvasSlice = createSlice({
|
||||
syncScaledSize(state);
|
||||
}
|
||||
});
|
||||
builder.addMatcher(newSessionRequested, (state) => {
|
||||
return resetState(state);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import type { StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
|
||||
|
||||
import { newSessionRequested } from './actions';
|
||||
|
||||
type CanvasStagingAreaState = {
|
||||
stagedImages: StagingAreaImage[];
|
||||
selectedStagedImageIndex: number;
|
||||
@@ -43,6 +45,7 @@ export const canvasStagingAreaSlice = createSlice({
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(canvasReset, () => deepClone(initialState));
|
||||
builder.addMatcher(newSessionRequested, () => deepClone(initialState));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { LoRA } from 'features/controlLayers/store/types';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import type { LoRAModelConfig } from 'services/api/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { newSessionRequested } from './actions';
|
||||
|
||||
type LoRAsState = {
|
||||
loras: LoRA[];
|
||||
};
|
||||
@@ -34,6 +37,7 @@ export const lorasSlice = createSlice({
|
||||
},
|
||||
loraRecalled: (state, action: PayloadAction<{ lora: LoRA }>) => {
|
||||
const { lora } = action.payload;
|
||||
state.loras = state.loras.filter((l) => l.model.key !== lora.model.key && l.id !== lora.id);
|
||||
state.loras.push(lora);
|
||||
},
|
||||
loraDeleted: (state, action: PayloadAction<{ id: string }>) => {
|
||||
@@ -60,6 +64,12 @@ export const lorasSlice = createSlice({
|
||||
state.loras = [];
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addMatcher(newSessionRequested, () => {
|
||||
// When a new session is requested, clear all LoRAs
|
||||
return deepClone(initialState);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { loraAdded, loraRecalled, loraDeleted, loraWeightChanged, loraIsEnabledChanged, loraAllDeleted } =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
||||
import type {
|
||||
@@ -26,6 +27,8 @@ import type {
|
||||
} from 'features/parameters/types/parameterSchemas';
|
||||
import { clamp } from 'lodash-es';
|
||||
|
||||
import { newSessionRequested } from './actions';
|
||||
|
||||
export type ParamsState = {
|
||||
maskBlur: number;
|
||||
maskBlurMethod: ParameterMaskBlurMethod;
|
||||
@@ -259,6 +262,21 @@ export const paramsSlice = createSlice({
|
||||
state.canvasCoherenceMinDenoise = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addMatcher(newSessionRequested, (state) => {
|
||||
// When a new session is requested, we need to keep the current model selections, plus dependent state
|
||||
// like VAE precision. Everything else gets reset to default.
|
||||
const newState = deepClone(initialState);
|
||||
newState.model = state.model;
|
||||
newState.vae = state.vae;
|
||||
newState.fluxVAE = state.fluxVAE;
|
||||
newState.vaePrecision = state.vaePrecision;
|
||||
newState.t5EncoderModel = state.t5EncoderModel;
|
||||
newState.clipEmbedModel = state.clipEmbedModel;
|
||||
newState.refinerModel = state.refinerModel;
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
|
||||
@@ -58,7 +58,10 @@ const zTool = z.enum(['brush', 'eraser', 'move', 'rect', 'view', 'bbox', 'colorP
|
||||
export type Tool = z.infer<typeof zTool>;
|
||||
|
||||
const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, {
|
||||
message: 'Must have an even number of points',
|
||||
message: 'Must have an even number of coordinate components',
|
||||
});
|
||||
const zPointsWithPressure = z.array(z.number()).refine((points) => points.length % 3 === 0, {
|
||||
message: 'Must have a number of components divisible by 3',
|
||||
});
|
||||
|
||||
const zRgbColor = z.object({
|
||||
@@ -110,6 +113,16 @@ const zCanvasBrushLineState = z.object({
|
||||
});
|
||||
export type CanvasBrushLineState = z.infer<typeof zCanvasBrushLineState>;
|
||||
|
||||
const zCanvasBrushLineWithPressureState = z.object({
|
||||
id: zId,
|
||||
type: z.literal('brush_line_with_pressure'),
|
||||
strokeWidth: z.number().min(1),
|
||||
points: zPointsWithPressure,
|
||||
color: zRgbaColor,
|
||||
clip: zRect.nullable(),
|
||||
});
|
||||
export type CanvasBrushLineWithPressureState = z.infer<typeof zCanvasBrushLineWithPressureState>;
|
||||
|
||||
const zCanvasEraserLineState = z.object({
|
||||
id: zId,
|
||||
type: z.literal('eraser_line'),
|
||||
@@ -119,6 +132,15 @@ const zCanvasEraserLineState = z.object({
|
||||
});
|
||||
export type CanvasEraserLineState = z.infer<typeof zCanvasEraserLineState>;
|
||||
|
||||
const zCanvasEraserLineWithPressureState = z.object({
|
||||
id: zId,
|
||||
type: z.literal('eraser_line_with_pressure'),
|
||||
strokeWidth: z.number().min(1),
|
||||
points: zPointsWithPressure,
|
||||
clip: zRect.nullable(),
|
||||
});
|
||||
export type CanvasEraserLineWithPressureState = z.infer<typeof zCanvasEraserLineWithPressureState>;
|
||||
|
||||
const zCanvasRectState = z.object({
|
||||
id: zId,
|
||||
type: z.literal('rect'),
|
||||
@@ -139,6 +161,8 @@ const zCanvasObjectState = z.union([
|
||||
zCanvasBrushLineState,
|
||||
zCanvasEraserLineState,
|
||||
zCanvasRectState,
|
||||
zCanvasBrushLineWithPressureState,
|
||||
zCanvasEraserLineWithPressureState,
|
||||
]);
|
||||
export type CanvasObjectState = z.infer<typeof zCanvasObjectState>;
|
||||
|
||||
@@ -359,8 +383,12 @@ export type EntityIdentifierPayload<
|
||||
} & T;
|
||||
|
||||
export type EntityMovedPayload = EntityIdentifierPayload<{ position: Coordinate }>;
|
||||
export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{ brushLine: CanvasBrushLineState }>;
|
||||
export type EntityEraserLineAddedPayload = EntityIdentifierPayload<{ eraserLine: CanvasEraserLineState }>;
|
||||
export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{
|
||||
brushLine: CanvasBrushLineState | CanvasBrushLineWithPressureState;
|
||||
}>;
|
||||
export type EntityEraserLineAddedPayload = EntityIdentifierPayload<{
|
||||
eraserLine: CanvasEraserLineState | CanvasEraserLineWithPressureState;
|
||||
}>;
|
||||
export type EntityRectAddedPayload = EntityIdentifierPayload<{ rect: CanvasRectState }>;
|
||||
export type EntityRasterizedPayload = EntityIdentifierPayload<{
|
||||
imageObject: CanvasImageState;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -19,9 +19,14 @@ export const ImageMenuItemCopy = memo(() => {
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiCopyBold />} onClickCapture={onClick}>
|
||||
{t('parameters.copyImage')}
|
||||
</MenuItem>
|
||||
<IconButton
|
||||
icon={<PiCopyBold />}
|
||||
aria-label={t('parameters.copyImage')}
|
||||
tooltip={t('parameters.copyImage')}
|
||||
onClickCapture={onClick}
|
||||
variant="ghost"
|
||||
colorScheme="base"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
@@ -16,9 +16,14 @@ export const ImageMenuItemDelete = memo(() => {
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem isDestructive icon={<PiTrashSimpleBold />} onClickCapture={onClick}>
|
||||
{t('gallery.deleteImage', { count: 1 })}
|
||||
</MenuItem>
|
||||
<IconButton
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClickCapture={onClick}
|
||||
aria-label={t('gallery.deleteImage', { count: 1 })}
|
||||
tooltip={t('gallery.deleteImage', { count: 1 })}
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useDownloadImage } from 'common/hooks/useDownloadImage';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -15,9 +15,14 @@ export const ImageMenuItemDownload = memo(() => {
|
||||
}, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={onClick}>
|
||||
{t('parameters.downloadImage')}
|
||||
</MenuItem>
|
||||
<IconButton
|
||||
icon={<PiDownloadSimpleBold />}
|
||||
aria-label={t('parameters.downloadImage')}
|
||||
tooltip={t('parameters.downloadImage')}
|
||||
variant="ghost"
|
||||
colorScheme="base"
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowSquareOutBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemOpenInNewTab = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const onClick = useCallback(() => {
|
||||
window.open(imageDTO.image_url, '_blank');
|
||||
}, [imageDTO.image_url]);
|
||||
|
||||
return (
|
||||
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiArrowSquareOutBold />}>
|
||||
{t('common.openInNewTab')}
|
||||
</MenuItem>
|
||||
<IconButton
|
||||
onClickCapture={onClick}
|
||||
aria-label={t('common.openInNewTab')}
|
||||
tooltip={t('common.openInNewTab')}
|
||||
icon={<PiArrowSquareOutBold />}
|
||||
variant="ghost"
|
||||
colorScheme="base"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -14,9 +14,14 @@ export const ImageMenuItemOpenInViewer = memo(() => {
|
||||
}, [imageDTO, imageViewer]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiArrowsOutBold />} onClick={onClick}>
|
||||
{t('gallery.openInViewer')}
|
||||
</MenuItem>
|
||||
<IconButton
|
||||
icon={<PiArrowsOutBold />}
|
||||
onClick={onClick}
|
||||
aria-label={t('common.openInViewer')}
|
||||
tooltip={t('common.openInViewer')}
|
||||
variant="ghost"
|
||||
colorScheme="base"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
@@ -22,9 +22,15 @@ export const ImageMenuItemSelectForCompare = memo(() => {
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiImagesBold />} isDisabled={!maySelectForCompare} onClick={onClick}>
|
||||
{t('gallery.selectForCompare')}
|
||||
</MenuItem>
|
||||
<IconButton
|
||||
icon={<PiImagesBold />}
|
||||
isDisabled={!maySelectForCompare}
|
||||
onClick={onClick}
|
||||
aria-label={t('gallery.selectForCompare')}
|
||||
tooltip={t('gallery.selectForCompare')}
|
||||
variant="ghost"
|
||||
colorScheme="base"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { Flex, MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { ImageMenuItemChangeBoard } from 'features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard';
|
||||
import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/ImageMenuItemCopy';
|
||||
import { ImageMenuItemDelete } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDelete';
|
||||
@@ -23,11 +23,14 @@ type SingleSelectionMenuItemsProps = {
|
||||
const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) => {
|
||||
return (
|
||||
<ImageDTOContextProvider value={imageDTO}>
|
||||
<ImageMenuItemOpenInNewTab />
|
||||
<ImageMenuItemCopy />
|
||||
<ImageMenuItemDownload />
|
||||
<ImageMenuItemOpenInViewer />
|
||||
<ImageMenuItemSelectForCompare />
|
||||
<Flex gap={2}>
|
||||
<ImageMenuItemOpenInNewTab />
|
||||
<ImageMenuItemCopy />
|
||||
<ImageMenuItemDownload />
|
||||
<ImageMenuItemOpenInViewer />
|
||||
<ImageMenuItemSelectForCompare />
|
||||
<ImageMenuItemDelete />
|
||||
</Flex>
|
||||
<MenuDivider />
|
||||
<ImageMenuItemLoadWorkflow />
|
||||
<ImageMenuItemMetadataRecallActions />
|
||||
@@ -38,8 +41,6 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) =
|
||||
<MenuDivider />
|
||||
<ImageMenuItemChangeBoard />
|
||||
<ImageMenuItemStarUnstar />
|
||||
<MenuDivider />
|
||||
<ImageMenuItemDelete />
|
||||
</ImageDTOContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -88,8 +88,9 @@ const CurrentImagePreview = () => {
|
||||
exit={exit}
|
||||
position="absolute"
|
||||
top={0}
|
||||
width="full"
|
||||
height="full"
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
pointerEvents="none"
|
||||
>
|
||||
<NextPrevImageButtons />
|
||||
|
||||
@@ -7,12 +7,7 @@ import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
|
||||
|
||||
const nextPrevButtonStyles: ChakraProps['sx'] = {
|
||||
color: 'base.100',
|
||||
pointerEvents: 'auto',
|
||||
};
|
||||
|
||||
const NextPrevImageButtons = () => {
|
||||
const NextPrevImageButtons = ({ inset = 8 }: { inset?: ChakraProps['insetInlineStart' | 'insetInlineEnd'] }) => {
|
||||
const { t } = useTranslation();
|
||||
const { prevImage, nextImage, isOnFirstImageOfView, isOnLastImageOfView } = useGalleryNavigation();
|
||||
|
||||
@@ -61,32 +56,36 @@ const NextPrevImageButtons = () => {
|
||||
|
||||
return (
|
||||
<Box pos="relative" h="full" w="full">
|
||||
<Box pos="absolute" top="50%" transform="translate(0, -50%)" insetInlineStart={1}>
|
||||
{shouldShowLeftArrow && (
|
||||
<IconButton
|
||||
aria-label={t('accessibility.previousImage')}
|
||||
icon={<PiCaretLeftBold size={64} />}
|
||||
variant="unstyled"
|
||||
onClick={onClickLeftArrow}
|
||||
boxSize={16}
|
||||
sx={nextPrevButtonStyles}
|
||||
isDisabled={isFetching}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box pos="absolute" top="50%" transform="translate(0, -50%)" insetInlineEnd={6}>
|
||||
{shouldShowRightArrow && (
|
||||
<IconButton
|
||||
aria-label={t('accessibility.nextImage')}
|
||||
icon={<PiCaretRightBold size={64} />}
|
||||
variant="unstyled"
|
||||
onClick={onClickRightArrow}
|
||||
boxSize={16}
|
||||
sx={nextPrevButtonStyles}
|
||||
isDisabled={isFetching}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{shouldShowLeftArrow && (
|
||||
<IconButton
|
||||
position="absolute"
|
||||
top="50%"
|
||||
transform="translate(0, -50%)"
|
||||
aria-label={t('accessibility.previousImage')}
|
||||
icon={<PiCaretLeftBold size={64} />}
|
||||
variant="unstyled"
|
||||
onClick={onClickLeftArrow}
|
||||
isDisabled={isFetching}
|
||||
color="base.100"
|
||||
pointerEvents="auto"
|
||||
insetInlineStart={inset}
|
||||
/>
|
||||
)}
|
||||
{shouldShowRightArrow && (
|
||||
<IconButton
|
||||
position="absolute"
|
||||
top="50%"
|
||||
transform="translate(0, -50%)"
|
||||
aria-label={t('accessibility.nextImage')}
|
||||
icon={<PiCaretRightBold size={64} />}
|
||||
variant="unstyled"
|
||||
onClick={onClickRightArrow}
|
||||
isDisabled={isFetching}
|
||||
color="base.100"
|
||||
pointerEvents="auto"
|
||||
insetInlineEnd={inset}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,11 +46,12 @@ export const useImageActions = (imageDTO: ImageDTO) => {
|
||||
setHasSeed(false);
|
||||
}
|
||||
|
||||
// Need to catch all of these to avoid unhandled promise rejections bubbling up to instrumented error handlers
|
||||
const promptParseResults = await Promise.allSettled([
|
||||
handlers.positivePrompt.parse(metadata),
|
||||
handlers.negativePrompt.parse(metadata),
|
||||
handlers.sdxlPositiveStylePrompt.parse(metadata),
|
||||
handlers.sdxlNegativeStylePrompt.parse(metadata),
|
||||
handlers.positivePrompt.parse(metadata).catch(() => {}),
|
||||
handlers.negativePrompt.parse(metadata).catch(() => {}),
|
||||
handlers.sdxlPositiveStylePrompt.parse(metadata).catch(() => {}),
|
||||
handlers.sdxlNegativeStylePrompt.parse(metadata).catch(() => {}),
|
||||
]);
|
||||
if (promptParseResults.some((result) => result.status === 'fulfilled')) {
|
||||
setHasPrompts(true);
|
||||
@@ -97,9 +98,14 @@ export const useImageActions = (imageDTO: ImageDTO) => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
handlers.seed.parse(metadata).then((seed) => {
|
||||
handlers.seed.recall && handlers.seed.recall(seed, true);
|
||||
});
|
||||
handlers.seed
|
||||
.parse(metadata)
|
||||
.then((seed) => {
|
||||
handlers.seed.recall?.(seed, true);
|
||||
})
|
||||
.catch(() => {
|
||||
// no-op, the toast will show the error
|
||||
});
|
||||
}, [metadata]);
|
||||
|
||||
const recallPrompts = useCallback(() => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { newSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import type { ParameterHRFMethod, ParameterStrength } from 'features/parameters/types/parameterSchemas';
|
||||
|
||||
interface HRFState {
|
||||
@@ -31,6 +33,11 @@ export const hrfSlice = createSlice({
|
||||
state.hrfMethod = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addMatcher(newSessionRequested, () => {
|
||||
return deepClone(initialHRFState);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setHrfEnabled, setHrfStrength, setHrfMethod } = hrfSlice.actions;
|
||||
|
||||
@@ -47,7 +47,9 @@ const MetadataViewLoRA = ({
|
||||
if (!handlers.recallItem) {
|
||||
return;
|
||||
}
|
||||
handlers.recallItem(lora, true);
|
||||
handlers.recallItem(lora, true).catch(() => {
|
||||
// no-op, the toast will show the error
|
||||
});
|
||||
}, [handlers, lora]);
|
||||
|
||||
const [renderedValue, setRenderedValue] = useState<React.ReactNode>(null);
|
||||
|
||||
@@ -21,13 +21,15 @@ export const useMetadataItem = <T,>(metadata: unknown, handlers: MetadataHandler
|
||||
const [renderedValueInternal, setRenderedValueInternal] = useState<React.ReactNode>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const _parse = async () => {
|
||||
try {
|
||||
const parsed = await handlers.parse(metadata);
|
||||
setValue(parsed);
|
||||
} catch (e) {
|
||||
setValue(MetadataParseFailedToken);
|
||||
}
|
||||
const _parse = () => {
|
||||
handlers
|
||||
.parse(metadata)
|
||||
.then((parsed) => {
|
||||
setValue(parsed);
|
||||
})
|
||||
.catch(() => {
|
||||
setValue(MetadataParseFailedToken);
|
||||
});
|
||||
};
|
||||
_parse();
|
||||
}, [handlers, metadata]);
|
||||
@@ -37,7 +39,7 @@ export const useMetadataItem = <T,>(metadata: unknown, handlers: MetadataHandler
|
||||
const label = useMemo(() => handlers.getLabel(), [handlers]);
|
||||
|
||||
useEffect(() => {
|
||||
const _renderValue = async () => {
|
||||
const _renderValue = () => {
|
||||
if (value === MetadataParsePendingToken) {
|
||||
setRenderedValueInternal(null);
|
||||
return;
|
||||
@@ -47,9 +49,14 @@ export const useMetadataItem = <T,>(metadata: unknown, handlers: MetadataHandler
|
||||
return;
|
||||
}
|
||||
|
||||
const rendered = await handlers.renderValue(value);
|
||||
|
||||
setRenderedValueInternal(rendered);
|
||||
handlers
|
||||
.renderValue(value)
|
||||
.then((rendered) => {
|
||||
setRenderedValueInternal(rendered);
|
||||
})
|
||||
.catch(() => {
|
||||
// no-op
|
||||
});
|
||||
};
|
||||
|
||||
_renderValue();
|
||||
@@ -69,7 +76,9 @@ export const useMetadataItem = <T,>(metadata: unknown, handlers: MetadataHandler
|
||||
if (!handlers.recall || value === MetadataParsePendingToken || value === MetadataParseFailedToken) {
|
||||
return null;
|
||||
}
|
||||
handlers.recall(value, true);
|
||||
handlers.recall(value, true).catch(() => {
|
||||
// no-op, the toast will show the error
|
||||
});
|
||||
}, [handlers, value]);
|
||||
|
||||
const valueOrNull = useMemo(() => {
|
||||
|
||||
@@ -16,6 +16,7 @@ const FORMAT_NAME_MAP: Record<AnyModelConfig['format'], string> = {
|
||||
t5_encoder: 't5_encoder',
|
||||
bnb_quantized_int8b: 'bnb_quantized_int8b',
|
||||
bnb_quantized_nf4b: 'quantized',
|
||||
gguf_quantized: 'gguf',
|
||||
};
|
||||
|
||||
const FORMAT_COLOR_MAP: Record<AnyModelConfig['format'], string> = {
|
||||
@@ -28,6 +29,7 @@ const FORMAT_COLOR_MAP: Record<AnyModelConfig['format'], string> = {
|
||||
t5_encoder: 'base',
|
||||
bnb_quantized_int8b: 'base',
|
||||
bnb_quantized_nf4b: 'base',
|
||||
gguf_quantized: 'base',
|
||||
};
|
||||
|
||||
const ModelFormatBadge = ({ format }: Props) => {
|
||||
|
||||
@@ -73,7 +73,7 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
|
||||
{props.children}
|
||||
{isHovering && (
|
||||
<motion.div key="nextPrevButtons" initial={initial} animate={animate} exit={exit} style={styles}>
|
||||
<NextPrevImageButtons />
|
||||
<NextPrevImageButtons inset={2} />
|
||||
</motion.div>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
type Props = {
|
||||
sx?: ChakraProps['sx'];
|
||||
};
|
||||
|
||||
const CancelCurrentQueueItemIconButton = ({ sx }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { cancelQueueItem, isLoading, isDisabled } = useCancelCurrentQueueItem();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
aria-label={t('queue.cancel')}
|
||||
tooltip={t('queue.cancelTooltip')}
|
||||
icon={<PiXBold size="16px" />}
|
||||
onClick={cancelQueueItem}
|
||||
colorScheme="error"
|
||||
sx={sx}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CancelCurrentQueueItemIconButton);
|
||||
@@ -1,5 +1,9 @@
|
||||
import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import {
|
||||
useNewCanvasSession,
|
||||
useNewGallerySession,
|
||||
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
|
||||
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
|
||||
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
|
||||
@@ -8,7 +12,16 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiListBold, PiPauseFill, PiPlayFill, PiQueueBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import {
|
||||
PiImageBold,
|
||||
PiListBold,
|
||||
PiPaintBrushBold,
|
||||
PiPauseFill,
|
||||
PiPlayFill,
|
||||
PiQueueBold,
|
||||
PiTrashSimpleBold,
|
||||
PiXBold,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
export const QueueActionsMenuButton = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -16,6 +29,8 @@ export const QueueActionsMenuButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const isPauseEnabled = useFeatureStatus('pauseQueue');
|
||||
const isResumeEnabled = useFeatureStatus('resumeQueue');
|
||||
const { newGallerySessionWithDialog } = useNewGallerySession();
|
||||
const { newCanvasSessionWithDialog } = useNewCanvasSession();
|
||||
const clearQueue = useClearQueue();
|
||||
const {
|
||||
resumeProcessor,
|
||||
@@ -36,39 +51,57 @@ export const QueueActionsMenuButton = memo(() => {
|
||||
<Menu placement="bottom-end">
|
||||
<MenuButton ref={ref} as={IconButton} size="lg" aria-label="Queue Actions Menu" icon={<PiListBold />} />
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
isDestructive
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClick={clearQueue.openDialog}
|
||||
isLoading={clearQueue.isLoading}
|
||||
isDisabled={clearQueue.isDisabled}
|
||||
>
|
||||
{t('queue.clearTooltip')}
|
||||
</MenuItem>
|
||||
{isResumeEnabled && (
|
||||
<MenuItem
|
||||
icon={<PiPlayFill />}
|
||||
onClick={resumeProcessor}
|
||||
isLoading={isLoadingResumeProcessor}
|
||||
isDisabled={isDisabledResumeProcessor}
|
||||
>
|
||||
{t('queue.resumeTooltip')}
|
||||
<MenuGroup title={t('common.new')}>
|
||||
<MenuItem icon={<PiImageBold />} onClick={newGallerySessionWithDialog}>
|
||||
{t('controlLayers.newGallerySession')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isPauseEnabled && (
|
||||
<MenuItem
|
||||
icon={<PiPauseFill />}
|
||||
onClick={pauseProcessor}
|
||||
isLoading={isLoadingPauseProcessor}
|
||||
isDisabled={isDisabledPauseProcessor}
|
||||
>
|
||||
{t('queue.pauseTooltip')}
|
||||
<MenuItem icon={<PiPaintBrushBold />} onClick={newCanvasSessionWithDialog}>
|
||||
{t('controlLayers.newCanvasSession')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuDivider />
|
||||
<MenuItem icon={<PiQueueBold />} onClick={openQueue}>
|
||||
{t('queue.openQueue')}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup title={t('queue.queue')}>
|
||||
<MenuItem
|
||||
isDestructive
|
||||
icon={<PiXBold />}
|
||||
onClick={clearQueue.openDialog}
|
||||
isLoading={clearQueue.isLoading}
|
||||
isDisabled={clearQueue.isDisabled}
|
||||
>
|
||||
{t('queue.cancelTooltip')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
isDestructive
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClick={clearQueue.openDialog}
|
||||
isLoading={clearQueue.isLoading}
|
||||
isDisabled={clearQueue.isDisabled}
|
||||
>
|
||||
{t('queue.clearTooltip')}
|
||||
</MenuItem>
|
||||
{isResumeEnabled && (
|
||||
<MenuItem
|
||||
icon={<PiPlayFill />}
|
||||
onClick={resumeProcessor}
|
||||
isLoading={isLoadingResumeProcessor}
|
||||
isDisabled={isDisabledResumeProcessor}
|
||||
>
|
||||
{t('queue.resumeTooltip')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isPauseEnabled && (
|
||||
<MenuItem
|
||||
icon={<PiPauseFill />}
|
||||
onClick={pauseProcessor}
|
||||
isLoading={isLoadingPauseProcessor}
|
||||
isDisabled={isDisabledPauseProcessor}
|
||||
>
|
||||
{t('queue.pauseTooltip')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem icon={<PiQueueBold />} onClick={openQueue}>
|
||||
{t('queue.openQueue')}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
{/* The badge is dynamically positioned, needs a ref to the target element */}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { TooltipProps } from '@invoke-ai/ui-library';
|
||||
import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
@@ -20,19 +21,19 @@ const selectPromptsCount = createSelector(selectParamsSlice, selectDynamicPrompt
|
||||
getShouldProcessPrompt(params.positivePrompt) ? dynamicPrompts.prompts.length : 1
|
||||
);
|
||||
|
||||
type Props = {
|
||||
type Props = TooltipProps & {
|
||||
prepend?: boolean;
|
||||
};
|
||||
|
||||
export const QueueButtonTooltip = (props: PropsWithChildren<Props>) => {
|
||||
export const QueueButtonTooltip = ({ prepend, children, ...rest }: PropsWithChildren<Props>) => {
|
||||
return (
|
||||
<Tooltip label={<TooltipContent prepend={props.prepend} />} maxW={512}>
|
||||
{props.children}
|
||||
<Tooltip label={<TooltipContent prepend={prepend} />} maxW={512} {...rest}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipContent = memo(({ prepend = false }: Props) => {
|
||||
const TooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isReady, reasons } = useIsReadyToEnqueue();
|
||||
const sendToCanvas = useAppSelector(selectSendToCanvas);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { newSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import { atom } from 'nanostores';
|
||||
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
|
||||
|
||||
@@ -45,6 +47,9 @@ export const stylePresetSlice = createSlice({
|
||||
state.activeStylePresetId = null;
|
||||
}
|
||||
});
|
||||
builder.addMatcher(newSessionRequested, () => {
|
||||
return deepClone(initialState);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -30,12 +30,14 @@ import {
|
||||
logIsEnabledChanged,
|
||||
selectSystemShouldAntialiasProgressImage,
|
||||
selectSystemShouldConfirmOnDelete,
|
||||
selectSystemShouldConfirmOnNewSession,
|
||||
selectSystemShouldEnableInformationalPopovers,
|
||||
selectSystemShouldUseNSFWChecker,
|
||||
selectSystemShouldUseWatermarker,
|
||||
setShouldConfirmOnDelete,
|
||||
setShouldEnableInformationalPopovers,
|
||||
shouldAntialiasProgressImageChanged,
|
||||
shouldConfirmOnNewSessionToggled,
|
||||
shouldUseNSFWCheckerChanged,
|
||||
shouldUseWatermarkerChanged,
|
||||
} from 'features/system/store/systemSlice';
|
||||
@@ -104,6 +106,10 @@ const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps)
|
||||
const shouldUseNSFWChecker = useAppSelector(selectSystemShouldUseNSFWChecker);
|
||||
const shouldUseWatermarker = useAppSelector(selectSystemShouldUseWatermarker);
|
||||
const shouldEnableInformationalPopovers = useAppSelector(selectSystemShouldEnableInformationalPopovers);
|
||||
const shouldConfirmOnNewSession = useAppSelector(selectSystemShouldConfirmOnNewSession);
|
||||
const onToggleConfirmOnNewSession = useCallback(() => {
|
||||
dispatch(shouldConfirmOnNewSessionToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
const clearStorage = useClearStorage();
|
||||
|
||||
@@ -181,6 +187,10 @@ const SettingsModal = ({ config = defaultConfig, children }: SettingsModalProps)
|
||||
<FormLabel>{t('settings.confirmOnDelete')}</FormLabel>
|
||||
<Switch isChecked={shouldConfirmOnDelete} onChange={handleChangeShouldConfirmOnDelete} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>{t('settings.confirmOnNewSession')}</FormLabel>
|
||||
<Switch isChecked={shouldConfirmOnNewSession} onChange={onToggleConfirmOnNewSession} />
|
||||
</FormControl>
|
||||
</StickyScrollable>
|
||||
|
||||
<StickyScrollable title={t('settings.generation')}>
|
||||
|
||||
@@ -12,6 +12,7 @@ const initialSystemState: SystemState = {
|
||||
_version: 1,
|
||||
shouldConfirmOnDelete: true,
|
||||
shouldAntialiasProgressImage: false,
|
||||
shouldConfirmOnNewSession: true,
|
||||
language: 'en',
|
||||
shouldUseNSFWChecker: false,
|
||||
shouldUseWatermarker: false,
|
||||
@@ -56,6 +57,9 @@ export const systemSlice = createSlice({
|
||||
setShouldEnableInformationalPopovers(state, action: PayloadAction<boolean>) {
|
||||
state.shouldEnableInformationalPopovers = action.payload;
|
||||
},
|
||||
shouldConfirmOnNewSessionToggled(state) {
|
||||
state.shouldConfirmOnNewSession = !state.shouldConfirmOnNewSession;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,6 +73,7 @@ export const {
|
||||
shouldUseNSFWCheckerChanged,
|
||||
shouldUseWatermarkerChanged,
|
||||
setShouldEnableInformationalPopovers,
|
||||
shouldConfirmOnNewSessionToggled,
|
||||
} = systemSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
@@ -103,3 +108,4 @@ export const selectSystemShouldAntialiasProgressImage = createSystemSelector(
|
||||
export const selectSystemShouldEnableInformationalPopovers = createSystemSelector(
|
||||
(system) => system.shouldEnableInformationalPopovers
|
||||
);
|
||||
export const selectSystemShouldConfirmOnNewSession = createSystemSelector((system) => system.shouldConfirmOnNewSession);
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface SystemState {
|
||||
_version: 1;
|
||||
shouldConfirmOnDelete: boolean;
|
||||
shouldAntialiasProgressImage: boolean;
|
||||
shouldConfirmOnNewSession: boolean;
|
||||
language: Language;
|
||||
shouldUseNSFWChecker: boolean;
|
||||
shouldUseWatermarker: boolean;
|
||||
|
||||
@@ -33,7 +33,7 @@ import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
|
||||
import ResizeHandle from './tabs/ResizeHandle';
|
||||
|
||||
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%' };
|
||||
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 };
|
||||
|
||||
const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCollapsed);
|
||||
const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed);
|
||||
@@ -117,42 +117,40 @@ export const AppContent = memo(() => {
|
||||
return (
|
||||
<Flex id="invoke-app-tabs" w="full" h="full" gap={4} p={4}>
|
||||
<VerticalNavBar />
|
||||
<Flex position="relative" w="full" h="full" gap={4} minW={0}>
|
||||
<PanelGroup
|
||||
ref={imperativePanelGroupRef}
|
||||
id="app-panel-group"
|
||||
autoSaveId="app-panel-group"
|
||||
direction="horizontal"
|
||||
style={panelStyles}
|
||||
>
|
||||
{withLeftPanel && (
|
||||
<>
|
||||
<Panel order={0} collapsible style={panelStyles} {...leftPanel.panelProps}>
|
||||
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||
<QueueControls />
|
||||
<Box position="relative" w="full" h="full">
|
||||
<LeftPanelContent />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Panel>
|
||||
<ResizeHandle id="left-main-handle" {...leftPanel.resizeHandleProps} />
|
||||
</>
|
||||
)}
|
||||
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
|
||||
<MainPanelContent />
|
||||
</Panel>
|
||||
{withRightPanel && (
|
||||
<>
|
||||
<ResizeHandle id="main-right-handle" {...rightPanel.resizeHandleProps} />
|
||||
<Panel order={2} style={panelStyles} collapsible {...rightPanel.panelProps}>
|
||||
<RightPanelContent />
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
{withLeftPanel && <FloatingParametersPanelButtons panelApi={leftPanel} />}
|
||||
{withRightPanel && <FloatingGalleryButton panelApi={rightPanel} />}
|
||||
</Flex>
|
||||
<PanelGroup
|
||||
ref={imperativePanelGroupRef}
|
||||
id="app-panel-group"
|
||||
autoSaveId="app-panel-group"
|
||||
direction="horizontal"
|
||||
style={panelStyles}
|
||||
>
|
||||
{withLeftPanel && (
|
||||
<>
|
||||
<Panel order={0} collapsible style={panelStyles} {...leftPanel.panelProps}>
|
||||
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||
<QueueControls />
|
||||
<Box position="relative" w="full" h="full">
|
||||
<LeftPanelContent />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Panel>
|
||||
<ResizeHandle id="left-main-handle" {...leftPanel.resizeHandleProps} />
|
||||
</>
|
||||
)}
|
||||
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
|
||||
<MainPanelContent />
|
||||
{withLeftPanel && <FloatingParametersPanelButtons panelApi={leftPanel} />}
|
||||
{withRightPanel && <FloatingGalleryButton panelApi={rightPanel} />}
|
||||
</Panel>
|
||||
{withRightPanel && (
|
||||
<>
|
||||
<ResizeHandle id="main-right-handle" {...rightPanel.resizeHandleProps} />
|
||||
<Panel order={2} style={panelStyles} collapsible {...rightPanel.panelProps}>
|
||||
<RightPanelContent />
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flex, IconButton, Portal, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -11,25 +11,17 @@ type Props = {
|
||||
const FloatingGalleryButton = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!props.panelApi.isCollapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Flex pos="absolute" transform="translate(0, -50%)" minW={8} top="50%" insetInlineEnd="21px" zIndex={11}>
|
||||
<Tooltip label={t('accessibility.showGalleryPanel')} placement="start">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.showGalleryPanel')}
|
||||
onClick={props.panelApi.expand}
|
||||
icon={<PiImagesSquareBold size="20px" />}
|
||||
p={0}
|
||||
h={48}
|
||||
borderEndRadius={0}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Portal>
|
||||
<Flex pos="absolute" transform="translate(0, -50%)" minW={8} top="50%" insetInlineEnd={2}>
|
||||
<Tooltip label={t('accessibility.toggleRightPanel')} placement="start">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.toggleRightPanel')}
|
||||
onClick={props.panelApi.toggle}
|
||||
icon={<PiImagesSquareBold />}
|
||||
h={48}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { ButtonGroup, Flex, Icon, IconButton, Portal, spinAnimation, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import CancelCurrentQueueItemIconButton from 'features/queue/components/CancelCurrentQueueItemIconButton';
|
||||
import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -13,14 +17,10 @@ import {
|
||||
PiSlidersHorizontalBold,
|
||||
PiSparkleFill,
|
||||
PiTrashSimpleBold,
|
||||
PiXBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
const floatingButtonStyles: SystemStyleObject = {
|
||||
borderStartRadius: 0,
|
||||
flexGrow: 1,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
panelApi: UsePanelReturn;
|
||||
};
|
||||
@@ -29,8 +29,11 @@ const FloatingSidePanelButtons = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const queue = useInvoke();
|
||||
const shift = useShiftModifier();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const imageViewer = useImageViewer();
|
||||
const clearQueue = useClearQueue();
|
||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||
const cancelCurrent = useCancelCurrentQueueItem();
|
||||
|
||||
const queueButtonIcon = useMemo(() => {
|
||||
const isProcessing = (queueStatus?.queue.in_progress ?? 0) > 0;
|
||||
@@ -38,62 +41,64 @@ const FloatingSidePanelButtons = (props: Props) => {
|
||||
return <Icon boxSize={6} as={PiCircleNotchBold} animation={spinAnimation} />;
|
||||
}
|
||||
if (shift) {
|
||||
return <PiLightningFill size="16px" />;
|
||||
return <PiLightningFill />;
|
||||
}
|
||||
return <PiSparkleFill size="16px" />;
|
||||
return <PiSparkleFill />;
|
||||
}, [queue.isDisabled, queueStatus?.queue.in_progress, shift]);
|
||||
|
||||
if (!props.panelApi.isCollapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Flex
|
||||
pos="absolute"
|
||||
transform="translate(0, -50%)"
|
||||
minW={8}
|
||||
top="50%"
|
||||
insetInlineStart="63px"
|
||||
direction="column"
|
||||
gap={2}
|
||||
h={48}
|
||||
zIndex={11}
|
||||
>
|
||||
<ButtonGroup orientation="vertical" flexGrow={3}>
|
||||
<Flex pos="absolute" transform="translate(0, -50%)" top="50%" insetInlineStart={2} direction="column" gap={2}>
|
||||
{tab === 'canvas' && !imageViewer.isOpen && (
|
||||
<CanvasManagerProviderGate>
|
||||
<ToolChooser />
|
||||
</CanvasManagerProviderGate>
|
||||
)}
|
||||
<ButtonGroup orientation="vertical" h={48}>
|
||||
<Tooltip label={t('accessibility.toggleLeftPanel')} placement="end">
|
||||
<IconButton
|
||||
tooltip={t('accessibility.showOptionsPanel')}
|
||||
aria-label={t('accessibility.showOptionsPanel')}
|
||||
onClick={props.panelApi.expand}
|
||||
sx={floatingButtonStyles}
|
||||
icon={<PiSlidersHorizontalBold size="16px" />}
|
||||
aria-label={t('accessibility.toggleLeftPanel')}
|
||||
onClick={props.panelApi.toggle}
|
||||
icon={<PiSlidersHorizontalBold />}
|
||||
flexGrow={1}
|
||||
/>
|
||||
<QueueButtonTooltip prepend={shift}>
|
||||
<IconButton
|
||||
aria-label={t('queue.queueBack')}
|
||||
onClick={shift ? queue.queueFront : queue.queueBack}
|
||||
isLoading={queue.isLoading}
|
||||
isDisabled={queue.isDisabled}
|
||||
icon={queueButtonIcon}
|
||||
colorScheme="invokeYellow"
|
||||
sx={floatingButtonStyles}
|
||||
/>
|
||||
</QueueButtonTooltip>
|
||||
<CancelCurrentQueueItemIconButton sx={floatingButtonStyles} />
|
||||
</ButtonGroup>
|
||||
<IconButton
|
||||
isDisabled={clearQueue.isDisabled}
|
||||
isLoading={clearQueue.isLoading}
|
||||
aria-label={t('queue.clear')}
|
||||
tooltip={t('queue.clearTooltip')}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
colorScheme="error"
|
||||
onClick={clearQueue.openDialog}
|
||||
data-testid={t('queue.clear')}
|
||||
sx={floatingButtonStyles}
|
||||
/>
|
||||
</Flex>
|
||||
</Portal>
|
||||
</Tooltip>
|
||||
<QueueButtonTooltip prepend={shift} placement="end">
|
||||
<IconButton
|
||||
aria-label={t('queue.queueBack')}
|
||||
onClick={shift ? queue.queueFront : queue.queueBack}
|
||||
isLoading={queue.isLoading}
|
||||
isDisabled={queue.isDisabled}
|
||||
icon={queueButtonIcon}
|
||||
colorScheme="invokeYellow"
|
||||
flexGrow={1}
|
||||
/>
|
||||
</QueueButtonTooltip>
|
||||
<Tooltip label={t('queue.cancelTooltip')} placement="end">
|
||||
<IconButton
|
||||
isDisabled={cancelCurrent.isDisabled}
|
||||
isLoading={cancelCurrent.isLoading}
|
||||
aria-label={t('queue.cancelTooltip')}
|
||||
icon={<PiXBold />}
|
||||
onClick={cancelCurrent.cancelQueueItem}
|
||||
colorScheme="error"
|
||||
flexGrow={1}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t('queue.clearTooltip')} placement="end">
|
||||
<IconButton
|
||||
isDisabled={clearQueue.isDisabled}
|
||||
isLoading={clearQueue.isLoading}
|
||||
aria-label={t('queue.clearTooltip')}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
colorScheme="error"
|
||||
onClick={clearQueue.openDialog}
|
||||
data-testid={t('queue.clear')}
|
||||
flexGrow={1}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { newSessionRequested } from 'features/controlLayers/store/actions';
|
||||
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
@@ -45,6 +46,9 @@ export const uiSlice = createSlice({
|
||||
builder.addCase(workflowLoadRequested, (state) => {
|
||||
state.activeTab = 'workflows';
|
||||
});
|
||||
builder.addMatcher(newSessionRequested, (state) => {
|
||||
state.activeTab = 'canvas';
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4125,7 +4125,7 @@ export type components = {
|
||||
* @default checkpoint
|
||||
* @enum {string}
|
||||
*/
|
||||
format: "checkpoint" | "bnb_quantized_nf4b";
|
||||
format: "checkpoint" | "bnb_quantized_nf4b" | "gguf_quantized";
|
||||
/**
|
||||
* Config Path
|
||||
* @description path to the checkpoint model config file
|
||||
@@ -10811,7 +10811,7 @@ export type components = {
|
||||
* @default checkpoint
|
||||
* @enum {string}
|
||||
*/
|
||||
format: "checkpoint" | "bnb_quantized_nf4b";
|
||||
format: "checkpoint" | "bnb_quantized_nf4b" | "gguf_quantized";
|
||||
/**
|
||||
* Config Path
|
||||
* @description path to the checkpoint model config file
|
||||
@@ -10901,7 +10901,7 @@ export type components = {
|
||||
* @default checkpoint
|
||||
* @enum {string}
|
||||
*/
|
||||
format: "checkpoint" | "bnb_quantized_nf4b";
|
||||
format: "checkpoint" | "bnb_quantized_nf4b" | "gguf_quantized";
|
||||
/**
|
||||
* Config Path
|
||||
* @description path to the checkpoint model config file
|
||||
@@ -10995,6 +10995,96 @@ export type components = {
|
||||
/** @default */
|
||||
repo_variant?: components["schemas"]["ModelRepoVariant"] | null;
|
||||
};
|
||||
/**
|
||||
* MainGGUFCheckpointConfig
|
||||
* @description Model config for main checkpoint models.
|
||||
*/
|
||||
MainGGUFCheckpointConfig: {
|
||||
/**
|
||||
* Key
|
||||
* @description A unique key for this model.
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Hash
|
||||
* @description The hash of the model file(s).
|
||||
*/
|
||||
hash: string;
|
||||
/**
|
||||
* Path
|
||||
* @description Path to the model on the filesystem. Relative paths are relative to the Invoke root directory.
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* Name
|
||||
* @description Name of the model.
|
||||
*/
|
||||
name: string;
|
||||
/** @description The base model. */
|
||||
base: components["schemas"]["BaseModelType"];
|
||||
/**
|
||||
* Description
|
||||
* @description Model description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Source
|
||||
* @description The original source of the model (path, URL or repo_id).
|
||||
*/
|
||||
source: string;
|
||||
/** @description The type of source */
|
||||
source_type: components["schemas"]["ModelSourceType"];
|
||||
/**
|
||||
* Source Api Response
|
||||
* @description The original API response from the source, as stringified JSON.
|
||||
*/
|
||||
source_api_response?: string | null;
|
||||
/**
|
||||
* Cover Image
|
||||
* @description Url for image to preview model
|
||||
*/
|
||||
cover_image?: string | null;
|
||||
/**
|
||||
* Type
|
||||
* @default main
|
||||
* @constant
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "main";
|
||||
/**
|
||||
* Trigger Phrases
|
||||
* @description Set of trigger phrases for this model
|
||||
*/
|
||||
trigger_phrases?: string[] | null;
|
||||
/** @description Default settings for this model */
|
||||
default_settings?: components["schemas"]["MainModelDefaultSettings"] | null;
|
||||
/** @default normal */
|
||||
variant?: components["schemas"]["ModelVariantType"];
|
||||
/**
|
||||
* Format
|
||||
* @description Format of the provided checkpoint model
|
||||
* @default checkpoint
|
||||
* @enum {string}
|
||||
*/
|
||||
format: "checkpoint" | "bnb_quantized_nf4b" | "gguf_quantized";
|
||||
/**
|
||||
* Config Path
|
||||
* @description path to the checkpoint model config file
|
||||
*/
|
||||
config_path: string;
|
||||
/**
|
||||
* Converted At
|
||||
* @description When this model was last converted to diffusers
|
||||
*/
|
||||
converted_at?: number | null;
|
||||
/** @default epsilon */
|
||||
prediction_type?: components["schemas"]["SchedulerPredictionType"];
|
||||
/**
|
||||
* Upcast Attention
|
||||
* @default false
|
||||
*/
|
||||
upcast_attention?: boolean;
|
||||
};
|
||||
/** MainModelDefaultSettings */
|
||||
MainModelDefaultSettings: {
|
||||
/**
|
||||
@@ -11868,7 +11958,7 @@ export type components = {
|
||||
* @description Storage format of model.
|
||||
* @enum {string}
|
||||
*/
|
||||
ModelFormat: "diffusers" | "checkpoint" | "lycoris" | "onnx" | "olive" | "embedding_file" | "embedding_folder" | "invokeai" | "t5_encoder" | "bnb_quantized_int8b" | "bnb_quantized_nf4b";
|
||||
ModelFormat: "diffusers" | "checkpoint" | "lycoris" | "onnx" | "olive" | "embedding_file" | "embedding_folder" | "invokeai" | "t5_encoder" | "bnb_quantized_int8b" | "bnb_quantized_nf4b" | "gguf_quantized";
|
||||
/** ModelIdentifierField */
|
||||
ModelIdentifierField: {
|
||||
/**
|
||||
@@ -12168,7 +12258,7 @@ export type components = {
|
||||
* Config Out
|
||||
* @description After successful installation, this will hold the configuration object.
|
||||
*/
|
||||
config_out?: (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"]) | null;
|
||||
config_out?: (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["MainGGUFCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"]) | null;
|
||||
/**
|
||||
* Inplace
|
||||
* @description Leave model in its current location; otherwise install under models directory
|
||||
@@ -12254,7 +12344,7 @@ export type components = {
|
||||
* Config
|
||||
* @description The model's config
|
||||
*/
|
||||
config: components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
config: components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["MainGGUFCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
/**
|
||||
* @description The submodel type, if any
|
||||
* @default null
|
||||
@@ -12275,7 +12365,7 @@ export type components = {
|
||||
* Config
|
||||
* @description The model's config
|
||||
*/
|
||||
config: components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
config: components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["MainGGUFCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
/**
|
||||
* @description The submodel type, if any
|
||||
* @default null
|
||||
@@ -12416,7 +12506,7 @@ export type components = {
|
||||
*/
|
||||
ModelsList: {
|
||||
/** Models */
|
||||
models: (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"])[];
|
||||
models: (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["MainGGUFCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"])[];
|
||||
};
|
||||
/**
|
||||
* Multiply Integers
|
||||
@@ -16533,7 +16623,7 @@ export type components = {
|
||||
* @default checkpoint
|
||||
* @enum {string}
|
||||
*/
|
||||
format: "checkpoint" | "bnb_quantized_nf4b";
|
||||
format: "checkpoint" | "bnb_quantized_nf4b" | "gguf_quantized";
|
||||
/**
|
||||
* Config Path
|
||||
* @description path to the checkpoint model config file
|
||||
@@ -17061,7 +17151,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
"application/json": components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["MainGGUFCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@@ -17093,7 +17183,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
"application/json": components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["MainGGUFCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
};
|
||||
};
|
||||
/** @description Bad request */
|
||||
@@ -17190,7 +17280,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
"application/json": components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["MainGGUFCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
};
|
||||
};
|
||||
/** @description Bad request */
|
||||
@@ -17690,7 +17780,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
"application/json": components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"] | components["schemas"]["MainBnbQuantized4bCheckpointConfig"] | components["schemas"]["MainGGUFCheckpointConfig"] | components["schemas"]["VAEDiffusersConfig"] | components["schemas"]["VAECheckpointConfig"] | components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"] | components["schemas"]["LoRALyCORISConfig"] | components["schemas"]["LoRADiffusersConfig"] | components["schemas"]["T5EncoderConfig"] | components["schemas"]["T5EncoderBnbQuantizedLlmInt8bConfig"] | components["schemas"]["TextualInversionFileConfig"] | components["schemas"]["TextualInversionFolderConfig"] | components["schemas"]["IPAdapterInvokeAIConfig"] | components["schemas"]["IPAdapterCheckpointConfig"] | components["schemas"]["T2IAdapterConfig"] | components["schemas"]["SpandrelImageToImageConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["CLIPEmbedDiffusersConfig"];
|
||||
};
|
||||
};
|
||||
/** @description Bad request */
|
||||
|
||||
@@ -67,26 +67,23 @@ export default defineConfig(({ mode }) => {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
},
|
||||
server: {
|
||||
// Proxy HTTP requests to the flask server
|
||||
proxy: {
|
||||
// Proxy socket.io to the nodes socketio server
|
||||
'/ws/socket.io': {
|
||||
target: 'ws://127.0.0.1:9090',
|
||||
ws: true,
|
||||
},
|
||||
// Proxy openapi schema definiton
|
||||
'/openapi.json': {
|
||||
target: 'http://127.0.0.1:9090/openapi.json',
|
||||
rewrite: (path) => path.replace(/^\/openapi.json/, ''),
|
||||
changeOrigin: true,
|
||||
},
|
||||
// proxy nodes api
|
||||
'/api/': {
|
||||
target: 'http://127.0.0.1:9090/api/',
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
test: {
|
||||
typecheck: {
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "5.0.2"
|
||||
__version__ = "5.1.0rc2"
|
||||
|
||||
@@ -39,6 +39,7 @@ dependencies = [
|
||||
"compel==2.0.2",
|
||||
"controlnet-aux==0.0.7",
|
||||
"diffusers[torch]==0.27.2",
|
||||
"gguf==0.10.0",
|
||||
"invisible-watermark==0.2.0", # needed to install SDXL base and refiner using their repo_ids
|
||||
"mediapipe==0.10.7", # needed for "mediapipeface" controlnet model
|
||||
"numpy==1.26.4", # >1.24.0 is needed to use the 'strict' argument to np.testing.assert_array_equal()
|
||||
@@ -51,10 +52,10 @@ dependencies = [
|
||||
"sentencepiece==0.2.0",
|
||||
"spandrel==0.3.4",
|
||||
"timm==0.6.13", # needed to override timm latest in controlnet_aux, see https://github.com/isl-org/ZoeDepth/issues/26
|
||||
"torch==2.2.2",
|
||||
"torch==2.4.1",
|
||||
"torchmetrics==0.11.4",
|
||||
"torchsde==0.2.6",
|
||||
"torchvision==0.17.2",
|
||||
"torchvision==0.19.1",
|
||||
"transformers==4.41.1",
|
||||
|
||||
# Core application dependencies, pinned for reproducible builds.
|
||||
@@ -100,7 +101,7 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
"xformers" = [
|
||||
# Core generation dependencies, pinned for reproducible builds.
|
||||
"xformers==0.0.25post1; sys_platform!='darwin'",
|
||||
"xformers==0.0.28.post1; sys_platform!='darwin'",
|
||||
# Auxiliary dependencies, pinned only if necessary.
|
||||
"triton; sys_platform=='linux'",
|
||||
]
|
||||
|
||||
0
scripts/__init__.py
Normal file
0
scripts/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user