Merge branch 'lstein/global-configuration' of github.com:invoke-ai/InvokeAI into lstein/global-configuration

This commit is contained in:
Lincoln Stein 2023-05-17 15:23:21 -04:00
commit eca1e449a8
170 changed files with 1360 additions and 3642 deletions

View File

@ -98,7 +98,8 @@ class CompelInvocation(BaseInvocation):
# TODO: support legacy blend? # TODO: support legacy blend?
prompt: Union[FlattenedPrompt, Blend] = Compel.parse_prompt_string(prompt_str) conjunction = Compel.parse_prompt_string(prompt_str)
prompt: Union[FlattenedPrompt, Blend] = conjunction.prompts[0]
if context.services.configuration.log_tokenization: if context.services.configuration.log_tokenization:
log_tokenization_for_prompt_object(prompt, tokenizer) log_tokenization_for_prompt_object(prompt, tokenizer)

View File

@ -5,7 +5,12 @@ from typing import Literal
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import numpy as np import numpy as np
from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig from .baseinvocation import (
BaseInvocation,
BaseInvocationOutput,
InvocationContext,
InvocationConfig,
)
class MathInvocationConfig(BaseModel): class MathInvocationConfig(BaseModel):
@ -22,19 +27,21 @@ class MathInvocationConfig(BaseModel):
class IntOutput(BaseInvocationOutput): class IntOutput(BaseInvocationOutput):
"""An integer output""" """An integer output"""
#fmt: off
# fmt: off
type: Literal["int_output"] = "int_output" type: Literal["int_output"] = "int_output"
a: int = Field(default=None, description="The output integer") a: int = Field(default=None, description="The output integer")
#fmt: on # fmt: on
class AddInvocation(BaseInvocation, MathInvocationConfig): class AddInvocation(BaseInvocation, MathInvocationConfig):
"""Adds two numbers""" """Adds two numbers"""
#fmt: off
# fmt: off
type: Literal["add"] = "add" type: Literal["add"] = "add"
a: int = Field(default=0, description="The first number") a: int = Field(default=0, description="The first number")
b: int = Field(default=0, description="The second number") b: int = Field(default=0, description="The second number")
#fmt: on # fmt: on
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=self.a + self.b) return IntOutput(a=self.a + self.b)
@ -42,11 +49,12 @@ class AddInvocation(BaseInvocation, MathInvocationConfig):
class SubtractInvocation(BaseInvocation, MathInvocationConfig): class SubtractInvocation(BaseInvocation, MathInvocationConfig):
"""Subtracts two numbers""" """Subtracts two numbers"""
#fmt: off
# fmt: off
type: Literal["sub"] = "sub" type: Literal["sub"] = "sub"
a: int = Field(default=0, description="The first number") a: int = Field(default=0, description="The first number")
b: int = Field(default=0, description="The second number") b: int = Field(default=0, description="The second number")
#fmt: on # fmt: on
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=self.a - self.b) return IntOutput(a=self.a - self.b)
@ -54,11 +62,12 @@ class SubtractInvocation(BaseInvocation, MathInvocationConfig):
class MultiplyInvocation(BaseInvocation, MathInvocationConfig): class MultiplyInvocation(BaseInvocation, MathInvocationConfig):
"""Multiplies two numbers""" """Multiplies two numbers"""
#fmt: off
# fmt: off
type: Literal["mul"] = "mul" type: Literal["mul"] = "mul"
a: int = Field(default=0, description="The first number") a: int = Field(default=0, description="The first number")
b: int = Field(default=0, description="The second number") b: int = Field(default=0, description="The second number")
#fmt: on # fmt: on
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=self.a * self.b) return IntOutput(a=self.a * self.b)
@ -66,11 +75,12 @@ class MultiplyInvocation(BaseInvocation, MathInvocationConfig):
class DivideInvocation(BaseInvocation, MathInvocationConfig): class DivideInvocation(BaseInvocation, MathInvocationConfig):
"""Divides two numbers""" """Divides two numbers"""
#fmt: off
# fmt: off
type: Literal["div"] = "div" type: Literal["div"] = "div"
a: int = Field(default=0, description="The first number") a: int = Field(default=0, description="The first number")
b: int = Field(default=0, description="The second number") b: int = Field(default=0, description="The second number")
#fmt: on # fmt: on
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=int(self.a / self.b)) return IntOutput(a=int(self.a / self.b))
@ -78,8 +88,13 @@ class DivideInvocation(BaseInvocation, MathInvocationConfig):
class RandomIntInvocation(BaseInvocation): class RandomIntInvocation(BaseInvocation):
"""Outputs a single random integer.""" """Outputs a single random integer."""
#fmt: off
# fmt: off
type: Literal["rand_int"] = "rand_int" type: Literal["rand_int"] = "rand_int"
#fmt: on low: int = Field(default=0, description="The inclusive low value")
high: int = Field(
default=np.iinfo(np.int32).max, description="The exclusive high value"
)
# fmt: on
def invoke(self, context: InvocationContext) -> IntOutput: def invoke(self, context: InvocationContext) -> IntOutput:
return IntOutput(a=np.random.randint(0, np.iinfo(np.int32).max)) return IntOutput(a=np.random.randint(self.low, self.high))

View File

