Compare commits

..

76 Commits

Author SHA1 Message Date
Ryan Dick
9b763b9e4c Fix issue with seamless context managers when seamless is not configured. 2024-01-05 10:31:58 -05:00
Sergey Borisov
7f3be627c2 Add more seamless configuration options. 2024-01-05 09:57:28 -05:00
psychedelicious
cb7e56a9a3 chore(ui): lint 2024-01-05 08:34:46 -05:00
psychedelicious
1a710a4c12 fix(ui): restore prev colors for workflow editor
Brand colors are now prefixed with "invoke".
2024-01-05 08:34:46 -05:00
psychedelicious
d8d266d3be fix(ui): fix field title spacing
Closes #5405
2024-01-05 08:34:46 -05:00
psychedelicious
4716632c23 fix(ui): tsc 2024-01-06 00:03:07 +11:00
psychedelicious
3c4150d153 fix(ui): update most other selectors
Just a few stragglers left. Good enough for now.
2024-01-06 00:03:07 +11:00
psychedelicious
b71b14d582 fix(ui): update workflow selectors 2024-01-06 00:03:07 +11:00
psychedelicious
73481d4aec feat(ui): clean up canvas selectors
Do not memoize unless absolutely necessary. Minor perf improvement
2024-01-06 00:03:07 +11:00
psychedelicious
2c049a3b94 feat(ui): clean up a few selectors that do not need to be memoized 2024-01-06 00:03:07 +11:00
psychedelicious
367de44a8b fix(ui): tidy remaining selectors
These were just using overly verbose syntax - like explicitly typing `state: RootState`, which is unnecessary.
2024-01-06 00:03:07 +11:00
psychedelicious
f5f378d04b fix(ui): revert back to lrumemoize 2024-01-06 00:03:07 +11:00
psychedelicious
823edbfdef fix(ui): fix more state => state selectors 2024-01-06 00:03:07 +11:00
psychedelicious
29bbb27289 fix(ui): re-add reselect patch
Accidentally removed it last commit.
2024-01-06 00:03:07 +11:00
psychedelicious
a23502f7ff fix(ui): do not use state => state as an input selector
This is a no-no, whoops!
2024-01-06 00:03:07 +11:00
psychedelicious
ce64dbefce chore(ui): lint 2024-01-06 00:03:07 +11:00
psychedelicious
b47afdc3b5 feat(ui): patch reselect to use lruMemoize only
Pending resolution of https://github.com/reduxjs/reselect/issues/635, we can patch `reselect` to use `lruMemoize` exclusively.

Pin RTK and react-redux versions too just to be safe.