@ -16,6 +16,7 @@ from compel.prompt_parser import (
FlattenedPrompt, FlattenedPrompt,
Fragment, Fragment,
PromptParser, PromptParser,
Conjunction,
) )
import invokeai.backend.util.logging as logger import invokeai.backend.util.logging as logger
@ -26,58 +27,48 @@ from ..util import torch_dtype
config = get_invokeai_config() config = get_invokeai_config()
def get_uc_and_c_and_ec( def get_uc_and_c_and_ec(prompt_string,
prompt_string, model, log_tokens=False, skip_normalize_legacy_blend=False model: InvokeAIDiffuserComponent,
): log_tokens=False, skip_normalize_legacy_blend=False):
# lazy-load any deferred textual inversions. # lazy-load any deferred textual inversions.
# this might take a couple of seconds the first time a textual inversion is used. # this might take a couple of seconds the first time a textual inversion is used.
model.textual_inversion_manager.create_deferred_token_ids_for_any_trigger_terms( model.textual_inversion_manager.create_deferred_token_ids_for_any_trigger_terms(prompt_string)
prompt_string
)
tokenizer = model.tokenizer compel = Compel(tokenizer=model.tokenizer,
compel = Compel( text_encoder=model.text_encoder,
tokenizer=tokenizer, textual_inversion_manager=model.textual_inversion_manager,
text_encoder=model.text_encoder, dtype_for_device_getter=torch_dtype,
textual_inversion_manager=model.textual_inversion_manager, truncate_long_prompts=False,
dtype_for_device_getter=torch_dtype, )
truncate_long_prompts=False
)
# get rid of any newline characters # get rid of any newline characters
prompt_string = prompt_string.replace("\n", " ") prompt_string = prompt_string.replace("\n", " ")
( positive_prompt_string, negative_prompt_string = split_prompt_to_positive_and_negative(prompt_string)
positive_prompt_string,
negative_prompt_string,
) = split_prompt_to_positive_and_negative(prompt_string)
legacy_blend = try_parse_legacy_blend(
positive_prompt_string, skip_normalize_legacy_blend
)
positive_prompt: Union[FlattenedPrompt, Blend]
if legacy_blend is not None:
positive_prompt = legacy_blend
else:
positive_prompt = Compel.parse_prompt_string(positive_prompt_string)
negative_prompt: Union[FlattenedPrompt, Blend] = Compel.parse_prompt_string(
negative_prompt_string
)
legacy_blend = try_parse_legacy_blend(positive_prompt_string, skip_normalize_legacy_blend)
positive_conjunction: Conjunction
if legacy_blend is not None:
positive_conjunction = legacy_blend
else:
positive_conjunction = Compel.parse_prompt_string(positive_prompt_string)
positive_prompt = positive_conjunction.prompts[0]
negative_conjunction = Compel.parse_prompt_string(negative_prompt_string)
negative_prompt: FlattenedPrompt | Blend = negative_conjunction.prompts[0]
tokens_count = get_max_token_count(model.tokenizer, positive_prompt)
if log_tokens or config.log_tokenization: if log_tokens or config.log_tokenization:
log_tokenization(positive_prompt, negative_prompt, tokenizer=tokenizer) log_tokenization(positive_prompt, negative_prompt, tokenizer=model.tokenizer)
c, options = compel.build_conditioning_tensor_for_prompt_object(positive_prompt) c, options = compel.build_conditioning_tensor_for_prompt_object(positive_prompt)
uc, _ = compel.build_conditioning_tensor_for_prompt_object(negative_prompt) uc, _ = compel.build_conditioning_tensor_for_prompt_object(negative_prompt)
[c, uc] = compel.pad_conditioning_tensors_to_same_length([c, uc]) [c, uc] = compel.pad_conditioning_tensors_to_same_length([c, uc])
tokens_count = get_max_token_count(tokenizer, positive_prompt) ec = InvokeAIDiffuserComponent.ExtraConditioningInfo(tokens_count_including_eos_bos=tokens_count,
cross_attention_control_args=options.get(
ec = InvokeAIDiffuserComponent.ExtraConditioningInfo( 'cross_attention_control', None))
tokens_count_including_eos_bos=tokens_count,
cross_attention_control_args=options.get("cross_attention_control", None),
)
return uc, c, ec return uc, c, ec
def get_prompt_structure( def get_prompt_structure(
prompt_string, skip_normalize_legacy_blend: bool = False prompt_string, skip_normalize_legacy_blend: bool = False
) -> (Union[FlattenedPrompt, Blend], FlattenedPrompt): ) -> (Union[FlattenedPrompt, Blend], FlattenedPrompt):
@ -88,18 +79,17 @@ def get_prompt_structure(
legacy_blend = try_parse_legacy_blend( legacy_blend = try_parse_legacy_blend(
positive_prompt_string, skip_normalize_legacy_blend positive_prompt_string, skip_normalize_legacy_blend
) )
positive_prompt: Union[FlattenedPrompt, Blend] positive_prompt: Conjunction
if legacy_blend is not None: if legacy_blend is not None:
positive_prompt = legacy_blend positive_conjunction = legacy_blend
else: else:
positive_prompt = Compel.parse_prompt_string(positive_prompt_string) positive_conjunction = Compel.parse_prompt_string(positive_prompt_string)
negative_prompt: Union[FlattenedPrompt, Blend] = Compel.parse_prompt_string( positive_prompt = positive_conjunction.prompts[0]
negative_prompt_string negative_conjunction = Compel.parse_prompt_string(negative_prompt_string)
) negative_prompt: FlattenedPrompt|Blend = negative_conjunction.prompts[0]
return positive_prompt, negative_prompt return positive_prompt, negative_prompt
def get_max_token_count( def get_max_token_count(
tokenizer, prompt: Union[FlattenedPrompt, Blend], truncate_if_too_long=False tokenizer, prompt: Union[FlattenedPrompt, Blend], truncate_if_too_long=False
) -> int: ) -> int:
@ -246,22 +236,21 @@ def log_tokenization_for_text(text, tokenizer, display_label=None, truncate_if_t
logger.info(f"[TOKENLOG] Tokens Discarded ({totalTokens - usedTokens}):") logger.info(f"[TOKENLOG] Tokens Discarded ({totalTokens - usedTokens}):")
logger.debug(f"{discarded}\x1b[0m") logger.debug(f"{discarded}\x1b[0m")
def try_parse_legacy_blend(text: str, skip_normalize: bool = False) -> Optional[Conjunction]:
def try_parse_legacy_blend(text: str, skip_normalize: bool = False) -> Optional[Blend]:
weighted_subprompts = split_weighted_subprompts(text, skip_normalize=skip_normalize) weighted_subprompts = split_weighted_subprompts(text, skip_normalize=skip_normalize)
if len(weighted_subprompts) <= 1: if len(weighted_subprompts) <= 1:
return None return None
strings = [x[0] for x in weighted_subprompts] strings = [x[0] for x in weighted_subprompts]
weights = [x[1] for x in weighted_subprompts]
pp = PromptParser() pp = PromptParser()
parsed_conjunctions = [pp.parse_conjunction(x) for x in strings] parsed_conjunctions = [pp.parse_conjunction(x) for x in strings]
flattened_prompts = [x.prompts[0] for x in parsed_conjunctions] flattened_prompts = []
weights = []
return Blend( for i, x in enumerate(parsed_conjunctions):
prompts=flattened_prompts, weights=weights, normalize_weights=not skip_normalize if len(x.prompts)>0:
) flattened_prompts.append(x.prompts[0])
weights.append(weighted_subprompts[i][1])
return Conjunction([Blend(prompts=flattened_prompts, weights=weights, normalize_weights=not skip_normalize)])
def split_weighted_subprompts(text, skip_normalize=False) -> list: def split_weighted_subprompts(text, skip_normalize=False) -> list:
""" """

View File

@ -548,8 +548,9 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
additional_guidance = [] additional_guidance = []
extra_conditioning_info = conditioning_data.extra extra_conditioning_info = conditioning_data.extra
with self.invokeai_diffuser.custom_attention_context( with self.invokeai_diffuser.custom_attention_context(
extra_conditioning_info=extra_conditioning_info, self.invokeai_diffuser.model,
step_count=len(self.scheduler.timesteps), extra_conditioning_info=extra_conditioning_info,
step_count=len(self.scheduler.timesteps),
): ):
yield PipelineIntermediateState( yield PipelineIntermediateState(
run_id=run_id, run_id=run_id,

View File

@ -10,6 +10,7 @@ import diffusers
import psutil import psutil
import torch import torch
from compel.cross_attention_control import Arguments from compel.cross_attention_control import Arguments
from diffusers.models.unet_2d_condition import UNet2DConditionModel
from diffusers.models.attention_processor import AttentionProcessor from diffusers.models.attention_processor import AttentionProcessor
from torch import nn from torch import nn
@ -352,8 +353,7 @@ def restore_default_cross_attention(
else: else:
remove_attention_function(model) remove_attention_function(model)
def setup_cross_attention_control_attention_processors(unet: UNet2DConditionModel, context: Context):
def override_cross_attention(model, context: Context, is_running_diffusers=False):
""" """
Inject attention parameters and functions into the passed in model to enable cross attention editing. Inject attention parameters and functions into the passed in model to enable cross attention editing.
@ -372,37 +372,22 @@ def override_cross_attention(model, context: Context, is_running_diffusers=False
indices = torch.arange(max_length, dtype=torch.long) indices = torch.arange(max_length, dtype=torch.long)
for name, a0, a1, b0, b1 in context.arguments.edit_opcodes: for name, a0, a1, b0, b1 in context.arguments.edit_opcodes:
if b0 < max_length: if b0 < max_length:
if name == "equal": # or (name == "replace" and a1 - a0 == b1 - b0): if name == "equal":# or (name == "replace" and a1 - a0 == b1 - b0):
# these tokens have not been edited # these tokens have not been edited
indices[b0:b1] = indices_target[a0:a1] indices[b0:b1] = indices_target[a0:a1]
mask[b0:b1] = 1 mask[b0:b1] = 1
context.cross_attention_mask = mask.to(device) context.cross_attention_mask = mask.to(device)
context.cross_attention_index_map = indices.to(device) context.cross_attention_index_map = indices.to(device)
if is_running_diffusers: old_attn_processors = unet.attn_processors
unet = model if torch.backends.mps.is_available():
old_attn_processors = unet.attn_processors # see note in StableDiffusionGeneratorPipeline.__init__ about borked slicing on MPS
if torch.backends.mps.is_available(): unet.set_attn_processor(SwapCrossAttnProcessor())
# see note in StableDiffusionGeneratorPipeline.__init__ about borked slicing on MPS
unet.set_attn_processor(SwapCrossAttnProcessor())
else:
# try to re-use an existing slice size
default_slice_size = 4
slice_size = next(
(
p.slice_size
for p in old_attn_processors.values()
if type(p) is SlicedAttnProcessor
),
default_slice_size,
)
unet.set_attn_processor(SlicedSwapCrossAttnProcesser(slice_size=slice_size))
return old_attn_processors
else: else:
context.register_cross_attention_modules(model) # try to re-use an existing slice size
inject_attention_function(model, context) default_slice_size = 4
return None slice_size = next((p.slice_size for p in old_attn_processors.values() if type(p) is SlicedAttnProcessor), default_slice_size)
unet.set_attn_processor(SlicedSwapCrossAttnProcesser(slice_size=slice_size))
def get_cross_attention_modules( def get_cross_attention_modules(
model, which: CrossAttentionType model, which: CrossAttentionType

View File

@ -5,6 +5,7 @@ from typing import Any, Callable, Dict, Optional, Union
import numpy as np import numpy as np
import torch import torch
from diffusers import UNet2DConditionModel
from diffusers.models.attention_processor import AttentionProcessor from diffusers.models.attention_processor import AttentionProcessor
from typing_extensions import TypeAlias from typing_extensions import TypeAlias
@ -17,8 +18,8 @@ from .cross_attention_control import (
CrossAttentionType, CrossAttentionType,
SwapCrossAttnContext, SwapCrossAttnContext,
get_cross_attention_modules, get_cross_attention_modules,
override_cross_attention,
restore_default_cross_attention, restore_default_cross_attention,
setup_cross_attention_control_attention_processors,
) )
from .cross_attention_map_saving import AttentionMapSaver from .cross_attention_map_saving import AttentionMapSaver
@ -80,24 +81,35 @@ class InvokeAIDiffuserComponent:
self.cross_attention_control_context = None self.cross_attention_control_context = None
self.sequential_guidance = config.sequential_guidance self.sequential_guidance = config.sequential_guidance
@classmethod
@contextmanager @contextmanager
def custom_attention_context( def custom_attention_context(
self, extra_conditioning_info: Optional[ExtraConditioningInfo], step_count: int cls,
unet: UNet2DConditionModel, # note: also may futz with the text encoder depending on requested LoRAs
extra_conditioning_info: Optional[ExtraConditioningInfo],
step_count: int
): ):
do_swap = ( old_attn_processors = None
extra_conditioning_info is not None if extra_conditioning_info and (
and extra_conditioning_info.wants_cross_attention_control extra_conditioning_info.wants_cross_attention_control
) ):
old_attn_processor = None old_attn_processors = unet.attn_processors
if do_swap: # Load lora conditions into the model
old_attn_processor = self.override_cross_attention( if extra_conditioning_info.wants_cross_attention_control:
extra_conditioning_info, step_count=step_count cross_attention_control_context = Context(
) arguments=extra_conditioning_info.cross_attention_control_args,
step_count=step_count,
)
setup_cross_attention_control_attention_processors(
unet,
cross_attention_control_context,
)
try: try:
yield None yield None
finally: finally:
if old_attn_processor is not None: if old_attn_processors is not None:
self.restore_default_cross_attention(old_attn_processor) unet.set_attn_processor(old_attn_processors)
# TODO resuscitate attention map saving # TODO resuscitate attention map saving
# self.remove_attention_map_saving() # self.remove_attention_map_saving()

View File

@ -15,15 +15,3 @@ The `postinstall` script patches a few packages and runs the Chakra CLI to gener
### Patch `@chakra-ui/cli` ### Patch `@chakra-ui/cli`
See: <https://github.com/chakra-ui/chakra-ui/issues/7394> See: <https://github.com/chakra-ui/chakra-ui/issues/7394>
### Patch `redux-persist`
We want to persist the canvas state to `localStorage` but many canvas operations change data very quickly, so we need to debounce the writes to `localStorage`.
`redux-persist` is unfortunately unmaintained. The repo's current code is nonfunctional, but the last release's code depends on a package that was removed from `npm` for being malware, so we cannot just fork it.
So, we have to patch it directly. Perhaps a better way would be to write a debounced storage adapter, but I couldn't figure out how to do that.
### Patch `redux-deep-persist`
This package makes blacklisting and whitelisting persist configs very simple, but we have to patch it to match `redux-persist` for the types to work.

View File

@ -89,18 +89,13 @@
"react-i18next": "^12.2.2", "react-i18next": "^12.2.2",
"react-icons": "^4.7.1", "react-icons": "^4.7.1",
"react-konva": "^18.2.7", "react-konva": "^18.2.7",
"react-konva-utils": "^1.0.4",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-resizable-panels": "^0.0.42", "react-resizable-panels": "^0.0.42",
"react-rnd": "^10.4.1",
"react-transition-group": "^4.4.5",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"react-virtuoso": "^4.3.5", "react-virtuoso": "^4.3.5",
"react-zoom-pan-pinch": "^3.0.7", "react-zoom-pan-pinch": "^3.0.7",
"reactflow": "^11.7.0", "reactflow": "^11.7.0",
"redux-deep-persist": "^1.0.7",
"redux-dynamic-middlewares": "^2.2.0", "redux-dynamic-middlewares": "^2.2.0",
"redux-persist": "^6.0.0",
"redux-remember": "^3.3.1", "redux-remember": "^3.3.1",
"roarr": "^7.15.0", "roarr": "^7.15.0",
"serialize-error": "^11.0.0", "serialize-error": "^11.0.0",

View File

@ -1,24 +0,0 @@
diff --git a/node_modules/redux-deep-persist/lib/types.d.ts b/node_modules/redux-deep-persist/lib/types.d.ts
index b67b8c2..7fc0fa1 100644
--- a/node_modules/redux-deep-persist/lib/types.d.ts
+++ b/node_modules/redux-deep-persist/lib/types.d.ts
@@ -35,6 +35,7 @@ export interface PersistConfig<S, RS = any, HSS = any, ESS = any> {
whitelist?: Array<string>;
transforms?: Array<Transform<HSS, ESS, S, RS>>;
throttle?: number;
+ debounce?: number;
migrate?: PersistMigrate;
stateReconciler?: false | StateReconciler<S>;
getStoredState?: (config: PersistConfig<S, RS, HSS, ESS>) => Promise<PersistedState>;
diff --git a/node_modules/redux-deep-persist/src/types.ts b/node_modules/redux-deep-persist/src/types.ts
index 398ac19..cbc5663 100644
--- a/node_modules/redux-deep-persist/src/types.ts
+++ b/node_modules/redux-deep-persist/src/types.ts
@@ -91,6 +91,7 @@ export interface PersistConfig<S, RS = any, HSS = any, ESS = any> {
whitelist?: Array<string>;
transforms?: Array<Transform<HSS, ESS, S, RS>>;
throttle?: number;
+ debounce?: number;
migrate?: PersistMigrate;
stateReconciler?: false | StateReconciler<S>;
/**

View File

@ -1,116 +0,0 @@
diff --git a/node_modules/redux-persist/es/createPersistoid.js b/node_modules/redux-persist/es/createPersistoid.js
index 8b43b9a..184faab 100644
--- a/node_modules/redux-persist/es/createPersistoid.js
+++ b/node_modules/redux-persist/es/createPersistoid.js
@@ -6,6 +6,7 @@ export default function createPersistoid(config) {
var whitelist = config.whitelist || null;
var transforms = config.transforms || [];
var throttle = config.throttle || 0;
+ var debounce = config.debounce || 0;
var storageKey = "".concat(config.keyPrefix !== undefined ? config.keyPrefix : KEY_PREFIX).concat(config.key);
var storage = config.storage;
var serialize;
@@ -28,30 +29,37 @@ export default function createPersistoid(config) {
var timeIterator = null;
var writePromise = null;
- var update = function update(state) {
- // add any changed keys to the queue
- Object.keys(state).forEach(function (key) {
- if (!passWhitelistBlacklist(key)) return; // is keyspace ignored? noop
+ // Timer for debounced `update()`
+ let timer = 0;
- if (lastState[key] === state[key]) return; // value unchanged? noop
+ function update(state) {
+ // Debounce the update
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ // add any changed keys to the queue
+ Object.keys(state).forEach(function (key) {
+ if (!passWhitelistBlacklist(key)) return; // is keyspace ignored? noop
- if (keysToProcess.indexOf(key) !== -1) return; // is key already queued? noop
+ if (lastState[key] === state[key]) return; // value unchanged? noop
- keysToProcess.push(key); // add key to queue
- }); //if any key is missing in the new state which was present in the lastState,
- //add it for processing too
+ if (keysToProcess.indexOf(key) !== -1) return; // is key already queued? noop
- Object.keys(lastState).forEach(function (key) {
- if (state[key] === undefined && passWhitelistBlacklist(key) && keysToProcess.indexOf(key) === -1 && lastState[key] !== undefined) {
- keysToProcess.push(key);
- }
- }); // start the time iterator if not running (read: throttle)
+ keysToProcess.push(key); // add key to queue
+ }); //if any key is missing in the new state which was present in the lastState,
+ //add it for processing too
- if (timeIterator === null) {
- timeIterator = setInterval(processNextKey, throttle);
- }
+ Object.keys(lastState).forEach(function (key) {
+ if (state[key] === undefined && passWhitelistBlacklist(key) && keysToProcess.indexOf(key) === -1 && lastState[key] !== undefined) {
+ keysToProcess.push(key);
+ }
+ }); // start the time iterator if not running (read: throttle)
+
+ if (timeIterator === null) {
+ timeIterator = setInterval(processNextKey, throttle);
+ }
- lastState = state;
+ lastState = state;
+ }, debounce)
};
function processNextKey() {
diff --git a/node_modules/redux-persist/es/types.js.flow b/node_modules/redux-persist/es/types.js.flow
index c50d3cd..39d8be2 100644
--- a/node_modules/redux-persist/es/types.js.flow
+++ b/node_modules/redux-persist/es/types.js.flow
@@ -19,6 +19,7 @@ export type PersistConfig = {
whitelist?: Array<string>,
transforms?: Array<Transform>,
throttle?: number,
+ debounce?: number,
migrate?: (PersistedState, number) => Promise<PersistedState>,
stateReconciler?: false | Function,
getStoredState?: PersistConfig => Promise<PersistedState>, // used for migrations
diff --git a/node_modules/redux-persist/lib/types.js.flow b/node_modules/redux-persist/lib/types.js.flow
index c50d3cd..39d8be2 100644
--- a/node_modules/redux-persist/lib/types.js.flow
+++ b/node_modules/redux-persist/lib/types.js.flow
@@ -19,6 +19,7 @@ export type PersistConfig = {
whitelist?: Array<string>,
transforms?: Array<Transform>,
throttle?: number,
+ debounce?: number,
migrate?: (PersistedState, number) => Promise<PersistedState>,
stateReconciler?: false | Function,
getStoredState?: PersistConfig => Promise<PersistedState>, // used for migrations
diff --git a/node_modules/redux-persist/src/types.js b/node_modules/redux-persist/src/types.js
index c50d3cd..39d8be2 100644
--- a/node_modules/redux-persist/src/types.js
+++ b/node_modules/redux-persist/src/types.js
@@ -19,6 +19,7 @@ export type PersistConfig = {
whitelist?: Array<string>,
transforms?: Array<Transform>,
throttle?: number,
+ debounce?: number,
migrate?: (PersistedState, number) => Promise<PersistedState>,
stateReconciler?: false | Function,
getStoredState?: PersistConfig => Promise<PersistedState>, // used for migrations
diff --git a/node_modules/redux-persist/types/types.d.ts b/node_modules/redux-persist/types/types.d.ts
index b3733bc..2a1696c 100644
--- a/node_modules/redux-persist/types/types.d.ts
+++ b/node_modules/redux-persist/types/types.d.ts
@@ -35,6 +35,7 @@ declare module "redux-persist/es/types" {
whitelist?: Array<string>;
transforms?: Array<Transform<HSS, ESS, S, RS>>;
throttle?: number;
+ debounce?: number;
migrate?: PersistMigrate;
stateReconciler?: false | StateReconciler<S>;
/**

View File

@ -450,7 +450,7 @@
"cfgScale": "CFG Scale", "cfgScale": "CFG Scale",
"width": "Width", "width": "Width",
"height": "Height", "height": "Height",
"sampler": "Sampler", "scheduler": "Scheduler",
"seed": "Seed", "seed": "Seed",
"imageToImage": "Image to Image", "imageToImage": "Image to Image",
"randomizeSeed": "Randomize Seed", "randomizeSeed": "Randomize Seed",
@ -552,8 +552,8 @@
"canceled": "Processing Canceled", "canceled": "Processing Canceled",
"tempFoldersEmptied": "Temp Folder Emptied", "tempFoldersEmptied": "Temp Folder Emptied",
"uploadFailed": "Upload failed", "uploadFailed": "Upload failed",
"uploadFailedMultipleImagesDesc": "Multiple images pasted, may only upload one image at a time",
"uploadFailedUnableToLoadDesc": "Unable to load file", "uploadFailedUnableToLoadDesc": "Unable to load file",
"uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
"downloadImageStarted": "Image Download Started", "downloadImageStarted": "Image Download Started",
"imageCopied": "Image Copied", "imageCopied": "Image Copied",
"imageLinkCopied": "Image Link Copied", "imageLinkCopied": "Image Link Copied",

View File

@ -2,14 +2,11 @@ import ImageUploader from 'common/components/ImageUploader';
import SiteHeader from 'features/system/components/SiteHeader'; import SiteHeader from 'features/system/components/SiteHeader';
import ProgressBar from 'features/system/components/ProgressBar'; import ProgressBar from 'features/system/components/ProgressBar';
import InvokeTabs from 'features/ui/components/InvokeTabs'; import InvokeTabs from 'features/ui/components/InvokeTabs';
import useToastWatcher from 'features/system/hooks/useToastWatcher';
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton'; import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons'; import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
import { Box, Flex, Grid, Portal } from '@chakra-ui/react'; import { Box, Flex, Grid, Portal } from '@chakra-ui/react';
import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
import GalleryDrawer from 'features/gallery/components/ImageGalleryPanel'; import GalleryDrawer from 'features/gallery/components/GalleryPanel';
import Lightbox from 'features/lightbox/components/Lightbox'; import Lightbox from 'features/lightbox/components/Lightbox';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { memo, ReactNode, useCallback, useEffect, useState } from 'react'; import { memo, ReactNode, useCallback, useEffect, useState } from 'react';
@ -17,13 +14,14 @@ import { motion, AnimatePresence } from 'framer-motion';
import Loading from 'common/components/Loading/Loading'; import Loading from 'common/components/Loading/Loading';
import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady'; import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
import { PartialAppConfig } from 'app/types/invokeai'; import { PartialAppConfig } from 'app/types/invokeai';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { configChanged } from 'features/system/store/configSlice'; import { configChanged } from 'features/system/store/configSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useLogger } from 'app/logging/useLogger'; import { useLogger } from 'app/logging/useLogger';
import ParametersDrawer from 'features/ui/components/ParametersDrawer'; import ParametersDrawer from 'features/ui/components/ParametersDrawer';
import { languageSelector } from 'features/system/store/systemSelectors'; import { languageSelector } from 'features/system/store/systemSelectors';
import i18n from 'i18n'; import i18n from 'i18n';
import Toaster from './Toaster';
import GlobalHotkeys from './GlobalHotkeys';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
@ -38,9 +36,6 @@ const App = ({
headerComponent, headerComponent,
setIsReady, setIsReady,
}: Props) => { }: Props) => {
useToastWatcher();
useGlobalHotkeys();
const language = useAppSelector(languageSelector); const language = useAppSelector(languageSelector);
const log = useLogger(); const log = useLogger();
@ -77,65 +72,69 @@ const App = ({
}, [isApplicationReady, setIsReady]); }, [isApplicationReady, setIsReady]);
return ( return (
<Grid w="100vw" h="100vh" position="relative" overflow="hidden"> <>
{isLightboxEnabled && <Lightbox />} <Grid w="100vw" h="100vh" position="relative" overflow="hidden">
<ImageUploader> {isLightboxEnabled && <Lightbox />}
<ProgressBar /> <ImageUploader>
<Grid <ProgressBar />
gap={4} <Grid
p={4}
gridAutoRows="min-content auto"
w={APP_WIDTH}
h={APP_HEIGHT}
>
{headerComponent || <SiteHeader />}
<Flex
gap={4} gap={4}
w={{ base: '100vw', xl: 'full' }} p={4}
h="full" gridAutoRows="min-content auto"
flexDir={{ base: 'column', xl: 'row' }} w={APP_WIDTH}
h={APP_HEIGHT}
> >
<InvokeTabs /> {headerComponent || <SiteHeader />}
</Flex> <Flex
</Grid> gap={4}
</ImageUploader> w={{ base: '100vw', xl: 'full' }}
h="full"
flexDir={{ base: 'column', xl: 'row' }}
>
<InvokeTabs />
</Flex>
</Grid>
</ImageUploader>
<GalleryDrawer /> <GalleryDrawer />
<ParametersDrawer /> <ParametersDrawer />
<AnimatePresence> <AnimatePresence>
{!isApplicationReady && !loadingOverridden && ( {!isApplicationReady && !loadingOverridden && (
<motion.div <motion.div
key="loading" key="loading"
initial={{ opacity: 1 }} initial={{ opacity: 1 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
style={{ zIndex: 3 }} style={{ zIndex: 3 }}
> >
<Box position="absolute" top={0} left={0} w="100vw" h="100vh"> <Box position="absolute" top={0} left={0} w="100vw" h="100vh">
<Loading /> <Loading />
</Box> </Box>
<Box <Box
onClick={handleOverrideClicked} onClick={handleOverrideClicked}
position="absolute" position="absolute"
top={0} top={0}
right={0} right={0}
cursor="pointer" cursor="pointer"
w="2rem" w="2rem"
h="2rem" h="2rem"
/> />
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
<Portal> <Portal>
<FloatingParametersPanelButtons /> <FloatingParametersPanelButtons />
</Portal> </Portal>
<Portal> <Portal>
<FloatingGalleryButton /> <FloatingGalleryButton />
</Portal> </Portal>
</Grid> </Grid>
<Toaster />
<GlobalHotkeys />
</>
); );
}; };

View File

@ -0,0 +1,44 @@
import { Flex, Spinner, Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
import { memo } from 'react';
const selector = createSelector(systemSelector, (system) => {
const { isUploading } = system;
let tooltip = '';
if (isUploading) {
tooltip = 'Uploading...';
}
return {
tooltip,
shouldShow: isUploading,
};
});
export const AuxiliaryProgressIndicator = () => {
const { shouldShow, tooltip } = useAppSelector(selector);
if (!shouldShow) {
return null;
}
return (
<Flex
sx={{
alignItems: 'center',
justifyContent: 'center',
color: 'base.600',
}}
>
<Tooltip label={tooltip} placement="right" hasArrow>
<Spinner />
</Tooltip>
</Flex>
);
};
export default memo(AuxiliaryProgressIndicator);

View File

@ -10,6 +10,7 @@ import {
togglePinParametersPanel, togglePinParametersPanel,
} from 'features/ui/store/uiSlice'; } from 'features/ui/store/uiSlice';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import React, { memo } from 'react';
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook'; import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
const globalHotkeysSelector = createSelector( const globalHotkeysSelector = createSelector(
@ -27,7 +28,11 @@ const globalHotkeysSelector = createSelector(
// TODO: Does not catch keypresses while focused in an input. Maybe there is a way? // TODO: Does not catch keypresses while focused in an input. Maybe there is a way?
export const useGlobalHotkeys = () => { /**
* Logical component. Handles app-level global hotkeys.
* @returns null
*/
const GlobalHotkeys: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { shift } = useAppSelector(globalHotkeysSelector); const { shift } = useAppSelector(globalHotkeysSelector);
@ -75,4 +80,8 @@ export const useGlobalHotkeys = () => {
useHotkeys('4', () => { useHotkeys('4', () => {
dispatch(setActiveTab('nodes')); dispatch(setActiveTab('nodes'));
}); });
return null;
}; };
export default memo(GlobalHotkeys);

View File

@ -0,0 +1,65 @@
import { useToast, UseToastOptions } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { toastQueueSelector } from 'features/system/store/systemSelectors';
import { addToast, clearToastQueue } from 'features/system/store/systemSlice';
import { useCallback, useEffect } from 'react';
export type MakeToastArg = string | UseToastOptions;
/**
* Makes a toast from a string or a UseToastOptions object.
* If a string is passed, the toast will have the status 'info' and will be closable with a duration of 2500ms.
*/
export const makeToast = (arg: MakeToastArg): UseToastOptions => {
if (typeof arg === 'string') {
return {
title: arg,
status: 'info',
isClosable: true,
duration: 2500,
};
}
return { status: 'info', isClosable: true, duration: 2500, ...arg };
};
/**
* Logical component. Watches the toast queue and makes toasts when the queue is not empty.
* @returns null
*/
const Toaster = () => {
const dispatch = useAppDispatch();
const toastQueue = useAppSelector(toastQueueSelector);
const toast = useToast();
useEffect(() => {
toastQueue.forEach((t) => {
toast(t);
});
toastQueue.length > 0 && dispatch(clearToastQueue());
}, [dispatch, toast, toastQueue]);
return null;
};
/**
* Returns a function that can be used to make a toast.
* @example
* const toaster = useAppToaster();
* toaster('Hello world!');
* toaster({ title: 'Hello world!', status: 'success' });
* @returns A function that can be used to make a toast.
* @see makeToast
* @see MakeToastArg
* @see UseToastOptions
*/
export const useAppToaster = () => {
const dispatch = useAppDispatch();
const toaster = useCallback(
(arg: MakeToastArg) => dispatch(addToast(makeToast(arg))),
[dispatch]
);
return toaster;
};
export default Toaster;

View File

@ -1,6 +1,6 @@
// TODO: use Enums? // TODO: use Enums?
export const SCHEDULERS: Array<string> = [ export const SCHEDULERS = [
'ddim', 'ddim',
'lms', 'lms',
'euler', 'euler',
@ -17,7 +17,12 @@ export const SCHEDULERS: Array<string> = [
'heun', 'heun',
'heun_k', 'heun_k',
'unipc', 'unipc',
]; ] as const;
export type Scheduler = (typeof SCHEDULERS)[number];
export const isScheduler = (x: string): x is Scheduler =>
SCHEDULERS.includes(x as Scheduler);
// Valid image widths // Valid image widths
export const WIDTHS: Array<number> = Array.from(Array(64)).map( export const WIDTHS: Array<number> = Array.from(Array(64)).map(

View File

@ -15,6 +15,10 @@ import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas';
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes'; import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage'; import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage';
import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage'; import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage';
import { addCanvasSavedToGalleryListener } from './listeners/canvasSavedToGallery';
import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage';
import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard';
import { addCanvasMergedListener } from './listeners/canvasMerged';
export const listenerMiddleware = createListenerMiddleware(); export const listenerMiddleware = createListenerMiddleware();
@ -43,3 +47,8 @@ addUserInvokedCanvasListener();
addUserInvokedNodesListener(); addUserInvokedNodesListener();
addUserInvokedTextToImageListener(); addUserInvokedTextToImageListener();
addUserInvokedImageToImageListener(); addUserInvokedImageToImageListener();
addCanvasSavedToGalleryListener();
addCanvasDownloadedAsImageListener();
addCanvasCopiedToClipboardListener();
addCanvasMergedListener();

View File

@ -0,0 +1,33 @@
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
import { copyBlobToClipboard } from 'features/canvas/util/copyBlobToClipboard';
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
export const addCanvasCopiedToClipboardListener = () => {
startAppListening({
actionCreator: canvasCopiedToClipboard,
effect: async (action, { dispatch, getState }) => {
const state = getState();
const blob = await getBaseLayerBlob(state);
if (!blob) {
moduleLog.error('Problem getting base layer blob');
dispatch(
addToast({
title: 'Problem Copying Canvas',
description: 'Unable to export base layer',
status: 'error',
})
);
return;
}
copyBlobToClipboard(blob);
},
});
};

View File

@ -0,0 +1,33 @@
import { canvasDownloadedAsImage } from 'features/canvas/store/actions';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { downloadBlob } from 'features/canvas/util/downloadBlob';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
export const addCanvasDownloadedAsImageListener = () => {
startAppListening({
actionCreator: canvasDownloadedAsImage,
effect: async (action, { dispatch, getState }) => {
const state = getState();
const blob = await getBaseLayerBlob(state);
if (!blob) {
moduleLog.error('Problem getting base layer blob');
dispatch(
addToast({
title: 'Problem Downloading Canvas',
description: 'Unable to export base layer',
status: 'error',
})
);
return;
}
downloadBlob(blob, 'mergedCanvas.png');
},
});
};

View File

@ -1,31 +0,0 @@
import { canvasGraphBuilt } from 'features/nodes/store/actions';
import { startAppListening } from '..';
import {
canvasSessionIdChanged,
stagingAreaInitialized,
} from 'features/canvas/store/canvasSlice';
import { sessionInvoked } from 'services/thunks/session';
export const addCanvasGraphBuiltListener = () =>
startAppListening({
actionCreator: canvasGraphBuilt,
effect: async (action, { dispatch, getState, take }) => {
const [{ meta }] = await take(sessionInvoked.fulfilled.match);
const { sessionId } = meta.arg;
const state = getState();
if (!state.canvas.layerState.stagingArea.boundingBox) {
dispatch(
stagingAreaInitialized({
sessionId,
boundingBox: {
...state.canvas.boundingBoxCoordinates,
...state.canvas.boundingBoxDimensions,
},
})
);
}
dispatch(canvasSessionIdChanged(sessionId));
},
});

View File

@ -0,0 +1,88 @@
import { canvasMerged } from 'features/canvas/store/actions';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
import { imageUploaded } from 'services/thunks/image';
import { v4 as uuidv4 } from 'uuid';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
export const addCanvasMergedListener = () => {
startAppListening({
actionCreator: canvasMerged,
effect: async (action, { dispatch, getState, take }) => {
const state = getState();
const blob = await getBaseLayerBlob(state, true);
if (!blob) {
moduleLog.error('Problem getting base layer blob');
dispatch(
addToast({
title: 'Problem Merging Canvas',
description: 'Unable to export base layer',
status: 'error',
})
);
return;
}
const canvasBaseLayer = getCanvasBaseLayer();
if (!canvasBaseLayer) {
moduleLog.error('Problem getting canvas base layer');
dispatch(
addToast({
title: 'Problem Merging Canvas',
description: 'Unable to export base layer',
status: 'error',
})
);
return;
}
const baseLayerRect = canvasBaseLayer.getClientRect({
relativeTo: canvasBaseLayer.getParent(),
});
const filename = `mergedCanvas_${uuidv4()}.png`;
dispatch(
imageUploaded({
imageType: 'intermediates',
formData: {
file: new File([blob], filename, { type: 'image/png' }),
},
})
);
const [{ payload }] = await take(
(action): action is ReturnType<typeof imageUploaded.fulfilled> =>
imageUploaded.fulfilled.match(action) &&
action.meta.arg.formData.file.name === filename
);
const mergedCanvasImage = deserializeImageResponse(payload.response);
dispatch(
setMergedCanvas({
kind: 'image',
layer: 'base',
image: mergedCanvasImage,
...baseLayerRect,
})
);
dispatch(
addToast({
title: 'Canvas Merged',
status: 'success',
})
);
},
});
};

View File

@ -0,0 +1,40 @@
import { canvasSavedToGallery } from 'features/canvas/store/actions';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { imageUploaded } from 'services/thunks/image';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
export const addCanvasSavedToGalleryListener = () => {
startAppListening({
actionCreator: canvasSavedToGallery,
effect: async (action, { dispatch, getState }) => {
const state = getState();
const blob = await getBaseLayerBlob(state);
if (!blob) {
moduleLog.error('Problem getting base layer blob');
dispatch(
addToast({
title: 'Problem Saving Canvas',
description: 'Unable to export base layer',
status: 'error',
})
);
return;
}
dispatch(
imageUploaded({
imageType: 'results',
formData: {
file: new File([blob], 'mergedCanvas.png', { type: 'image/png' }),
},
})
);
},
});
};

View File

@ -3,6 +3,10 @@ import { startAppListening } from '..';
import { uploadAdded } from 'features/gallery/store/uploadsSlice'; import { uploadAdded } from 'features/gallery/store/uploadsSlice';
import { imageSelected } from 'features/gallery/store/gallerySlice'; import { imageSelected } from 'features/gallery/store/gallerySlice';
import { imageUploaded } from 'services/thunks/image'; import { imageUploaded } from 'services/thunks/image';
import { addToast } from 'features/system/store/systemSlice';
import { initialImageSelected } from 'features/parameters/store/actions';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { resultAdded } from 'features/gallery/store/resultsSlice';
export const addImageUploadedListener = () => { export const addImageUploadedListener = () => {
startAppListening({ startAppListening({
@ -11,14 +15,31 @@ export const addImageUploadedListener = () => {
action.payload.response.image_type !== 'intermediates', action.payload.response.image_type !== 'intermediates',
effect: (action, { dispatch, getState }) => { effect: (action, { dispatch, getState }) => {
const { response } = action.payload; const { response } = action.payload;
const { imageType } = action.meta.arg;
const state = getState(); const state = getState();
const image = deserializeImageResponse(response); const image = deserializeImageResponse(response);
dispatch(uploadAdded(image)); if (imageType === 'uploads') {
dispatch(uploadAdded(image));
if (state.gallery.shouldAutoSwitchToNewImages) { dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
dispatch(imageSelected(image));
if (state.gallery.shouldAutoSwitchToNewImages) {
dispatch(imageSelected(image));
}
if (action.meta.arg.activeTabName === 'img2img') {
dispatch(initialImageSelected(image));
}
if (action.meta.arg.activeTabName === 'unifiedCanvas') {
dispatch(setInitialCanvasImage(image));
}
}
if (imageType === 'results') {
dispatch(resultAdded(image));
} }
}, },
}); });

View File

@ -2,11 +2,11 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { Image, isInvokeAIImage } from 'app/types/invokeai'; import { Image, isInvokeAIImage } from 'app/types/invokeai';
import { selectResultsById } from 'features/gallery/store/resultsSlice'; import { selectResultsById } from 'features/gallery/store/resultsSlice';
import { selectUploadsById } from 'features/gallery/store/uploadsSlice'; import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
import { makeToast } from 'features/system/hooks/useToastWatcher';
import { t } from 'i18next'; import { t } from 'i18next';
import { addToast } from 'features/system/store/systemSlice'; import { addToast } from 'features/system/store/systemSlice';
import { startAppListening } from '..'; import { startAppListening } from '..';
import { initialImageSelected } from 'features/parameters/store/actions'; import { initialImageSelected } from 'features/parameters/store/actions';
import { makeToast } from 'app/components/Toaster';
export const addInitialImageSelectedListener = () => { export const addInitialImageSelectedListener = () => {
startAppListening({ startAppListening({

View File

@ -1,6 +1,6 @@
import { startAppListening } from '..'; import { startAppListening } from '..';
import { sessionCreated, sessionInvoked } from 'services/thunks/session'; import { sessionCreated, sessionInvoked } from 'services/thunks/session';
import { buildCanvasGraphAndBlobs } from 'features/nodes/util/graphBuilders/buildCanvasGraph'; import { buildCanvasGraphComponents } from 'features/nodes/util/graphBuilders/buildCanvasGraph';
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { canvasGraphBuilt } from 'features/nodes/store/actions'; import { canvasGraphBuilt } from 'features/nodes/store/actions';
import { imageUploaded } from 'services/thunks/image'; import { imageUploaded } from 'services/thunks/image';
@ -11,9 +11,17 @@ import {
stagingAreaInitialized, stagingAreaInitialized,
} from 'features/canvas/store/canvasSlice'; } from 'features/canvas/store/canvasSlice';
import { userInvoked } from 'app/store/actions'; import { userInvoked } from 'app/store/actions';
import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
const moduleLog = log.child({ namespace: 'invoke' }); const moduleLog = log.child({ namespace: 'invoke' });
/**
* This listener is responsible for building the canvas graph and blobs when the user invokes the canvas.
* It is also responsible for uploading the base and mask layers to the server.
*/
export const addUserInvokedCanvasListener = () => { export const addUserInvokedCanvasListener = () => {
startAppListening({ startAppListening({
predicate: (action): action is ReturnType<typeof userInvoked> => predicate: (action): action is ReturnType<typeof userInvoked> =>
@ -21,25 +29,49 @@ export const addUserInvokedCanvasListener = () => {
effect: async (action, { getState, dispatch, take }) => { effect: async (action, { getState, dispatch, take }) => {
const state = getState(); const state = getState();
const data = await buildCanvasGraphAndBlobs(state); // Build canvas blobs
const canvasBlobsAndImageData = await getCanvasData(state);
if (!data) { if (!canvasBlobsAndImageData) {
moduleLog.error('Unable to create canvas data');
return;
}
const { baseBlob, baseImageData, maskBlob, maskImageData } =
canvasBlobsAndImageData;
// Determine the generation mode
const generationMode = getCanvasGenerationMode(
baseImageData,
maskImageData
);
if (state.system.enableImageDebugging) {
const baseDataURL = await blobToDataURL(baseBlob);
const maskDataURL = await blobToDataURL(maskBlob);
openBase64ImageInTab([
{ base64: maskDataURL, caption: 'mask b64' },
{ base64: baseDataURL, caption: 'image b64' },
]);
}
moduleLog.debug(`Generation mode: ${generationMode}`);
// Build the canvas graph
const graphComponents = await buildCanvasGraphComponents(
state,
generationMode
);
if (!graphComponents) {
moduleLog.error('Problem building graph'); moduleLog.error('Problem building graph');
return; return;
} }
const { const { rangeNode, iterateNode, baseNode, edges } = graphComponents;
rangeNode,
iterateNode,
baseNode,
edges,
baseBlob,
maskBlob,
generationMode,
} = data;
// Upload the base layer, to be used as init image
const baseFilename = `${uuidv4()}.png`; const baseFilename = `${uuidv4()}.png`;
const maskFilename = `${uuidv4()}.png`;
dispatch( dispatch(
imageUploaded({ imageUploaded({
@ -66,6 +98,9 @@ export const addUserInvokedCanvasListener = () => {
}; };
} }
// Upload the mask layer image
const maskFilename = `${uuidv4()}.png`;
if (baseNode.type === 'inpaint') { if (baseNode.type === 'inpaint') {
dispatch( dispatch(
imageUploaded({ imageUploaded({
@ -103,9 +138,12 @@ export const addUserInvokedCanvasListener = () => {
dispatch(canvasGraphBuilt(graph)); dispatch(canvasGraphBuilt(graph));
moduleLog({ data: graph }, 'Canvas graph built'); moduleLog({ data: graph }, 'Canvas graph built');
// Actually create the session
dispatch(sessionCreated({ graph })); dispatch(sessionCreated({ graph }));
// Wait for the session to be invoked (this is just the HTTP request to start processing)
const [{ meta }] = await take(sessionInvoked.fulfilled.match); const [{ meta }] = await take(sessionInvoked.fulfilled.match);
const { sessionId } = meta.arg; const { sessionId } = meta.arg;
if (!state.canvas.layerState.stagingArea.boundingBox) { if (!state.canvas.layerState.stagingArea.boundingBox) {

View File

@ -5,6 +5,7 @@ import {
Input, Input,
InputProps, InputProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { ChangeEvent, memo } from 'react'; import { ChangeEvent, memo } from 'react';
interface IAIInputProps extends InputProps { interface IAIInputProps extends InputProps {
@ -31,7 +32,7 @@ const IAIInput = (props: IAIInputProps) => {
{...formControlProps} {...formControlProps}
> >
{label !== '' && <FormLabel>{label}</FormLabel>} {label !== '' && <FormLabel>{label}</FormLabel>}
<Input {...rest} /> <Input {...rest} onPaste={stopPastePropagation} />
</FormControl> </FormControl>
); );
}; };

View File

@ -14,6 +14,7 @@ import {
Tooltip, Tooltip,
TooltipProps, TooltipProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { FocusEvent, memo, useEffect, useState } from 'react'; import { FocusEvent, memo, useEffect, useState } from 'react';
@ -125,6 +126,7 @@ const IAINumberInput = (props: Props) => {
onChange={handleOnChange} onChange={handleOnChange}
onBlur={handleBlur} onBlur={handleBlur}
{...rest} {...rest}
onPaste={stopPastePropagation}
> >
<NumberInputField {...numberInputFieldProps} /> <NumberInputField {...numberInputFieldProps} />
{showStepper && ( {showStepper && (

View File

@ -0,0 +1,9 @@
import { Textarea, TextareaProps, forwardRef } from '@chakra-ui/react';
import { stopPastePropagation } from 'common/util/stopPastePropagation';
import { memo } from 'react';
const IAITextarea = forwardRef((props: TextareaProps, ref) => {
return <Textarea ref={ref} onPaste={stopPastePropagation} {...props} />;
});
export default memo(IAITextarea);

View File

@ -1,4 +1,4 @@
import { Box, useToast } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext'; import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import useImageUploader from 'common/hooks/useImageUploader'; import useImageUploader from 'common/hooks/useImageUploader';
@ -10,12 +10,33 @@ import {
ReactNode, ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useMemo,
useRef,
useState, useState,
} from 'react'; } from 'react';
import { FileRejection, useDropzone } from 'react-dropzone'; import { FileRejection, useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { imageUploaded } from 'services/thunks/image'; import { imageUploaded } from 'services/thunks/image';
import ImageUploadOverlay from './ImageUploadOverlay'; import ImageUploadOverlay from './ImageUploadOverlay';
import { useAppToaster } from 'app/components/Toaster';
import { filter, map, some } from 'lodash-es';
import { createSelector } from '@reduxjs/toolkit';
import { systemSelector } from 'features/system/store/systemSelectors';
import { ErrorCode } from 'react-dropzone';
const selector = createSelector(
[systemSelector, activeTabNameSelector],
(system, activeTabName) => {
const { isConnected, isUploading } = system;
const isUploaderDisabled = !isConnected || isUploading;
return {
isUploaderDisabled,
activeTabName,
};
}
);
type ImageUploaderProps = { type ImageUploaderProps = {
children: ReactNode; children: ReactNode;
@ -24,38 +45,49 @@ type ImageUploaderProps = {
const ImageUploader = (props: ImageUploaderProps) => { const ImageUploader = (props: ImageUploaderProps) => {
const { children } = props; const { children } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const activeTabName = useAppSelector(activeTabNameSelector); const { isUploaderDisabled, activeTabName } = useAppSelector(selector);
const toast = useToast({}); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false); const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
const { setOpenUploader } = useImageUploader(); const { setOpenUploaderFunction } = useImageUploader();
const fileRejectionCallback = useCallback( const fileRejectionCallback = useCallback(
(rejection: FileRejection) => { (rejection: FileRejection) => {
setIsHandlingUpload(true); setIsHandlingUpload(true);
const msg = rejection.errors.reduce(
(acc: string, cur: { message: string }) => `${acc}\n${cur.message}`, toaster({
''
);
toast({
title: t('toast.uploadFailed'), title: t('toast.uploadFailed'),
description: msg, description: rejection.errors.map((error) => error.message).join('\n'),
status: 'error', status: 'error',
isClosable: true,
}); });
}, },
[t, toast] [t, toaster]
); );
const fileAcceptedCallback = useCallback( const fileAcceptedCallback = useCallback(
async (file: File) => { async (file: File) => {
dispatch(imageUploaded({ imageType: 'uploads', formData: { file } })); dispatch(
imageUploaded({
imageType: 'uploads',
formData: { file },
activeTabName,
})
);
}, },
[dispatch] [dispatch, activeTabName]
); );
const onDrop = useCallback( const onDrop = useCallback(
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => { (acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
if (fileRejections.length > 1) {
toaster({
title: t('toast.uploadFailed'),
description: t('toast.uploadFailedInvalidUploadDesc'),
status: 'error',
});
return;
}
fileRejections.forEach((rejection: FileRejection) => { fileRejections.forEach((rejection: FileRejection) => {
fileRejectionCallback(rejection); fileRejectionCallback(rejection);
}); });
@ -64,7 +96,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
fileAcceptedCallback(file); fileAcceptedCallback(file);
}); });
}, },
[fileAcceptedCallback, fileRejectionCallback] [t, toaster, fileAcceptedCallback, fileRejectionCallback]
); );
const { const {
@ -73,92 +105,73 @@ const ImageUploader = (props: ImageUploaderProps) => {
isDragAccept, isDragAccept,
isDragReject, isDragReject,
isDragActive, isDragActive,
inputRef,
open, open,
} = useDropzone({ } = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] }, accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },
noClick: true, noClick: true,
onDrop, onDrop,
onDragOver: () => setIsHandlingUpload(true), onDragOver: () => setIsHandlingUpload(true),
maxFiles: 1, disabled: isUploaderDisabled,
multiple: false,
}); });
setOpenUploader(open);
useEffect(() => { useEffect(() => {
const pasteImageListener = (e: ClipboardEvent) => { // This is a hack to allow pasting images into the uploader
const dataTransferItemList = e.clipboardData?.items; const handlePaste = async (e: ClipboardEvent) => {
if (!dataTransferItemList) return; if (!inputRef.current) {
const imageItems: Array<DataTransferItem> = [];
for (const item of dataTransferItemList) {
if (
item.kind === 'file' &&
['image/png', 'image/jpg'].includes(item.type)
) {
imageItems.push(item);
}
}
if (!imageItems.length) return;
e.stopImmediatePropagation();
if (imageItems.length > 1) {
toast({
description: t('toast.uploadFailedMultipleImagesDesc'),
status: 'error',
isClosable: true,
});
return; return;
} }
const file = imageItems[0].getAsFile(); if (e.clipboardData?.files) {
// Set the files on the inputRef
if (!file) { inputRef.current.files = e.clipboardData.files;
toast({ // Dispatch the change event, dropzone catches this and we get to use its own validation
description: t('toast.uploadFailedUnableToLoadDesc'), inputRef.current?.dispatchEvent(new Event('change', { bubbles: true }));
status: 'error',
isClosable: true,
});
return;
} }
dispatch(imageUploaded({ imageType: 'uploads', formData: { file } }));
}; };
document.addEventListener('paste', pasteImageListener);
// Set the open function so we can open the uploader from anywhere
setOpenUploaderFunction(open);
// Add the paste event listener
document.addEventListener('paste', handlePaste);
return () => { return () => {
document.removeEventListener('paste', pasteImageListener); document.removeEventListener('paste', handlePaste);
setOpenUploaderFunction(() => {
return;
});
}; };
}, [t, dispatch, toast, activeTabName]); }, [inputRef, open, setOpenUploaderFunction]);
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes( const overlaySecondaryText = useMemo(() => {
activeTabName if (['img2img', 'unifiedCanvas'].includes(activeTabName)) {
) return ` to ${String(t(`common.${activeTabName}` as ResourceKey))}`;
? ` to ${String(t(`common.${activeTabName}` as ResourceKey))}` }
: ``;
return '';
}, [t, activeTabName]);
return ( return (
<ImageUploaderTriggerContext.Provider value={open}> <Box
<Box {...getRootProps({ style: {} })}
{...getRootProps({ style: {} })} onKeyDown={(e: KeyboardEvent) => {
onKeyDown={(e: KeyboardEvent) => { // Bail out if user hits spacebar - do not open the uploader
// Bail out if user hits spacebar - do not open the uploader if (e.key === ' ') return;
if (e.key === ' ') return; }}
}} >
> <input {...getInputProps()} />
<input {...getInputProps()} /> {children}
{children} {isDragActive && isHandlingUpload && (
{isDragActive && isHandlingUpload && ( <ImageUploadOverlay
<ImageUploadOverlay isDragAccept={isDragAccept}
isDragAccept={isDragAccept} isDragReject={isDragReject}
isDragReject={isDragReject} overlaySecondaryText={overlaySecondaryText}
overlaySecondaryText={overlaySecondaryText} setIsHandlingUpload={setIsHandlingUpload}
setIsHandlingUpload={setIsHandlingUpload} />
/> )}
)} </Box>
</Box>
</ImageUploaderTriggerContext.Provider>
); );
}; };

View File

@ -1,6 +1,5 @@
import { Flex, Heading, Icon } from '@chakra-ui/react'; import { Flex, Heading, Icon } from '@chakra-ui/react';
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext'; import useImageUploader from 'common/hooks/useImageUploader';
import { useContext } from 'react';
import { FaUpload } from 'react-icons/fa'; import { FaUpload } from 'react-icons/fa';
type ImageUploaderButtonProps = { type ImageUploaderButtonProps = {
@ -9,11 +8,7 @@ type ImageUploaderButtonProps = {
const ImageUploaderButton = (props: ImageUploaderButtonProps) => { const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
const { styleClass } = props; const { styleClass } = props;
const open = useContext(ImageUploaderTriggerContext); const { openUploader } = useImageUploader();
const handleClickUpload = () => {
open && open();
};
return ( return (
<Flex <Flex
@ -26,7 +21,7 @@ const ImageUploaderButton = (props: ImageUploaderButtonProps) => {
className={styleClass} className={styleClass}
> >
<Flex <Flex
onClick={handleClickUpload} onClick={openUploader}
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',

View File

@ -1,19 +1,18 @@
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
import { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaUpload } from 'react-icons/fa'; import { FaUpload } from 'react-icons/fa';
import IAIIconButton from './IAIIconButton'; import IAIIconButton from './IAIIconButton';
import useImageUploader from 'common/hooks/useImageUploader';
const ImageUploaderIconButton = () => { const ImageUploaderIconButton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const openImageUploader = useContext(ImageUploaderTriggerContext); const { openUploader } = useImageUploader();
return ( return (
<IAIIconButton <IAIIconButton
aria-label={t('accessibility.uploadImage')} aria-label={t('accessibility.uploadImage')}
tooltip="Upload Image" tooltip="Upload Image"
icon={<FaUpload />} icon={<FaUpload />}
onClick={openImageUploader || undefined} onClick={openUploader}
/> />
); );
}; };

View File

@ -6,10 +6,12 @@ import { FaUndo, FaUpload } from 'react-icons/fa';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { clearInitialImage } from 'features/parameters/store/generationSlice'; import { clearInitialImage } from 'features/parameters/store/generationSlice';
import useImageUploader from 'common/hooks/useImageUploader';
const InitialImageButtons = () => { const InitialImageButtons = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { openUploader } = useImageUploader();
const handleResetInitialImage = useCallback(() => { const handleResetInitialImage = useCallback(() => {
dispatch(clearInitialImage()); dispatch(clearInitialImage());
@ -27,7 +29,11 @@ const InitialImageButtons = () => {
aria-label={t('accessibility.reset')} aria-label={t('accessibility.reset')}
onClick={handleResetInitialImage} onClick={handleResetInitialImage}
/> />
<IAIIconButton icon={<FaUpload />} aria-label={t('common.upload')} /> <IAIIconButton
icon={<FaUpload />}
onClick={openUploader}
aria-label={t('common.upload')}
/>
</ButtonGroup> </ButtonGroup>
</Flex> </Flex>
); );

View File

@ -24,7 +24,6 @@ const Loading = () => {
height="24px !important" height="24px !important"
right="1.5rem" right="1.5rem"
bottom="1.5rem" bottom="1.5rem"
speed="1.2s"
/> />
</Flex> </Flex>
); );

View File

@ -1,29 +0,0 @@
import { Flex, Heading, Text, VStack } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import WorkInProgress from './WorkInProgress';
export const PostProcessingWIP = () => {
const { t } = useTranslation();
return (
<WorkInProgress>
<Flex
sx={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
w: '100%',
h: '100%',
gap: 4,
textAlign: 'center',
}}
>
<Heading>{t('common.postProcessing')}</Heading>
<VStack maxW="50rem" gap={4}>
<Text>{t('common.postProcessDesc1')}</Text>
<Text>{t('common.postProcessDesc2')}</Text>
<Text>{t('common.postProcessDesc3')}</Text>
</VStack>
</Flex>
</WorkInProgress>
);
};

View File

@ -1,28 +0,0 @@
import { Flex, Heading, Text, VStack } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import WorkInProgress from './WorkInProgress';
export default function TrainingWIP() {
const { t } = useTranslation();
return (
<WorkInProgress>
<Flex
sx={{
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
w: '100%',
h: '100%',
gap: 4,
textAlign: 'center',
}}
>
<Heading>{t('common.training')}</Heading>
<VStack maxW="50rem" gap={4}>
<Text>{t('common.trainingDesc1')}</Text>
<Text>{t('common.trainingDesc2')}</Text>
</VStack>
</Flex>
</WorkInProgress>
);
}

View File

@ -1,26 +0,0 @@
import { Flex } from '@chakra-ui/react';
import { ReactNode } from 'react';
type WorkInProgressProps = {
children: ReactNode;
};
const WorkInProgress = (props: WorkInProgressProps) => {
const { children } = props;
return (
<Flex
sx={{
width: '100%',
height: '100%',
bg: 'base.850',
borderRadius: 'base',
position: 'relative',
}}
>
{children}
</Flex>
);
};
export default WorkInProgress;

View File

@ -1,35 +0,0 @@
import { RefObject, useEffect } from 'react';
const watchers: {
ref: RefObject<HTMLElement>;
enable: boolean;
callback: () => void;
}[] = [];
const useClickOutsideWatcher = () => {
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
watchers.forEach(({ ref, enable, callback }) => {
if (enable && ref.current && !ref.current.contains(e.target as Node)) {
callback();
}
});
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return {
addWatcher: (watcher: {
ref: RefObject<HTMLElement>;
callback: () => void;
enable: boolean;
}) => {
watchers.push(watcher);
},
};
};
export default useClickOutsideWatcher;

View File

@ -1,13 +1,22 @@
let openFunction: () => void; import { useCallback } from 'react';
let openUploader = () => {
return;
};
const useImageUploader = () => { const useImageUploader = () => {
return { const setOpenUploaderFunction = useCallback(
setOpenUploader: (open?: () => void) => { (openUploaderFunction?: () => void) => {
if (open) { if (openUploaderFunction) {
openFunction = open; openUploader = openUploaderFunction;
} }
}, },
openUploader: openFunction, []
);
return {
setOpenUploaderFunction,
openUploader,
}; };
}; };

View File

@ -1,17 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function useUpdateTranslations(fn: () => void) {
const { i18n } = useTranslation();
const currentLang = localStorage.getItem('i18nextLng');
React.useEffect(() => {
fn();
}, [fn]);
React.useEffect(() => {
i18n.on('languageChanged', () => {
fn();
});
}, [fn, i18n, currentLang]);
}

View File

@ -1,20 +0,0 @@
import { createIcon } from '@chakra-ui/react';
const ImageToImageIcon = createIcon({
displayName: 'ImageToImageIcon',
viewBox: '0 0 3543 3543',
path: (
<g transform="matrix(1.10943,0,0,1.10943,-206.981,-213.533)">
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M688.533,2405.95L542.987,2405.95C349.532,2405.95 192.47,2248.89 192.47,2055.44L192.47,542.987C192.47,349.532 349.532,192.47 542.987,192.47L2527.88,192.47C2721.33,192.47 2878.4,349.532 2878.4,542.987L2878.4,1172.79L3023.94,1172.79C3217.4,1172.79 3374.46,1329.85 3374.46,1523.3C3374.46,1523.3 3374.46,3035.75 3374.46,3035.75C3374.46,3229.21 3217.4,3386.27 3023.94,3386.27L1039.05,3386.27C845.595,3386.27 688.533,3229.21 688.533,3035.75L688.533,2405.95ZM3286.96,2634.37L3286.96,1523.3C3286.96,1378.14 3169.11,1260.29 3023.94,1260.29C3023.94,1260.29 1039.05,1260.29 1039.05,1260.29C893.887,1260.29 776.033,1378.14 776.033,1523.3L776.033,2489.79L1440.94,1736.22L2385.83,2775.59L2880.71,2200.41L3286.96,2634.37ZM2622.05,1405.51C2778.5,1405.51 2905.51,1532.53 2905.51,1688.98C2905.51,1845.42 2778.5,1972.44 2622.05,1972.44C2465.6,1972.44 2338.58,1845.42 2338.58,1688.98C2338.58,1532.53 2465.6,1405.51 2622.05,1405.51ZM2790.9,1172.79L1323.86,1172.79L944.882,755.906L279.97,1509.47L279.97,542.987C279.97,397.824 397.824,279.97 542.987,279.97C542.987,279.97 2527.88,279.97 2527.88,279.97C2673.04,279.97 2790.9,397.824 2790.9,542.987L2790.9,1172.79ZM2125.98,425.197C2282.43,425.197 2409.45,552.213 2409.45,708.661C2409.45,865.11 2282.43,992.126 2125.98,992.126C1969.54,992.126 1842.52,865.11 1842.52,708.661C1842.52,552.213 1969.54,425.197 2125.98,425.197Z"
/>
</g>
),
defaultProps: {
boxSize: '24px',
},
});
export default ImageToImageIcon;

File diff suppressed because one or more lines are too long

View File

@ -1,19 +0,0 @@
import { createIcon } from '@chakra-ui/react';
const NodesIcon = createIcon({
displayName: 'NodesIcon',
viewBox: '0 0 3543 3543',
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M3543.31,770.787C3543.31,515.578 3336.11,308.38 3080.9,308.38L462.407,308.38C207.197,308.38 0,515.578 0,770.787L0,2766.03C0,3021.24 207.197,3228.44 462.407,3228.44L3080.9,3228.44C3336.11,3228.44 3543.31,3021.24 3543.31,2766.03C3543.31,2766.03 3543.31,770.787 3543.31,770.787ZM3427.88,770.787L3427.88,2766.03C3427.88,2957.53 3272.4,3113.01 3080.9,3113.01C3080.9,3113.01 462.407,3113.01 462.407,3113.01C270.906,3113.01 115.431,2957.53 115.431,2766.03L115.431,770.787C115.431,579.286 270.906,423.812 462.407,423.812L3080.9,423.812C3272.4,423.812 3427.88,579.286 3427.88,770.787ZM1214.23,1130.69L1321.47,1130.69C1324.01,1130.69 1326.54,1130.53 1329.05,1130.2C1329.05,1130.2 1367.3,1125.33 1397.94,1149.8C1421.63,1168.72 1437.33,1204.3 1437.33,1265.48L1437.33,2078.74L1220.99,2078.74C1146.83,2078.74 1086.61,2138.95 1086.61,2213.12L1086.61,2762.46C1086.61,2836.63 1146.83,2896.84 1220.99,2896.84L1770.34,2896.84C1844.5,2896.84 1904.71,2836.63 1904.71,2762.46L1904.71,2213.12C1904.71,2138.95 1844.5,2078.74 1770.34,2078.74L1554,2078.74L1554,1604.84C1625.84,1658.19 1703.39,1658.1 1703.39,1658.1C1703.54,1658.1 1703.69,1658.11 1703.84,1658.11L2362.2,1658.11L2362.2,1874.44C2362.2,1948.61 2422.42,2008.82 2496.58,2008.82L3045.93,2008.82C3120.09,2008.82 3180.3,1948.61 3180.3,1874.44L3180.3,1325.1C3180.3,1250.93 3120.09,1190.72 3045.93,1190.72L2496.58,1190.72C2422.42,1190.72 2362.2,1250.93 2362.2,1325.1L2362.2,1558.97L2362.2,1541.44L1704.23,1541.44C1702.2,1541.37 1650.96,1539.37 1609.51,1499.26C1577.72,1468.49 1554,1416.47 1554,1331.69L1554,1265.48C1554,1153.86 1513.98,1093.17 1470.76,1058.64C1411.24,1011.1 1338.98,1012.58 1319.15,1014.03L1214.23,1014.03L1214.23,796.992C1214.23,722.828 1154.02,662.617 1079.85,662.617L530.507,662.617C456.343,662.617 396.131,722.828 396.131,796.992L396.131,1346.34C396.131,1420.5 456.343,1480.71 530.507,1480.71L1079.85,1480.71C1154.02,1480.71 1214.23,1420.5 1214.23,1346.34L1214.23,1130.69Z"
/>
),
defaultProps: {
boxSize: '24px',
},
});
export default NodesIcon;

File diff suppressed because one or more lines are too long

View File

@ -1,19 +0,0 @@
import { createIcon } from '@chakra-ui/react';
const PostprocessingIcon = createIcon({
displayName: 'PostprocessingIcon',
viewBox: '0 0 3543 3543',
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M709.477,1596.53L992.591,1275.66L2239.09,2646.81L2891.95,1888.03L3427.88,2460.51L3427.88,994.78C3427.88,954.66 3421.05,916.122 3408.5,880.254L3521.9,855.419C3535.8,899.386 3543.31,946.214 3543.31,994.78L3543.31,2990.02C3543.31,3245.23 3336.11,3452.43 3080.9,3452.43C3080.9,3452.43 462.407,3452.43 462.407,3452.43C207.197,3452.43 -0,3245.23 -0,2990.02L-0,994.78C-0,739.571 207.197,532.373 462.407,532.373L505.419,532.373L504.644,532.546L807.104,600.085C820.223,601.729 832.422,607.722 841.77,617.116C850.131,625.517 855.784,636.21 858.055,647.804L462.407,647.804C270.906,647.804 115.431,803.279 115.431,994.78L115.431,2075.73L-0,2101.5L115.431,2127.28L115.431,2269.78L220.47,2150.73L482.345,2209.21C503.267,2211.83 522.722,2221.39 537.63,2236.37C552.538,2251.35 562.049,2270.9 564.657,2291.93L671.84,2776.17L779.022,2291.93C781.631,2270.9 791.141,2251.35 806.05,2236.37C820.958,2221.39 840.413,2211.83 861.334,2209.21L1353.15,2101.5L861.334,1993.8C840.413,1991.18 820.958,1981.62 806.05,1966.64C791.141,1951.66 781.631,1932.11 779.022,1911.08L709.477,1596.53ZM671.84,1573.09L725.556,2006.07C726.863,2016.61 731.63,2026.4 739.101,2033.91C746.573,2041.42 756.323,2046.21 766.808,2047.53L1197.68,2101.5L766.808,2155.48C756.323,2156.8 746.573,2161.59 739.101,2169.09C731.63,2176.6 726.863,2186.4 725.556,2196.94L671.84,2629.92L618.124,2196.94C616.817,2186.4 612.05,2176.6 604.579,2169.09C597.107,2161.59 587.357,2156.8 576.872,2155.48L146.001,2101.5L576.872,2047.53C587.357,2046.21 597.107,2041.42 604.579,2033.91C612.05,2026.4 616.817,2016.61 618.124,2006.07L671.84,1573.09ZM609.035,1710.36L564.657,1911.08C562.049,1932.11 552.538,1951.66 537.63,1966.64C522.722,1981.62 503.267,1991.18 482.345,1993.8L328.665,2028.11L609.035,1710.36ZM2297.12,938.615L2451.12,973.003C2480.59,976.695 2507.99,990.158 2528.99,1011.26C2549.99,1032.37 2563.39,1059.9 2567.07,1089.52L2672.73,1566.9C2634.5,1580.11 2593.44,1587.29 2550.72,1587.29C2344.33,1587.29 2176.77,1419.73 2176.77,1213.34C2176.77,1104.78 2223.13,1006.96 2297.12,938.615ZM2718.05,76.925L2793.72,686.847C2795.56,701.69 2802.27,715.491 2812.8,726.068C2823.32,736.644 2837.06,743.391 2851.83,745.242L3458.78,821.28L2851.83,897.318C2837.06,899.168 2823.32,905.916 2812.8,916.492C2802.27,927.068 2795.56,940.87 2793.72,955.712L2718.05,1565.63L2642.38,955.712C2640.54,940.87 2633.83,927.068 2623.3,916.492C2612.78,905.916 2599.04,899.168 2584.27,897.318L1977.32,821.28L2584.27,745.242C2599.04,743.391 2612.78,736.644 2623.3,726.068C2633.83,715.491 2640.54,701.69 2642.38,686.847L2718.05,76.925ZM2883.68,1043.06C2909.88,1094.13 2924.67,1152.02 2924.67,1213.34C2924.67,1335.4 2866.06,1443.88 2775.49,1512.14L2869.03,1089.52C2871.07,1073.15 2876.07,1057.42 2883.68,1043.06ZM925.928,201.2L959.611,472.704C960.431,479.311 963.42,485.455 968.105,490.163C972.79,494.871 978.904,497.875 985.479,498.698L1255.66,532.546L985.479,566.395C978.904,567.218 972.79,570.222 968.105,574.93C963.42,579.638 960.431,585.781 959.611,592.388L925.928,863.893L892.245,592.388C891.425,585.781 888.436,579.638 883.751,574.93C879.066,570.222 872.952,567.218 866.378,566.395L596.195,532.546L866.378,498.698C872.952,497.875 879.066,494.871 883.751,490.163C888.436,485.455 891.425,479.311 892.245,472.704L925.928,201.2ZM2864.47,532.373L3080.9,532.373C3258.7,532.373 3413.2,632.945 3490.58,780.281L3319.31,742.773C3257.14,683.925 3173.2,647.804 3080.9,647.804L2927.07,647.804C2919.95,642.994 2913.25,637.473 2907.11,631.298C2886.11,610.194 2872.71,582.655 2869.03,553.04L2864.47,532.373ZM1352.36,532.373L2571.64,532.373L2567.07,553.04C2563.39,582.655 2549.99,610.194 2528.99,631.298C2522.85,637.473 2516.16,642.994 2509.03,647.804L993.801,647.804C996.072,636.21 1001.73,625.517 1010.09,617.116C1019.43,607.722 1031.63,601.729 1044.75,600.085L1353.15,532.546L1352.36,532.373Z"
/>
),
defaultProps: {
boxSize: '24px',
},
});
export default PostprocessingIcon;

File diff suppressed because one or more lines are too long

View File

@ -1,19 +0,0 @@
import { createIcon } from '@chakra-ui/react';
const TrainingIcon = createIcon({
displayName: 'TrainingIcon',
viewBox: '0 0 3544 3544',
path: (
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M0,768.593L0,2774.71C0,2930.6 78.519,3068.3 198.135,3150.37C273.059,3202.68 364.177,3233.38 462.407,3233.38C462.407,3233.38 3080.9,3233.38 3080.9,3233.38C3179.13,3233.38 3270.25,3202.68 3345.17,3150.37C3464.79,3068.3 3543.31,2930.6 3543.31,2774.71L3543.31,768.593C3543.31,517.323 3339.31,313.324 3088.04,313.324L455.269,313.324C203.999,313.324 0,517.323 0,768.593ZM3427.88,775.73L3427.88,2770.97C3427.88,2962.47 3272.4,3117.95 3080.9,3117.95L462.407,3117.95C270.906,3117.95 115.431,2962.47 115.431,2770.97C115.431,2770.97 115.431,775.73 115.431,775.73C115.431,584.229 270.906,428.755 462.407,428.755C462.407,428.755 3080.9,428.755 3080.9,428.755C3272.4,428.755 3427.88,584.229 3427.88,775.73ZM796.24,1322.76L796.24,1250.45C796.24,1199.03 836.16,1157.27 885.331,1157.27C885.331,1157.27 946.847,1157.27 946.847,1157.27C996.017,1157.27 1035.94,1199.03 1035.94,1250.45L1035.94,1644.81L2507.37,1644.81L2507.37,1250.45C2507.37,1199.03 2547.29,1157.27 2596.46,1157.27C2596.46,1157.27 2657.98,1157.27 2657.98,1157.27C2707.15,1157.27 2747.07,1199.03 2747.07,1250.45L2747.07,1322.76C2756.66,1319.22 2767.02,1317.29 2777.83,1317.29C2777.83,1317.29 2839.34,1317.29 2839.34,1317.29C2888.51,1317.29 2928.43,1357.21 2928.43,1406.38L2928.43,1527.32C2933.51,1526.26 2938.77,1525.71 2944.16,1525.71L2995.3,1525.71C3036.18,1525.71 3069.37,1557.59 3069.37,1596.86C3069.37,1596.86 3069.37,1946.44 3069.37,1946.44C3069.37,1985.72 3036.18,2017.6 2995.3,2017.6C2995.3,2017.6 2944.16,2017.6 2944.16,2017.6C2938.77,2017.6 2933.51,2017.04 2928.43,2015.99L2928.43,2136.92C2928.43,2186.09 2888.51,2226.01 2839.34,2226.01L2777.83,2226.01C2767.02,2226.01 2756.66,2224.08 2747.07,2220.55L2747.07,2292.85C2747.07,2344.28 2707.15,2386.03 2657.98,2386.03C2657.98,2386.03 2596.46,2386.03 2596.46,2386.03C2547.29,2386.03 2507.37,2344.28 2507.37,2292.85L2507.37,1898.5L1035.94,1898.5L1035.94,2292.85C1035.94,2344.28 996.017,2386.03 946.847,2386.03C946.847,2386.03 885.331,2386.03 885.331,2386.03C836.16,2386.03 796.24,2344.28 796.24,2292.85L796.24,2220.55C786.651,2224.08 776.29,2226.01 765.482,2226.01L703.967,2226.01C654.796,2226.01 614.876,2186.09 614.876,2136.92L614.876,2015.99C609.801,2017.04 604.539,2017.6 599.144,2017.6C599.144,2017.6 548.003,2017.6 548.003,2017.6C507.125,2017.6 473.937,1985.72 473.937,1946.44C473.937,1946.44 473.937,1596.86 473.937,1596.86C473.937,1557.59 507.125,1525.71 548.003,1525.71L599.144,1525.71C604.539,1525.71 609.801,1526.26 614.876,1527.32L614.876,1406.38C614.876,1357.21 654.796,1317.29 703.967,1317.29C703.967,1317.29 765.482,1317.29 765.482,1317.29C776.29,1317.29 786.651,1319.22 796.24,1322.76ZM977.604,1250.45C977.604,1232.7 963.822,1218.29 946.847,1218.29L885.331,1218.29C868.355,1218.29 854.573,1232.7 854.573,1250.45L854.573,2292.85C854.573,2310.61 868.355,2325.02 885.331,2325.02L946.847,2325.02C963.822,2325.02 977.604,2310.61 977.604,2292.85L977.604,1250.45ZM2565.7,1250.45C2565.7,1232.7 2579.49,1218.29 2596.46,1218.29L2657.98,1218.29C2674.95,1218.29 2688.73,1232.7 2688.73,1250.45L2688.73,2292.85C2688.73,2310.61 2674.95,2325.02 2657.98,2325.02L2596.46,2325.02C2579.49,2325.02 2565.7,2310.61 2565.7,2292.85L2565.7,1250.45ZM673.209,1406.38L673.209,2136.92C673.209,2153.9 686.991,2167.68 703.967,2167.68L765.482,2167.68C782.458,2167.68 796.24,2153.9 796.24,2136.92L796.24,1406.38C796.24,1389.41 782.458,1375.63 765.482,1375.63L703.967,1375.63C686.991,1375.63 673.209,1389.41 673.209,1406.38ZM2870.1,1406.38L2870.1,2136.92C2870.1,2153.9 2856.32,2167.68 2839.34,2167.68L2777.83,2167.68C2760.85,2167.68 2747.07,2153.9 2747.07,2136.92L2747.07,1406.38C2747.07,1389.41 2760.85,1375.63 2777.83,1375.63L2839.34,1375.63C2856.32,1375.63 2870.1,1389.41 2870.1,1406.38ZM614.876,1577.5C610.535,1574.24 605.074,1572.3 599.144,1572.3L548.003,1572.3C533.89,1572.3 522.433,1583.3 522.433,1596.86L522.433,1946.44C522.433,1960 533.89,1971.01 548.003,1971.01L599.144,1971.01C605.074,1971.01 610.535,1969.07 614.876,1965.81L614.876,1577.5ZM2928.43,1965.81L2928.43,1577.5C2932.77,1574.24 2938.23,1572.3 2944.16,1572.3L2995.3,1572.3C3009.42,1572.3 3020.87,1583.3 3020.87,1596.86L3020.87,1946.44C3020.87,1960 3009.42,1971.01 2995.3,1971.01L2944.16,1971.01C2938.23,1971.01 2932.77,1969.07 2928.43,1965.81ZM2507.37,1703.14L1035.94,1703.14L1035.94,1840.16L2507.37,1840.16L2507.37,1898.38L2507.37,1659.46L2507.37,1703.14Z"
/>
),
defaultProps: {
boxSize: '24px',
},
});
export default TrainingIcon;

File diff suppressed because one or more lines are too long

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 3543 3543" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.10943,0,0,1.10943,-206.981,-213.533)">
<path d="M688.533,2405.95L542.987,2405.95C349.532,2405.95 192.47,2248.89 192.47,2055.44L192.47,542.987C192.47,349.532 349.532,192.47 542.987,192.47L2527.88,192.47C2721.33,192.47 2878.4,349.532 2878.4,542.987L2878.4,1172.79L3023.94,1172.79C3217.4,1172.79 3374.46,1329.85 3374.46,1523.3C3374.46,1523.3 3374.46,3035.75 3374.46,3035.75C3374.46,3229.21 3217.4,3386.27 3023.94,3386.27L1039.05,3386.27C845.595,3386.27 688.533,3229.21 688.533,3035.75L688.533,2405.95ZM3286.96,2634.37L3286.96,1523.3C3286.96,1378.14 3169.11,1260.29 3023.94,1260.29C3023.94,1260.29 1039.05,1260.29 1039.05,1260.29C893.887,1260.29 776.033,1378.14 776.033,1523.3L776.033,2489.79L1440.94,1736.22L2385.83,2775.59L2880.71,2200.41L3286.96,2634.37ZM2622.05,1405.51C2778.5,1405.51 2905.51,1532.53 2905.51,1688.98C2905.51,1845.42 2778.5,1972.44 2622.05,1972.44C2465.6,1972.44 2338.58,1845.42 2338.58,1688.98C2338.58,1532.53 2465.6,1405.51 2622.05,1405.51ZM2790.9,1172.79L1323.86,1172.79L944.882,755.906L279.97,1509.47L279.97,542.987C279.97,397.824 397.824,279.97 542.987,279.97C542.987,279.97 2527.88,279.97 2527.88,279.97C2673.04,279.97 2790.9,397.824 2790.9,542.987L2790.9,1172.79ZM2125.98,425.197C2282.43,425.197 2409.45,552.213 2409.45,708.661C2409.45,865.11 2282.43,992.126 2125.98,992.126C1969.54,992.126 1842.52,865.11 1842.52,708.661C1842.52,552.213 1969.54,425.197 2125.98,425.197Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 3543 3543" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M3543.31,770.787C3543.31,515.578 3336.11,308.38 3080.9,308.38L462.407,308.38C207.197,308.38 0,515.578 0,770.787L0,2766.03C0,3021.24 207.197,3228.44 462.407,3228.44L3080.9,3228.44C3336.11,3228.44 3543.31,3021.24 3543.31,2766.03C3543.31,2766.03 3543.31,770.787 3543.31,770.787ZM3427.88,770.787L3427.88,2766.03C3427.88,2957.53 3272.4,3113.01 3080.9,3113.01C3080.9,3113.01 462.407,3113.01 462.407,3113.01C270.906,3113.01 115.431,2957.53 115.431,2766.03L115.431,770.787C115.431,579.286 270.906,423.812 462.407,423.812L3080.9,423.812C3272.4,423.812 3427.88,579.286 3427.88,770.787ZM1214.23,1130.69L1321.47,1130.69C1324.01,1130.69 1326.54,1130.53 1329.05,1130.2C1329.05,1130.2 1367.3,1125.33 1397.94,1149.8C1421.63,1168.72 1437.33,1204.3 1437.33,1265.48L1437.33,2078.74L1220.99,2078.74C1146.83,2078.74 1086.61,2138.95 1086.61,2213.12L1086.61,2762.46C1086.61,2836.63 1146.83,2896.84 1220.99,2896.84L1770.34,2896.84C1844.5,2896.84 1904.71,2836.63 1904.71,2762.46L1904.71,2213.12C1904.71,2138.95 1844.5,2078.74 1770.34,2078.74L1554,2078.74L1554,1604.84C1625.84,1658.19 1703.39,1658.1 1703.39,1658.1C1703.54,1658.1 1703.69,1658.11 1703.84,1658.11L2362.2,1658.11L2362.2,1874.44C2362.2,1948.61 2422.42,2008.82 2496.58,2008.82L3045.93,2008.82C3120.09,2008.82 3180.3,1948.61 3180.3,1874.44L3180.3,1325.1C3180.3,1250.93 3120.09,1190.72 3045.93,1190.72L2496.58,1190.72C2422.42,1190.72 2362.2,1250.93 2362.2,1325.1L2362.2,1558.97L2362.2,1541.44L1704.23,1541.44C1702.2,1541.37 1650.96,1539.37 1609.51,1499.26C1577.72,1468.49 1554,1416.47 1554,1331.69L1554,1265.48C1554,1153.86 1513.98,1093.17 1470.76,1058.64C1411.24,1011.1 1338.98,1012.58 1319.15,1014.03L1214.23,1014.03L1214.23,796.992C1214.23,722.828 1154.02,662.617 1079.85,662.617L530.507,662.617C456.343,662.617 396.131,722.828 396.131,796.992L396.131,1346.34C396.131,1420.5 456.343,1480.71 530.507,1480.71L1079.85,1480.71C1154.02,1480.71 1214.23,1420.5 1214.23,1346.34L1214.23,1130.69Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 3543 3543" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<path d="M709.477,1596.53L992.591,1275.66L2239.09,2646.81L2891.95,1888.03L3427.88,2460.51L3427.88,994.78C3427.88,954.66 3421.05,916.122 3408.5,880.254L3521.9,855.419C3535.8,899.386 3543.31,946.214 3543.31,994.78L3543.31,2990.02C3543.31,3245.23 3336.11,3452.43 3080.9,3452.43C3080.9,3452.43 462.407,3452.43 462.407,3452.43C207.197,3452.43 -0,3245.23 -0,2990.02L-0,994.78C-0,739.571 207.197,532.373 462.407,532.373L505.419,532.373L504.644,532.546L807.104,600.085C820.223,601.729 832.422,607.722 841.77,617.116C850.131,625.517 855.784,636.21 858.055,647.804L462.407,647.804C270.906,647.804 115.431,803.279 115.431,994.78L115.431,2075.73L-0,2101.5L115.431,2127.28L115.431,2269.78L220.47,2150.73L482.345,2209.21C503.267,2211.83 522.722,2221.39 537.63,2236.37C552.538,2251.35 562.049,2270.9 564.657,2291.93L671.84,2776.17L779.022,2291.93C781.631,2270.9 791.141,2251.35 806.05,2236.37C820.958,2221.39 840.413,2211.83 861.334,2209.21L1353.15,2101.5L861.334,1993.8C840.413,1991.18 820.958,1981.62 806.05,1966.64C791.141,1951.66 781.631,1932.11 779.022,1911.08L709.477,1596.53ZM671.84,1573.09L725.556,2006.07C726.863,2016.61 731.63,2026.4 739.101,2033.91C746.573,2041.42 756.323,2046.21 766.808,2047.53L1197.68,2101.5L766.808,2155.48C756.323,2156.8 746.573,2161.59 739.101,2169.09C731.63,2176.6 726.863,2186.4 725.556,2196.94L671.84,2629.92L618.124,2196.94C616.817,2186.4 612.05,2176.6 604.579,2169.09C597.107,2161.59 587.357,2156.8 576.872,2155.48L146.001,2101.5L576.872,2047.53C587.357,2046.21 597.107,2041.42 604.579,2033.91C612.05,2026.4 616.817,2016.61 618.124,2006.07L671.84,1573.09ZM609.035,1710.36L564.657,1911.08C562.049,1932.11 552.538,1951.66 537.63,1966.64C522.722,1981.62 503.267,1991.18 482.345,1993.8L328.665,2028.11L609.035,1710.36ZM2297.12,938.615L2451.12,973.003C2480.59,976.695 2507.99,990.158 2528.99,1011.26C2549.99,1032.37 2563.39,1059.9 2567.07,1089.52L2672.73,1566.9C2634.5,1580.11 2593.44,1587.29 2550.72,1587.29C2344.33,1587.29 2176.77,1419.73 2176.77,1213.34C2176.77,1104.78 2223.13,1006.96 2297.12,938.615ZM2718.05,76.925L2793.72,686.847C2795.56,701.69 2802.27,715.491 2812.8,726.068C2823.32,736.644 2837.06,743.391 2851.83,745.242L3458.78,821.28L2851.83,897.318C2837.06,899.168 2823.32,905.916 2812.8,916.492C2802.27,927.068 2795.56,940.87 2793.72,955.712L2718.05,1565.63L2642.38,955.712C2640.54,940.87 2633.83,927.068 2623.3,916.492C2612.78,905.916 2599.04,899.168 2584.27,897.318L1977.32,821.28L2584.27,745.242C2599.04,743.391 2612.78,736.644 2623.3,726.068C2633.83,715.491 2640.54,701.69 2642.38,686.847L2718.05,76.925ZM2883.68,1043.06C2909.88,1094.13 2924.67,1152.02 2924.67,1213.34C2924.67,1335.4 2866.06,1443.88 2775.49,1512.14L2869.03,1089.52C2871.07,1073.15 2876.07,1057.42 2883.68,1043.06ZM925.928,201.2L959.611,472.704C960.431,479.311 963.42,485.455 968.105,490.163C972.79,494.871 978.904,497.875 985.479,498.698L1255.66,532.546L985.479,566.395C978.904,567.218 972.79,570.222 968.105,574.93C963.42,579.638 960.431,585.781 959.611,592.388L925.928,863.893L892.245,592.388C891.425,585.781 888.436,579.638 883.751,574.93C879.066,570.222 872.952,567.218 866.378,566.395L596.195,532.546L866.378,498.698C872.952,497.875 879.066,494.871 883.751,490.163C888.436,485.455 891.425,479.311 892.245,472.704L925.928,201.2ZM2864.47,532.373L3080.9,532.373C3258.7,532.373 3413.2,632.945 3490.58,780.281L3319.31,742.773C3257.14,683.925 3173.2,647.804 3080.9,647.804L2927.07,647.804C2919.95,642.994 2913.25,637.473 2907.11,631.298C2886.11,610.194 2872.71,582.655 2869.03,553.04L2864.47,532.373ZM1352.36,532.373L2571.64,532.373L2567.07,553.04C2563.39,582.655 2549.99,610.194 2528.99,631.298C2522.85,637.473 2516.16,642.994 2509.03,647.804L993.801,647.804C996.072,636.21 1001.73,625.517 1010.09,617.116C1019.43,607.722 1031.63,601.729 1044.75,600.085L1353.15,532.546L1352.36,532.373Z" style="stroke:white;stroke-opacity:0;stroke-width:1px;"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 3544 3544" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M0,768.593L0,2774.71C0,2930.6 78.519,3068.3 198.135,3150.37C273.059,3202.68 364.177,3233.38 462.407,3233.38C462.407,3233.38 3080.9,3233.38 3080.9,3233.38C3179.13,3233.38 3270.25,3202.68 3345.17,3150.37C3464.79,3068.3 3543.31,2930.6 3543.31,2774.71L3543.31,768.593C3543.31,517.323 3339.31,313.324 3088.04,313.324L455.269,313.324C203.999,313.324 0,517.323 0,768.593ZM3427.88,775.73L3427.88,2770.97C3427.88,2962.47 3272.4,3117.95 3080.9,3117.95L462.407,3117.95C270.906,3117.95 115.431,2962.47 115.431,2770.97C115.431,2770.97 115.431,775.73 115.431,775.73C115.431,584.229 270.906,428.755 462.407,428.755C462.407,428.755 3080.9,428.755 3080.9,428.755C3272.4,428.755 3427.88,584.229 3427.88,775.73ZM796.24,1322.76L796.24,1250.45C796.24,1199.03 836.16,1157.27 885.331,1157.27C885.331,1157.27 946.847,1157.27 946.847,1157.27C996.017,1157.27 1035.94,1199.03 1035.94,1250.45L1035.94,1644.81L2507.37,1644.81L2507.37,1250.45C2507.37,1199.03 2547.29,1157.27 2596.46,1157.27C2596.46,1157.27 2657.98,1157.27 2657.98,1157.27C2707.15,1157.27 2747.07,1199.03 2747.07,1250.45L2747.07,1322.76C2756.66,1319.22 2767.02,1317.29 2777.83,1317.29C2777.83,1317.29 2839.34,1317.29 2839.34,1317.29C2888.51,1317.29 2928.43,1357.21 2928.43,1406.38L2928.43,1527.32C2933.51,1526.26 2938.77,1525.71 2944.16,1525.71L2995.3,1525.71C3036.18,1525.71 3069.37,1557.59 3069.37,1596.86C3069.37,1596.86 3069.37,1946.44 3069.37,1946.44C3069.37,1985.72 3036.18,2017.6 2995.3,2017.6C2995.3,2017.6 2944.16,2017.6 2944.16,2017.6C2938.77,2017.6 2933.51,2017.04 2928.43,2015.99L2928.43,2136.92C2928.43,2186.09 2888.51,2226.01 2839.34,2226.01L2777.83,2226.01C2767.02,2226.01 2756.66,2224.08 2747.07,2220.55L2747.07,2292.85C2747.07,2344.28 2707.15,2386.03 2657.98,2386.03C2657.98,2386.03 2596.46,2386.03 2596.46,2386.03C2547.29,2386.03 2507.37,2344.28 2507.37,2292.85L2507.37,1898.5L1035.94,1898.5L1035.94,2292.85C1035.94,2344.28 996.017,2386.03 946.847,2386.03C946.847,2386.03 885.331,2386.03 885.331,2386.03C836.16,2386.03 796.24,2344.28 796.24,2292.85L796.24,2220.55C786.651,2224.08 776.29,2226.01 765.482,2226.01L703.967,2226.01C654.796,2226.01 614.876,2186.09 614.876,2136.92L614.876,2015.99C609.801,2017.04 604.539,2017.6 599.144,2017.6C599.144,2017.6 548.003,2017.6 548.003,2017.6C507.125,2017.6 473.937,1985.72 473.937,1946.44C473.937,1946.44 473.937,1596.86 473.937,1596.86C473.937,1557.59 507.125,1525.71 548.003,1525.71L599.144,1525.71C604.539,1525.71 609.801,1526.26 614.876,1527.32L614.876,1406.38C614.876,1357.21 654.796,1317.29 703.967,1317.29C703.967,1317.29 765.482,1317.29 765.482,1317.29C776.29,1317.29 786.651,1319.22 796.24,1322.76ZM977.604,1250.45C977.604,1232.7 963.822,1218.29 946.847,1218.29L885.331,1218.29C868.355,1218.29 854.573,1232.7 854.573,1250.45L854.573,2292.85C854.573,2310.61 868.355,2325.02 885.331,2325.02L946.847,2325.02C963.822,2325.02 977.604,2310.61 977.604,2292.85L977.604,1250.45ZM2565.7,1250.45C2565.7,1232.7 2579.49,1218.29 2596.46,1218.29L2657.98,1218.29C2674.95,1218.29 2688.73,1232.7 2688.73,1250.45L2688.73,2292.85C2688.73,2310.61 2674.95,2325.02 2657.98,2325.02L2596.46,2325.02C2579.49,2325.02 2565.7,2310.61 2565.7,2292.85L2565.7,1250.45ZM673.209,1406.38L673.209,2136.92C673.209,2153.9 686.991,2167.68 703.967,2167.68L765.482,2167.68C782.458,2167.68 796.24,2153.9 796.24,2136.92L796.24,1406.38C796.24,1389.41 782.458,1375.63 765.482,1375.63L703.967,1375.63C686.991,1375.63 673.209,1389.41 673.209,1406.38ZM2870.1,1406.38L2870.1,2136.92C2870.1,2153.9 2856.32,2167.68 2839.34,2167.68L2777.83,2167.68C2760.85,2167.68 2747.07,2153.9 2747.07,2136.92L2747.07,1406.38C2747.07,1389.41 2760.85,1375.63 2777.83,1375.63L2839.34,1375.63C2856.32,1375.63 2870.1,1389.41 2870.1,1406.38ZM614.876,1577.5C610.535,1574.24 605.074,1572.3 599.144,1572.3L548.003,1572.3C533.89,1572.3 522.433,1583.3 522.433,1596.86L522.433,1946.44C522.433,1960 533.89,1971.01 548.003,1971.01L599.144,1971.01C605.074,1971.01 610.535,1969.07 614.876,1965.81L614.876,1577.5ZM2928.43,1965.81L2928.43,1577.5C2932.77,1574.24 2938.23,1572.3 2944.16,1572.3L2995.3,1572.3C3009.42,1572.3 3020.87,1583.3 3020.87,1596.86L3020.87,1946.44C3020.87,1960 3009.42,1971.01 2995.3,1971.01L2944.16,1971.01C2938.23,1971.01 2932.77,1969.07 2928.43,1965.81ZM2507.37,1703.14L1035.94,1703.14L1035.94,1840.16L2507.37,1840.16L2507.37,1898.38L2507.37,1659.46L2507.37,1703.14Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -1,348 +0,0 @@
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants';
import { Dimensions } from 'features/canvas/store/canvasTypes';
import { GenerationState } from 'features/parameters/store/generationSlice';
import { SystemState } from 'features/system/store/systemSlice';
import { Vector2d } from 'konva/lib/types';
import {
CanvasState,
isCanvasMaskLine,
} from 'features/canvas/store/canvasTypes';
import generateMask from 'features/canvas/util/generateMask';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import type {
FacetoolType,
UpscalingLevel,
} from 'features/parameters/store/postprocessingSlice';
import { PostprocessingState } from 'features/parameters/store/postprocessingSlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
import openBase64ImageInTab from './openBase64ImageInTab';
import randomInt from './randomInt';
import { stringToSeedWeightsArray } from './seedWeightPairs';
import { getIsImageDataTransparent, getIsImageDataWhite } from './arrayBuffer';
export type FrontendToBackendParametersConfig = {
generationMode: InvokeTabName;
generationState: GenerationState;
postprocessingState: PostprocessingState;
canvasState: CanvasState;
systemState: SystemState;
imageToProcessUrl?: string;
};
export type BackendGenerationParameters = {
prompt: string;
iterations: number;
steps: number;
cfg_scale: number;
threshold: number;
perlin: number;
height: number;
width: number;
sampler_name: string;
seed: number;
progress_images: boolean;
progress_latents: boolean;
save_intermediates: number;
generation_mode: InvokeTabName;
init_mask: string;
init_img?: string;
fit?: boolean;
seam_size?: number;
seam_blur?: number;
seam_strength?: number;
seam_steps?: number;
tile_size?: number;
infill_method?: string;
force_outpaint?: boolean;
seamless?: boolean;
hires_fix?: boolean;
strength?: number;
invert_mask?: boolean;
inpaint_replace?: number;
bounding_box?: Vector2d & Dimensions;
inpaint_width?: number;
inpaint_height?: number;
with_variations?: Array<Array<number>>;
variation_amount?: number;
enable_image_debugging?: boolean;
h_symmetry_time_pct?: number;
v_symmetry_time_pct?: number;
};
export type BackendEsrGanParameters = {
level: UpscalingLevel;
denoise_str: number;
strength: number;
};
export type BackendFacetoolParameters = {
type: FacetoolType;
strength: number;
codeformer_fidelity?: number;
};
export type BackendParameters = {
generationParameters: BackendGenerationParameters;
esrganParameters: false | BackendEsrGanParameters;
facetoolParameters: false | BackendFacetoolParameters;
};
/**
* Translates/formats frontend state into parameters suitable
* for consumption by the API.
*/
export const frontendToBackendParameters = (
config: FrontendToBackendParametersConfig
): BackendParameters => {
const canvasBaseLayer = getCanvasBaseLayer();
const {
generationMode,
generationState,
postprocessingState,
canvasState,
systemState,
} = config;
const {
codeformerFidelity,
facetoolStrength,
facetoolType,
hiresFix,
hiresStrength,
shouldRunESRGAN,
shouldRunFacetool,
upscalingLevel,
upscalingStrength,
upscalingDenoising,
} = postprocessingState;
const {
cfgScale,
height,
img2imgStrength,
infillMethod,
initialImage,
iterations,
perlin,
prompt,
negativePrompt,
sampler,
seamBlur,
seamless,
seamSize,
seamSteps,
seamStrength,
seed,
seedWeights,
shouldFitToWidthHeight,
shouldGenerateVariations,
shouldRandomizeSeed,
steps,
threshold,
tileSize,
variationAmount,
width,
shouldUseSymmetry,
horizontalSymmetrySteps,
verticalSymmetrySteps,
} = generationState;
const {
shouldDisplayInProgressType,
saveIntermediatesInterval,
enableImageDebugging,
} = systemState;
const generationParameters: BackendGenerationParameters = {
prompt,
iterations,
steps,
cfg_scale: cfgScale,
threshold,
perlin,
height,
width,
sampler_name: sampler,
seed,
progress_images: shouldDisplayInProgressType === 'full-res',
progress_latents: shouldDisplayInProgressType === 'latents',
save_intermediates: saveIntermediatesInterval,
generation_mode: generationMode,
init_mask: '',
};
let esrganParameters: false | BackendEsrGanParameters = false;
let facetoolParameters: false | BackendFacetoolParameters = false;
if (negativePrompt !== '') {
generationParameters.prompt = `${prompt} [${negativePrompt}]`;
}
generationParameters.seed = shouldRandomizeSeed
? randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)
: seed;
// Symmetry Settings
if (shouldUseSymmetry) {
if (horizontalSymmetrySteps > 0) {
generationParameters.h_symmetry_time_pct = Math.max(
0,
Math.min(1, horizontalSymmetrySteps / steps)
);
}
if (verticalSymmetrySteps > 0) {
generationParameters.v_symmetry_time_pct = Math.max(
0,
Math.min(1, verticalSymmetrySteps / steps)
);
}
}
// txt2img exclusive parameters
if (generationMode === 'txt2img') {
generationParameters.hires_fix = hiresFix;
if (hiresFix) generationParameters.strength = hiresStrength;
}
// parameters common to txt2img and img2img
if (['txt2img', 'img2img'].includes(generationMode)) {
generationParameters.seamless = seamless;
if (shouldRunESRGAN) {
esrganParameters = {
level: upscalingLevel,
denoise_str: upscalingDenoising,
strength: upscalingStrength,
};
}
if (shouldRunFacetool) {
facetoolParameters = {
type: facetoolType,
strength: facetoolStrength,
};
if (facetoolType === 'codeformer') {
facetoolParameters.codeformer_fidelity = codeformerFidelity;
}
}
}
// img2img exclusive parameters
if (generationMode === 'img2img' && initialImage) {
generationParameters.init_img =
typeof initialImage === 'string' ? initialImage : initialImage.url;
generationParameters.strength = img2imgStrength;
generationParameters.fit = shouldFitToWidthHeight;
}
// inpainting exclusive parameters
if (generationMode === 'unifiedCanvas' && canvasBaseLayer) {
const {
layerState: { objects },
boundingBoxCoordinates,
boundingBoxDimensions,
stageScale,
isMaskEnabled,
shouldPreserveMaskedArea,
boundingBoxScaleMethod: boundingBoxScale,
scaledBoundingBoxDimensions,
} = canvasState;
const boundingBox = {
...boundingBoxCoordinates,
...boundingBoxDimensions,
};
const { dataURL: maskDataURL, imageData: maskImageData } = generateMask(
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
boundingBox
);
generationParameters.init_mask = maskDataURL;
generationParameters.fit = false;
generationParameters.strength = img2imgStrength;
generationParameters.invert_mask = shouldPreserveMaskedArea;
generationParameters.bounding_box = boundingBox;
const tempScale = canvasBaseLayer.scale();
canvasBaseLayer.scale({
x: 1 / stageScale,
y: 1 / stageScale,
});
const absPos = canvasBaseLayer.getAbsolutePosition();
const imageDataURL = canvasBaseLayer.toDataURL({
x: boundingBox.x + absPos.x,
y: boundingBox.y + absPos.y,
width: boundingBox.width,
height: boundingBox.height,
});
const ctx = canvasBaseLayer.getContext();
const imageData = ctx.getImageData(
boundingBox.x + absPos.x,
boundingBox.y + absPos.y,
boundingBox.width,
boundingBox.height
);
const doesBaseHaveTransparency = getIsImageDataTransparent(imageData);
const doesMaskHaveTransparency = getIsImageDataWhite(maskImageData);
if (enableImageDebugging) {
openBase64ImageInTab([
{ base64: maskDataURL, caption: 'mask sent as init_mask' },
{ base64: imageDataURL, caption: 'image sent as init_img' },
]);
}
canvasBaseLayer.scale(tempScale);
generationParameters.init_img = imageDataURL;
generationParameters.progress_images = false;
if (boundingBoxScale !== 'none') {
generationParameters.inpaint_width = scaledBoundingBoxDimensions.width;
generationParameters.inpaint_height = scaledBoundingBoxDimensions.height;
}
generationParameters.seam_size = seamSize;
generationParameters.seam_blur = seamBlur;
generationParameters.seam_strength = seamStrength;
generationParameters.seam_steps = seamSteps;
generationParameters.tile_size = tileSize;
generationParameters.infill_method = infillMethod;
generationParameters.force_outpaint = false;
}
if (shouldGenerateVariations) {
generationParameters.variation_amount = variationAmount;
if (seedWeights) {
generationParameters.with_variations =
stringToSeedWeightsArray(seedWeights);
}
} else {
generationParameters.variation_amount = 0;
}
if (enableImageDebugging) {
generationParameters.enable_image_debugging = enableImageDebugging;
}
return {
generationParameters,
esrganParameters,
facetoolParameters,
};
};

View File

@ -0,0 +1,5 @@
import { ClipboardEvent } from 'react';
export const stopPastePropagation = (e: ClipboardEvent) => {
e.stopPropagation();
};

View File

@ -81,7 +81,7 @@ const IAICanvasResizer = () => {
height: '100%', height: '100%',
}} }}
> >
<Spinner thickness="2px" speed="1s" size="xl" /> <Spinner thickness="2px" size="xl" />
</Flex> </Flex>
); );
}; };

View File

@ -21,7 +21,6 @@ import {
CanvasLayer, CanvasLayer,
LAYER_NAMES_DICT, LAYER_NAMES_DICT,
} from 'features/canvas/store/canvasTypes'; } from 'features/canvas/store/canvasTypes';
import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploadCanvas';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider'; import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
@ -44,6 +43,12 @@ import IAICanvasRedoButton from './IAICanvasRedoButton';
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover'; import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions'; import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
import IAICanvasUndoButton from './IAICanvasUndoButton'; import IAICanvasUndoButton from './IAICanvasUndoButton';
import {
canvasCopiedToClipboard,
canvasDownloadedAsImage,
canvasMerged,
canvasSavedToGallery,
} from 'features/canvas/store/actions';
export const selector = createSelector( export const selector = createSelector(
[systemSelector, canvasSelector, isStagingSelector], [systemSelector, canvasSelector, isStagingSelector],
@ -70,14 +75,8 @@ export const selector = createSelector(
const IAICanvasToolbar = () => { const IAICanvasToolbar = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { const { isProcessing, isStaging, isMaskEnabled, layer, tool } =
isProcessing, useAppSelector(selector);
isStaging,
isMaskEnabled,
layer,
tool,
shouldCropToBoundingBoxOnSave,
} = useAppSelector(selector);
const canvasBaseLayer = getCanvasBaseLayer(); const canvasBaseLayer = getCanvasBaseLayer();
const { t } = useTranslation(); const { t } = useTranslation();
@ -183,42 +182,19 @@ const IAICanvasToolbar = () => {
}; };
const handleMergeVisible = () => { const handleMergeVisible = () => {
dispatch( dispatch(canvasMerged());
mergeAndUploadCanvas({
cropVisible: false,
shouldSetAsInitialImage: true,
})
);
}; };
const handleSaveToGallery = () => { const handleSaveToGallery = () => {
dispatch( dispatch(canvasSavedToGallery());
mergeAndUploadCanvas({
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
shouldSaveToGallery: true,
})
);
}; };
const handleCopyImageToClipboard = () => { const handleCopyImageToClipboard = () => {
dispatch( dispatch(canvasCopiedToClipboard());
mergeAndUploadCanvas({
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
shouldCopy: true,
})
);
}; };
const handleDownloadAsImage = () => { const handleDownloadAsImage = () => {
dispatch( dispatch(canvasDownloadedAsImage());
mergeAndUploadCanvas({
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
shouldDownload: true,
})
);
}; };
const handleChangeLayer = (e: ChangeEvent<HTMLSelectElement>) => { const handleChangeLayer = (e: ChangeEvent<HTMLSelectElement>) => {

View File

@ -0,0 +1,13 @@
import { createAction } from '@reduxjs/toolkit';
export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery');
export const canvasCopiedToClipboard = createAction(
'canvas/canvasCopiedToClipboard'
);
export const canvasDownloadedAsImage = createAction(
'canvas/canvasDownloadedAsImage'
);
export const canvasMerged = createAction('canvas/canvasMerged');

View File

@ -3,18 +3,8 @@ import { CanvasState } from './canvasTypes';
/** /**
* Canvas slice persist denylist * Canvas slice persist denylist
*/ */
const itemsToDenylist: (keyof CanvasState)[] = [
'cursorPosition',
'isCanvasInitialized',
'doesCanvasNeedScaling',
];
export const canvasPersistDenylist: (keyof CanvasState)[] = [ export const canvasPersistDenylist: (keyof CanvasState)[] = [
'cursorPosition', 'cursorPosition',
'isCanvasInitialized', 'isCanvasInitialized',
'doesCanvasNeedScaling', 'doesCanvasNeedScaling',
]; ];
export const canvasDenylist = itemsToDenylist.map(
(denylistItem) => `canvas.${denylistItem}`
);

View File

@ -1,172 +0,0 @@
import { AnyAction, ThunkAction } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/types/invokeai';
import { RootState } from 'app/store/store';
// import { addImage } from 'features/gallery/store/gallerySlice';
import {
addToast,
setCurrentStatus,
setIsCancelable,
setIsProcessing,
setProcessingIndeterminateTask,
} from 'features/system/store/systemSlice';
import i18n from 'i18n';
import { v4 as uuidv4 } from 'uuid';
import copyImage from '../../util/copyImage';
import downloadFile from '../../util/downloadFile';
import { getCanvasBaseLayer } from '../../util/konvaInstanceProvider';
import layerToDataURL from '../../util/layerToDataURL';
import { setMergedCanvas } from '../canvasSlice';
import { CanvasState } from '../canvasTypes';
type MergeAndUploadCanvasConfig = {
cropVisible?: boolean;
cropToBoundingBox?: boolean;
shouldSaveToGallery?: boolean;
shouldDownload?: boolean;
shouldCopy?: boolean;
shouldSetAsInitialImage?: boolean;
};
const defaultConfig: MergeAndUploadCanvasConfig = {
cropVisible: false,
cropToBoundingBox: false,
shouldSaveToGallery: false,
shouldDownload: false,
shouldCopy: false,
shouldSetAsInitialImage: true,
};
export const mergeAndUploadCanvas =
(config = defaultConfig): ThunkAction<void, RootState, unknown, AnyAction> =>
async (dispatch, getState) => {
const {
cropVisible,
cropToBoundingBox,
shouldSaveToGallery,
shouldDownload,
shouldCopy,
shouldSetAsInitialImage,
} = config;
dispatch(setProcessingIndeterminateTask('Exporting Image'));
dispatch(setIsCancelable(false));
const state = getState() as RootState;
const {
stageScale,
boundingBoxCoordinates,
boundingBoxDimensions,
stageCoordinates,
} = state.canvas as CanvasState;
const canvasBaseLayer = getCanvasBaseLayer();
if (!canvasBaseLayer) {
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
return;
}
const { dataURL, boundingBox: originalBoundingBox } = layerToDataURL(
canvasBaseLayer,
stageScale,
stageCoordinates,
cropToBoundingBox
? { ...boundingBoxCoordinates, ...boundingBoxDimensions }
: undefined
);
if (!dataURL) {
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
return;
}
const formData = new FormData();
formData.append(
'data',
JSON.stringify({
dataURL,
filename: 'merged_canvas.png',
kind: shouldSaveToGallery ? 'result' : 'temp',
cropVisible,
})
);
const response = await fetch(`${window.location.origin}/upload`, {
method: 'POST',
body: formData,
});
const image = (await response.json()) as InvokeAI.ImageUploadResponse;
const { url, width, height } = image;
const newImage: InvokeAI._Image = {
uuid: uuidv4(),
category: shouldSaveToGallery ? 'result' : 'user',
...image,
};
if (shouldDownload) {
downloadFile(url);
dispatch(
addToast({
title: i18n.t('toast.downloadImageStarted'),
status: 'success',
duration: 2500,
isClosable: true,
})
);
}
if (shouldCopy) {
copyImage(url, width, height);
dispatch(
addToast({
title: i18n.t('toast.imageCopied'),
status: 'success',
duration: 2500,
isClosable: true,
})
);
}
if (shouldSaveToGallery) {
dispatch(addImage({ image: newImage, category: 'result' }));
dispatch(
addToast({
title: i18n.t('toast.imageSavedToGallery'),
status: 'success',
duration: 2500,
isClosable: true,
})
);
}
if (shouldSetAsInitialImage) {
dispatch(
setMergedCanvas({
kind: 'image',
layer: 'base',
...originalBoundingBox,
image: newImage,
})
);
dispatch(
addToast({
title: i18n.t('toast.canvasMerged'),
status: 'success',
duration: 2500,
isClosable: true,
})
);
}
dispatch(setIsProcessing(false));
dispatch(setCurrentStatus(i18n.t('common.statusConnected')));
dispatch(setIsCancelable(true));
};

View File

@ -0,0 +1,9 @@
export const blobToDataURL = (blob: Blob): Promise<string> => {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (_e) => resolve(reader.result as string);
reader.onerror = (_e) => reject(reader.error);
reader.onabort = (_e) => reject(new Error('Read aborted'));
reader.readAsDataURL(blob);
});
};

View File

@ -9,8 +9,12 @@ const calculateCoordinates = (
contentHeight: number, contentHeight: number,
scale: number scale: number
): Vector2d => { ): Vector2d => {
const x = containerWidth / 2 - (containerX + contentWidth / 2) * scale; const x = Math.floor(
const y = containerHeight / 2 - (containerY + contentHeight / 2) * scale; containerWidth / 2 - (containerX + contentWidth / 2) * scale
);
const y = Math.floor(
containerHeight / 2 - (containerY + contentHeight / 2) * scale
);
return { x, y }; return { x, y };
}; };

View File

@ -0,0 +1,10 @@
/**
* Copies a blob to the clipboard by calling navigator.clipboard.write().
*/
export const copyBlobToClipboard = (blob: Blob) => {
navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
};

View File

@ -1,34 +0,0 @@
/**
* Copies an image to the clipboard by drawing it to a canvas and then
* calling toBlob() on the canvas.
*/
const copyImage = (url: string, width: number, height: number) => {
const imageElement = document.createElement('img');
imageElement.addEventListener('load', () => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
if (!context) return;
context.drawImage(imageElement, 0, 0);
canvas.toBlob((blob) => {
blob &&
navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
});
canvas.remove();
imageElement.remove();
});
imageElement.src = url;
};
export default copyImage;

View File

@ -0,0 +1,61 @@
import { CanvasMaskLine } from 'features/canvas/store/canvasTypes';
import Konva from 'konva';
import { IRect } from 'konva/lib/types';
/**
* Creates a stage from array of mask objects.
* We cannot just convert the mask layer to a blob because it uses a texture with transparent areas.
* So instead we create a new stage with the mask layer and composite it onto a white background.
*/
const createMaskStage = async (
lines: CanvasMaskLine[],
boundingBox: IRect
): Promise<Konva.Stage> => {
// create an offscreen canvas and add the mask to it
const { width, height } = boundingBox;
const offscreenContainer = document.createElement('div');
const maskStage = new Konva.Stage({
container: offscreenContainer,
width: width,
height: height,
});
const baseLayer = new Konva.Layer();
const maskLayer = new Konva.Layer();
// composite the image onto the mask layer
baseLayer.add(
new Konva.Rect({
...boundingBox,
fill: 'white',
})
);
lines.forEach((line) =>
maskLayer.add(
new Konva.Line({
points: line.points,
stroke: 'black',
strokeWidth: line.strokeWidth * 2,
tension: 0,
lineCap: 'round',
lineJoin: 'round',
shadowForStrokeEnabled: false,
globalCompositeOperation:
line.tool === 'brush' ? 'source-over' : 'destination-out',
})
)
);
maskStage.add(baseLayer);
maskStage.add(maskLayer);
// you'd think we can't do this until we finish with the maskStage, but we can
offscreenContainer.remove();
return maskStage;
};
export default createMaskStage;

View File

@ -0,0 +1,11 @@
/** Download a blob as a file */
export const downloadBlob = (blob: Blob, fileName: string) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
a.remove();
};

View File

@ -1,14 +0,0 @@
/**
* Downloads a file, given its URL.
*/
const downloadFile = (url: string) => {
const a = document.createElement('a');
a.href = url;
a.download = '';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
a.remove();
};
export default downloadFile;

View File

@ -1,170 +0,0 @@
// import { CanvasMaskLine } from 'features/canvas/store/canvasTypes';
// import Konva from 'konva';
// import { Stage } from 'konva/lib/Stage';
// import { IRect } from 'konva/lib/types';
// /**
// * Generating a mask image from InpaintingCanvas.tsx is not as simple
// * as calling toDataURL() on the canvas, because the mask may be represented
// * by colored lines or transparency, or the user may have inverted the mask
// * display.
// *
// * So we need to regenerate the mask image by creating an offscreen canvas,
// * drawing the mask and compositing everything correctly to output a valid
// * mask image.
// */
// export const getStageDataURL = (stage: Stage, boundingBox: IRect): string => {
// // create an offscreen canvas and add the mask to it
// // const { stage, offscreenContainer } = buildMaskStage(lines, boundingBox);
// const dataURL = stage.toDataURL({ ...boundingBox });
// // const imageData = stage
// // .toCanvas()
// // .getContext('2d')
// // ?.getImageData(
// // boundingBox.x,
// // boundingBox.y,
// // boundingBox.width,
// // boundingBox.height
// // );
// // offscreenContainer.remove();
// // return { dataURL, imageData };
// return dataURL;
// };
// export const getStageImageData = (
// stage: Stage,
// boundingBox: IRect
// ): ImageData | undefined => {
// const imageData = stage
// .toCanvas()
// .getContext('2d')
// ?.getImageData(
// boundingBox.x,
// boundingBox.y,
// boundingBox.width,
// boundingBox.height
// );
// return imageData;
// };
// export const buildMaskStage = (
// lines: CanvasMaskLine[],
// boundingBox: IRect
// ): { stage: Stage; offscreenContainer: HTMLDivElement } => {
// // create an offscreen canvas and add the mask to it
// const { width, height } = boundingBox;
// const offscreenContainer = document.createElement('div');
// const stage = new Konva.Stage({
// container: offscreenContainer,
// width: width,
// height: height,
// });
// const baseLayer = new Konva.Layer();
// const maskLayer = new Konva.Layer();
// // composite the image onto the mask layer
// baseLayer.add(
// new Konva.Rect({
// ...boundingBox,
// fill: 'white',
// })
// );
// lines.forEach((line) =>
// maskLayer.add(
// new Konva.Line({
// points: line.points,
// stroke: 'black',
// strokeWidth: line.strokeWidth * 2,
// tension: 0,
// lineCap: 'round',
// lineJoin: 'round',
// shadowForStrokeEnabled: false,
// globalCompositeOperation:
// line.tool === 'brush' ? 'source-over' : 'destination-out',
// })
// )
// );
// stage.add(baseLayer);
// stage.add(maskLayer);
// return { stage, offscreenContainer };
// };
import { CanvasMaskLine } from 'features/canvas/store/canvasTypes';
import Konva from 'konva';
import { IRect } from 'konva/lib/types';
import { canvasToBlob } from './canvasToBlob';
/**
* Generating a mask image from InpaintingCanvas.tsx is not as simple
* as calling toDataURL() on the canvas, because the mask may be represented
* by colored lines or transparency, or the user may have inverted the mask
* display.
*
* So we need to regenerate the mask image by creating an offscreen canvas,
* drawing the mask and compositing everything correctly to output a valid
* mask image.
*/
const generateMask = async (lines: CanvasMaskLine[], boundingBox: IRect) => {
// create an offscreen canvas and add the mask to it
const { width, height } = boundingBox;
const offscreenContainer = document.createElement('div');
const stage = new Konva.Stage({
container: offscreenContainer,
width: width,
height: height,
});
const baseLayer = new Konva.Layer();
const maskLayer = new Konva.Layer();
// composite the image onto the mask layer
baseLayer.add(
new Konva.Rect({
...boundingBox,
fill: 'white',
})
);
lines.forEach((line) =>
maskLayer.add(
new Konva.Line({
points: line.points,
stroke: 'black',
strokeWidth: line.strokeWidth * 2,
tension: 0,
lineCap: 'round',
lineJoin: 'round',
shadowForStrokeEnabled: false,
globalCompositeOperation:
line.tool === 'brush' ? 'source-over' : 'destination-out',
})
)
);
stage.add(baseLayer);
stage.add(maskLayer);
const maskDataURL = stage.toDataURL(boundingBox);
const maskBlob = await canvasToBlob(stage.toCanvas(boundingBox));
offscreenContainer.remove();
return { maskDataURL, maskBlob };
};
export default generateMask;

View File

@ -0,0 +1,38 @@
import { getCanvasBaseLayer } from './konvaInstanceProvider';
import { RootState } from 'app/store/store';
import { konvaNodeToBlob } from './konvaNodeToBlob';
export const getBaseLayerBlob = async (
state: RootState,
withoutBoundingBox?: boolean
) => {
const canvasBaseLayer = getCanvasBaseLayer();
if (!canvasBaseLayer) {
return;
}
const {
shouldCropToBoundingBoxOnSave,
boundingBoxCoordinates,
boundingBoxDimensions,
} = state.canvas;
const clonedBaseLayer = canvasBaseLayer.clone();
clonedBaseLayer.scale({ x: 1, y: 1 });
const absPos = clonedBaseLayer.getAbsolutePosition();
const boundingBox =
shouldCropToBoundingBoxOnSave && !withoutBoundingBox
? {
x: boundingBoxCoordinates.x + absPos.x,
y: boundingBoxCoordinates.y + absPos.y,
width: boundingBoxDimensions.width,
height: boundingBoxDimensions.height,
}
: clonedBaseLayer.getClientRect();
return konvaNodeToBlob(clonedBaseLayer, boundingBox);
};

View File

@ -2,17 +2,15 @@ import { RootState } from 'app/store/store';
import { getCanvasBaseLayer, getCanvasStage } from './konvaInstanceProvider'; import { getCanvasBaseLayer, getCanvasStage } from './konvaInstanceProvider';
import { isCanvasMaskLine } from '../store/canvasTypes'; import { isCanvasMaskLine } from '../store/canvasTypes';
import { log } from 'app/logging/useLogger'; import { log } from 'app/logging/useLogger';
import { import createMaskStage from './createMaskStage';
areAnyPixelsBlack, import { konvaNodeToImageData } from './konvaNodeToImageData';
getImageDataTransparency, import { konvaNodeToBlob } from './konvaNodeToBlob';
} from 'common/util/arrayBuffer';
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import generateMask from './generateMask';
import { dataURLToImageData } from './dataURLToImageData';
import { canvasToBlob } from './canvasToBlob';
const moduleLog = log.child({ namespace: 'getCanvasDataURLs' }); const moduleLog = log.child({ namespace: 'getCanvasDataURLs' });
/**
* Gets Blob and ImageData objects for the base and mask layers
*/
export const getCanvasData = async (state: RootState) => { export const getCanvasData = async (state: RootState) => {
const canvasBaseLayer = getCanvasBaseLayer(); const canvasBaseLayer = getCanvasBaseLayer();
const canvasStage = getCanvasStage(); const canvasStage = getCanvasStage();
@ -26,11 +24,7 @@ export const getCanvasData = async (state: RootState) => {
layerState: { objects }, layerState: { objects },
boundingBoxCoordinates, boundingBoxCoordinates,
boundingBoxDimensions, boundingBoxDimensions,
stageScale,
isMaskEnabled, isMaskEnabled,
shouldPreserveMaskedArea,
boundingBoxScaleMethod: boundingBoxScale,
scaledBoundingBoxDimensions,
} = state.canvas; } = state.canvas;
const boundingBox = { const boundingBox = {
@ -38,22 +32,14 @@ export const getCanvasData = async (state: RootState) => {
...boundingBoxDimensions, ...boundingBoxDimensions,
}; };
// generationParameters.fit = false; // Clone the base layer so we don't affect the visible base layer
const clonedBaseLayer = canvasBaseLayer.clone();
// generationParameters.strength = img2imgStrength; // Scale it to 100% so we get full resolution
clonedBaseLayer.scale({ x: 1, y: 1 });
// generationParameters.invert_mask = shouldPreserveMaskedArea; // absolute position is needed to get the bounding box coords relative to the base layer
const absPos = clonedBaseLayer.getAbsolutePosition();
// generationParameters.bounding_box = boundingBox;
const tempScale = canvasBaseLayer.scale();
canvasBaseLayer.scale({
x: 1 / stageScale,
y: 1 / stageScale,
});
const absPos = canvasBaseLayer.getAbsolutePosition();
const offsetBoundingBox = { const offsetBoundingBox = {
x: boundingBox.x + absPos.x, x: boundingBox.x + absPos.x,
@ -62,67 +48,25 @@ export const getCanvasData = async (state: RootState) => {
height: boundingBox.height, height: boundingBox.height,
}; };
const baseDataURL = canvasBaseLayer.toDataURL(offsetBoundingBox); // For the base layer, use the offset boundingBox
const baseBlob = await canvasToBlob( const baseBlob = await konvaNodeToBlob(clonedBaseLayer, offsetBoundingBox);
canvasBaseLayer.toCanvas(offsetBoundingBox) const baseImageData = await konvaNodeToImageData(
clonedBaseLayer,
offsetBoundingBox
); );
canvasBaseLayer.scale(tempScale); // For the mask layer, use the normal boundingBox
const maskStage = await createMaskStage(
const { maskDataURL, maskBlob } = await generateMask( isMaskEnabled ? objects.filter(isCanvasMaskLine) : [], // only include mask lines, and only if mask is enabled
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
boundingBox boundingBox
); );
const maskBlob = await konvaNodeToBlob(maskStage, boundingBox);
const baseImageData = await dataURLToImageData( const maskImageData = await konvaNodeToImageData(maskStage, boundingBox);
baseDataURL,
boundingBox.width,
boundingBox.height
);
const maskImageData = await dataURLToImageData(
maskDataURL,
boundingBox.width,
boundingBox.height
);
const {
isPartiallyTransparent: baseIsPartiallyTransparent,
isFullyTransparent: baseIsFullyTransparent,
} = getImageDataTransparency(baseImageData.data);
const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data);
if (state.system.enableImageDebugging) {
openBase64ImageInTab([
{ base64: maskDataURL, caption: 'mask b64' },
{ base64: baseDataURL, caption: 'image b64' },
]);
}
// generationParameters.init_img = imageDataURL;
// generationParameters.progress_images = false;
// if (boundingBoxScale !== 'none') {
// generationParameters.inpaint_width = scaledBoundingBoxDimensions.width;
// generationParameters.inpaint_height = scaledBoundingBoxDimensions.height;
// }
// generationParameters.seam_size = seamSize;
// generationParameters.seam_blur = seamBlur;
// generationParameters.seam_strength = seamStrength;
// generationParameters.seam_steps = seamSteps;
// generationParameters.tile_size = tileSize;
// generationParameters.infill_method = infillMethod;
// generationParameters.force_outpaint = false;
return { return {
baseDataURL,
baseBlob, baseBlob,
maskDataURL, baseImageData,
maskBlob, maskBlob,
baseIsPartiallyTransparent, maskImageData,
baseIsFullyTransparent,
doesMaskHaveBlackPixels,
}; };
}; };

View File

@ -0,0 +1,31 @@
import {
areAnyPixelsBlack,
getImageDataTransparency,
} from 'common/util/arrayBuffer';
export const getCanvasGenerationMode = (
baseImageData: ImageData,
maskImageData: ImageData
) => {
const {
isPartiallyTransparent: baseIsPartiallyTransparent,
isFullyTransparent: baseIsFullyTransparent,
} = getImageDataTransparency(baseImageData.data);
// check mask for black
const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data);
if (baseIsPartiallyTransparent) {
if (baseIsFullyTransparent) {
return 'txt2img';
}
return 'outpaint';
} else {
if (doesMaskHaveBlackPixels) {
return 'inpaint';
}
return 'img2img';
}
};

View File

@ -0,0 +1,16 @@
import Konva from 'konva';
import { IRect } from 'konva/lib/types';
import { canvasToBlob } from './canvasToBlob';
/**
* Converts a Konva node to a Blob
* @param node - The Konva node to convert to a Blob
* @param boundingBox - The bounding box to crop to
* @returns A Promise that resolves with Blob of the node cropped to the bounding box
*/
export const konvaNodeToBlob = async (
node: Konva.Node,
boundingBox: IRect
): Promise<Blob> => {
return await canvasToBlob(node.toCanvas(boundingBox));
};

View File

@ -0,0 +1,16 @@
import Konva from 'konva';
import { IRect } from 'konva/lib/types';
/**
* Converts a Konva node to a dataURL
* @param node - The Konva node to convert to a dataURL
* @param boundingBox - The bounding box to crop to
* @returns A dataURL of the node cropped to the bounding box
*/
export const konvaNodeToDataURL = (
node: Konva.Node,
boundingBox: IRect
): string => {
// get a dataURL of the bbox'd region
return node.toDataURL(boundingBox);
};

View File

@ -0,0 +1,23 @@
import Konva from 'konva';
import { IRect } from 'konva/lib/types';
import { dataURLToImageData } from './dataURLToImageData';
/**
* Converts a Konva node to an ImageData object
* @param node - The Konva node to convert to an ImageData object
* @param boundingBox - The bounding box to crop to
* @returns A Promise that resolves with ImageData object of the node cropped to the bounding box
*/
export const konvaNodeToImageData = async (
node: Konva.Node,
boundingBox: IRect
): Promise<ImageData> => {
// get a dataURL of the bbox'd region
const dataURL = node.toDataURL(boundingBox);
return await dataURLToImageData(
dataURL,
boundingBox.width,
boundingBox.height
);
};

View File

@ -1,53 +0,0 @@
import Konva from 'konva';
import { IRect, Vector2d } from 'konva/lib/types';
const layerToDataURL = (
layer: Konva.Layer,
stageScale: number,
stageCoordinates: Vector2d,
boundingBox?: IRect
) => {
const tempScale = layer.scale();
const relativeClientRect = layer.getClientRect({
relativeTo: layer.getParent(),
});
// Scale the canvas before getting it as a Blob
layer.scale({
x: 1 / stageScale,
y: 1 / stageScale,
});
const { x, y, width, height } = layer.getClientRect();
const dataURLBoundingBox = boundingBox
? {
x: boundingBox.x + stageCoordinates.x,
y: boundingBox.y + stageCoordinates.y,
width: boundingBox.width,
height: boundingBox.height,
}
: {
x: x,
y: y,
width: width,
height: height,
};
const dataURL = layer.toDataURL(dataURLBoundingBox);
// Unscale the canvas
layer.scale(tempScale);
return {
dataURL,
boundingBox: {
x: relativeClientRect.x,
y: relativeClientRect.y,
width: width,
height: height,
},
};
};
export default layerToDataURL;

View File

@ -5,15 +5,8 @@ import {
ButtonGroup, ButtonGroup,
Flex, Flex,
FlexProps, FlexProps,
IconButton,
Link, Link,
Menu,
MenuButton,
MenuItemOption,
MenuList,
MenuOptionGroup,
useDisclosure, useDisclosure,
useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
// import { runESRGAN, runFacetool } from 'app/socketio/actions'; // import { runESRGAN, runFacetool } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@ -54,10 +47,7 @@ import {
FaTrash, FaTrash,
FaWrench, FaWrench,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { import { gallerySelector } from '../store/gallerySelectors';
gallerySelector,
selectedImageSelector,
} from '../store/gallerySelectors';
import DeleteImageModal from './DeleteImageModal'; import DeleteImageModal from './DeleteImageModal';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
@ -70,6 +60,7 @@ import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceR
import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings'; import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings';
import { allParametersSet } from 'features/parameters/store/generationSlice'; import { allParametersSet } from 'features/parameters/store/generationSlice';
import DeleteImageButton from './ImageActionButtons/DeleteImageButton'; import DeleteImageButton from './ImageActionButtons/DeleteImageButton';
import { useAppToaster } from 'app/components/Toaster';
const currentImageButtonsSelector = createSelector( const currentImageButtonsSelector = createSelector(
[ [
@ -79,15 +70,15 @@ const currentImageButtonsSelector = createSelector(
uiSelector, uiSelector,
lightboxSelector, lightboxSelector,
activeTabNameSelector, activeTabNameSelector,
selectedImageSelector,
], ],
(system, gallery, postprocessing, ui, lightbox, activeTabName, image) => { (system, gallery, postprocessing, ui, lightbox, activeTabName) => {
const { const {
isProcessing, isProcessing,
isConnected, isConnected,
isGFPGANAvailable, isGFPGANAvailable,
isESRGANAvailable, isESRGANAvailable,
shouldConfirmOnDelete, shouldConfirmOnDelete,
progressImage,
} = system; } = system;
const { upscalingLevel, facetoolStrength } = postprocessing; const { upscalingLevel, facetoolStrength } = postprocessing;
@ -96,7 +87,7 @@ const currentImageButtonsSelector = createSelector(
const { shouldShowImageDetails, shouldHidePreview } = ui; const { shouldShowImageDetails, shouldHidePreview } = ui;
const { intermediateImage, currentImage } = gallery; const { selectedImage } = gallery;
return { return {
canDeleteImage: isConnected && !isProcessing, canDeleteImage: isConnected && !isProcessing,
@ -107,15 +98,14 @@ const currentImageButtonsSelector = createSelector(
isESRGANAvailable, isESRGANAvailable,
upscalingLevel, upscalingLevel,
facetoolStrength, facetoolStrength,
shouldDisableToolbarButtons: Boolean(intermediateImage) || !currentImage, shouldDisableToolbarButtons: Boolean(progressImage) || !selectedImage,
currentImage,
shouldShowImageDetails, shouldShowImageDetails,
activeTabName, activeTabName,
isLightboxOpen, isLightboxOpen,
shouldHidePreview, shouldHidePreview,
image, image: selectedImage,
seed: image?.metadata?.invokeai?.node?.seed, seed: selectedImage?.metadata?.invokeai?.node?.seed,
prompt: image?.metadata?.invokeai?.node?.prompt, prompt: selectedImage?.metadata?.invokeai?.node?.prompt,
}; };
}, },
{ {
@ -164,7 +154,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
onClose: onDeleteDialogClose, onClose: onDeleteDialogClose,
} = useDisclosure(); } = useDisclosure();
const toast = useToast(); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const { recallPrompt, recallSeed, recallAllParameters } = useParameters(); const { recallPrompt, recallSeed, recallAllParameters } = useParameters();
@ -213,7 +203,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const url = getImageUrl(); const url = getImageUrl();
if (!url) { if (!url) {
toast({ toaster({
title: t('toast.problemCopyingImageLink'), title: t('toast.problemCopyingImageLink'),
status: 'error', status: 'error',
duration: 2500, duration: 2500,
@ -224,14 +214,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
} }
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
toast({ toaster({
title: t('toast.imageLinkCopied'), title: t('toast.imageLinkCopied'),
status: 'success', status: 'success',
duration: 2500, duration: 2500,
isClosable: true, isClosable: true,
}); });
}); });
}, [toast, shouldTransformUrls, getUrl, t, image]); }, [toaster, shouldTransformUrls, getUrl, t, image]);
const handlePreviewVisibility = useCallback(() => { const handlePreviewVisibility = useCallback(() => {
dispatch(setShouldHidePreview(!shouldHidePreview)); dispatch(setShouldHidePreview(!shouldHidePreview));
@ -346,13 +336,13 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
dispatch(setActiveTab('unifiedCanvas')); dispatch(setActiveTab('unifiedCanvas'));
} }
toast({ toaster({
title: t('toast.sentToUnifiedCanvas'), title: t('toast.sentToUnifiedCanvas'),
status: 'success', status: 'success',
duration: 2500, duration: 2500,
isClosable: true, isClosable: true,
}); });
}, [image, isLightboxOpen, dispatch, activeTabName, toast, t]); }, [image, isLightboxOpen, dispatch, activeTabName, toaster, t]);
useHotkeys( useHotkeys(
'i', 'i',
@ -360,7 +350,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
if (image) { if (image) {
handleClickShowImageDetails(); handleClickShowImageDetails();
} else { } else {
toast({ toaster({
title: t('toast.metadataLoadFailed'), title: t('toast.metadataLoadFailed'),
status: 'error', status: 'error',
duration: 2500, duration: 2500,
@ -368,7 +358,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}); });
} }
}, },
[image, shouldShowImageDetails] [image, shouldShowImageDetails, toaster]
); );
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {

View File

@ -4,18 +4,18 @@ import { useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { selectedImageSelector } from '../store/gallerySelectors'; import { gallerySelector } from '../store/gallerySelectors';
import CurrentImageButtons from './CurrentImageButtons'; import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview'; import CurrentImagePreview from './CurrentImagePreview';
import { FaImage } from 'react-icons/fa'; import { FaImage } from 'react-icons/fa';
export const currentImageDisplaySelector = createSelector( export const currentImageDisplaySelector = createSelector(
[systemSelector, selectedImageSelector], [systemSelector, gallerySelector],
(system, selectedImage) => { (system, gallery) => {
const { progressImage } = system; const { progressImage } = system;
return { return {
hasAnImageToDisplay: selectedImage || progressImage, hasAnImageToDisplay: gallery.selectedImage || progressImage,
}; };
}, },
{ {

View File

@ -1,6 +1,6 @@
import { Box, Flex, Image } from '@chakra-ui/react'; import { Box, Flex, Image } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGetUrl } from 'common/util/getUrl'; import { useGetUrl } from 'common/util/getUrl';
import { uiSelector } from 'features/ui/store/uiSelectors'; import { uiSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
@ -8,11 +8,13 @@ import { isEqual } from 'lodash-es';
import { gallerySelector } from '../store/gallerySelectors'; import { gallerySelector } from '../store/gallerySelectors';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons'; import NextPrevImageButtons from './NextPrevImageButtons';
import CurrentImageHidden from './CurrentImageHidden';
import { DragEvent, memo, useCallback } from 'react'; import { DragEvent, memo, useCallback } from 'react';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import ImageFallbackSpinner from './ImageFallbackSpinner'; import ImageFallbackSpinner from './ImageFallbackSpinner';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { configSelector } from '../../system/store/configSelectors';
import { useAppToaster } from 'app/components/Toaster';
import { imageSelected } from '../store/gallerySlice';
export const imagesSelector = createSelector( export const imagesSelector = createSelector(
[uiSelector, gallerySelector, systemSelector], [uiSelector, gallerySelector, systemSelector],
@ -49,7 +51,10 @@ const CurrentImagePreview = () => {
shouldShowProgressInViewer, shouldShowProgressInViewer,
shouldAntialiasProgressImage, shouldAntialiasProgressImage,
} = useAppSelector(imagesSelector); } = useAppSelector(imagesSelector);
const { shouldFetchImages } = useAppSelector(configSelector);
const { getUrl } = useGetUrl(); const { getUrl } = useGetUrl();
const toaster = useAppToaster();
const dispatch = useAppDispatch();
const handleDragStart = useCallback( const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => { (e: DragEvent<HTMLDivElement>) => {
@ -63,6 +68,17 @@ const CurrentImagePreview = () => {
[image] [image]
); );
const handleError = useCallback(() => {
dispatch(imageSelected());
if (shouldFetchImages) {
toaster({
title: 'Something went wrong, please refresh',
status: 'error',
isClosable: true,
});
}
}, [dispatch, toaster, shouldFetchImages]);
return ( return (
<Flex <Flex
sx={{ sx={{
@ -104,6 +120,7 @@ const CurrentImagePreview = () => {
position: 'absolute', position: 'absolute',
borderRadius: 'base', borderRadius: 'base',
}} }}
onError={handleError}
/> />
<ImageMetadataOverlay image={image} /> <ImageMetadataOverlay image={image} />
</> </>

View File

@ -1,10 +1,6 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { gallerySelector } from 'features/gallery/store/gallerySelectors'; import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import { import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice';
// selectNextImage,
// selectPrevImage,
setGalleryImageMinimumWidth,
} from 'features/gallery/store/gallerySlice';
import { clamp, isEqual } from 'lodash-es'; import { clamp, isEqual } from 'lodash-es';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -23,20 +19,7 @@ import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvas
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { memo } from 'react'; import { memo } from 'react';
// const GALLERY_TAB_WIDTHS: Record< const selector = createSelector(
// InvokeTabName,
// { galleryMinWidth: number; galleryMaxWidth: number }
// > = {
// txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// generate: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 },
// nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// training: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// };
const galleryPanelSelector = createSelector(
[ [
activeTabNameSelector, activeTabNameSelector,
uiSelector, uiSelector,
@ -76,41 +59,13 @@ const GalleryDrawer = () => {
// isStaging, // isStaging,
// isResizable, // isResizable,
// isLightboxOpen, // isLightboxOpen,
} = useAppSelector(galleryPanelSelector); } = useAppSelector(selector);
// const handleSetShouldPinGallery = () => {
// dispatch(togglePinGalleryPanel());
// dispatch(requestCanvasRescale());
// };
// const handleToggleGallery = () => {
// dispatch(toggleGalleryPanel());
// shouldPinGallery && dispatch(requestCanvasRescale());
// };
const handleCloseGallery = () => { const handleCloseGallery = () => {
dispatch(setShouldShowGallery(false)); dispatch(setShouldShowGallery(false));
shouldPinGallery && dispatch(requestCanvasRescale()); shouldPinGallery && dispatch(requestCanvasRescale());
}; };
// const resolution = useResolution();
// useHotkeys(
// 'g',
// () => {
// handleToggleGallery();
// },
// [shouldPinGallery]
// );
// useHotkeys(
// 'shift+g',
// () => {
// handleSetShouldPinGallery();
// },
// [shouldPinGallery]
// );
useHotkeys( useHotkeys(
'esc', 'esc',
() => { () => {
@ -155,54 +110,6 @@ const GalleryDrawer = () => {
[galleryImageMinimumWidth] [galleryImageMinimumWidth]
); );
// const calcGalleryMinHeight = () => {
// if (resolution === 'desktop') return;
// return 300;
// };
// const imageGalleryContent = () => {
// return (
// <Flex
// w="100vw"
// h={{ base: 300, xl: '100vh' }}
// paddingRight={{ base: 8, xl: 0 }}
// paddingBottom={{ base: 4, xl: 0 }}
// >
// <ImageGalleryContent />
// </Flex>
// );
// };
// const resizableImageGalleryContent = () => {
// return (
// <ResizableDrawer
// direction="right"
// isResizable={isResizable || !shouldPinGallery}
// isOpen={shouldShowGallery}
// onClose={handleCloseGallery}
// isPinned={shouldPinGallery && !isLightboxOpen}
// minWidth={
// shouldPinGallery
// ? GALLERY_TAB_WIDTHS[activeTabName].galleryMinWidth
// : 200
// }
// maxWidth={
// shouldPinGallery
// ? GALLERY_TAB_WIDTHS[activeTabName].galleryMaxWidth
// : undefined
// }
// minHeight={calcGalleryMinHeight()}
// >
// <ImageGalleryContent />
// </ResizableDrawer>
// );
// };
// const renderImageGallery = () => {
// if (['mobile', 'tablet'].includes(resolution)) return imageGalleryContent();
// return resizableImageGalleryContent();
// };
if (shouldPinGallery) { if (shouldPinGallery) {
return null; return null;
} }
@ -218,8 +125,6 @@ const GalleryDrawer = () => {
<ImageGalleryContent /> <ImageGalleryContent />
</ResizableDrawer> </ResizableDrawer>
); );
// return renderImageGallery();
}; };
export default memo(GalleryDrawer); export default memo(GalleryDrawer);

View File

@ -62,7 +62,10 @@ const GalleryProgressImage = () => {
imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated', imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
}} }}
/> />
<Spinner sx={{ position: 'absolute', top: 1, right: 1, opacity: 0.7 }} /> <Spinner
sx={{ position: 'absolute', top: 1, right: 1, opacity: 0.7 }}
speed="1.2s"
/>
</Flex> </Flex>
); );
}; };

View File

@ -6,7 +6,6 @@ import {
MenuItem, MenuItem,
MenuList, MenuList,
useDisclosure, useDisclosure,
useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { imageSelected } from 'features/gallery/store/gallerySlice'; import { imageSelected } from 'features/gallery/store/gallerySlice';
@ -35,6 +34,7 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useParameters } from 'features/parameters/hooks/useParameters'; import { useParameters } from 'features/parameters/hooks/useParameters';
import { initialImageSelected } from 'features/parameters/store/actions'; import { initialImageSelected } from 'features/parameters/store/actions';
import { requestedImageDeletion } from '../store/actions'; import { requestedImageDeletion } from '../store/actions';
import { useAppToaster } from 'app/components/Toaster';
export const selector = createSelector( export const selector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector], [gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
@ -101,7 +101,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const [isHovered, setIsHovered] = useState<boolean>(false); const [isHovered, setIsHovered] = useState<boolean>(false);
const toast = useToast(); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
@ -176,7 +176,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
dispatch(setActiveTab('unifiedCanvas')); dispatch(setActiveTab('unifiedCanvas'));
} }
toast({ toaster({
title: t('toast.sentToUnifiedCanvas'), title: t('toast.sentToUnifiedCanvas'),
status: 'success', status: 'success',
duration: 2500, duration: 2500,

View File

@ -1,442 +0,0 @@
// import { NumberSize, Resizable } from 're-resizable';
// import {
// Box,
// ButtonGroup,
// Flex,
// Grid,
// Icon,
// chakra,
// useTheme,
// } from '@chakra-ui/react';
// import { requestImages } from 'app/socketio/actions';
// import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
// import IAIButton from 'common/components/IAIButton';
// import IAICheckbox from 'common/components/IAICheckbox';
// import IAIIconButton from 'common/components/IAIIconButton';
// import IAIPopover from 'common/components/IAIPopover';
// import IAISlider from 'common/components/IAISlider';
// import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
// import { imageGallerySelector } from 'features/gallery/store/gallerySelectors';
// import {
// selectNextImage,
// selectPrevImage,
// setCurrentCategory,
// setGalleryImageMinimumWidth,
// setGalleryImageObjectFit,
// setGalleryWidth,
// setShouldAutoSwitchToNewImages,
// setShouldHoldGalleryOpen,
// setShouldUseSingleGalleryColumn,
// } from 'features/gallery/store/gallerySlice';
// import {
// setShouldPinGallery,
// setShouldShowGallery,
// } from 'features/ui/store/uiSlice';
// import { InvokeTabName } from 'features/ui/store/tabMap';
// import { clamp } from 'lodash-es';
// import { Direction } from 're-resizable/lib/resizer';
// import React, {
// ChangeEvent,
// useCallback,
// useEffect,
// useRef,
// useState,
// } from 'react';
// import { useHotkeys } from 'react-hotkeys-hook';
// import { useTranslation } from 'react-i18next';
// import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
// import { FaImage, FaUser, FaWrench } from 'react-icons/fa';
// import { MdPhotoLibrary } from 'react-icons/md';
// import { CSSTransition } from 'react-transition-group';
// import HoverableImage from './HoverableImage';
// import { APP_GALLERY_HEIGHT_PINNED } from 'theme/util/constants';
// import './ImageGallery.css';
// import { no_scrollbar } from 'theme/components/scrollbar';
// import ImageGalleryContent from './ImageGalleryContent';
// const ChakraResizeable = chakra(Resizable, {
// shouldForwardProp: (prop) => !['sx'].includes(prop),
// });
// const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320;
// const GALLERY_IMAGE_WIDTH_OFFSET = 40;
// const GALLERY_TAB_WIDTHS: Record<
// InvokeTabName,
// { galleryMinWidth: number; galleryMaxWidth: number }
// > = {
// txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 },
// nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// postprocess: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// training: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// };
// const LIGHTBOX_GALLERY_WIDTH = 400;
// export default function ImageGallery() {
// const dispatch = useAppDispatch();
// const { direction } = useTheme();
// const { t } = useTranslation();
// const {
// images,
// currentCategory,
// currentImageUuid,
// shouldPinGallery,
// shouldShowGallery,
// galleryImageMinimumWidth,
// galleryGridTemplateColumns,
// activeTabName,
// galleryImageObjectFit,
// shouldHoldGalleryOpen,
// shouldAutoSwitchToNewImages,
// areMoreImagesAvailable,
// galleryWidth,
// isLightboxOpen,
// isStaging,
// shouldEnableResize,
// shouldUseSingleGalleryColumn,
// } = useAppSelector(imageGallerySelector);
// const { galleryMinWidth, galleryMaxWidth } = isLightboxOpen
// ? {
// galleryMinWidth: LIGHTBOX_GALLERY_WIDTH,
// galleryMaxWidth: LIGHTBOX_GALLERY_WIDTH,
// }
// : GALLERY_TAB_WIDTHS[activeTabName];
// const [shouldShowButtons, setShouldShowButtons] = useState<boolean>(
// galleryWidth >= GALLERY_SHOW_BUTTONS_MIN_WIDTH
// );
// const [isResizing, setIsResizing] = useState(false);
// const [galleryResizeHeight, setGalleryResizeHeight] = useState(0);
// const galleryRef = useRef<HTMLDivElement>(null);
// const galleryContainerRef = useRef<HTMLDivElement>(null);
// const timeoutIdRef = useRef<number | null>(null);
// useEffect(() => {
// setShouldShowButtons(galleryWidth >= GALLERY_SHOW_BUTTONS_MIN_WIDTH);
// }, [galleryWidth]);
// const handleSetShouldPinGallery = () => {
// !shouldPinGallery && dispatch(setShouldShowGallery(true));
// dispatch(setShouldPinGallery(!shouldPinGallery));
// dispatch(setDoesCanvasNeedScaling(true));
// };
// const handleToggleGallery = () => {
// shouldShowGallery ? handleCloseGallery() : handleOpenGallery();
// };
// const handleOpenGallery = () => {
// dispatch(setShouldShowGallery(true));
// shouldPinGallery && dispatch(setDoesCanvasNeedScaling(true));
// };
// const handleCloseGallery = useCallback(() => {
// dispatch(setShouldShowGallery(false));
// dispatch(setShouldHoldGalleryOpen(false));
// setTimeout(
// () => shouldPinGallery && dispatch(setDoesCanvasNeedScaling(true)),
// 400
// );
// }, [dispatch, shouldPinGallery]);
// const handleClickLoadMore = () => {
// dispatch(requestImages(currentCategory));
// };
// const handleChangeGalleryImageMinimumWidth = (v: number) => {
// dispatch(setGalleryImageMinimumWidth(v));
// };
// const setCloseGalleryTimer = () => {
// if (shouldHoldGalleryOpen) return;
// timeoutIdRef.current = window.setTimeout(() => handleCloseGallery(), 500);
// };
// const cancelCloseGalleryTimer = () => {
// timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
// };
// useHotkeys(
// 'g',
// () => {
// handleToggleGallery();
// },
// [shouldShowGallery, shouldPinGallery]
// );
// useHotkeys(
// 'left',
// () => {
// dispatch(selectPrevImage());
// },
// {
// enabled: !isStaging || activeTabName !== 'unifiedCanvas',
// },
// [isStaging]
// );
// useHotkeys(
// 'right',
// () => {
// dispatch(selectNextImage());
// },
// {
// enabled: !isStaging || activeTabName !== 'unifiedCanvas',
// },
// [isStaging]
// );
// useHotkeys(
// 'shift+g',
// () => {
// handleSetShouldPinGallery();
// },
// [shouldPinGallery]
// );
// useHotkeys(
// 'esc',
// () => {
// dispatch(setShouldShowGallery(false));
// },
// {
// enabled: () => !shouldPinGallery,
// preventDefault: true,
// },
// [shouldPinGallery]
// );
// const IMAGE_SIZE_STEP = 32;
// useHotkeys(
// 'shift+up',
// () => {
// if (galleryImageMinimumWidth < 256) {
// const newMinWidth = clamp(
// galleryImageMinimumWidth + IMAGE_SIZE_STEP,
// 32,
// 256
// );
// dispatch(setGalleryImageMinimumWidth(newMinWidth));
// }
// },
// [galleryImageMinimumWidth]
// );
// useHotkeys(
// 'shift+down',
// () => {
// if (galleryImageMinimumWidth > 32) {
// const newMinWidth = clamp(
// galleryImageMinimumWidth - IMAGE_SIZE_STEP,
// 32,
// 256
// );
// dispatch(setGalleryImageMinimumWidth(newMinWidth));
// }
// },
// [galleryImageMinimumWidth]
// );
// useEffect(() => {
// function handleClickOutside(e: MouseEvent) {
// if (
// !shouldPinGallery &&
// galleryRef.current &&
// !galleryRef.current.contains(e.target as Node)
// ) {
// handleCloseGallery();
// }
// }
// document.addEventListener('mousedown', handleClickOutside);
// return () => {
// document.removeEventListener('mousedown', handleClickOutside);
// };
// }, [handleCloseGallery, shouldPinGallery]);
// return (
// <CSSTransition
// nodeRef={galleryRef}
// in={shouldShowGallery || shouldHoldGalleryOpen}
// unmountOnExit
// timeout={200}
// classNames={`${direction}-image-gallery-css-transition`}
// >
// <Box
// className={`${direction}-image-gallery-css-transition`}
// sx={
// shouldPinGallery
// ? { zIndex: 1, insetInlineEnd: 0 }
// : {
// zIndex: 100,
// position: 'fixed',
// height: '100vh',
// top: 0,
// insetInlineEnd: 0,
// }
// }
// ref={galleryRef}
// onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
// onMouseEnter={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
// onMouseOver={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
// >
// <ChakraResizeable
// sx={{
// padding: 4,
// display: 'flex',
// flexDirection: 'column',
// rowGap: 4,
// borderRadius: shouldPinGallery ? 'base' : 0,
// borderInlineStartWidth: 5,
// // boxShadow: '0 0 1rem blackAlpha.700',
// bg: 'base.850',
// borderColor: 'base.700',
// }}
// minWidth={galleryMinWidth}
// maxWidth={shouldPinGallery ? galleryMaxWidth : window.innerWidth}
// data-pinned={shouldPinGallery}
// handleStyles={
// direction === 'rtl'
// ? {
// right: {
// width: '15px',
// },
// }
// : {
// left: {
// width: '15px',
// },
// }
// }
// enable={
// direction === 'rtl'
// ? {
// right: shouldEnableResize,
// }
// : {
// left: shouldEnableResize,
// }
// }
// size={{
// width: galleryWidth,
// height: shouldPinGallery ? '100%' : '100vh',
// }}
// onResizeStart={(
// _event:
// | React.MouseEvent<HTMLElement>
// | React.TouchEvent<HTMLElement>,
// _direction: Direction,
// elementRef: HTMLElement
// ) => {
// setGalleryResizeHeight(elementRef.clientHeight);
// elementRef.style.height = `${elementRef.clientHeight}px`;
// if (shouldPinGallery) {
// elementRef.style.position = 'fixed';
// elementRef.style.insetInlineEnd = '1rem';
// setIsResizing(true);
// }
// }}
// onResizeStop={(
// _event: MouseEvent | TouchEvent,
// _direction: Direction,
// elementRef: HTMLElement,
// delta: NumberSize
// ) => {
// const newWidth = shouldPinGallery
// ? clamp(
// Number(galleryWidth) + delta.width,
// galleryMinWidth,
// Number(galleryMaxWidth)
// )
// : Number(galleryWidth) + delta.width;
// dispatch(setGalleryWidth(newWidth));
// elementRef.removeAttribute('data-resize-alert');
// if (shouldPinGallery) {
// console.log('unpin');
// elementRef.style.position = 'relative';
// elementRef.style.removeProperty('inset-inline-end');
// elementRef.style.setProperty(
// 'height',
// shouldPinGallery ? '100%' : '100vh'
// );
// setIsResizing(false);
// dispatch(setDoesCanvasNeedScaling(true));
// }
// }}
// onResize={(
// _event: MouseEvent | TouchEvent,
// _direction: Direction,
// elementRef: HTMLElement,
// delta: NumberSize
// ) => {
// const newWidth = clamp(
// Number(galleryWidth) + delta.width,
// galleryMinWidth,
// Number(
// shouldPinGallery ? galleryMaxWidth : 0.95 * window.innerWidth
// )
// );
// if (
// newWidth >= GALLERY_SHOW_BUTTONS_MIN_WIDTH &&
// !shouldShowButtons
// ) {
// setShouldShowButtons(true);
// } else if (
// newWidth < GALLERY_SHOW_BUTTONS_MIN_WIDTH &&
// shouldShowButtons
// ) {
// setShouldShowButtons(false);
// }
// if (
// galleryImageMinimumWidth >
// newWidth - GALLERY_IMAGE_WIDTH_OFFSET
// ) {
// dispatch(
// setGalleryImageMinimumWidth(
// newWidth - GALLERY_IMAGE_WIDTH_OFFSET
// )
// );
// }
// if (shouldPinGallery) {
// if (newWidth >= galleryMaxWidth) {
// elementRef.setAttribute('data-resize-alert', 'true');
// } else {
// elementRef.removeAttribute('data-resize-alert');
// }
// }
// elementRef.style.height = `${galleryResizeHeight}px`;
// }}
// >
// <ImageGalleryContent />
// </ChakraResizeable>
// {isResizing && (
// <Box
// style={{
// width: `${galleryWidth}px`,
// height: '100%',
// }}
// />
// )}
// </Box>
// </CSSTransition>
// );
// }
export default {};

View File

@ -15,10 +15,7 @@ import IAICheckbox from 'common/components/IAICheckbox';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover'; import IAIPopover from 'common/components/IAIPopover';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { import { gallerySelector } from 'features/gallery/store/gallerySelectors';
gallerySelector,
imageGallerySelector,
} from 'features/gallery/store/gallerySelectors';
import { import {
setCurrentCategory, setCurrentCategory,
setGalleryImageMinimumWidth, setGalleryImageMinimumWidth,
@ -57,11 +54,12 @@ import { Virtuoso, VirtuosoGrid } from 'react-virtuoso';
import { Image as ImageType } from 'app/types/invokeai'; import { Image as ImageType } from 'app/types/invokeai';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import GalleryProgressImage from './GalleryProgressImage'; import GalleryProgressImage from './GalleryProgressImage';
import { uiSelector } from 'features/ui/store/uiSelectors';
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290; const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
const PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER'; const PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER';
const selector = createSelector( const categorySelector = createSelector(
[(state: RootState) => state], [(state: RootState) => state],
(state) => { (state) => {
const { results, uploads, system, gallery } = state; const { results, uploads, system, gallery } = state;
@ -92,6 +90,33 @@ const selector = createSelector(
defaultSelectorOptions defaultSelectorOptions
); );
const mainSelector = createSelector(
[gallerySelector, uiSelector],
(gallery, ui) => {
const {
currentCategory,
galleryImageMinimumWidth,
galleryImageObjectFit,
shouldAutoSwitchToNewImages,
shouldUseSingleGalleryColumn,
selectedImage,
} = gallery;
const { shouldPinGallery } = ui;
return {
currentCategory,
shouldPinGallery,
galleryImageMinimumWidth,
galleryImageObjectFit,
shouldAutoSwitchToNewImages,
shouldUseSingleGalleryColumn,
selectedImage,
};
},
defaultSelectorOptions
);
const ImageGalleryContent = () => { const ImageGalleryContent = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -113,7 +138,6 @@ const ImageGalleryContent = () => {
}); });
const { const {
// images,
currentCategory, currentCategory,
shouldPinGallery, shouldPinGallery,
galleryImageMinimumWidth, galleryImageMinimumWidth,
@ -121,10 +145,10 @@ const ImageGalleryContent = () => {
shouldAutoSwitchToNewImages, shouldAutoSwitchToNewImages,
shouldUseSingleGalleryColumn, shouldUseSingleGalleryColumn,
selectedImage, selectedImage,
} = useAppSelector(imageGallerySelector); } = useAppSelector(mainSelector);
const { images, areMoreImagesAvailable, isLoading } = const { images, areMoreImagesAvailable, isLoading } =
useAppSelector(selector); useAppSelector(categorySelector);
const handleClickLoadMore = () => { const handleClickLoadMore = () => {
if (currentCategory === 'results') { if (currentCategory === 'results') {

View File

@ -19,7 +19,7 @@ import {
setHeight, setHeight,
setImg2imgStrength, setImg2imgStrength,
setPerlin, setPerlin,
setSampler, setScheduler,
setSeamless, setSeamless,
setSeed, setSeed,
setSeedWeights, setSeedWeights,
@ -202,9 +202,9 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
)} )}
{node.scheduler && ( {node.scheduler && (
<MetadataItem <MetadataItem
label="Sampler" label="Scheduler"
value={node.scheduler} value={node.scheduler}
onClick={() => dispatch(setSampler(node.scheduler))} onClick={() => dispatch(setScheduler(node.scheduler))}
/> />
)} )}
{node.steps && ( {node.steps && (

View File

@ -1,28 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { gallerySelector } from '../store/gallerySelectors';
const selector = createSelector(gallerySelector, (gallery) => ({
resultImages: gallery.categories.result.images,
userImages: gallery.categories.user.images,
}));
const useGetImageByUuid = () => {
const { resultImages, userImages } = useAppSelector(selector);
return (uuid: string) => {
const resultImagesResult = resultImages.find(
(image) => image.uuid === uuid
);
if (resultImagesResult) {
return resultImagesResult;
}
const userImagesResult = userImages.find((image) => image.uuid === uuid);
if (userImagesResult) {
return userImagesResult;
}
};
};
export default useGetImageByUuid;

View File

@ -3,16 +3,7 @@ import { GalleryState } from './gallerySlice';
/** /**
* Gallery slice persist denylist * Gallery slice persist denylist
*/ */
const itemsToDenylist: (keyof GalleryState)[] = [
'currentCategory',
'shouldAutoSwitchToNewImages',
];
export const galleryPersistDenylist: (keyof GalleryState)[] = [ export const galleryPersistDenylist: (keyof GalleryState)[] = [
'currentCategory', 'currentCategory',
'shouldAutoSwitchToNewImages', 'shouldAutoSwitchToNewImages',
]; ];
export const galleryDenylist = itemsToDenylist.map(
(denylistItem) => `gallery.${denylistItem}`
);

View File

@ -1,83 +1,3 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import {
activeTabNameSelector,
uiSelector,
} from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash-es';
import { selectResultsById, selectResultsEntities } from './resultsSlice';
import { selectUploadsAll, selectUploadsById } from './uploadsSlice';
export const gallerySelector = (state: RootState) => state.gallery; export const gallerySelector = (state: RootState) => state.gallery;
export const imageGallerySelector = createSelector(
[
(state: RootState) => state,
gallerySelector,
uiSelector,
lightboxSelector,
activeTabNameSelector,
],
(state, gallery, ui, lightbox, activeTabName) => {
const {
currentCategory,
galleryImageMinimumWidth,
galleryImageObjectFit,
shouldAutoSwitchToNewImages,
galleryWidth,
shouldUseSingleGalleryColumn,
selectedImage,
} = gallery;
const { shouldPinGallery } = ui;
const { isLightboxOpen } = lightbox;
const images =
currentCategory === 'results'
? selectResultsEntities(state)
: selectUploadsAll(state);
return {
shouldPinGallery,
galleryImageMinimumWidth,
galleryImageObjectFit,
galleryGridTemplateColumns: shouldUseSingleGalleryColumn
? 'auto'
: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
shouldAutoSwitchToNewImages,
currentCategory,
images,
galleryWidth,
shouldEnableResize:
isLightboxOpen ||
(activeTabName === 'unifiedCanvas' && shouldPinGallery)
? false
: true,
shouldUseSingleGalleryColumn,
selectedImage,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
export const selectedImageSelector = createSelector(
[(state: RootState) => state, gallerySelector],
(state, gallery) => {
const selectedImage = gallery.selectedImage;
if (selectedImage?.type === 'results') {
return selectResultsById(state, selectedImage.name);
}
if (selectedImage?.type === 'uploads') {
return selectUploadsById(state, selectedImage.name);
}
}
);

View File

@ -10,14 +10,10 @@ import {
type GalleryImageObjectFitType = 'contain' | 'cover'; type GalleryImageObjectFitType = 'contain' | 'cover';
export interface GalleryState { export interface GalleryState {
/**
* The selected image
*/
selectedImage?: Image; selectedImage?: Image;
galleryImageMinimumWidth: number; galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType; galleryImageObjectFit: GalleryImageObjectFitType;
shouldAutoSwitchToNewImages: boolean; shouldAutoSwitchToNewImages: boolean;
galleryWidth: number;
shouldUseSingleGalleryColumn: boolean; shouldUseSingleGalleryColumn: boolean;
currentCategory: 'results' | 'uploads'; currentCategory: 'results' | 'uploads';
} }
@ -26,7 +22,6 @@ export const initialGalleryState: GalleryState = {
galleryImageMinimumWidth: 64, galleryImageMinimumWidth: 64,
galleryImageObjectFit: 'cover', galleryImageObjectFit: 'cover',
shouldAutoSwitchToNewImages: true, shouldAutoSwitchToNewImages: true,
galleryWidth: 300,
shouldUseSingleGalleryColumn: false, shouldUseSingleGalleryColumn: false,
currentCategory: 'results', currentCategory: 'results',
}; };
@ -58,9 +53,6 @@ export const gallerySlice = createSlice({
) => { ) => {
state.currentCategory = action.payload; state.currentCategory = action.payload;
}, },
setGalleryWidth: (state, action: PayloadAction<number>) => {
state.galleryWidth = action.payload;
},
setShouldUseSingleGalleryColumn: ( setShouldUseSingleGalleryColumn: (
state, state,
action: PayloadAction<boolean> action: PayloadAction<boolean>
@ -93,24 +85,28 @@ export const gallerySlice = createSlice({
builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => { builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => {
// rehydrate selectedImage URL when results list comes in // rehydrate selectedImage URL when results list comes in
// solves case when outdated URL is in local storage // solves case when outdated URL is in local storage
if (state.selectedImage) { const selectedImage = state.selectedImage;
if (selectedImage) {
const selectedImageInResults = action.payload.items.find( const selectedImageInResults = action.payload.items.find(
(image) => image.image_name === state.selectedImage!.name (image) => image.image_name === selectedImage.name
); );
if (selectedImageInResults) { if (selectedImageInResults) {
state.selectedImage.url = selectedImageInResults.image_url; selectedImage.url = selectedImageInResults.image_url;
state.selectedImage = selectedImage;
} }
} }
}); });
builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => { builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => {
// rehydrate selectedImage URL when results list comes in // rehydrate selectedImage URL when results list comes in
// solves case when outdated URL is in local storage // solves case when outdated URL is in local storage
if (state.selectedImage) { const selectedImage = state.selectedImage;
if (selectedImage) {
const selectedImageInResults = action.payload.items.find( const selectedImageInResults = action.payload.items.find(
(image) => image.image_name === state.selectedImage!.name (image) => image.image_name === selectedImage.name
); );
if (selectedImageInResults) { if (selectedImageInResults) {
state.selectedImage.url = selectedImageInResults.image_url; selectedImage.url = selectedImageInResults.image_url;
state.selectedImage = selectedImage;
} }
} }
}); });
@ -122,7 +118,6 @@ export const {
setGalleryImageMinimumWidth, setGalleryImageMinimumWidth,
setGalleryImageObjectFit, setGalleryImageObjectFit,
setShouldAutoSwitchToNewImages, setShouldAutoSwitchToNewImages,
setGalleryWidth,
setShouldUseSingleGalleryColumn, setShouldUseSingleGalleryColumn,
setCurrentCategory, setCurrentCategory,
} = gallerySlice.actions; } = gallerySlice.actions;

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