This reduces the major GC events that were causing lag/stutters in the app, particularly in canvas and workflow editor.
2024-01-06 00:03:07 +11:00
psychedelicious
cde9c3090f fix(ui): use useAppSelector instead of useSelector 2024-01-06 00:03:07 +11:00
psychedelicious
6924b04d7c feat(ui): use lruMemoize for all entity adapter selectors 2024-01-06 00:03:07 +11:00
blessedcoolant
83fbd4bdf2 ui: slightly reposition floating bars. 2024-01-05 23:59:08 +11:00
Lincoln Stein
6460dcc7e0 use torch.bfloat16 on cuda systems 2024-01-04 23:25:52 -05:00
Millun Atluri
59aa009c93 {release} v3.6.0rc3 (#5408)
## What type of PR is this? (check all applicable)

Release v3.6.0rc3


## Have you discussed this change with the InvokeAI team?
- [X] Yes
- [ ] No, because:

      
## Have you updated all relevant documentation?
- [] Yes
- [X] No


## Description
Next release candidate

## Related Tickets & Documents
N/A
<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #
- Closes #

## QA Instructions, Screenshots, Recordings
[Uploading InvokeAI-installer-v3.6.0rc3.zip…](Installer zip)

<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Merge Plan
This PR can be merged when approved
<!--
A merge plan describes how this PR should be handled after it is
approved.

Example merge plans:
- "This PR can be merged when approved"
- "This must be squash-merged when approved"
- "DO NOT MERGE - I will rebase and tidy commits before merging"
- "#dev-chat on discord needs to be advised of this change when it is
merged"

A merge plan is particularly important for large PRs or PRs that touch
the
database in any way.
-->

## Added/updated tests?

- [ ] Yes
- [ ] No : _please replace this line with details on why tests
      have not been included_

## [optional] Are there any post deployment tasks we need to perform?
Release on PyPI & Github
2024-01-05 10:39:20 +11:00
Millun Atluri
59d2a012cd {release} v3.6.0rc3 2024-01-05 09:40:21 +11:00
Kent Keirsey
7e3b620830 Update README.md 2024-01-04 15:56:44 -05:00
psychedelicious
e16b55816f fix(ui): clarify comparison in usePanel 2024-01-05 07:09:37 +11:00
psychedelicious
895cb8637e fix(ui): fix panel resize bug
A bug that caused panels to be collapsed on a fresh indexedDb in was fixed in dd32c632cd, but this re-introduced a different bug that caused the panels to expand on window resize, if they were already collapsed.

Revert the previous change and instead add one imperative resize outside the observer, so that on startup, we set both panels to their minimum sizes.
2024-01-05 07:09:37 +11:00
Riccardo Giovanetti
fe5bceb1ed translationBot(ui): update translation (Italian)
Currently translated at 97.3% (1363 of 1400 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-01-05 07:04:04 +11:00
Mary Hipp
5d475a40f5 lint 2024-01-04 12:51:36 -05:00
Mary Hipp
bca7ea1674 option to override logo component 2024-01-04 12:51:36 -05:00
Mary Hipp
f27bb402fb render one or the other 2024-01-04 11:11:10 -05:00
Mary Hipp Rogers
dd32c632cd fix default panel width (#5403)
* fix default panel width

* lint

---------

Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local>
2024-01-04 11:04:21 -05:00
Mary Hipp Rogers
9e2e740033 custom components for nav, gallery header, and app info (#5400)
* replace custom header with custom nav component to go below settings

* add option for custom gallery header

* add option for custom app info text on logo hover

* add data-testid for tabs

* remove descriptions

* lint

* lint

---------

Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local>
2024-01-04 15:30:27 +00:00
psychedelicious
d6362ce0bd fix(ui): fix invalid nesting of button in button 2024-01-04 09:36:59 -05:00
psychedelicious
2347a00a70 fix(ui): do not show loading state on floating invoke button if disabled 2024-01-04 09:36:59 -05:00
psychedelicious
0b7dc721cf chore(ui): lint 2024-01-04 09:36:59 -05:00
psychedelicious
ac04a834ef feat(ui): tidy hotkeysmodal state 2024-01-04 09:36:59 -05:00
psychedelicious
bbca053b48 feat(ui): style settings modal 2024-01-04 09:36:59 -05:00
psychedelicious
fcf2006502 feat(ui): increase brightnesst of accordion title 2024-01-04 09:36:59 -05:00
psychedelicious
ac0d0019bd chore(ui): lint 2024-01-04 09:36:59 -05:00
psychedelicious
2d922a0a65 feat(ui): restore floating options and gallery buttons 2024-01-04 09:36:59 -05:00
psychedelicious
8db14911d7 feat(ui): give tooltips padding from screen edge
We can pass a popperjs modifier to the tooltip to give it this padding.
2024-01-04 09:36:59 -05:00
psychedelicious
01bab58b20 fix(ui): do not resize panel when window resizes if panel is collapsed 2024-01-04 09:36:59 -05:00
psychedelicious
7a57bc99cf feat(ui): statusindicator changes
We are now using the lefthand vertical strip for the settings menu button. This is a good place for the status indicator.

Really, we only need to display something *if there is a problem*. If the app is processing, the progress bar indicates that.

For the case where the panels are collapsed, I'll add the floating buttons back in some form, and we'll indicate via those if the app is processing something.
2024-01-04 09:36:59 -05:00
psychedelicious
d3b6d86e74 feat(ui): tweak badge styles 2024-01-04 09:36:59 -05:00
psychedelicious
360b6cb286 fix(ui): fix logo version tooltip 2024-01-04 09:36:59 -05:00
psychedelicious
8f9e9e639e fix(ui): fix hotkey key & untranslated string 2024-01-04 09:36:59 -05:00
psychedelicious
6930d8ba41 feat(ui): do not wrap expander content in box 2024-01-04 09:36:59 -05:00
psychedelicious
7ad74e680d feat(ui): make invexpander button styles less complex
just make it like a normal button - normal and hover state, no difference when its expanded. the icon clearly indicates this, and you see the extra components
2024-01-04 09:36:59 -05:00
psychedelicious
c56a6a4ddd feat(ui): make expander divider button, add hover, remove color
On one hand I like the color but on the other it makes this divider a focus point, which doesn't really makes sense to me. I tried several shades but think it adds a bit too much distraction for your eyes.
2024-01-04 09:36:59 -05:00
psychedelicious
afad764a00 feat(ui): make badges a bit paler
too stabby in the eye region
2024-01-04 09:36:59 -05:00
psychedelicious
49a72bd714 feat(ui): use wrench icon in settings menu for settings 2024-01-04 09:36:59 -05:00
psychedelicious
8cf14287b6 feat(ui): simplify App.tsx layout
There was an extra div, needed for the fullscreen file upload dropzone, that made styling the main app containers a bit awkward.

Refactor the uploader a bit to simplify this - no longer need so many app-level wrappers. Much cleaner.
2024-01-04 09:36:59 -05:00
blessedcoolant
0db47dd5e7 ui: Bolden text & add activation color for expanded state 2024-01-04 09:36:59 -05:00
blessedcoolant
71f6f77ae8 ui: Change background and padding of advanced settings 2024-01-04 09:36:59 -05:00
blessedcoolant
6f16229c41 fix: tone down the base color saturation by one step 2024-01-04 09:36:59 -05:00
blessedcoolant
0cc0d794d1 fix: Minor alignment issues with the queue badge 2024-01-04 09:36:59 -05:00
blessedcoolant
535639cb95 feat: Update status and progress colors to match new theme 2024-01-04 09:36:59 -05:00
blessedcoolant
2250bca8d9 feat: Remove Header
Remove header and incorporate everything else into the side bar and other areas
2024-01-04 09:36:59 -05:00
psychedelicious
4ce39a5974 fix(ui): remove unused icons 2024-01-04 13:59:25 +11:00
psychedelicious
644e9287f0 chore(ui): control adapter docstrings 2024-01-04 13:59:25 +11:00
psychedelicious
6a5e0be022 fix(ui): reduce minStepsBetweenThumbs for ca begin/end 2024-01-04 13:59:25 +11:00
psychedelicious
707f0f7091 feat(ui): update favicon to new logo 2024-01-04 13:59:25 +11:00
psychedelicious
8e709fe05a fix(ui): remove shift+enter to cancel
Whoops!
2024-01-04 13:59:25 +11:00
Hosted Weblate
154da609cb translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2024-01-04 01:19:31 +11:00
psychedelicious
21975d6268 chore(ui): lint 2024-01-03 09:09:50 -05:00
psychedelicious
31035b3e63 fix(ui): fix scaled bbox sliders
Removed logic related to aspect ratio from the components.

When the main bbox changes, if the scale method is auto, the reducers will handle the scaled bbox size appropriately.

Somehow linking up the manual mode to the aspect ratio is tricky, and instead of adding complexity for a rarely-used mode, I'm leaving manual mode as fully manual.
2024-01-03 09:09:50 -05:00
psychedelicious
6c05818887 fix(ui): workaround canvas weirdness with locked aspect ratio
Cannot figure out how to allow the bbox to be transformed when aspect ratio is locked from all handles. Only the bottom right handle works as expected.

As a workaround, when the aspect ratio is locked, you can only resize the bbox from the bottom right handle.
2024-01-03 09:09:50 -05:00
psychedelicious
77c5b051f0 fix(ui): clean up actionsDenylist 2024-01-03 09:09:50 -05:00
psychedelicious
4fdc4c15f9 feat(ui): add optimal size handling 2024-01-03 09:09:50 -05:00
psychedelicious
1a4be78013 fix(ui): make aspect ratio preview pixel-perfect size 2024-01-03 09:09:50 -05:00
psychedelicious
eb16ad3d6f fix(ui): remove old esc hotkey 2024-01-03 09:09:50 -05:00
psychedelicious
1fee08639d feat(ui): tweak board search UI 2024-01-03 09:09:50 -05:00
psychedelicious
7caaf40835 feat(ui): reworked hotkeys modal
- Displays all as list
- Uses chakra `Kbd` component for keys
- Provides search box
2024-01-03 09:09:50 -05:00
psychedelicious
6bfe994622 feat(ui): bump fontSize in fallback compoennt 2024-01-03 09:09:50 -05:00
psychedelicious
8a6f03cd46 feat(ui): improved panel interactions 2024-01-03 09:09:50 -05:00
psychedelicious
4ce9f9dc36 fix(ui): scaled bounding box uses canvas aspect ratio 2024-01-03 09:09:50 -05:00
362 changed files with 4044 additions and 4028 deletions

View File

@@ -1,10 +1,10 @@
<div align="center">
![project hero](https://github.com/invoke-ai/InvokeAI/assets/31807370/1a917d94-e099-4fa1-a70f-7dd8d0691018)
![project hero](https://github.com/invoke-ai/InvokeAI/assets/31807370/6e3728c7-e90e-4711-905c-3b55844ff5be)
# Invoke AI - Generative AI for Professional Creatives
## Professional Creative Tools for Stable Diffusion, Custom-Trained Models, and more.
To learn more about Invoke AI, get started instantly, or implement our Business solutions, visit [invoke.ai](https://invoke.ai)
# Invoke - Professional Creative AI Tools for Visual Media
## To learn more about Invoke, or implement our Business solutions, visit [invoke.com](https://www.invoke.com/about)
[![discord badge]][discord link]
@@ -56,7 +56,9 @@ the foundation for multiple commercial products.
<div align="center">
![canvas preview](https://github.com/invoke-ai/InvokeAI/raw/main/docs/assets/canvas_preview.png)
![Highlighted Features - Canvas and Workflows](https://github.com/invoke-ai/InvokeAI/assets/31807370/708f7a82-084f-4860-bfbe-e2588c53548d)
</div>

View File

@@ -1,5 +1,6 @@
# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654)
import contextlib
from contextlib import ExitStack
from functools import singledispatchmethod
from typing import List, Literal, Optional, Union
@@ -716,10 +717,23 @@ class DenoiseLatentsInvocation(BaseInvocation):
**self.unet.unet.model_dump(),
context=context,
)
# Prepare seamless context, if configured.
seamless_context = contextlib.nullcontext()
seamless_config = self.unet.seamless
if seamless_config is not None:
seamless_context = set_seamless(
model=unet_info.context.model,
axes=seamless_config.axes,
skipped_layers=seamless_config.skipped_layers,
skip_second_resnet=seamless_config.skip_second_resnet,
skip_conv2=seamless_config.skip_conv2,
)
with (
ExitStack() as exit_stack,
ModelPatcher.apply_freeu(unet_info.context.model, self.unet.freeu_config),
set_seamless(unet_info.context.model, self.unet.seamless_axes),
seamless_context,
unet_info as unet,
# Apply the LoRA after unet has been moved to its target device for faster patching.
ModelPatcher.apply_lora_unet(unet, _lora_loader()),
@@ -826,7 +840,19 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata):
context=context,
)
with set_seamless(vae_info.context.model, self.vae.seamless_axes), vae_info as vae:
# Prepare seamless context, if configured.
seamless_context = contextlib.nullcontext()
seamless_config = self.vae.seamless
if seamless_config is not None:
seamless_context = set_seamless(
model=vae_info.context.model,
axes=seamless_config.axes,
skipped_layers=seamless_config.skipped_layers,
skip_second_resnet=seamless_config.skip_second_resnet,
skip_conv2=seamless_config.skip_conv2,
)
with seamless_context, vae_info as vae:
latents = latents.to(vae.device)
if self.fp32:
vae.to(dtype=torch.float32)

View File

@@ -19,6 +19,13 @@ from .baseinvocation import (
)
class SeamlessSettings(BaseModel):
axes: List[str] = Field(description="Axes('x' and 'y') to which apply seamless")
skipped_layers: int = Field(description="How much down layers skip when applying seamless")
skip_second_resnet: bool = Field(description="Skip or not second resnet in down blocks when applying seamless")
skip_conv2: bool = Field(description="Skip or not conv2 in down blocks when applying seamless")
class ModelInfo(BaseModel):
model_name: str = Field(description="Info to load submodel")
base_model: BaseModelType = Field(description="Base model")
@@ -36,8 +43,8 @@ class UNetField(BaseModel):
unet: ModelInfo = Field(description="Info to load unet submodel")
scheduler: ModelInfo = Field(description="Info to load scheduler submodel")
loras: List[LoraInfo] = Field(description="Loras to apply on model loading")
seamless_axes: List[str] = Field(default_factory=list, description='Axes("x" and "y") to which apply seamless')
freeu_config: Optional[FreeUConfig] = Field(default=None, description="FreeU configuration")
seamless: Optional[SeamlessSettings] = Field(default=None, description="Seamless settings applied to model")
class ClipField(BaseModel):
@@ -50,7 +57,7 @@ class ClipField(BaseModel):
class VaeField(BaseModel):
# TODO: better naming?
vae: ModelInfo = Field(description="Info to load vae submodel")
seamless_axes: List[str] = Field(default_factory=list, description='Axes("x" and "y") to which apply seamless')
seamless: Optional[SeamlessSettings] = Field(default=None, description="Seamless settings applied to model")
@invocation_output("unet_output")
@@ -451,6 +458,11 @@ class SeamlessModeInvocation(BaseInvocation):
)
seamless_y: bool = InputField(default=True, input=Input.Any, description="Specify whether Y axis is seamless")
seamless_x: bool = InputField(default=True, input=Input.Any, description="Specify whether X axis is seamless")
skipped_layers: int = InputField(default=0, input=Input.Any, description="How much model's down layers to skip")
skip_second_resnet: bool = InputField(
default=True, input=Input.Any, description="Skip or not second resnet in down layers"
)
skip_conv2: bool = InputField(default=True, input=Input.Any, description="Skip or not conv2 in down layers")
def invoke(self, context: InvocationContext) -> SeamlessModeOutput:
# Conditionally append 'x' and 'y' based on seamless_x and seamless_y
@@ -465,9 +477,19 @@ class SeamlessModeInvocation(BaseInvocation):
seamless_axes_list.append("y")
if unet is not None:
unet.seamless_axes = seamless_axes_list
unet.seamless = SeamlessSettings(
axes=seamless_axes_list,
skipped_layers=self.skipped_layers,
skip_second_resnet=self.skip_second_resnet,
skip_conv2=self.skip_conv2,
)
if vae is not None:
vae.seamless_axes = seamless_axes_list
vae.seamless = SeamlessSettings(
axes=seamless_axes_list,
skipped_layers=self.skipped_layers,
skip_second_resnet=self.skip_second_resnet,
skip_conv2=self.skip_conv2,
)
return SeamlessModeOutput(unet=unet, vae=vae)

View File

@@ -25,71 +25,55 @@ def _conv_forward_asymmetric(self, input, weight, bias):
@contextmanager
def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axes: List[str]):
def set_seamless(
model: Union[UNet2DConditionModel, AutoencoderKL],
axes: List[str],
skipped_layers: int,
skip_second_resnet: bool,
skip_conv2: bool,
):
try:
to_restore = []
for m_name, m in model.named_modules():
if isinstance(model, UNet2DConditionModel):
if ".attentions." in m_name:
if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
continue
if isinstance(model, UNet2DConditionModel) and m_name.startswith("down_blocks.") and ".resnets." in m_name:
# down_blocks.1.resnets.1.conv1
_, block_num, _, resnet_num, submodule_name = m_name.split(".")
block_num = int(block_num)
resnet_num = int(resnet_num)
# if block_num >= seamless_down_blocks:
if block_num >= len(model.down_blocks) - skipped_layers:
continue
if ".resnets." in m_name:
if ".conv2" in m_name:
continue
if ".conv_shortcut" in m_name:
continue
"""
if isinstance(model, UNet2DConditionModel):
if False and ".upsamplers." in m_name:
if resnet_num > 0 and skip_second_resnet:
continue
if False and ".downsamplers." in m_name:
if submodule_name == "conv2" and skip_conv2:
continue
if True and ".resnets." in m_name:
if True and ".conv1" in m_name:
if False and "down_blocks" in m_name:
continue
if False and "mid_block" in m_name:
continue
if False and "up_blocks" in m_name:
continue
m.asymmetric_padding_mode = {}
m.asymmetric_padding = {}
m.asymmetric_padding_mode["x"] = "circular" if ("x" in axes) else "constant"
m.asymmetric_padding["x"] = (
m._reversed_padding_repeated_twice[0],
m._reversed_padding_repeated_twice[1],
0,
0,
)
m.asymmetric_padding_mode["y"] = "circular" if ("y" in axes) else "constant"
m.asymmetric_padding["y"] = (
0,
0,
m._reversed_padding_repeated_twice[2],
m._reversed_padding_repeated_twice[3],
)
if True and ".conv2" in m_name:
continue
if True and ".conv_shortcut" in m_name:
continue
if True and ".attentions." in m_name:
continue
if False and m_name in ["conv_in", "conv_out"]:
continue
"""
if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
m.asymmetric_padding_mode = {}
m.asymmetric_padding = {}
m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant"
m.asymmetric_padding["x"] = (
m._reversed_padding_repeated_twice[0],
m._reversed_padding_repeated_twice[1],
0,
0,
)
m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant"
m.asymmetric_padding["y"] = (
0,
0,
m._reversed_padding_repeated_twice[2],
m._reversed_padding_repeated_twice[3],
)
to_restore.append((m, m._conv_forward))
m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d)
to_restore.append((m, m._conv_forward))
m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d)
yield

View File

@@ -44,7 +44,7 @@ def torch_dtype(device: torch.device) -> torch.dtype:
if config.full_precision:
return torch.float32
if choose_precision(device) == "float16":
return torch.float16
return torch.bfloat16 if device.type == "cuda" else torch.float16
else:
return torch.float32

View File

@@ -1,24 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="shortcut icon" type="icon" href="favicon.ico" />
<style>
html,
body {
padding: 0;
margin: 0;
}
</style>
</head>
<body dir="ltr">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="mask-icon" href="/invoke-key-ylw-sm.svg" color="#E6FD13" sizes="any" />
<link rel="icon" href="/invoke-key-char-on-ylw.svg" />
<style>
html,
body {
padding: 0;
margin: 0;
}
</style>
</head>
<body dir="ltr">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -68,7 +68,7 @@
"@fontsource-variable/inter": "^5.0.16",
"@mantine/form": "6.0.21",
"@nanostores/react": "^0.7.1",
"@reduxjs/toolkit": "^2.0.1",
"@reduxjs/toolkit": "2.0.1",
"@roarr/browser-log-writer": "^1.3.0",
"chakra-react-select": "^4.7.6",
"compare-versions": "^6.1.0",
@@ -94,7 +94,7 @@
"react-i18next": "^14.0.0",
"react-icons": "^4.12.0",
"react-konva": "^18.2.10",
"react-redux": "^9.0.4",
"react-redux": "9.0.4",
"react-resizable-panels": "^1.0.7",
"react-select": "5.8.0",
"react-textarea-autosize": "^8.5.3",
@@ -167,5 +167,10 @@
"vite-plugin-dts": "^3.7.0",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.3"
},
"pnpm": {
"patchedDependencies": {
"reselect@5.0.1": "patches/reselect@5.0.1.patch"
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,11 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
reselect@5.0.1:
hash: kvbgwzjyy4x4fnh7znyocvb75q
path: patches/reselect@5.0.1.patch
dependencies:
'@chakra-ui/anatomy':
specifier: ^2.2.2
@@ -54,7 +59,7 @@ dependencies:
specifier: ^0.7.1
version: 0.7.1(nanostores@0.9.5)(react@18.2.0)
'@reduxjs/toolkit':
specifier: ^2.0.1
specifier: 2.0.1
version: 2.0.1(react-redux@9.0.4)(react@18.2.0)
'@roarr/browser-log-writer':
specifier: ^1.3.0
@@ -132,7 +137,7 @@ dependencies:
specifier: ^18.2.10
version: 18.2.10(konva@9.3.0)(react-dom@18.2.0)(react@18.2.0)
react-redux:
specifier: ^9.0.4
specifier: 9.0.4
version: 9.0.4(@types/react@18.2.46)(react@18.2.0)(redux@5.0.1)
react-resizable-panels:
specifier: ^1.0.7
@@ -4565,7 +4570,7 @@ packages:
react-redux: 9.0.4(@types/react@18.2.46)(react@18.2.0)(redux@5.0.1)
redux: 5.0.1
redux-thunk: 3.1.0(redux@5.0.1)
reselect: 5.0.1
reselect: 5.0.1(patch_hash=kvbgwzjyy4x4fnh7znyocvb75q)
dev: false
/@roarr/browser-log-writer@1.3.0:
@@ -11932,9 +11937,10 @@ packages:
hasBin: true
dev: true
/reselect@5.0.1:
/reselect@5.0.1(patch_hash=kvbgwzjyy4x4fnh7znyocvb75q):
resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==}
dev: false
patched: true
/resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="#E6FD13"/>
<path d="M19.2378 10.9H25V7H7V10.9H12.7622L19.2378 21.1H25V25H7V21.1H12.7622" stroke="#181818" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 272 B

View File

@@ -83,10 +83,6 @@
"title": "خيارات التثبيت",
"desc": "ثبت لوحة الخيارات"
},
"toggleViewer": {
"title": "تبديل العارض",
"desc": "فتح وإغلاق مشاهد الصور"
},
"toggleGallery": {
"title": "تبديل المعرض",
"desc": "فتح وإغلاق درابزين المعرض"
@@ -147,10 +143,6 @@
"title": "الصورة التالية",
"desc": "عرض الصورة التالية في الصالة"
},
"toggleGalleryPin": {
"title": "تبديل تثبيت الصالة",
"desc": "يثبت ويفتح تثبيت الصالة على الواجهة الرسومية"
},
"increaseGalleryThumbSize": {
"title": "زيادة حجم صورة الصالة",
"desc": "يزيد حجم الصور المصغرة في الصالة"

View File

@@ -168,10 +168,6 @@
"title": "Optionen anheften",
"desc": "Anheften des Optionsfeldes"
},
"toggleViewer": {
"title": "Bildbetrachter umschalten",
"desc": "Bildbetrachter öffnen und schließen"
},
"toggleGallery": {
"title": "Galerie umschalten",
"desc": "Öffnen und Schließen des Galerie-Schubfachs"
@@ -232,10 +228,6 @@
"title": "Nächstes Bild",
"desc": "Nächstes Bild in Galerie anzeigen"
},
"toggleGalleryPin": {
"title": "Galerie anheften umschalten",
"desc": "Heftet die Galerie an die Benutzeroberfläche bzw. löst die sie"
},
"increaseGalleryThumbSize": {
"title": "Größe der Galeriebilder erhöhen",
"desc": "Vergrößert die Galerie-Miniaturansichten"

View File

@@ -86,6 +86,7 @@
"copyError": "$t(gallery.copy) Error",
"close": "Close",
"on": "On",
"or": "or",
"checkpoint": "Checkpoint",
"communityLabel": "Community",
"controlNet": "ControlNet",
@@ -156,6 +157,7 @@
"save": "Save",
"saveAs": "Save As",
"settingsLabel": "Settings",
"preferencesLabel": "Preferences",
"simple": "Simple",
"somethingWentWrong": "Something went wrong",
"statusConnected": "Connected",
@@ -426,6 +428,9 @@
"problemDeletingImagesDesc": "One or more images could not be deleted"
},
"hotkeys": {
"searchHotkeys": "Search Hotkeys",
"clearSearch": "Clear Search",
"noHotkeysFound": "No Hotkeys Found",
"acceptStagingImage": {
"desc": "Accept Current Staging Area Image",
"title": "Accept Staging Image"
@@ -434,7 +439,7 @@
"desc": "Opens the add node menu",
"title": "Add Nodes"
},
"appHotkeys": "App Hotkeys",
"appHotkeys": "App",
"cancel": {
"desc": "Cancel current queue item",
"title": "Cancel"
@@ -499,8 +504,8 @@
"desc": "Focus the prompt input area",
"title": "Focus Prompt"
},
"galleryHotkeys": "Gallery Hotkeys",
"generalHotkeys": "General Hotkeys",
"galleryHotkeys": "Gallery",
"generalHotkeys": "General",
"hideMask": {
"desc": "Hide and unhide mask",
"title": "Hide Mask"
@@ -521,7 +526,7 @@
"desc": "Generate an image",
"title": "Invoke"
},
"keyboardShortcuts": "Keyboard Shortcuts",
"keyboardShortcuts": "Hotkeys",
"maximizeWorkSpace": {
"desc": "Close panels and maximize work area",
"title": "Maximize Workspace"
@@ -542,7 +547,7 @@
"desc": "Next Staging Area Image",
"title": "Next Staging Image"
},
"nodesHotkeys": "Nodes Hotkeys",
"nodesHotkeys": "Nodes",
"pinOptions": {
"desc": "Pin the options panel",
"title": "Pin Options"
@@ -611,31 +616,31 @@
"desc": "Open and close the gallery drawer",
"title": "Toggle Gallery"
},
"toggleGalleryPin": {
"desc": "Pins and unpins the gallery to the UI",
"title": "Toggle Gallery Pin"
"toggleOptions": {
"desc": "Open and close the options panel",
"title": "Toggle Options"
},
"toggleOptionsAndGallery": {
"desc": "Open and close the options and gallery panels",
"title": "Toggle Options and Gallery"
},
"resetOptionsAndGallery": {
"desc": "Resets the options and gallery panels",
"title": "Reset Options and Gallery"
},
"toggleLayer": {
"desc": "Toggles mask/base layer selection",
"title": "Toggle Layer"
},
"toggleOptions": {
"desc": "Open and close the options panel",
"title": "Toggle Options"
},
"toggleSnap": {
"desc": "Toggles Snap to Grid",
"title": "Toggle Snap"
},
"toggleViewer": {
"desc": "Open and close Image Viewer",
"title": "Toggle Viewer"
},
"undoStroke": {
"desc": "Undo a brush stroke",
"title": "Undo Stroke"
},
"unifiedCanvasHotkeys": "Unified Canvas Hotkeys",
"unifiedCanvasHotkeys": "Unified Canvas",
"upscale": {
"desc": "Upscale the current image",
"title": "Upscale"
@@ -1076,6 +1081,11 @@
"aspect": "Aspect",
"aspectRatio": "Aspect Ratio",
"aspectRatioFree": "Free",
"lockAspectRatio": "Lock Aspect Ratio",
"swapDimensions": "Swap Dimensions",
"setToOptimalSize": "Optimize size for model",
"setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (may be too small)",
"setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (may be too large)",
"boundingBoxHeader": "Bounding Box",
"boundingBoxHeight": "Bounding Box Height",
"boundingBoxWidth": "Bounding Box Width",

View File

@@ -127,10 +127,6 @@
"title": "Fijar opciones",
"desc": "Fijar el panel de opciones"
},
"toggleViewer": {
"title": "Alternar visor",
"desc": "Mostar y ocultar el visor de imágenes"
},
"toggleGallery": {
"title": "Alternar galería",
"desc": "Mostar y ocultar la galería de imágenes"
@@ -191,10 +187,6 @@
"title": "Imagen siguiente",
"desc": "Muetra la imagen siguiente en la galería"
},
"toggleGalleryPin": {
"title": "Alternar fijado de galería",
"desc": "Fijar o desfijar la galería en la interfaz"
},
"increaseGalleryThumbSize": {
"title": "Aumentar imagen en galería",
"desc": "Aumenta el tamaño de las miniaturas de la galería"

View File

@@ -96,10 +96,6 @@
"title": "Epinglage des options",
"desc": "Epingler le panneau d'options"
},
"toggleViewer": {
"title": "Affichage de la visionneuse",
"desc": "Afficher et masquer la visionneuse d'image"
},
"toggleGallery": {
"title": "Affichage de la galerie",
"desc": "Afficher et masquer la galerie"
@@ -160,10 +156,6 @@
"title": "Image suivante",
"desc": "Afficher l'image suivante dans la galerie"
},
"toggleGalleryPin": {
"title": "Activer/désactiver l'épinglage de la galerie",
"desc": "Épingle ou dépingle la galerie à l'interface"
},
"increaseGalleryThumbSize": {
"title": "Augmenter la taille des miniatures de la galerie",
"desc": "Augmente la taille des miniatures de la galerie"

View File

@@ -192,10 +192,6 @@
"title": "הצמד הגדרות",
"desc": "הצמד את פאנל ההגדרות"
},
"toggleViewer": {
"title": "הצג את חלון ההצגה",
"desc": "פתח וסגור את מציג התמונות"
},
"changeTabs": {
"title": "החלף לשוניות",
"desc": "החלף לאיזור עבודה אחר"
@@ -236,10 +232,6 @@
"title": "תמונה קודמת",
"desc": "הצג את התמונה הקודמת בגלריה"
},
"toggleGalleryPin": {
"title": "הצג את מצמיד הגלריה",
"desc": "הצמדה וביטול הצמדה של הגלריה לממשק המשתמש"
},
"decreaseGalleryThumbSize": {
"title": "הקטנת גודל תמונת גלריה",
"desc": "מקטין את גודל התמונות הממוזערות של הגלריה"

View File

@@ -114,7 +114,9 @@
"nextPage": "Pagina successiva",
"saveAs": "Salva come",
"unsaved": "Non salvato",
"direction": "Direzione"
"direction": "Direzione",
"advancedOptions": "Opzioni avanzate",
"free": "Libero"
},
"gallery": {
"generations": "Generazioni",
@@ -132,7 +134,7 @@
"noImagesInGallery": "Nessuna immagine da visualizzare",
"deleteImage": "Elimina l'immagine",
"deleteImagePermanent": "Le immagini eliminate non possono essere ripristinate.",
"deleteImageBin": "Le immagini eliminate verranno spostate nel Cestino del tuo sistema operativo.",
"deleteImageBin": "Le immagini eliminate verranno spostate nel cestino del tuo sistema operativo.",
"assets": "Risorse",
"autoAssignBoardOnClick": "Assegna automaticamente la bacheca al clic",
"featuresWillReset": "Se elimini questa immagine, quelle funzionalità verranno immediatamente ripristinate.",
@@ -157,18 +159,18 @@
"problemDeletingImages": "Problema durante l'eliminazione delle immagini"
},
"hotkeys": {
"keyboardShortcuts": "Tasti rapidi",
"appHotkeys": "Tasti di scelta rapida dell'applicazione",
"generalHotkeys": "Tasti di scelta rapida generali",
"galleryHotkeys": "Tasti di scelta rapida della galleria",
"unifiedCanvasHotkeys": "Tasti di scelta rapida Tela Unificata",
"keyboardShortcuts": "Tasti di scelta rapida",
"appHotkeys": "Applicazione",
"generalHotkeys": "Generale",
"galleryHotkeys": "Galleria",
"unifiedCanvasHotkeys": "Tela Unificata",
"invoke": {
"title": "Invoke",
"desc": "Genera un'immagine"
},
"cancel": {
"title": "Annulla",
"desc": "Annulla la generazione dell'immagine"
"desc": "Annulla l'elemento della coda corrente"
},
"focusPrompt": {
"title": "Metti a fuoco il Prompt",
@@ -182,12 +184,8 @@
"title": "Appunta le opzioni",
"desc": "Blocca il pannello delle opzioni"
},
"toggleViewer": {
"title": "Attiva/disattiva visualizzatore",
"desc": "Apre e chiude il visualizzatore immagini"
},
"toggleGallery": {
"title": "Attiva/disattiva Galleria",
"title": "Attiva/disattiva galleria",
"desc": "Apre e chiude il pannello della galleria"
},
"maximizeWorkSpace": {
@@ -246,10 +244,6 @@
"title": "Immagine successiva",
"desc": "Visualizza l'immagine successiva nella galleria"
},
"toggleGalleryPin": {
"title": "Attiva/disattiva il blocco della galleria",
"desc": "Blocca/sblocca la galleria dall'interfaccia utente"
},
"increaseGalleryThumbSize": {
"title": "Aumenta dimensione immagini nella galleria",
"desc": "Aumenta la dimensione delle miniature della galleria"
@@ -362,11 +356,26 @@
"title": "Accetta l'immagine della sessione",
"desc": "Accetta l'immagine dell'area della sessione corrente"
},
"nodesHotkeys": "Tasti di scelta rapida dei Nodi",
"nodesHotkeys": "Nodi",
"addNodes": {
"title": "Aggiungi Nodi",
"desc": "Apre il menu Aggiungi Nodi"
}
},
"cancelAndClear": {
"desc": "Annulla l'elemento della coda corrente e cancella tutti gli elementi in sospeso",
"title": "Annulla e cancella"
},
"resetOptionsAndGallery": {
"title": "Ripristina Opzioni e Galleria",
"desc": "Reimposta le opzioni e i pannelli della galleria"
},
"searchHotkeys": "Cerca tasti di scelta rapida",
"noHotkeysFound": "Nessun tasto di scelta rapida trovato",
"toggleOptionsAndGallery": {
"desc": "Apre e chiude le opzioni e i pannelli della galleria",
"title": "Attiva/disattiva le Opzioni e la Galleria"
},
"clearSearch": "Cancella ricerca"
},
"modelManager": {
"modelManager": "Gestione Modelli",
@@ -581,8 +590,8 @@
"hidePreview": "Nascondi l'anteprima",
"showPreview": "Mostra l'anteprima",
"noiseSettings": "Rumore",
"seamlessXAxis": "Asse X",
"seamlessYAxis": "Asse Y",
"seamlessXAxis": "Piastrella senza cucitura Asse X",
"seamlessYAxis": "Piastrella senza cucitura Asse Y",
"scheduler": "Campionatore",
"boundingBoxWidth": "Larghezza riquadro di delimitazione",
"boundingBoxHeight": "Altezza riquadro di delimitazione",
@@ -642,7 +651,14 @@
"unmasked": "No maschera",
"cfgRescaleMultiplier": "Moltiplicatore riscala CFG",
"cfgRescale": "Riscala CFG",
"useSize": "Usa Dimensioni"
"useSize": "Usa Dimensioni",
"setToOptimalSize": "Ottimizza le dimensioni per il modello",
"setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (potrebbe essere troppo piccolo)",
"imageSize": "Dimensione dell'immagine",
"lockAspectRatio": "Blocca proporzioni",
"swapDimensions": "Scambia dimensioni",
"aspect": "Aspetto",
"setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (potrebbe essere troppo grande)"
},
"settings": {
"models": "Modelli",
@@ -1213,12 +1229,13 @@
"minConfidence": "Confidenza minima",
"scribble": "Scribble",
"amult": "Angolo di illuminazione",
"coarse": "Approssimativo"
"coarse": "Approssimativo",
"resizeSimple": "Ridimensiona (semplice)"
},
"queue": {
"queueFront": "Aggiungi all'inizio della coda",
"queueBack": "Aggiungi alla coda",
"queueCountPrediction": "Aggiungi {{predicted}} alla coda",
"queueCountPrediction": "{{promptsCount}} prompt × {{iterations}} iterazioni -> {{count}} generazioni",
"queue": "Coda",
"status": "Stato",
"pruneSucceeded": "Rimossi {{item_count}} elementi completati dalla coda",
@@ -1276,7 +1293,8 @@
"graphFailedToQueue": "Impossibile mettere in coda il grafico",
"queueMaxExceeded": "È stato superato il limite massimo di {{max_queue_size}} e {{skip}} elementi verrebbero saltati",
"batchFieldValues": "Valori Campi Lotto",
"time": "Tempo"
"time": "Tempo",
"openQueue": "Apri coda"
},
"embedding": {
"noMatchingEmbedding": "Nessun Incorporamento corrispondente",
@@ -1296,7 +1314,12 @@
"noLoRAsInstalled": "Nessun LoRA installato",
"esrganModel": "Modello ESRGAN",
"addLora": "Aggiungi LoRA",
"noLoRAsLoaded": "Nessuna LoRA caricata"
"noLoRAsLoaded": "Nessun LoRA caricato",
"noMainModelSelected": "Nessun modello principale selezionato",
"allLoRAsAdded": "Tutti i LoRA aggiunti",
"defaultVAE": "VAE predefinito",
"incompatibleBaseModel": "Modello base incompatibile",
"loraAlreadyAdded": "LoRA già aggiunto"
},
"invocationCache": {
"disable": "Disabilita",
@@ -1330,7 +1353,9 @@
"promptsWithCount_many": "{{count}} Prompt",
"promptsWithCount_other": "{{count}} Prompt",
"dynamicPrompts": "Prompt dinamici",
"promptsPreview": "Anteprima dei prompt"
"promptsPreview": "Anteprima dei prompt",
"showDynamicPrompts": "Mostra prompt dinamici",
"loading": "Generazione prompt dinamici..."
},
"popovers": {
"paramScheduler": {
@@ -1556,7 +1581,7 @@
"scheduler": "Campionatore",
"noModelsAvailable": "Nessun modello disponibile",
"denoisingStrength": "Forza di riduzione del rumore",
"concatPromptStyle": "Concatena Prompt & Stile",
"concatPromptStyle": "Collega Prompt & Stile",
"loading": "Caricamento...",
"steps": "Passi",
"refinerStart": "Inizio Affinamento",
@@ -1567,7 +1592,8 @@
"useRefiner": "Utilizza l'affinatore",
"refinermodel": "Modello Affinatore",
"posAestheticScore": "Punteggio estetico positivo",
"posStylePrompt": "Prompt Stile positivo"
"posStylePrompt": "Prompt Stile positivo",
"freePromptStyle": "Prompt di stile manuale"
},
"metadata": {
"initImage": "Immagine iniziale",
@@ -1640,5 +1666,28 @@
},
"app": {
"storeNotInitialized": "Il negozio non è inizializzato"
},
"accordions": {
"compositing": {
"infillTab": "Riempimento",
"coherenceTab": "Passaggio di coerenza",
"title": "Composizione"
},
"control": {
"controlAdaptersTab": "Adattatori di Controllo",
"ipTab": "Prompt immagine",
"title": "Controllo"
},
"generation": {
"title": "Generazione",
"conceptsTab": "Concetti",
"modelTab": "Modello"
},
"advanced": {
"title": "Avanzate"
},
"image": {
"title": "Immagine"
}
}
}

View File

@@ -133,10 +133,6 @@
"title": "ピン",
"desc": "オプションパネルを固定"
},
"toggleViewer": {
"title": "ビュワーのトグル",
"desc": "ビュワーを開閉"
},
"toggleGallery": {
"title": "ギャラリーのトグル",
"desc": "ギャラリードロワーの開閉"
@@ -197,10 +193,6 @@
"title": "次の画像",
"desc": "ギャラリー内の1つ後の画像を表示"
},
"toggleGalleryPin": {
"title": "ギャラリードロワーの固定",
"desc": "ギャラリーをUIにピン留め/解除"
},
"increaseGalleryThumbSize": {
"title": "ギャラリーの画像を拡大",
"desc": "ギャラリーのサムネイル画像を拡大"

View File

@@ -407,10 +407,6 @@
"maxFaces": "Max Faces"
},
"hotkeys": {
"toggleGalleryPin": {
"title": "Gallery Pin 전환",
"desc": "갤러리를 UI에 고정했다가 풉니다"
},
"toggleSnap": {
"desc": "Snap을 Grid로 전환",
"title": "Snap 전환"
@@ -600,10 +596,6 @@
"desc": "노드 추가 메뉴 열기",
"title": "노드 추가"
},
"toggleViewer": {
"desc": "이미지 뷰어 열기 및 닫기",
"title": "Viewer 전환"
},
"undoStroke": {
"title": "Stroke 실행 취소",
"desc": "brush stroke 실행 취소"

View File

@@ -148,10 +148,6 @@
"title": "Zet Opties vast",
"desc": "Zet het deelscherm Opties vast"
},
"toggleViewer": {
"title": "Zet Viewer vast",
"desc": "Opent of sluit Afbeeldingsviewer"
},
"toggleGallery": {
"title": "Zet Galerij vast",
"desc": "Opent of sluit het deelscherm Galerij"
@@ -212,10 +208,6 @@
"title": "Volgende afbeelding",
"desc": "Toont de volgende afbeelding in de galerij"
},
"toggleGalleryPin": {
"title": "Zet galerij vast/los",
"desc": "Zet de galerij vast of los aan de gebruikersinterface"
},
"increaseGalleryThumbSize": {
"title": "Vergroot afbeeldingsgrootte galerij",
"desc": "Vergroot de grootte van de galerijminiaturen"

View File

@@ -86,10 +86,6 @@
"title": "Przypnij opcje",
"desc": "Przypina panel opcji"
},
"toggleViewer": {
"title": "Przełącz podgląd",
"desc": "Otwiera lub zamyka widok podglądu"
},
"toggleGallery": {
"title": "Przełącz galerię",
"desc": "Wysuwa lub chowa galerię"
@@ -150,10 +146,6 @@
"title": "Następny obraz",
"desc": "Aktywuje następny obraz z galerii"
},
"toggleGalleryPin": {
"title": "Przypnij galerię",
"desc": "Przypina lub odpina widok galerii"
},
"increaseGalleryThumbSize": {
"title": "Powiększ obrazy",
"desc": "Powiększa rozmiar obrazów w galerii"

View File

@@ -81,10 +81,6 @@
"hotkeys": {
"generalHotkeys": "Atalhos Gerais",
"galleryHotkeys": "Atalhos da Galeria",
"toggleViewer": {
"title": "Ativar Visualizador",
"desc": "Abrir e fechar o Visualizador de Imagens"
},
"maximizeWorkSpace": {
"desc": "Fechar painéis e maximixar área de trabalho",
"title": "Maximizar a Área de Trabalho"
@@ -232,10 +228,6 @@
"title": "Apagar Imagem",
"desc": "Apaga a imagem atual"
},
"toggleGalleryPin": {
"title": "Ativar Fixar Galeria",
"desc": "Fixa e desafixa a galeria na interface"
},
"increaseGalleryThumbSize": {
"title": "Aumentar Tamanho da Galeria de Imagem",
"desc": "Aumenta o tamanho das thumbs na galeria"

View File

@@ -103,10 +103,6 @@
"title": "Fixar Opções",
"desc": "Fixar o painel de opções"
},
"toggleViewer": {
"title": "Ativar Visualizador",
"desc": "Abrir e fechar o Visualizador de Imagens"
},
"toggleGallery": {
"title": "Ativar Galeria",
"desc": "Abrir e fechar a gaveta da galeria"
@@ -167,10 +163,6 @@
"title": "Próxima Imagem",
"desc": "Mostra a próxima imagem na galeria"
},
"toggleGalleryPin": {
"title": "Ativar Fixar Galeria",
"desc": "Fixa e desafixa a galeria na interface"
},
"increaseGalleryThumbSize": {
"title": "Aumentar Tamanho da Galeria de Imagem",
"desc": "Aumenta o tamanho das thumbs na galeria"

View File

@@ -189,10 +189,6 @@
"title": "Закрепить параметры",
"desc": "Закрепить панель параметров"
},
"toggleViewer": {
"title": "Показать просмотр",
"desc": "Открывать и закрывать просмотрщик изображений"
},
"toggleGallery": {
"title": "Показать галерею",
"desc": "Открывать и закрывать ящик галереи"
@@ -253,10 +249,6 @@
"title": "Следующее изображение",
"desc": "Отображение следующего изображения в галерее"
},
"toggleGalleryPin": {
"title": "Закрепить галерею",
"desc": "Закрепляет и открепляет галерею"
},
"increaseGalleryThumbSize": {
"title": "Увеличить размер миниатюр галереи",
"desc": "Увеличивает размер миниатюр галереи"

View File

@@ -129,10 +129,6 @@
"title": "Växla inställningar",
"desc": "Öppna och stäng alternativpanelen"
},
"toggleViewer": {
"title": "Växla visaren",
"desc": "Öppna och stäng bildvisaren"
},
"toggleGallery": {
"title": "Växla galleri",
"desc": "Öppna eller stäng galleribyrån"
@@ -193,10 +189,6 @@
"title": "Nästa bild",
"desc": "Visa nästa bild"
},
"toggleGalleryPin": {
"title": "Växla gallerinål",
"desc": "Nålar fast eller nålar av galleriet i gränssnittet"
},
"increaseGalleryThumbSize": {
"title": "Förstora galleriets bildstorlek",
"desc": "Förstora miniatyrbildernas storlek"

View File

@@ -111,10 +111,6 @@
"title": "Закріпити параметри",
"desc": "Закріпити панель параметрів"
},
"toggleViewer": {
"title": "Показати перегляд",
"desc": "Відкривати і закривати переглядач зображень"
},
"toggleGallery": {
"title": "Показати галерею",
"desc": "Відкривати і закривати скриньку галереї"
@@ -175,10 +171,6 @@
"title": "Наступне зображення",
"desc": "Відображення наступного зображення в галереї"
},
"toggleGalleryPin": {
"title": "Закріпити галерею",
"desc": "Закріплює і відкріплює галерею"
},
"increaseGalleryThumbSize": {
"title": "Збільшити розмір мініатюр галереї",
"desc": "Збільшує розмір мініатюр галереї"

View File

@@ -189,10 +189,6 @@
"title": "常开选项卡",
"desc": "保持选项浮窗常开"
},
"toggleViewer": {
"title": "切换图像查看器",
"desc": "打开或关闭图像查看器"
},
"toggleGallery": {
"title": "切换图库",
"desc": "打开或关闭图库"
@@ -253,10 +249,6 @@
"title": "下一张图像",
"desc": "显示图库中的下一张图像"
},
"toggleGalleryPin": {
"title": "切换图库常开",
"desc": "开关图库在界面中的常开模式"
},
"increaseGalleryThumbSize": {
"title": "增大预览尺寸",
"desc": "增大图库中预览的尺寸"

View File

@@ -1,22 +1,21 @@
import { Flex, Grid } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { Box } from '@chakra-ui/react';
import { useSocketIO } from 'app/hooks/useSocketIO';
import { useLogger } from 'app/logging/useLogger';
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
import { $headerComponent } from 'app/store/nanostores/headerComponent';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { PartialAppConfig } from 'app/types/invokeai';
import ImageUploader from 'common/components/ImageUploader';
import ImageUploadOverlay from 'common/components/ImageUploadOverlay';
import { useClearStorage } from 'common/hooks/useClearStorage';
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { useGlobalModifiersInit } from 'common/hooks/useGlobalModifiers';
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
import SiteHeader from 'features/system/components/SiteHeader';
import { configChanged } from 'features/system/store/configSlice';
import { languageSelector } from 'features/system/store/systemSelectors';
import InvokeTabs from 'features/ui/components/InvokeTabs';
import { AnimatePresence } from 'framer-motion';
import i18n from 'i18n';
import { size } from 'lodash-es';
import { memo, useCallback, useEffect } from 'react';
@@ -47,6 +46,9 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
useGlobalModifiersInit();
useGlobalHotkeys();
const { dropzone, isHandlingUpload, setIsHandlingUpload } =
useFullscreenDropzone();
const handleReset = useCallback(() => {
clearStorage();
location.reload();
@@ -68,23 +70,30 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
dispatch(appStarted());
}, [dispatch]);
const headerComponent = useStore($headerComponent);
return (
<ErrorBoundary
onReset={handleReset}
FallbackComponent={AppErrorBoundaryFallback}
>
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
<ImageUploader>
<Grid p={4} gridAutoRows="min-content auto" w="full" h="full">
{headerComponent || <SiteHeader />}
<Flex gap={4} w="full" h="full">
<InvokeTabs />
</Flex>
</Grid>
</ImageUploader>
</Grid>
<Box
id="invoke-app-wrapper"
w="100vw"
h="100vh"
position="relative"
overflow="hidden"
{...dropzone.getRootProps()}
>
<input {...dropzone.getInputProps()} />
<InvokeTabs />
<AnimatePresence>
{dropzone.isDragActive && isHandlingUpload && (
<ImageUploadOverlay
dropzone={dropzone}
setIsHandlingUpload={setIsHandlingUpload}
/>
)}
</AnimatePresence>
</Box>
<DeleteImageModal />
<ChangeBoardModal />
<DynamicPromptsModal />

View File

@@ -4,10 +4,12 @@ import type { Middleware } from '@reduxjs/toolkit';
import { $socketOptions } from 'app/hooks/useSocketIO';
import { $authToken } from 'app/store/nanostores/authToken';
import { $baseUrl } from 'app/store/nanostores/baseUrl';
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
import type { CustomStarUi } from 'app/store/nanostores/customStarUI';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { $headerComponent } from 'app/store/nanostores/headerComponent';
import { $galleryHeader } from 'app/store/nanostores/galleryHeader';
import { $isDebugging } from 'app/store/nanostores/isDebugging';
import { $logo } from 'app/store/nanostores/logo';
import { $projectId } from 'app/store/nanostores/projectId';
import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId';
import { $store } from 'app/store/nanostores/store';
@@ -28,9 +30,10 @@ interface Props extends PropsWithChildren {
apiUrl?: string;
token?: string;
config?: PartialAppConfig;
headerComponent?: ReactNode;
customNavComponent?: ReactNode;
middleware?: Middleware[];
projectId?: string;
galleryHeader?: ReactNode;
queueId?: string;
selectedImage?: {
imageName: string;
@@ -39,20 +42,23 @@ interface Props extends PropsWithChildren {
customStarUi?: CustomStarUi;
socketOptions?: Partial<ManagerOptions & SocketOptions>;
isDebugging?: boolean;
logo?: ReactNode;
}
const InvokeAIUI = ({
apiUrl,
token,
config,
headerComponent,
customNavComponent,
middleware,
projectId,
galleryHeader,
queueId,
selectedImage,
customStarUi,
socketOptions,
isDebugging = false,
logo,
}: Props) => {
useEffect(() => {
// configure API client token
@@ -108,14 +114,34 @@ const InvokeAIUI = ({
}, [customStarUi]);
useEffect(() => {
if (headerComponent) {
$headerComponent.set(headerComponent);
if (customNavComponent) {
$customNavComponent.set(customNavComponent);
}
return () => {
$headerComponent.set(undefined);
$customNavComponent.set(undefined);
};
}, [headerComponent]);
}, [customNavComponent]);
useEffect(() => {
if (galleryHeader) {
$galleryHeader.set(galleryHeader);
}
return () => {
$galleryHeader.set(undefined);
};
}, [galleryHeader]);
useEffect(() => {
if (logo) {
$logo.set(logo);
}
return () => {
$logo.set(undefined);
};
}, [logo]);
useEffect(() => {
if (socketOptions) {

View File

@@ -11,7 +11,7 @@ import { memo, useCallback, useEffect } from 'react';
*/
const Toaster = () => {
const dispatch = useAppDispatch();
const toastQueue = useAppSelector((state) => state.system.toastQueue);
const toastQueue = useAppSelector((s) => s.system.toastQueue);
const toast = useToast();
useEffect(() => {
toastQueue.forEach((t) => {

View File

@@ -1,6 +1,4 @@
import { createLogWriter } from '@roarr/browser-log-writer';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { useEffect, useMemo } from 'react';
import { ROARR, Roarr } from 'roarr';
@@ -8,17 +6,9 @@ import { ROARR, Roarr } from 'roarr';
import type { LoggerNamespace } from './logger';
import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP, logger } from './logger';
const selector = createMemoizedSelector(stateSelector, ({ system }) => {
const { consoleLogLevel, shouldLogToConsole } = system;
return {
consoleLogLevel,
shouldLogToConsole,
};
});
export const useLogger = (namespace: LoggerNamespace) => {
const { consoleLogLevel, shouldLogToConsole } = useAppSelector(selector);
const consoleLogLevel = useAppSelector((s) => s.system.consoleLogLevel);
const shouldLogToConsole = useAppSelector((s) => s.system.shouldLogToConsole);
// The provided Roarr browser log writer uses localStorage to config logging to console
useEffect(() => {

View File

@@ -1,4 +1,9 @@
import { createSelectorCreator, lruMemoize } from '@reduxjs/toolkit';
import {
createDraftSafeSelectorCreator,
createSelectorCreator,
lruMemoize,
} from '@reduxjs/toolkit';
import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors';
import { isEqual } from 'lodash-es';
/**
@@ -19,3 +24,12 @@ export const createLruSelector = createSelectorCreator({
memoize: lruMemoize,
argsMemoize: lruMemoize,
});
export const createLruDraftSafeSelector = createDraftSafeSelectorCreator({
memoize: lruMemoize,
argsMemoize: lruMemoize,
});
export const getSelectorsOptions: GetSelectorsOptions = {
createSelector: createLruDraftSafeSelector,
};

View File

@@ -8,7 +8,6 @@ import { initialPostprocessingState } from 'features/parameters/store/postproces
import { initialSDXLState } from 'features/sdxl/store/sdxlSlice';
import { initialConfigState } from 'features/system/store/configSlice';
import { initialSystemState } from 'features/system/store/systemSlice';
import { initialHotkeysState } from 'features/ui/store/hotkeysSlice';
import { initialUIState } from 'features/ui/store/uiSlice';
import { defaultsDeep } from 'lodash-es';
import type { UnserializeFunction } from 'redux-remember';
@@ -24,7 +23,6 @@ const initialStates: {
system: initialSystemState,
config: initialConfigState,
ui: initialUIState,
hotkeys: initialHotkeysState,
controlAdapters: initialControlAdapterState,
dynamicPrompts: initialDynamicPromptsState,
sdxl: initialSDXLState,

View File

@@ -3,19 +3,14 @@
*/
export const actionsDenylist = [
// very spammy canvas actions
'canvas/setCursorPosition',
'canvas/setStageCoordinates',
'canvas/setStageScale',
'canvas/setIsDrawing',
'canvas/setBoundingBoxCoordinates',
'canvas/setBoundingBoxDimensions',
'canvas/setIsDrawing',
'canvas/addPointToCurrentLine',
// bazillions during generation
'socket/socketGeneratorProgress',
'socket/appSocketGeneratorProgress',
// every time user presses shift
// 'hotkeys/shiftKeyPressed',
// this happens after every state change
'@@REMEMBER_PERSISTED',
];

View File

@@ -3,7 +3,7 @@ import { imageSelected } from 'features/gallery/store/gallerySlice';
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
import { imagesApi } from 'services/api/endpoints/images';
import type { ImageCache } from 'services/api/types';
import { getListImagesUrl, imagesAdapter } from 'services/api/util';
import { getListImagesUrl, imagesSelectors } from 'services/api/util';
import { startAppListening } from '..';
@@ -33,7 +33,7 @@ export const addFirstListImagesListener = () => {
if (data.ids.length > 0) {
// Select the first image
const firstImage = imagesAdapter.getSelectors().selectAll(data)[0];
const firstImage = imagesSelectors.selectAll(data)[0];
dispatch(imageSelected(firstImage ?? null));
}
},

View File

@@ -20,9 +20,15 @@ export const addDeleteBoardAndImagesFulfilledListener = () => {
let wasNodeEditorReset = false;
let wereControlAdaptersReset = false;
const state = getState();
const { generation, canvas, nodes, controlAdapters } = getState();
deleted_images.forEach((image_name) => {
const imageUsage = getImageUsage(state, image_name);
const imageUsage = getImageUsage(
generation,
canvas,
nodes,
controlAdapters,
image_name
);
if (imageUsage.isInitialImage && !wasInitialImageReset) {
dispatch(clearInitialImage());

View File

@@ -69,10 +69,12 @@ const predicate: AnyListenerPredicate<RootState> = (
return isProcessorSelected && hasControlImage;
};
const DEBOUNCE_MS = 300;
/**
* Listener that automatically processes a ControlNet image when its processor parameters are changed.
*
* The network request is debounced by 1 second.
* The network request is debounced.
*/
export const addControlNetAutoProcessListener = () => {
startAppListening({
@@ -85,7 +87,7 @@ export const addControlNetAutoProcessListener = () => {
cancelActiveListeners();
log.trace('ControlNet auto-process triggered');
// Delay before starting actual work
await delay(300);
await delay(DEBOUNCE_MS);
dispatch(controlAdapterImageProcessed({ id }));
},

View File

@@ -17,7 +17,7 @@ import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { clamp, forEach } from 'lodash-es';
import { api } from 'services/api';
import { imagesApi } from 'services/api/endpoints/images';
import { imagesAdapter } from 'services/api/util';
import { imagesSelectors } from 'services/api/util';
import { startAppListening } from '..';
@@ -53,9 +53,7 @@ export const addRequestedSingleImageDeletionListener = () => {
const { data } =
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
const cachedImageDTOs = data
? imagesAdapter.getSelectors().selectAll(data)
: [];
const cachedImageDTOs = data ? imagesSelectors.selectAll(data) : [];
const deletedImageIndex = cachedImageDTOs.findIndex(
(i) => i.image_name === image_name
@@ -187,7 +185,7 @@ export const addRequestedMultipleImageDeletionListener = () => {
imagesApi.endpoints.listImages.select(baseQueryArgs)(state);
const newSelectedImageDTO = data
? imagesAdapter.getSelectors().selectAll(data)[0]
? imagesSelectors.selectAll(data)[0]
: undefined;
if (newSelectedImageDTO) {

View File

@@ -13,7 +13,10 @@ import type {
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { workflowExposedFieldAdded } from 'features/nodes/store/workflowSlice';
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import {
initialImageChanged,
selectOptimalDimension,
} from 'features/parameters/store/generationSlice';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '../';
@@ -26,7 +29,7 @@ export const dndDropped = createAction<{
export const addImageDroppedListener = () => {
startAppListening({
actionCreator: dndDropped,
effect: async (action, { dispatch }) => {
effect: async (action, { dispatch, getState }) => {
const log = logger('dnd');
const { activeData, overData } = action.payload;
@@ -115,7 +118,12 @@ export const addImageDroppedListener = () => {
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
dispatch(setInitialCanvasImage(activeData.payload.imageDTO));
dispatch(
setInitialCanvasImage(
activeData.payload.imageDTO,
selectOptimalDimension(getState())
)
);
return;
}

View File

@@ -6,7 +6,10 @@ import {
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import {
initialImageChanged,
selectOptimalDimension,
} from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next';
import { omit } from 'lodash-es';
@@ -76,7 +79,9 @@ export const addImageUploadedFulfilledListener = () => {
}
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
dispatch(setInitialCanvasImage(imageDTO));
dispatch(
setInitialCanvasImage(imageDTO, selectOptimalDimension(state))
);
dispatch(
addToast({
...DEFAULT_UPLOADED_TOAST,

View File

@@ -1,5 +1,4 @@
import { logger } from 'app/logging/logger';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import {
controlAdapterIsEnabledChanged,
selectControlAdapterAll,
@@ -7,10 +6,8 @@ import {
import { loraRemoved } from 'features/lora/store/loraSlice';
import { modelSelected } from 'features/parameters/store/actions';
import {
heightChanged,
modelChanged,
vaeSelected,
widthChanged,
} from 'features/parameters/store/generationSlice';
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
import { addToast } from 'features/system/store/systemSlice';
@@ -84,22 +81,6 @@ export const addModelSelectedListener = () => {
}
}
// Update Width / Height / Bounding Box Dimensions on Model Change
if (
state.generation.model?.base_model !== newModel.base_model &&
state.ui.shouldAutoChangeDimensions
) {
if (['sdxl', 'sdxl-refiner'].includes(newModel.base_model)) {
dispatch(widthChanged(1024));
dispatch(heightChanged(1024));
dispatch(setBoundingBoxDimensions({ width: 1024, height: 1024 }));
} else {
dispatch(widthChanged(512));
dispatch(heightChanged(512));
dispatch(setBoundingBoxDimensions({ width: 512, height: 512 }));
}
}
dispatch(modelChanged(newModel));
},
});

View File

@@ -17,9 +17,9 @@ import {
import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice';
import { forEach, some } from 'lodash-es';
import {
mainModelsAdapter,
mainModelsAdapterSelectors,
modelsApi,
vaeModelsAdapter,
vaeModelsAdapterSelectors,
} from 'services/api/endpoints/models';
import type { TypeGuardFor } from 'services/api/types';
@@ -43,7 +43,7 @@ export const addModelsLoadedListener = () => {
);
const currentModel = getState().generation.model;
const models = mainModelsAdapter.getSelectors().selectAll(action.payload);
const models = mainModelsAdapterSelectors.selectAll(action.payload);
if (models.length === 0) {
// No models loaded at all
@@ -94,7 +94,7 @@ export const addModelsLoadedListener = () => {
);
const currentModel = getState().sdxl.refinerModel;
const models = mainModelsAdapter.getSelectors().selectAll(action.payload);
const models = mainModelsAdapterSelectors.selectAll(action.payload);
if (models.length === 0) {
// No models loaded at all
@@ -145,9 +145,7 @@ export const addModelsLoadedListener = () => {
return;
}
const firstModel = vaeModelsAdapter
.getSelectors()
.selectAll(action.payload)[0];
const firstModel = vaeModelsAdapterSelectors.selectAll(action.payload)[0];
if (!firstModel) {
// No custom VAEs loaded at all; use the default

View File

@@ -1,7 +1,10 @@
import { modelChanged } from 'features/parameters/store/generationSlice';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { NON_REFINER_BASE_MODELS } from 'services/api/constants';
import { mainModelsAdapter, modelsApi } from 'services/api/endpoints/models';
import {
mainModelsAdapterSelectors,
modelsApi,
} from 'services/api/endpoints/models';
import { startAppListening } from '..';
@@ -37,8 +40,7 @@ export const addTabChangedListener = () => {
}
// need to filter out all the invalid canvas models (currently refiner & any)
const validCanvasModels = mainModelsAdapter
.getSelectors()
const validCanvasModels = mainModelsAdapterSelectors
.selectAll(models)
.filter((model) =>
['sd-1', 'sd-2', 'sdxl'].includes(model.base_model)

View File

@@ -0,0 +1,4 @@
import { atom } from 'nanostores';
import type { ReactNode } from 'react';
export const $customNavComponent = atom<ReactNode | undefined>(undefined);

View File

@@ -1,4 +1,4 @@
import { atom } from 'nanostores';
import type { ReactNode } from 'react';
export const $headerComponent = atom<ReactNode | undefined>(undefined);
export const $galleryHeader = atom<ReactNode | undefined>(undefined);

View File

@@ -0,0 +1,4 @@
import { atom } from 'nanostores';
import type { ReactNode } from 'react';
export const $logo = atom<ReactNode | undefined>(undefined);

View File

@@ -151,6 +151,3 @@ export type RootState = ReturnType<ReturnType<typeof createStore>['getState']>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AppThunkDispatch = ThunkDispatch<RootState, any, UnknownAction>;
export type AppDispatch = ReturnType<typeof createStore>['dispatch'];
export function stateSelector(state: RootState) {
return state;
}

View File

@@ -65,7 +65,11 @@ export const IAINoContentFallback = memo((props: IAINoImageFallbackProps) => {
return (
<Flex sx={styles} {...rest}>
{icon && <Icon as={icon} boxSize={boxSize} opacity={0.7} />}
{props.label && <InvText textAlign="center">{props.label}</InvText>}
{props.label && (
<InvText textAlign="center" fontSize="md">
{props.label}
</InvText>
)}
</Flex>
);
});

View File

@@ -32,7 +32,7 @@ const IAIInformationalPopover = ({
...rest
}: Props) => {
const shouldEnableInformationalPopovers = useAppSelector(
(state) => state.system.shouldEnableInformationalPopovers
(s) => s.system.shouldEnableInformationalPopovers
);
const data = useMemo(() => POPOVER_DATA[feature], [feature]);

View File

@@ -1,28 +1,47 @@
import { Box, Flex, Heading } from '@chakra-ui/react';
import type { AnimationProps } from 'framer-motion';
import { motion } from 'framer-motion';
import { memo } from 'react';
import type { DropzoneState } from 'react-dropzone';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.1 },
};
type ImageUploadOverlayProps = {
isDragAccept: boolean;
isDragReject: boolean;
dropzone: DropzoneState;
setIsHandlingUpload: (isHandlingUpload: boolean) => void;
};
const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
const { t } = useTranslation();
const {
isDragAccept,
isDragReject: _isDragAccept,
setIsHandlingUpload,
} = props;
const { dropzone, setIsHandlingUpload } = props;
useHotkeys('esc', () => {
setIsHandlingUpload(false);
});
useHotkeys(
'esc',
() => {
setIsHandlingUpload(false);
},
[setIsHandlingUpload]
);
return (
<Box
key="image-upload-overlay"
initial={initial}
animate={animate}
exit={exit}
as={motion.div}
position="absolute"
top={0}
insetInlineStart={0}
@@ -67,7 +86,7 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
color="base.100"
borderColor="base.200"
>
{isDragAccept ? (
{dropzone.isDragAccept ? (
<Heading size="lg">{t('gallery.dropToUpload')}</Heading>
) : (
<>

View File

@@ -21,7 +21,7 @@ export const InvAccordionButton = (props: InvAccordionButtonProps) => {
{children}
<Spacer />
{badges?.map((b, i) => (
<InvBadge key={`${b}.${i}`} variant="solid">
<InvBadge key={`${b}.${i}`} colorScheme="invokeBlue">
{b}
</InvBadge>
))}

View File

@@ -23,7 +23,7 @@ const invokeAIButton = defineStyle((_props) => {
fontSize: 'sm',
border: 'none',
borderRadius: 'base',
color: 'base.400',
color: 'base.300',
_hover: {},
_expanded: {
borderBottomRadius: 'none',

View File

@@ -1,16 +1,16 @@
import { defineStyle, defineStyleConfig } from '@chakra-ui/react';
const baseStyle = defineStyle((props) => ({
fontSize: '9px',
fontSize: 9,
px: 2,
py: 1,
minW: 4,
lineHeight: 1,
borderRadius: 'sm',
bg: `${props.colorScheme}.300`,
bg: `${props.colorScheme}.200`,
color: 'base.900',
fontWeight: 'bold',
letterSpacing: 0.5,
letterSpacing: 0.6,
wordBreak: 'break-all',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
@@ -20,7 +20,6 @@ const baseStyle = defineStyle((props) => ({
export const badgeTheme = defineStyleConfig({
baseStyle,
defaultProps: {
variant: 'solid',
colorScheme: 'base',
},
});

View File

@@ -28,7 +28,13 @@ const meta: Meta<typeof InvButton> = {
export default meta;
type Story = StoryObj<typeof InvButton>;
const colorSchemes = ['base', 'invokeYellow', 'red', 'green', 'blue'] as const;
const colorSchemes = [
'base',
'invokeYellow',
'invokeRed',
'invokeGreen',
'invokeBlue',
] as const;
const variants = ['solid', 'outline', 'ghost', 'link'] as const;
const sizes = ['xs', 'sm', 'md', 'lg'] as const;

View File

@@ -12,7 +12,7 @@ export const InvButton = memo(
<InvTooltip label={tooltip}>
<Button
ref={ref}
colorScheme={isChecked ? 'blue' : 'base'}
colorScheme={isChecked ? 'invokeBlue' : 'base'}
{...rest}
>
{children}
@@ -22,7 +22,11 @@ export const InvButton = memo(
}
return (
<Button ref={ref} colorScheme={isChecked ? 'blue' : 'base'} {...rest}>
<Button
ref={ref}
colorScheme={isChecked ? 'invokeBlue' : 'base'}
{...rest}
>
{children}
</Button>
);

View File

@@ -204,7 +204,7 @@ export const buttonTheme = defineStyleConfig({
_hover: {
bg: 'none',
svg: {
fill: 'base.500',
fill: 'base.400',
},
},
'&[data-selected="true"]': {

View File

@@ -13,7 +13,7 @@ export const InvLabel = memo(
ref
) => {
const shouldEnableInformationalPopovers = useAppSelector(
(state) => state.system.shouldEnableInformationalPopovers
(s) => s.system.shouldEnableInformationalPopovers
);
const ctx = useContext(InvControlGroupContext);

View File

@@ -1,33 +1,57 @@
import { Divider, Flex } from '@chakra-ui/layout';
import type { SystemStyleObject } from '@chakra-ui/react';
import { Collapse, Icon, useDisclosure } from '@chakra-ui/react';
import type { InvExpanderProps } from 'common/components/InvExpander/types';
import { InvText } from 'common/components/InvText/wrapper';
import { t } from 'i18next';
import { BiCollapseVertical, BiExpandVertical } from 'react-icons/bi';
const buttonStyles: SystemStyleObject = {
color: 'base.400',
borderColor: 'base.400',
transitionDuration: 'normal',
transitionProperty: 'common',
':hover, :hover *': {
transitionDuration: 'normal',
transitionProperty: 'common',
color: 'base.300',
borderColor: 'base.300',
},
};
export const InvExpander = ({
children,
label = t('common.advancedOptions'),
defaultIsOpen = false,
}: InvExpanderProps) => {
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen });
return (
<Flex flexDir="column" w="full">
<Flex flexDir="row" alignItems="center" gap={3} py={4}>
<Divider w="unset" flexGrow={1} />
<Flex
as="button"
onClick={onToggle}
flexDir="row"
alignItems="center"
gap={2}
>
<Flex
as="button"
flexDir="row"
alignItems="center"
gap={3}
py={4}
px={2}
onClick={onToggle}
sx={buttonStyles}
>
<Divider w="unset" flexGrow={1} sx={buttonStyles} />
<Flex flexDir="row" alignItems="center" gap={2}>
<Icon
as={isOpen ? BiCollapseVertical : BiExpandVertical}
fontSize="12px"
color="base.400"
fontSize="14px"
sx={buttonStyles}
/>
<InvText variant="subtext" fontSize="sm" flexShrink={0} mb="2px">
<InvText
variant="subtext"
fontSize="sm"
fontWeight="semibold"
flexShrink={0}
sx={buttonStyles}
>
{label}
</InvText>
</Flex>

View File

@@ -1,11 +1,11 @@
import { defineStyle, defineStyleConfig } from '@chakra-ui/react';
const blue = defineStyle(() => ({
color: 'blue.300',
const invokeBlue = defineStyle(() => ({
color: 'invokeBlue.300',
}));
export const headingTheme = defineStyleConfig({
variants: {
blue,
invokeBlue,
},
});

View File

@@ -11,7 +11,7 @@ export const InvIconButton = memo(
<InvTooltip label={tooltip}>
<IconButton
ref={ref}
colorScheme={isChecked ? 'blue' : 'base'}
colorScheme={isChecked ? 'invokeBlue' : 'base'}
{...rest}
/>
</InvTooltip>
@@ -21,7 +21,7 @@ export const InvIconButton = memo(
return (
<IconButton
ref={ref}
colorScheme={isChecked ? 'blue' : 'base'}
colorScheme={isChecked ? 'invokeBlue' : 'base'}
{...rest}
/>
);

View File

@@ -16,6 +16,7 @@ export const baseStyle = definePartsStyle(() => ({
header: {
fontWeight: 'semibold',
fontSize: 'lg',
color: 'base.300',
},
closeButton: {
opacity: 0.5,

View File

@@ -1,35 +1,57 @@
import { progressAnatomy as parts } from '@chakra-ui/anatomy';
import {
createMultiStyleConfigHelpers,
defineStyle,
} from '@chakra-ui/styled-system';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import { generateStripe, getColorVar } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys);
const invokeAIFilledTrack = defineStyle((_props) => ({
bg: 'invokeYellow.500',
}));
const invokeAITrack = defineStyle((_props) => {
return {
bg: 'base.800',
};
});
const invokeAI = definePartsStyle((props) => ({
filledTrack: invokeAIFilledTrack(props),
track: invokeAITrack(props),
}));
export const progressTheme = defineMultiStyleConfig({
baseStyle: {
track: { borderRadius: '2px' },
},
variants: {
invokeAI,
},
defaultProps: {
variant: 'invokeAI',
},
baseStyle: definePartsStyle(
({ theme: t, colorScheme: c, hasStripe, isIndeterminate }) => {
const bgColor = `${c}.300`;
const addStripe = !isIndeterminate && hasStripe;
const gradient = `linear-gradient(
to right,
transparent 0%,
${getColorVar(t, bgColor)} 50%,
transparent 100%
)`;
return {
track: {
borderRadius: '2px',
bg: 'base.800',
},
filledTrack: {
borderRadius: '2px',
...(addStripe && generateStripe()),
...(isIndeterminate ? { bgImage: gradient } : { bgColor }),
},
};
}
),
});
// export const progressTheme = defineMultiStyleConfig({
// baseStyle: definePartsStyle(
// ({ theme: t, colorScheme: c, hasStripe, isIndeterminate }) => {
// const bgColor = `${c}.500`;
// const addStripe = !isIndeterminate && hasStripe;
// const gradient = `linear-gradient(
// to right,
// transparent 0%,
// ${getColorVar(t, bgColor)} 50%,
// transparent 100%
// )`;
// return {
// track: {
// borderRadius: '2px',
// bg: 'base.800',
// },
// filledTrack: {
// borderRadius: '2px',
// ...(addStripe && generateStripe("xs")),
// ...(isIndeterminate ? { bgImage: gradient } : { bgColor }),
// },
// };
// }
// ),
// });

View File

@@ -30,7 +30,7 @@ export const useGroupedModelInvSelect = <T extends AnyModelConfigEntity>(
): UseGroupedModelInvSelectReturn => {
const { t } = useTranslation();
const base_model = useAppSelector(
(state) => state.generation.model?.base_model ?? 'sdxl'
(s) => s.generation.model?.base_model ?? 'sdxl'
);
const { modelEntities, selectedModel, getIsDisabled, onChange, isLoading } =
arg;

View File

@@ -12,7 +12,7 @@ export const InvTab = memo(
{children}
<Spacer />
{badges?.map((b, i) => (
<InvBadge key={`${b}.${i}`} colorScheme="blue">
<InvBadge key={`${b}.${i}`} colorScheme="invokeYellow">
{b}
</InvBadge>
))}

View File

@@ -3,6 +3,15 @@ import { memo } from 'react';
import type { InvTooltipProps } from './types';
const modifiers: InvTooltipProps['modifiers'] = [
{
name: 'preventOverflow',
options: {
padding: 12,
},
},
];
export const InvTooltip = memo(
forwardRef<InvTooltipProps, typeof ChakraTooltip>(
(props: InvTooltipProps, ref) => {
@@ -12,6 +21,8 @@ export const InvTooltip = memo(
ref={ref}
hasArrow={hasArrow}
placement={placement}
arrowSize={8}
modifiers={modifiers}
{...rest}
>
{children}

View File

@@ -3,14 +3,15 @@ import { cssVar } from '@chakra-ui/theme-tools';
const $arrowBg = cssVar('popper-arrow-bg');
// define the base component styles
const baseStyle = defineStyle(() => ({
borderRadius: 'md',
shadow: 'dark-lg',
bg: 'base.200',
color: 'base.800',
[$arrowBg.variable]: 'colors.base.200',
pb: 1.5,
pt: 1,
px: 2,
pb: 1,
}));
// export the component theme

View File

@@ -1,31 +1,22 @@
import { Box } from '@chakra-ui/react';
import { useAppToaster } from 'app/components/Toaster';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import type { Accept, FileRejection } from 'react-dropzone';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useUploadImageMutation } from 'services/api/endpoints/images';
import type { PostUploadAction } from 'services/api/types';
import ImageUploadOverlay from './ImageUploadOverlay';
const accept: Accept = {
'image/png': ['.png'],
'image/jpeg': ['.jpg', '.jpeg', '.png'],
};
const dropzoneRootProps = { style: {} };
const selector = createMemoizedSelector(
[stateSelector, activeTabNameSelector],
({ gallery }, activeTabName) => {
const selectPostUploadAction = createMemoizedSelector(
activeTabNameSelector,
(activeTabName) => {
let postUploadAction: PostUploadAction = { type: 'TOAST' };
if (activeTabName === 'unifiedCanvas') {
@@ -36,19 +27,15 @@ const selector = createMemoizedSelector(
postUploadAction = { type: 'SET_INITIAL_IMAGE' };
}
const { autoAddBoardId } = gallery;
return {
autoAddBoardId,
postUploadAction,
};
return postUploadAction;
}
);
const ImageUploader = (props: PropsWithChildren) => {
const { autoAddBoardId, postUploadAction } = useAppSelector(selector);
const toaster = useAppToaster();
export const useFullscreenDropzone = () => {
const { t } = useTranslation();
const toaster = useAppToaster();
const postUploadAction = useAppSelector(selectPostUploadAction);
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
const [uploadImage] = useUploadImageMutation();
@@ -105,14 +92,7 @@ const ImageUploader = (props: PropsWithChildren) => {
setIsHandlingUpload(true);
}, []);
const {
getRootProps,
getInputProps,
isDragAccept,
isDragReject,
isDragActive,
inputRef,
} = useDropzone({
const dropzone = useDropzone({
accept,
noClick: true,
onDrop,
@@ -124,15 +104,17 @@ const ImageUploader = (props: PropsWithChildren) => {
useEffect(() => {
// This is a hack to allow pasting images into the uploader
const handlePaste = async (e: ClipboardEvent) => {
if (!inputRef.current) {
if (!dropzone.inputRef.current) {
return;
}
if (e.clipboardData?.files) {
// Set the files on the inputRef
inputRef.current.files = e.clipboardData.files;
// Set the files on the dropzone.inputRef
dropzone.inputRef.current.files = e.clipboardData.files;
// Dispatch the change event, dropzone catches this and we get to use its own validation
inputRef.current?.dispatchEvent(new Event('change', { bubbles: true }));
dropzone.inputRef.current?.dispatchEvent(
new Event('change', { bubbles: true })
);
}
};
@@ -142,42 +124,7 @@ const ImageUploader = (props: PropsWithChildren) => {
return () => {
document.removeEventListener('paste', handlePaste);
};
}, [inputRef]);
}, [dropzone.inputRef]);
return (
<Box {...getRootProps(dropzoneRootProps)}>
<input {...getInputProps()} />
{props.children}
<AnimatePresence>
{isDragActive && isHandlingUpload && (
<motion.div
key="image-upload-overlay"
initial={initial}
animate={animate}
exit={exit}
>
<ImageUploadOverlay
isDragAccept={isDragAccept}
isDragReject={isDragReject}
setIsHandlingUpload={setIsHandlingUpload}
/>
</motion.div>
)}
</AnimatePresence>
</Box>
);
};
export default memo(ImageUploader);
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.1 },
return { dropzone, isHandlingUpload, setIsHandlingUpload };
};

View File

@@ -52,7 +52,7 @@ export const useGlobalHotkeys = () => {
} = useCancelCurrentQueueItem();
useHotkeys(
['shift+x', 'shift+enter'],
['shift+x'],
cancelQueueItem,
{
enabled: () => !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,

View File

@@ -32,9 +32,7 @@ export const useImageUploadButton = ({
postUploadAction,
isDisabled,
}: UseImageUploadButtonArgs) => {
const autoAddBoardId = useAppSelector(
(state) => state.gallery.autoAddBoardId
);
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const [uploadImage] = useUploadImageMutation();
const onDropAccepted = useCallback(
(files: File[]) => {

View File

@@ -1,26 +1,39 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { selectControlAdapterAll } from 'features/controlAdapters/store/controlAdaptersSlice';
import {
selectControlAdapterAll,
selectControlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import i18n from 'i18next';
import { forEach } from 'lodash-es';
import { getConnectedEdges } from 'reactflow';
const selector = createMemoizedSelector(
[stateSelector, activeTabNameSelector],
[
selectControlAdaptersSlice,
selectGenerationSlice,
selectSystemSlice,
selectNodesSlice,
selectNodeTemplatesSlice,
selectDynamicPromptsSlice,
activeTabNameSelector,
],
(
{
controlAdapters,
generation,
system,
nodes,
nodeTemplates,
dynamicPrompts,
},
controlAdapters,
generation,
system,
nodes,
nodeTemplates,
dynamicPrompts,
activeTabName
) => {
const { initialImage, model, positivePrompt } = generation;

View File

@@ -1,7 +1,6 @@
import { Box, chakra, Flex } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import useCanvasDragMove from 'features/canvas/hooks/useCanvasDragMove';
import useCanvasHotkeys from 'features/canvas/hooks/useCanvasHotkeys';
@@ -17,7 +16,10 @@ import {
$isTransformingBoundingBox,
} from 'features/canvas/store/canvasNanostore';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { canvasResized } from 'features/canvas/store/canvasSlice';
import {
canvasResized,
selectCanvasSlice,
} from 'features/canvas/store/canvasSlice';
import {
setCanvasBaseLayer,
setCanvasStage,
@@ -40,57 +42,34 @@ import IAICanvasStatusText from './IAICanvasStatusText';
import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox';
import IAICanvasToolPreview from './IAICanvasToolPreview';
const selector = createMemoizedSelector(
[stateSelector, isStagingSelector],
({ canvas }, isStaging) => {
const {
isMaskEnabled,
stageScale,
shouldShowBoundingBox,
stageDimensions,
stageCoordinates,
tool,
shouldShowIntermediates,
shouldRestrictStrokesToBox,
shouldShowGrid,
shouldAntialias,
} = canvas;
return {
isMaskEnabled,
shouldShowBoundingBox,
shouldShowGrid,
stageCoordinates,
stageDimensions,
stageScale,
tool,
isStaging,
shouldShowIntermediates,
shouldAntialias,
shouldRestrictStrokesToBox,
};
}
);
const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
return {
stageCoordinates: canvas.stageCoordinates,
stageDimensions: canvas.stageDimensions,
};
});
const ChakraStage = chakra(Stage, {
shouldForwardProp: (prop) => !['sx'].includes(prop),
});
const IAICanvas = () => {
const {
isMaskEnabled,
shouldShowBoundingBox,
shouldShowGrid,
stageCoordinates,
stageDimensions,
stageScale,
tool,
isStaging,
shouldShowIntermediates,
shouldAntialias,
shouldRestrictStrokesToBox,
} = useAppSelector(selector);
useCanvasHotkeys();
const isStaging = useAppSelector(isStagingSelector);
const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
const shouldShowBoundingBox = useAppSelector(
(s) => s.canvas.shouldShowBoundingBox
);
const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
const tool = useAppSelector((s) => s.canvas.tool);
const shouldShowIntermediates = useAppSelector(
(s) => s.canvas.shouldShowIntermediates
);
const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias);
const shouldRestrictStrokesToBox = useAppSelector(
(s) => s.canvas.shouldRestrictStrokesToBox
);
const { stageCoordinates, stageDimensions } = useAppSelector(selector);
const dispatch = useAppDispatch();
const containerRef = useRef<HTMLDivElement>(null);
const stageRef = useRef<Konva.Stage | null>(null);
@@ -99,6 +78,7 @@ const IAICanvas = () => {
const isMovingStage = useStore($isMovingStage);
const isTransformingBoundingBox = useStore($isTransformingBoundingBox);
const isMouseOverBoundingBox = useStore($isMouseOverBoundingBox);
useCanvasHotkeys();
const canvasStageRefCallback = useCallback((el: Konva.Stage) => {
setCanvasStage(el as Konva.Stage);
stageRef.current = el;

View File

@@ -1,37 +1,36 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { memo } from 'react';
import { Group, Rect } from 'react-konva';
const selector = createMemoizedSelector(stateSelector, ({ canvas }) => {
const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
const {
boundingBoxCoordinates,
boundingBoxDimensions,
stageDimensions,
stageScale,
shouldDarkenOutsideBoundingBox,
stageCoordinates,
} = canvas;
return {
boundingBoxCoordinates,
boundingBoxDimensions,
shouldDarkenOutsideBoundingBox,
stageCoordinates,
stageDimensions,
stageScale,
};
});
const IAICanvasBoundingBoxOverlay = () => {
const {
boundingBoxCoordinates,
boundingBoxDimensions,
shouldDarkenOutsideBoundingBox,
stageCoordinates,
stageDimensions,
stageScale,
} = useAppSelector(selector);
const shouldDarkenOutsideBoundingBox = useAppSelector(
(s) => s.canvas.shouldDarkenOutsideBoundingBox
);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
return (
<Group listening={false}>

View File

@@ -1,23 +1,25 @@
// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import type { ReactElement } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { Group, Line as KonvaLine } from 'react-konva';
import { getArbitraryBaseColor } from 'theme/colors';
const selector = createMemoizedSelector([stateSelector], ({ canvas }) => {
const { stageScale, stageCoordinates, stageDimensions } = canvas;
return { stageScale, stageCoordinates, stageDimensions };
const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
return {
stageCoordinates: canvas.stageCoordinates,
stageDimensions: canvas.stageDimensions,
};
});
const baseGridLineColor = getArbitraryBaseColor(27);
const fineGridLineColor = getArbitraryBaseColor(18);
const IAICanvasGrid = () => {
const { stageScale, stageCoordinates, stageDimensions } =
useAppSelector(selector);
const { stageCoordinates, stageDimensions } = useAppSelector(selector);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
const gridSpacing = useMemo(() => {
if (stageScale >= 2) {

View File

@@ -1,32 +1,28 @@
import {
createLruSelector,
createMemoizedSelector,
} from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { memo, useEffect, useState } from 'react';
import { Image as KonvaImage } from 'react-konva';
const progressImageSelector = createLruSelector(
[stateSelector],
({ system, canvas }) => {
const progressImageSelector = createMemoizedSelector(
[selectSystemSlice, selectCanvasSlice],
(system, canvas) => {
const { denoiseProgress } = system;
const { batchIds } = canvas;
return denoiseProgress && batchIds.includes(denoiseProgress.batch_id)
? denoiseProgress.progress_image
: undefined;
return {
progressImage:
denoiseProgress && batchIds.includes(denoiseProgress.batch_id)
? denoiseProgress.progress_image
: undefined,
boundingBox: canvas.layerState.stagingArea.boundingBox,
};
}
);
const boundingBoxSelector = createMemoizedSelector(
[stateSelector],
({ canvas }) => canvas.layerState.stagingArea.boundingBox
);
const IAICanvasIntermediateImage = () => {
const progressImage = useAppSelector(progressImageSelector);
const boundingBox = useAppSelector(boundingBoxSelector);
const { progressImage, boundingBox } = useAppSelector(progressImageSelector);
const [loadedImageElement, setLoadedImageElement] =
useState<HTMLImageElement | null>(null);

View File

@@ -1,7 +1,8 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { getColoredMaskSVG } from 'features/canvas/util/getColoredMaskSVG';
import type Konva from 'konva';
import type { RectConfig } from 'konva/lib/shapes/Rect';
import { isNumber } from 'lodash-es';
@@ -9,109 +10,27 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Rect } from 'react-konva';
export const canvasMaskCompositerSelector = createMemoizedSelector(
stateSelector,
({ canvas }) => {
const { maskColor, stageCoordinates, stageDimensions, stageScale } = canvas;
selectCanvasSlice,
(canvas) => {
return {
stageCoordinates,
stageDimensions,
stageScale,
maskColorString: rgbaColorToString(maskColor),
stageCoordinates: canvas.stageCoordinates,
stageDimensions: canvas.stageDimensions,
};
}
);
type IAICanvasMaskCompositerProps = RectConfig;
const getColoredSVG = (color: string) => {
return `data:image/svg+xml;utf8,<?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="60px" height="60px" viewBox="0 0 30 30" 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;">
<g transform="matrix(0.5,0,0,0.5,0,0)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,2.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,7.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,10)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,12.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,15)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,17.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,20)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,22.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,25)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,27.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,30)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-2.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-7.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-10)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-12.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-15)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-17.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-20)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-22.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-25)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-27.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-30)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
</svg>`.replaceAll('black', color);
};
const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
const { ...rest } = props;
const { maskColorString, stageCoordinates, stageDimensions, stageScale } =
useAppSelector(canvasMaskCompositerSelector);
const { stageCoordinates, stageDimensions } = useAppSelector(
canvasMaskCompositerSelector
);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
const maskColorString = useAppSelector((s) =>
rgbaColorToString(s.canvas.maskColor)
);
const [fillPatternImage, setFillPatternImage] =
useState<HTMLImageElement | null>(null);
@@ -132,14 +51,14 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
image.onload = () => {
setFillPatternImage(image);
};
image.src = getColoredSVG(maskColorString);
image.src = getColoredMaskSVG(maskColorString);
}, [fillPatternImage, maskColorString]);
useEffect(() => {
if (!fillPatternImage) {
return;
}
fillPatternImage.src = getColoredSVG(maskColorString);
fillPatternImage.src = getColoredMaskSVG(maskColorString);
}, [fillPatternImage, maskColorString]);
useEffect(() => {

View File

@@ -1,18 +1,9 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { isCanvasMaskLine } from 'features/canvas/store/canvasTypes';
import type { GroupConfig } from 'konva/lib/Group';
import { memo } from 'react';
import { Group, Line } from 'react-konva';
export const canvasLinesSelector = createMemoizedSelector(
[stateSelector],
({ canvas }) => {
return canvas.layerState.objects.filter(isCanvasMaskLine);
}
);
type InpaintingCanvasLinesProps = GroupConfig;
/**
@@ -21,7 +12,7 @@ type InpaintingCanvasLinesProps = GroupConfig;
* Uses globalCompositeOperation to handle the brush and eraser tools.
*/
const IAICanvasLines = (props: InpaintingCanvasLinesProps) => {
const objects = useAppSelector((state) => state.canvas.layerState.objects);
const objects = useAppSelector((s) => s.canvas.layerState.objects);
return (
<Group listening={false} {...props}>

View File

@@ -12,7 +12,7 @@ import { Group, Line, Rect } from 'react-konva';
import IAICanvasImage from './IAICanvasImage';
const IAICanvasObjectRenderer = () => {
const objects = useAppSelector((state) => state.canvas.layerState.objects);
const objects = useAppSelector((s) => s.canvas.layerState.objects);
return (
<Group name="outpainting-objects" listening={false}>

View File

@@ -1,6 +1,6 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import type { GroupConfig } from 'konva/lib/Group';
import { memo } from 'react';
import { Group, Rect } from 'react-konva';
@@ -9,7 +9,7 @@ import IAICanvasImage from './IAICanvasImage';
const dash = [4, 4];
const selector = createMemoizedSelector([stateSelector], ({ canvas }) => {
const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
const {
layerState,
shouldShowStagingImage,

View File

@@ -1,7 +1,6 @@
import { Flex } from '@chakra-ui/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvButton } from 'common/components/InvButton/InvButton';
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
@@ -11,6 +10,7 @@ import {
discardStagedImages,
nextStagingAreaImage,
prevStagingAreaImage,
selectCanvasSlice,
setShouldShowStagingImage,
setShouldShowStagingOutline,
} from 'features/canvas/store/canvasSlice';
@@ -29,7 +29,7 @@ import {
} from 'react-icons/fa';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
const selector = createMemoizedSelector([stateSelector], ({ canvas }) => {
const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
const {
layerState: {
stagingArea: { images, selectedImageIndex },
@@ -142,7 +142,7 @@ const IAICanvasStagingAreaToolbar = () => {
aria-label={`${t('unifiedCanvas.previous')} (Left)`}
icon={<FaArrowLeft />}
onClick={handlePrevImage}
colorScheme="blue"
colorScheme="invokeBlue"
isDisabled={!shouldShowStagingImage}
/>
<InvButton
@@ -156,7 +156,7 @@ const IAICanvasStagingAreaToolbar = () => {
aria-label={`${t('unifiedCanvas.next')} (Right)`}
icon={<FaArrowRight />}
onClick={handleNextImage}
colorScheme="blue"
colorScheme="invokeBlue"
isDisabled={!shouldShowStagingImage}
/>
</InvButtonGroup>
@@ -166,7 +166,7 @@ const IAICanvasStagingAreaToolbar = () => {
aria-label={`${t('unifiedCanvas.accept')} (Enter)`}
icon={<FaCheck />}
onClick={handleAccept}
colorScheme="blue"
colorScheme="invokeBlue"
/>
<InvIconButton
tooltip={
@@ -182,7 +182,7 @@ const IAICanvasStagingAreaToolbar = () => {
data-alert={!shouldShowStagingImage}
icon={shouldShowStagingImage ? <FaEye /> : <FaEyeSlash />}
onClick={handleToggleShouldShowStagingImage}
colorScheme="blue"
colorScheme="invokeBlue"
/>
<InvIconButton
tooltip={t('unifiedCanvas.saveToGallery')}
@@ -190,7 +190,7 @@ const IAICanvasStagingAreaToolbar = () => {
isDisabled={!imageDTO || !imageDTO.is_intermediate}
icon={<FaSave />}
onClick={handleSaveToGallery}
colorScheme="blue"
colorScheme="invokeBlue"
/>
<InvIconButton
tooltip={t('unifiedCanvas.discardAll')}

View File

@@ -1,7 +1,7 @@
import { Box, Flex } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import roundToHundreth from 'features/canvas/util/roundToHundreth';
import GenerationModeStatusText from 'features/parameters/components/Canvas/GenerationModeStatusText';
import { memo } from 'react';
@@ -11,7 +11,7 @@ import IAICanvasStatusTextCursorPos from './IAICanvasStatusText/IAICanvasStatusT
const warningColor = 'var(--invokeai-colors-warning-500)';
const selector = createMemoizedSelector([stateSelector], ({ canvas }) => {
const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => {
const {
stageDimensions: { width: stageWidth, height: stageHeight },
stageCoordinates: { x: stageX, y: stageY },

View File

@@ -1,12 +1,12 @@
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import {
$cursorPosition,
$isMovingBoundingBox,
$isTransformingBoundingBox,
} from 'features/canvas/store/canvasNanostore';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import {
COLOR_PICKER_SIZE,
@@ -17,16 +17,9 @@ import { memo, useMemo } from 'react';
import { Circle, Group } from 'react-konva';
const canvasBrushPreviewSelector = createMemoizedSelector(
stateSelector,
({ canvas }) => {
selectCanvasSlice,
(canvas) => {
const {
brushSize,
colorPickerColor,
maskColor,
brushColor,
tool,
layer,
stageScale,
stageDimensions,
boundingBoxCoordinates,
boundingBoxDimensions,
@@ -82,17 +75,6 @@ const canvasBrushPreviewSelector = createMemoizedSelector(
// : undefined;
return {
radius: brushSize / 2,
colorPickerOuterRadius: COLOR_PICKER_SIZE / stageScale,
colorPickerInnerRadius:
(COLOR_PICKER_SIZE - COLOR_PICKER_STROKE_RADIUS + 1) / stageScale,
maskColorString: rgbaColorToString({ ...maskColor, a: 0.5 }),
brushColorString: rgbaColorToString(brushColor),
colorPickerColorString: rgbaColorToString(colorPickerColor),
tool,
layer,
strokeWidth: 1.5 / stageScale,
dotRadius: 1.5 / stageScale,
clip,
stageDimensions,
};
@@ -103,20 +85,28 @@ const canvasBrushPreviewSelector = createMemoizedSelector(
* Draws a black circle around the canvas brush preview.
*/
const IAICanvasToolPreview = (props: GroupConfig) => {
const {
radius,
maskColorString,
tool,
layer,
dotRadius,
strokeWidth,
brushColorString,
colorPickerColorString,
colorPickerInnerRadius,
colorPickerOuterRadius,
clip,
stageDimensions,
} = useAppSelector(canvasBrushPreviewSelector);
const radius = useAppSelector((s) => s.canvas.brushSize / 2);
const maskColorString = useAppSelector((s) =>
rgbaColorToString({ ...s.canvas.maskColor, a: 0.5 })
);
const tool = useAppSelector((s) => s.canvas.tool);
const layer = useAppSelector((s) => s.canvas.layer);
const dotRadius = useAppSelector((s) => 1.5 / s.canvas.stageScale);
const strokeWidth = useAppSelector((s) => 1.5 / s.canvas.stageScale);
const brushColorString = useAppSelector((s) =>
rgbaColorToString(s.canvas.brushColor)
);
const colorPickerColorString = useAppSelector((s) =>
rgbaColorToString(s.canvas.colorPickerColor)
);
const colorPickerInnerRadius = useAppSelector(
(s) =>
(COLOR_PICKER_SIZE - COLOR_PICKER_STROKE_RADIUS + 1) / s.canvas.stageScale
);
const colorPickerOuterRadius = useAppSelector(
(s) => COLOR_PICKER_SIZE / s.canvas.stageScale
);
const { clip, stageDimensions } = useAppSelector(canvasBrushPreviewSelector);
const cursorPosition = useStore($cursorPosition);
const isMovingBoundingBox = useStore($isMovingBoundingBox);

View File

@@ -1,6 +1,4 @@
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { $shift } from 'common/hooks/useGlobalModifiers';
import {
@@ -26,6 +24,8 @@ import {
CANVAS_GRID_SIZE_COARSE,
CANVAS_GRID_SIZE_FINE,
} from 'features/canvas/store/constants';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import type Konva from 'konva';
import type { GroupConfig } from 'konva/lib/Group';
import type { KonvaEventObject } from 'konva/lib/Node';
@@ -36,45 +36,23 @@ import { Group, Rect, Transformer } from 'react-konva';
const borderDash = [4, 4];
const boundingBoxPreviewSelector = createMemoizedSelector(
[stateSelector],
({ canvas }) => {
const {
boundingBoxCoordinates,
boundingBoxDimensions,
stageScale,
tool,
shouldSnapToGrid,
aspectRatio,
} = canvas;
return {
boundingBoxCoordinates,
boundingBoxDimensions,
stageScale,
shouldSnapToGrid,
tool,
hitStrokeWidth: 20 / stageScale,
aspectRatio,
};
}
);
type IAICanvasBoundingBoxPreviewProps = GroupConfig;
const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
const { ...rest } = props;
const dispatch = useAppDispatch();
const {
boundingBoxCoordinates,
boundingBoxDimensions,
stageScale,
shouldSnapToGrid,
tool,
hitStrokeWidth,
aspectRatio,
} = useAppSelector(boundingBoxPreviewSelector);
const boundingBoxCoordinates = useAppSelector(
(s) => s.canvas.boundingBoxCoordinates
);
const boundingBoxDimensions = useAppSelector(
(s) => s.canvas.boundingBoxDimensions
);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid);
const tool = useAppSelector((s) => s.canvas.tool);
const hitStrokeWidth = useAppSelector((s) => 20 / s.canvas.stageScale);
const aspectRatio = useAppSelector((s) => s.canvas.aspectRatio);
const optimalDimension = useAppSelector(selectOptimalDimension);
const transformerRef = useRef<Konva.Transformer>(null);
const shapeRef = useRef<Konva.Rect>(null);
const shift = useStore($shift);
@@ -140,72 +118,82 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
[dispatch, gridSize, shouldSnapToGrid]
);
const handleOnTransform = useCallback(() => {
/**
* The Konva Transformer changes the object's anchor point and scale factor,
* not its width and height. We need to un-scale the width and height before
* setting the values.
*/
if (!shapeRef.current) {
return;
}
const handleOnTransform = useCallback(
(_e: KonvaEventObject<Event>) => {
/**
* The Konva Transformer changes the object's anchor point and scale factor,
* not its width and height. We need to un-scale the width and height before
* setting the values.
*/
if (!shapeRef.current) {
return;
}
const rect = shapeRef.current;
const rect = shapeRef.current;
const scaleX = rect.scaleX();
const scaleY = rect.scaleY();
const scaleX = rect.scaleX();
const scaleY = rect.scaleY();
// undo the scaling
const width = Math.round(rect.width() * scaleX);
const height = Math.round(rect.height() * scaleY);
// undo the scaling
const width = Math.round(rect.width() * scaleX);
const height = Math.round(rect.height() * scaleY);
const x = Math.round(rect.x());
const y = Math.round(rect.y());
const x = Math.round(rect.x());
const y = Math.round(rect.y());
if (aspectRatio.isLocked) {
const newDimensions = calculateNewSize(
aspectRatio.value,
width * height
);
dispatch(
setBoundingBoxDimensions(
{
width: roundDownToMultipleMin(newDimensions.width, gridSize),
height: roundDownToMultipleMin(newDimensions.height, gridSize),
},
optimalDimension
)
);
} else {
dispatch(
setBoundingBoxDimensions(
{
width: roundDownToMultipleMin(width, gridSize),
height: roundDownToMultipleMin(height, gridSize),
},
optimalDimension
)
);
dispatch(
aspectRatioChanged({
isLocked: false,
id: 'Free',
value: width / height,
})
);
}
if (aspectRatio.isLocked) {
const newHeight = roundDownToMultipleMin(
width / aspectRatio.value,
gridSize
);
dispatch(
setBoundingBoxDimensions({
width: roundDownToMultipleMin(width, gridSize),
height: newHeight,
setBoundingBoxCoordinates({
x: shouldSnapToGrid ? roundDownToMultiple(x, gridSize) : x,
y: shouldSnapToGrid ? roundDownToMultiple(y, gridSize) : y,
})
);
} else {
dispatch(
setBoundingBoxDimensions({
width: roundDownToMultipleMin(width, gridSize),
height: roundDownToMultipleMin(height, gridSize),
})
);
dispatch(
aspectRatioChanged({
isLocked: false,
id: 'Free',
value: width / height,
})
);
}
dispatch(
setBoundingBoxCoordinates({
x: shouldSnapToGrid ? roundDownToMultiple(x, gridSize) : x,
y: shouldSnapToGrid ? roundDownToMultiple(y, gridSize) : y,
})
);
// Reset the scale now that the coords/dimensions have been un-scaled
rect.scaleX(1);
rect.scaleY(1);
}, [
aspectRatio.isLocked,
aspectRatio.value,
dispatch,
shouldSnapToGrid,
gridSize,
]);
// Reset the scale now that the coords/dimensions have been un-scaled
rect.scaleX(1);
rect.scaleY(1);
},
[
aspectRatio.isLocked,
aspectRatio.value,
dispatch,
shouldSnapToGrid,
gridSize,
optimalDimension,
]
);
const anchorDragBoundFunc = useCallback(
(
@@ -233,7 +221,6 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
x: roundDownToMultiple(newPos.x, scaledStep) + offsetX,
y: roundDownToMultiple(newPos.y, scaledStep) + offsetY,
};
console.log({ oldPos, newPos, newCoordinates });
return newCoordinates;
},
@@ -311,6 +298,18 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
stageScale,
]);
const enabledAnchors = useMemo(() => {
if (tool !== 'move') {
return emptyArray;
}
if (aspectRatio.isLocked) {
// TODO: The math to resize the bbox when locked and using other handles is confusing.
// Workaround for now is to only allow resizing from the bottom-right handle.
return ['bottom-right'];
}
return undefined;
}, [aspectRatio.isLocked, tool]);
return (
<Group {...rest}>
<Rect
@@ -356,7 +355,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
borderEnabled={true}
borderStroke="black"
draggable={false}
enabledAnchors={tool === 'move' ? undefined : emptyArray}
enabledAnchors={enabledAnchors}
flipEnabled={false}
ignoreStroke={true}
keepRatio={false}

View File

@@ -1,6 +1,4 @@
import { Box, Flex } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { InvButton } from 'common/components/InvButton/InvButton';
@@ -22,7 +20,6 @@ import {
setMaskColor,
setShouldPreserveMaskedArea,
} from 'features/canvas/store/canvasSlice';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import type { RgbaColor } from 'react-colorful';
@@ -30,33 +27,16 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaMask, FaSave, FaTrash } from 'react-icons/fa';
export const selector = createMemoizedSelector(
[stateSelector, isStagingSelector],
({ canvas }, isStaging) => {
const { maskColor, layer, isMaskEnabled, shouldPreserveMaskedArea } =
canvas;
return {
layer,
maskColor,
maskColorString: rgbaColorToString(maskColor),
isMaskEnabled,
shouldPreserveMaskedArea,
isStaging,
};
}
);
const IAICanvasMaskOptions = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const {
layer,
maskColor,
isMaskEnabled,
shouldPreserveMaskedArea,
isStaging,
} = useAppSelector(selector);
const layer = useAppSelector((s) => s.canvas.layer);
const maskColor = useAppSelector((s) => s.canvas.maskColor);
const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
const shouldPreserveMaskedArea = useAppSelector(
(s) => s.canvas.shouldPreserveMaskedArea
);
const isStaging = useAppSelector(isStagingSelector);
useHotkeys(
['q'],

View File

@@ -1,5 +1,3 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { redo } from 'features/canvas/store/canvasSlice';
@@ -9,21 +7,10 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaRedo } from 'react-icons/fa';
const canvasRedoSelector = createMemoizedSelector(
[stateSelector, activeTabNameSelector],
({ canvas }, activeTabName) => {
const { futureLayerStates } = canvas;
return {
canRedo: futureLayerStates.length > 0,
activeTabName,
};
}
);
const IAICanvasRedoButton = () => {
const dispatch = useAppDispatch();
const { canRedo, activeTabName } = useAppSelector(canvasRedoSelector);
const canRedo = useAppSelector((s) => s.canvas.futureLayerStates.length > 0);
const activeTabName = useAppSelector(activeTabNameSelector);
const { t } = useTranslation();

View File

@@ -1,6 +1,4 @@
import { Flex } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvCheckbox } from 'common/components/InvCheckbox/wrapper';
import { InvControl } from 'common/components/InvControl/InvControl';
@@ -28,50 +26,28 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaWrench } from 'react-icons/fa';
export const canvasControlsSelector = createMemoizedSelector(
[stateSelector],
({ canvas }) => {
const {
shouldAutoSave,
shouldCropToBoundingBoxOnSave,
shouldDarkenOutsideBoundingBox,
shouldShowCanvasDebugInfo,
shouldShowGrid,
shouldShowIntermediates,
shouldSnapToGrid,
shouldRestrictStrokesToBox,
shouldAntialias,
} = canvas;
return {
shouldAutoSave,
shouldCropToBoundingBoxOnSave,
shouldDarkenOutsideBoundingBox,
shouldShowCanvasDebugInfo,
shouldShowGrid,
shouldShowIntermediates,
shouldSnapToGrid,
shouldRestrictStrokesToBox,
shouldAntialias,
};
}
);
const IAICanvasSettingsButtonPopover = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const {
shouldAutoSave,
shouldCropToBoundingBoxOnSave,
shouldDarkenOutsideBoundingBox,
shouldShowCanvasDebugInfo,
shouldShowGrid,
shouldShowIntermediates,
shouldSnapToGrid,
shouldRestrictStrokesToBox,
shouldAntialias,
} = useAppSelector(canvasControlsSelector);
const shouldAutoSave = useAppSelector((s) => s.canvas.shouldAutoSave);
const shouldCropToBoundingBoxOnSave = useAppSelector(
(s) => s.canvas.shouldCropToBoundingBoxOnSave
);
const shouldDarkenOutsideBoundingBox = useAppSelector(
(s) => s.canvas.shouldDarkenOutsideBoundingBox
);
const shouldShowCanvasDebugInfo = useAppSelector(
(s) => s.canvas.shouldShowCanvasDebugInfo
);
const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid);
const shouldShowIntermediates = useAppSelector(
(s) => s.canvas.shouldShowIntermediates
);
const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid);
const shouldRestrictStrokesToBox = useAppSelector(
(s) => s.canvas.shouldRestrictStrokesToBox
);
const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias);
useHotkeys(
['n'],

View File

@@ -1,6 +1,4 @@
import { Box, Flex } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
@@ -36,23 +34,12 @@ import {
FaTimes,
} from 'react-icons/fa';
export const selector = createMemoizedSelector(
[stateSelector, isStagingSelector],
({ canvas }, isStaging) => {
const { tool, brushColor, brushSize } = canvas;
return {
tool,
isStaging,
brushColor,
brushSize,
};
}
);
const IAICanvasToolChooserOptions = () => {
const dispatch = useAppDispatch();
const { tool, brushColor, brushSize, isStaging } = useAppSelector(selector);
const tool = useAppSelector((s) => s.canvas.tool);
const brushColor = useAppSelector((s) => s.canvas.brushColor);
const brushSize = useAppSelector((s) => s.canvas.brushSize);
const isStaging = useAppSelector(isStagingSelector);
const { t } = useTranslation();
useHotkeys(

View File

@@ -1,6 +1,4 @@
import { Flex } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
import { InvControl } from 'common/components/InvControl/InvControl';
@@ -48,27 +46,13 @@ import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
import IAICanvasUndoButton from './IAICanvasUndoButton';
export const selector = createMemoizedSelector(
[stateSelector, isStagingSelector],
({ canvas }, isStaging) => {
const { tool, shouldCropToBoundingBoxOnSave, layer, isMaskEnabled } =
canvas;
return {
isStaging,
isMaskEnabled,
tool,
layer,
shouldCropToBoundingBoxOnSave,
};
}
);
const IAICanvasToolbar = () => {
const dispatch = useAppDispatch();
const { isStaging, isMaskEnabled, layer, tool } = useAppSelector(selector);
const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
const layer = useAppSelector((s) => s.canvas.layer);
const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
const canvasBaseLayer = getCanvasBaseLayer();
const { t } = useTranslation();
const { isClipboardAPIAvailable } = useCopyImageToClipboard();

View File

@@ -1,5 +1,3 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { undo } from 'features/canvas/store/canvasSlice';
@@ -9,24 +7,11 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaUndo } from 'react-icons/fa';
const canvasUndoSelector = createMemoizedSelector(
[stateSelector, activeTabNameSelector],
({ canvas }, activeTabName) => {
const { pastLayerStates } = canvas;
return {
canUndo: pastLayerStates.length > 0,
activeTabName,
};
}
);
const IAICanvasUndoButton = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { canUndo, activeTabName } = useAppSelector(canvasUndoSelector);
const activeTabName = useAppSelector(activeTabNameSelector);
const canUndo = useAppSelector((s) => s.canvas.pastLayerStates.length > 0);
const handleUndo = useCallback(() => {
dispatch(undo());

View File

@@ -12,7 +12,7 @@ import { useCallback } from 'react';
const useCanvasDrag = () => {
const dispatch = useAppDispatch();
const isStaging = useAppSelector(isStagingSelector);
const tool = useAppSelector((state) => state.canvas.tool);
const tool = useAppSelector((s) => s.canvas.tool);
const isMovingBoundingBox = useStore($isMovingBoundingBox);
const handleDragStart = useCallback(() => {
if (!((tool === 'move' || isStaging) && !isMovingBoundingBox)) {

View File

@@ -6,18 +6,18 @@ import { useEffect, useState } from 'react';
import { useDebounce } from 'react-use';
export const useCanvasGenerationMode = () => {
const layerState = useAppSelector((state) => state.canvas.layerState);
const layerState = useAppSelector((s) => s.canvas.layerState);
const boundingBoxCoordinates = useAppSelector(
(state) => state.canvas.boundingBoxCoordinates
(s) => s.canvas.boundingBoxCoordinates
);
const boundingBoxDimensions = useAppSelector(
(state) => state.canvas.boundingBoxDimensions
(s) => s.canvas.boundingBoxDimensions
);
const isMaskEnabled = useAppSelector((state) => state.canvas.isMaskEnabled);
const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
const shouldPreserveMaskedArea = useAppSelector(
(state) => state.canvas.shouldPreserveMaskedArea
(s) => s.canvas.shouldPreserveMaskedArea
);
const [generationMode, setGenerationMode] = useState<
GenerationMode | undefined

View File

@@ -1,5 +1,3 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
resetCanvasInteractionState,
@@ -16,49 +14,24 @@ import {
import type { CanvasTool } from 'features/canvas/store/canvasTypes';
import { getCanvasStage } from 'features/canvas/util/konvaInstanceProvider';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useRef } from 'react';
import { useCallback, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
const selector = createMemoizedSelector(
[stateSelector, activeTabNameSelector, isStagingSelector],
({ canvas }, activeTabName, isStaging) => {
const {
shouldLockBoundingBox,
shouldShowBoundingBox,
tool,
isMaskEnabled,
shouldSnapToGrid,
} = canvas;
return {
activeTabName,
shouldLockBoundingBox,
shouldShowBoundingBox,
tool,
isStaging,
isMaskEnabled,
shouldSnapToGrid,
};
}
);
const useInpaintingCanvasHotkeys = () => {
const dispatch = useAppDispatch();
const {
activeTabName,
shouldShowBoundingBox,
tool,
isStaging,
isMaskEnabled,
shouldSnapToGrid,
} = useAppSelector(selector);
const activeTabName = useAppSelector(activeTabNameSelector);
const shouldShowBoundingBox = useAppSelector(
(s) => s.canvas.shouldShowBoundingBox
);
const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled);
const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid);
const previousToolRef = useRef<CanvasTool | null>(null);
const canvasStage = getCanvasStage();
// Beta Keys
const handleClearMask = () => dispatch(clearMask());
const handleClearMask = useCallback(() => dispatch(clearMask()), [dispatch]);
useHotkeys(
['shift+c'],

View File

@@ -1,5 +1,3 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
setIsDrawing,
@@ -8,7 +6,6 @@ import {
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { addLine } from 'features/canvas/store/canvasSlice';
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import type { MutableRefObject } from 'react';
@@ -16,21 +13,10 @@ import { useCallback } from 'react';
import useColorPicker from './useColorUnderCursor';
const selector = createMemoizedSelector(
[activeTabNameSelector, stateSelector, isStagingSelector],
(activeTabName, { canvas }, isStaging) => {
const { tool } = canvas;
return {
tool,
activeTabName,
isStaging,
};
}
);
const useCanvasMouseDown = (stageRef: MutableRefObject<Konva.Stage | null>) => {
const dispatch = useAppDispatch();
const { tool, isStaging } = useAppSelector(selector);
const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
const { commitColorUnderCursor } = useColorPicker();
return useCallback(

View File

@@ -1,6 +1,4 @@
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
$isDrawing,
@@ -9,7 +7,6 @@ import {
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice';
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types';
import type { MutableRefObject } from 'react';
@@ -17,18 +14,6 @@ import { useCallback } from 'react';
import useColorPicker from './useColorUnderCursor';
const selector = createMemoizedSelector(
[activeTabNameSelector, stateSelector, isStagingSelector],
(activeTabName, { canvas }, isStaging) => {
const { tool } = canvas;
return {
tool,
activeTabName,
isStaging,
};
}
);
const useCanvasMouseMove = (
stageRef: MutableRefObject<Konva.Stage | null>,
didMouseMoveRef: MutableRefObject<boolean>,
@@ -36,7 +21,8 @@ const useCanvasMouseMove = (
) => {
const dispatch = useAppDispatch();
const isDrawing = useStore($isDrawing);
const { tool, isStaging } = useAppSelector(selector);
const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
const { updateColorUnderCursor } = useColorPicker();
return useCallback(() => {

View File

@@ -1,6 +1,4 @@
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
$isDrawing,
@@ -8,32 +6,20 @@ import {
setIsMovingStage,
} from 'features/canvas/store/canvasNanostore';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import {
// addPointToCurrentEraserLine,
addPointToCurrentLine,
} from 'features/canvas/store/canvasSlice';
import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice';
import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition';
import type Konva from 'konva';
import type { MutableRefObject } from 'react';
import { useCallback } from 'react';
const selector = createMemoizedSelector(
[stateSelector, isStagingSelector],
({ canvas }, isStaging) => {
return {
tool: canvas.tool,
isStaging,
};
}
);
const useCanvasMouseUp = (
stageRef: MutableRefObject<Konva.Stage | null>,
didMouseMoveRef: MutableRefObject<boolean>
) => {
const dispatch = useAppDispatch();
const isDrawing = useStore($isDrawing);
const { tool, isStaging } = useAppSelector(selector);
const tool = useAppSelector((s) => s.canvas.tool);
const isStaging = useAppSelector(isStagingSelector);
return useCallback(() => {
if (tool === 'move' || isStaging) {

View File

@@ -1,6 +1,4 @@
import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { $isMoveStageKeyHeld } from 'features/canvas/store/canvasNanostore';
import {
@@ -18,14 +16,9 @@ import { clamp } from 'lodash-es';
import type { MutableRefObject } from 'react';
import { useCallback } from 'react';
const selector = createMemoizedSelector(
[stateSelector],
(state) => state.canvas.stageScale
);
const useCanvasWheel = (stageRef: MutableRefObject<Konva.Stage | null>) => {
const dispatch = useAppDispatch();
const stageScale = useAppSelector(selector);
const stageScale = useAppSelector((s) => s.canvas.stageScale);
const isMoveStageKeyHeld = useStore($isMoveStageKeyHeld);
return useCallback(

View File

@@ -1,18 +1,17 @@
import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import type { RootState } from 'app/store/store';
import { stateSelector } from 'app/store/store';
import type { CanvasImage } from './canvasTypes';
import { selectCanvasSlice } from './canvasSlice';
import { isCanvasBaseImage } from './canvasTypes';
export const isStagingSelector = createMemoizedSelector(
[stateSelector],
({ canvas }) =>
export const isStagingSelector = createSelector(
selectCanvasSlice,
(canvas) =>
canvas.batchIds.length > 0 ||
canvas.layerState.stagingArea.images.length > 0
);
export const initialCanvasImageSelector = (
state: RootState
): CanvasImage | undefined =>
state.canvas.layerState.objects.find(isCanvasBaseImage);
export const initialCanvasImageSelector = createMemoizedSelector(
selectCanvasSlice,
(canvas) => canvas.layerState.objects.find(isCanvasBaseImage)
);

View File

@@ -1,5 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import {
roundDownToMultiple,
roundToMultiple,
@@ -11,6 +12,12 @@ import floorCoordinates from 'features/canvas/util/floorCoordinates';
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
import roundDimensionsToMultiple from 'features/canvas/util/roundDimensionsToMultiple';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import { modelChanged } from 'features/parameters/store/generationSlice';
import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types';
import {
getIsSizeOptimal,
getOptimalDimension,
} from 'features/parameters/util/optimalDimension';
import type { IRect, Vector2d } from 'konva/lib/types';
import { clamp, cloneDeep } from 'lodash-es';
import type { RgbaColor } from 'react-colorful';
@@ -86,6 +93,29 @@ export const initialCanvasState: CanvasState = {
},
};
const setBoundingBoxDimensionsReducer = (
state: CanvasState,
payload: Partial<Dimensions>,
optimalDimension: number
) => {
const boundingBoxDimensions = payload;
const newDimensions = roundDimensionsToMultiple(
{
...state.boundingBoxDimensions,
...boundingBoxDimensions,
},
CANVAS_GRID_SIZE_FINE
);
state.boundingBoxDimensions = newDimensions;
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
newDimensions,
optimalDimension
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
};
export const canvasSlice = createSlice({
name: 'canvas',
initialState: initialCanvasState,
@@ -132,117 +162,90 @@ export const canvasSlice = createSlice({
state.isMaskEnabled = action.payload;
state.layer = action.payload ? 'mask' : 'base';
},
setInitialCanvasImage: (state, action: PayloadAction<ImageDTO>) => {
const image = action.payload;
const { width, height } = image;
const { stageDimensions } = state;
setInitialCanvasImage: {
reducer: (state, action: PayloadActionWithOptimalDimension<ImageDTO>) => {
const { width, height, image_name } = action.payload;
const { optimalDimension } = action.meta;
const { stageDimensions } = state;
const newBoundingBoxDimensions = {
width: roundDownToMultiple(
clamp(width, CANVAS_GRID_SIZE_FINE, 512),
CANVAS_GRID_SIZE_FINE
),
height: roundDownToMultiple(
clamp(height, CANVAS_GRID_SIZE_FINE, 512),
CANVAS_GRID_SIZE_FINE
),
};
const newBoundingBoxDimensions = {
width: roundDownToMultiple(
clamp(width, CANVAS_GRID_SIZE_FINE, optimalDimension),
CANVAS_GRID_SIZE_FINE
),
height: roundDownToMultiple(
clamp(height, CANVAS_GRID_SIZE_FINE, optimalDimension),
CANVAS_GRID_SIZE_FINE
),
};
const newBoundingBoxCoordinates = {
x: roundToMultiple(
width / 2 - newBoundingBoxDimensions.width / 2,
CANVAS_GRID_SIZE_FINE
),
y: roundToMultiple(
height / 2 - newBoundingBoxDimensions.height / 2,
CANVAS_GRID_SIZE_FINE
),
};
const newBoundingBoxCoordinates = {
x: roundToMultiple(
width / 2 - newBoundingBoxDimensions.width / 2,
CANVAS_GRID_SIZE_FINE
),
y: roundToMultiple(
height / 2 - newBoundingBoxDimensions.height / 2,
CANVAS_GRID_SIZE_FINE
),
};
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
newBoundingBoxDimensions
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
newBoundingBoxDimensions,
optimalDimension
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
state.boundingBoxDimensions = newBoundingBoxDimensions;
state.boundingBoxCoordinates = newBoundingBoxCoordinates;
state.pastLayerStates.push(cloneDeep(state.layerState));
state.layerState = {
...cloneDeep(initialLayerState),
objects: [
{
kind: 'image',
layer: 'base',
x: 0,
y: 0,
width,
height,
imageName: image_name,
},
],
};
state.futureLayerStates = [];
state.batchIds = [];
const newScale = calculateScale(
stageDimensions.width,
stageDimensions.height,
width,
height,
STAGE_PADDING_PERCENTAGE
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
state.boundingBoxDimensions = newBoundingBoxDimensions;
state.boundingBoxCoordinates = newBoundingBoxCoordinates;
state.pastLayerStates.push(cloneDeep(state.layerState));
state.layerState = {
...cloneDeep(initialLayerState),
objects: [
{
kind: 'image',
layer: 'base',
x: 0,
y: 0,
width: width,
height: height,
imageName: image.image_name,
},
],
};
state.futureLayerStates = [];
state.batchIds = [];
const newScale = calculateScale(
stageDimensions.width,
stageDimensions.height,
width,
height,
STAGE_PADDING_PERCENTAGE
);
const newCoordinates = calculateCoordinates(
stageDimensions.width,
stageDimensions.height,
0,
0,
width,
height,
newScale
);
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
},
setBoundingBoxDimensions: (
state,
action: PayloadAction<Partial<Dimensions>>
) => {
const newDimensions = roundDimensionsToMultiple(
{
...state.boundingBoxDimensions,
...action.payload,
const newCoordinates = calculateCoordinates(
stageDimensions.width,
stageDimensions.height,
0,
0,
width,
height,
newScale
);
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
},
prepare: (payload: ImageDTO, optimalDimension: number) => ({
payload,
meta: {
optimalDimension,
},
CANVAS_GRID_SIZE_FINE
);
state.boundingBoxDimensions = newDimensions;
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(newDimensions);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
},
flipBoundingBoxAxes: (state) => {
const [currWidth, currHeight] = [
state.boundingBoxDimensions.width,
state.boundingBoxDimensions.height,
];
const [currScaledWidth, currScaledHeight] = [
state.scaledBoundingBoxDimensions.width,
state.scaledBoundingBoxDimensions.height,
];
state.boundingBoxDimensions = {
width: currHeight,
height: currWidth,
};
state.scaledBoundingBoxDimensions = {
width: currScaledHeight,
height: currScaledWidth,
};
}),
},
setBoundingBoxCoordinates: (state, action: PayloadAction<Vector2d>) => {
state.boundingBoxCoordinates = floorCoordinates(action.payload);
@@ -518,38 +521,6 @@ export const canvasSlice = createSlice({
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
} else {
const newScale = calculateScale(
stageWidth,
stageHeight,
512,
512,
STAGE_PADDING_PERCENTAGE
);
const newCoordinates = calculateCoordinates(
stageWidth,
stageHeight,
0,
0,
512,
512,
newScale
);
const newBoundingBoxDimensions = { width: 512, height: 512 };
state.stageScale = newScale;
state.stageCoordinates = newCoordinates;
state.boundingBoxCoordinates = { x: 0, y: 0 };
state.boundingBoxDimensions = newBoundingBoxDimensions;
if (state.boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
newBoundingBoxDimensions
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
}
},
nextStagingAreaImage: (state) => {
@@ -601,75 +572,69 @@ export const canvasSlice = createSlice({
state.shouldShowStagingImage = true;
state.batchIds = [];
},
fitBoundingBoxToStage: (state) => {
const {
boundingBoxDimensions,
boundingBoxCoordinates,
stageDimensions,
stageScale,
} = state;
const scaledStageWidth = stageDimensions.width / stageScale;
const scaledStageHeight = stageDimensions.height / stageScale;
setBoundingBoxScaleMethod: {
reducer: (
state,
action: PayloadActionWithOptimalDimension<BoundingBoxScaleMethod>
) => {
const boundingBoxScaleMethod = action.payload;
const { optimalDimension } = action.meta;
state.boundingBoxScaleMethod = boundingBoxScaleMethod;
if (
boundingBoxCoordinates.x < 0 ||
boundingBoxCoordinates.x + boundingBoxDimensions.width >
scaledStageWidth ||
boundingBoxCoordinates.y < 0 ||
boundingBoxCoordinates.y + boundingBoxDimensions.height >
scaledStageHeight
) {
const newBoundingBoxDimensions = {
width: roundDownToMultiple(
clamp(scaledStageWidth, CANVAS_GRID_SIZE_FINE, 512),
CANVAS_GRID_SIZE_FINE
),
height: roundDownToMultiple(
clamp(scaledStageHeight, CANVAS_GRID_SIZE_FINE, 512),
CANVAS_GRID_SIZE_FINE
),
};
const newBoundingBoxCoordinates = {
x: roundToMultiple(
scaledStageWidth / 2 - newBoundingBoxDimensions.width / 2,
CANVAS_GRID_SIZE_FINE
),
y: roundToMultiple(
scaledStageHeight / 2 - newBoundingBoxDimensions.height / 2,
CANVAS_GRID_SIZE_FINE
),
};
state.boundingBoxDimensions = newBoundingBoxDimensions;
state.boundingBoxCoordinates = newBoundingBoxCoordinates;
if (state.boundingBoxScaleMethod === 'auto') {
if (boundingBoxScaleMethod === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
newBoundingBoxDimensions
state.boundingBoxDimensions,
optimalDimension
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
}
},
setBoundingBoxScaleMethod: (
state,
action: PayloadAction<BoundingBoxScaleMethod>
) => {
state.boundingBoxScaleMethod = action.payload;
if (action.payload === 'auto') {
const scaledDimensions = getScaledBoundingBoxDimensions(
state.boundingBoxDimensions
);
state.scaledBoundingBoxDimensions = scaledDimensions;
}
},
prepare: (payload: BoundingBoxScaleMethod, optimalDimension: number) => ({
payload,
meta: {
optimalDimension,
},
}),
},
setScaledBoundingBoxDimensions: (
state,
action: PayloadAction<Dimensions>
action: PayloadAction<Partial<Dimensions>>
) => {
state.scaledBoundingBoxDimensions = action.payload;
state.scaledBoundingBoxDimensions = {
...state.scaledBoundingBoxDimensions,
...action.payload,
};
},
setBoundingBoxDimensions: {
reducer: (
state,
action: PayloadActionWithOptimalDimension<Partial<Dimensions>>
) => {
setBoundingBoxDimensionsReducer(
state,
action.payload,
action.meta.optimalDimension
);
},
prepare: (payload: Partial<Dimensions>, optimalDimension: number) => ({
payload,
meta: {
optimalDimension,
},
}),
},
scaledBoundingBoxDimensionsReset: {
reducer: (state, action: PayloadActionWithOptimalDimension) => {
const scaledDimensions = getScaledBoundingBoxDimensions(
state.boundingBoxDimensions,
action.meta.optimalDimension
);
state.scaledBoundingBoxDimensions = scaledDimensions;
},
prepare: (payload: void, optimalDimension: number) => ({
payload: undefined,
meta: { optimalDimension },
}),
},
setShouldShowStagingImage: (state, action: PayloadAction<boolean>) => {
state.shouldShowStagingImage = action.payload;
@@ -714,6 +679,22 @@ export const canvasSlice = createSlice({
},
},
extraReducers: (builder) => {
builder.addCase(modelChanged, (state, action) => {
const optimalDimension = getOptimalDimension(action.payload);
const { width, height } = state.boundingBoxDimensions;
if (getIsSizeOptimal(width, height, optimalDimension)) {
return;
}
setBoundingBoxDimensionsReducer(
state,
{
width,
height,
},
optimalDimension
);
});
builder.addCase(appSocketQueueItemStatusChanged, (state, action) => {
const batch_status = action.payload.data.batch_status;
if (!state.batchIds.includes(batch_status.batch_id)) {
@@ -754,7 +735,6 @@ export const {
commitColorPickerColor,
commitStagingAreaImage,
discardStagedImages,
fitBoundingBoxToStage,
nextStagingAreaImage,
prevStagingAreaImage,
redo,
@@ -764,7 +744,6 @@ export const {
setBoundingBoxDimensions,
setBoundingBoxPreviewFill,
setBoundingBoxScaleMethod,
flipBoundingBoxAxes,
setBrushColor,
setBrushSize,
setColorPickerColor,
@@ -799,6 +778,9 @@ export const {
canvasBatchIdAdded,
canvasBatchIdsReset,
aspectRatioChanged,
scaledBoundingBoxDimensionsReset,
} = canvasSlice.actions;
export default canvasSlice.reducer;
export const selectCanvasSlice = (state: RootState) => state.canvas;

View File

@@ -0,0 +1,81 @@
export const getColoredMaskSVG = (color: string) => {
return `data:image/svg+xml;utf8,<?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="60px" height="60px" viewBox="0 0 30 30" 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;">
<g transform="matrix(0.5,0,0,0.5,0,0)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,2.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,7.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,10)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,12.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,15)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,17.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,20)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,22.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,25)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,27.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,30)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-2.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-7.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-10)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-12.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-15)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-17.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-20)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-22.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-25)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-27.5)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(0.5,0,0,0.5,0,-30)">
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
</g>
</svg>`.replaceAll('black', color);
};

View File

@@ -2,19 +2,22 @@ import { roundToMultiple } from 'common/util/roundDownToMultiple';
import type { Dimensions } from 'features/canvas/store/canvasTypes';
import { CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/constants';
const getScaledBoundingBoxDimensions = (dimensions: Dimensions) => {
const getScaledBoundingBoxDimensions = (
dimensions: Dimensions,
optimalDimension: number
) => {
const { width, height } = dimensions;
const scaledDimensions = { width, height };
const targetArea = 512 * 512;
const targetArea = optimalDimension * optimalDimension;
const aspectRatio = width / height;
let currentArea = width * height;
let maxDimension = 448;
let maxDimension = optimalDimension - CANVAS_GRID_SIZE_FINE;
while (currentArea < targetArea) {
maxDimension += CANVAS_GRID_SIZE_FINE;
if (width === height) {
scaledDimensions.width = 512;
scaledDimensions.height = 512;
scaledDimensions.width = optimalDimension;
scaledDimensions.height = optimalDimension;
break;
} else {
if (aspectRatio > 1) {

View File

@@ -1,6 +1,5 @@
import { Flex } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvConfirmationAlertDialog } from 'common/components/InvConfirmationAlertDialog/InvConfirmationAlertDialog';
import { InvControl } from 'common/components/InvControl/InvControl';
@@ -13,6 +12,7 @@ import { InvText } from 'common/components/InvText/wrapper';
import {
changeBoardReset,
isModalOpenChanged,
selectChangeBoardModalSlice,
} from 'features/changeBoardModal/store/slice';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -22,23 +22,17 @@ import {
useRemoveImagesFromBoardMutation,
} from 'services/api/endpoints/images';
const selector = createMemoizedSelector(
[stateSelector],
({ changeBoardModal }) => {
const { isModalOpen, imagesToChange } = changeBoardModal;
return {
isModalOpen,
imagesToChange,
};
}
const selectImagesToChange = createMemoizedSelector(
selectChangeBoardModalSlice,
(changeBoardModal) => changeBoardModal.imagesToChange
);
const ChangeBoardModal = () => {
const dispatch = useAppDispatch();
const [selectedBoard, setSelectedBoard] = useState<string | null>();
const { data: boards, isFetching } = useListAllBoardsQuery();
const { imagesToChange, isModalOpen } = useAppSelector(selector);
const isModalOpen = useAppSelector((s) => s.changeBoardModal.isModalOpen);
const imagesToChange = useAppSelector(selectImagesToChange);
const [addImagesToBoard] = useAddImagesToBoardMutation();
const [removeImagesFromBoard] = useRemoveImagesFromBoardMutation();
const { t } = useTranslation();

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