Merge branch 'development' into backend-can-find-frontend
4
.gitignore
vendored
@ -194,10 +194,6 @@ checkpoints
|
|||||||
|
|
||||||
# Let the frontend manage its own gitignore
|
# Let the frontend manage its own gitignore
|
||||||
!frontend/*
|
!frontend/*
|
||||||
frontend/apt-get
|
|
||||||
frontend/dist
|
|
||||||
frontend/sudo
|
|
||||||
frontend/update
|
|
||||||
|
|
||||||
# Scratch folder
|
# Scratch folder
|
||||||
.scratch/
|
.scratch/
|
||||||
|
117
backend/modules/get_canvas_generation_mode.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
from PIL import Image, ImageChops
|
||||||
|
from PIL.Image import Image as ImageType
|
||||||
|
from typing import Union, Literal
|
||||||
|
|
||||||
|
# https://stackoverflow.com/questions/43864101/python-pil-check-if-image-is-transparent
|
||||||
|
def check_for_any_transparency(img: Union[ImageType, str]) -> bool:
|
||||||
|
if type(img) is str:
|
||||||
|
img = Image.open(str)
|
||||||
|
|
||||||
|
if img.info.get("transparency", None) is not None:
|
||||||
|
return True
|
||||||
|
if img.mode == "P":
|
||||||
|
transparent = img.info.get("transparency", -1)
|
||||||
|
for _, index in img.getcolors():
|
||||||
|
if index == transparent:
|
||||||
|
return True
|
||||||
|
elif img.mode == "RGBA":
|
||||||
|
extrema = img.getextrema()
|
||||||
|
if extrema[3][0] < 255:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_canvas_generation_mode(
|
||||||
|
init_img: Union[ImageType, str], init_mask: Union[ImageType, str]
|
||||||
|
) -> Literal["txt2img", "outpainting", "inpainting", "img2img",]:
|
||||||
|
if type(init_img) is str:
|
||||||
|
init_img = Image.open(init_img)
|
||||||
|
|
||||||
|
if type(init_mask) is str:
|
||||||
|
init_mask = Image.open(init_mask)
|
||||||
|
|
||||||
|
init_img = init_img.convert("RGBA")
|
||||||
|
|
||||||
|
# Get alpha from init_img
|
||||||
|
init_img_alpha = init_img.split()[-1]
|
||||||
|
init_img_alpha_mask = init_img_alpha.convert("L")
|
||||||
|
init_img_has_transparency = check_for_any_transparency(init_img)
|
||||||
|
|
||||||
|
if init_img_has_transparency:
|
||||||
|
init_img_is_fully_transparent = (
|
||||||
|
True if init_img_alpha_mask.getbbox() is None else False
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Mask images are white in areas where no change should be made, black where changes
|
||||||
|
should be made.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Fit the mask to init_img's size and convert it to greyscale
|
||||||
|
init_mask = init_mask.resize(init_img.size).convert("L")
|
||||||
|
|
||||||
|
"""
|
||||||
|
PIL.Image.getbbox() returns the bounding box of non-zero areas of the image, so we first
|
||||||
|
invert the mask image so that masked areas are white and other areas black == zero.
|
||||||
|
getbbox() now tells us if the are any masked areas.
|
||||||
|
"""
|
||||||
|
init_mask_bbox = ImageChops.invert(init_mask).getbbox()
|
||||||
|
init_mask_exists = False if init_mask_bbox is None else True
|
||||||
|
|
||||||
|
if init_img_has_transparency:
|
||||||
|
if init_img_is_fully_transparent:
|
||||||
|
return "txt2img"
|
||||||
|
else:
|
||||||
|
return "outpainting"
|
||||||
|
else:
|
||||||
|
if init_mask_exists:
|
||||||
|
return "inpainting"
|
||||||
|
else:
|
||||||
|
return "img2img"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Testing
|
||||||
|
init_img_opaque = "test_images/init-img_opaque.png"
|
||||||
|
init_img_partial_transparency = "test_images/init-img_partial_transparency.png"
|
||||||
|
init_img_full_transparency = "test_images/init-img_full_transparency.png"
|
||||||
|
init_mask_no_mask = "test_images/init-mask_no_mask.png"
|
||||||
|
init_mask_has_mask = "test_images/init-mask_has_mask.png"
|
||||||
|
|
||||||
|
print(
|
||||||
|
"OPAQUE IMAGE, NO MASK, expect img2img, got ",
|
||||||
|
get_canvas_generation_mode(init_img_opaque, init_mask_no_mask),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"IMAGE WITH TRANSPARENCY, NO MASK, expect outpainting, got ",
|
||||||
|
get_canvas_generation_mode(
|
||||||
|
init_img_partial_transparency, init_mask_no_mask
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"FULLY TRANSPARENT IMAGE NO MASK, expect txt2img, got ",
|
||||||
|
get_canvas_generation_mode(init_img_full_transparency, init_mask_no_mask),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"OPAQUE IMAGE, WITH MASK, expect inpainting, got ",
|
||||||
|
get_canvas_generation_mode(init_img_opaque, init_mask_has_mask),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"IMAGE WITH TRANSPARENCY, WITH MASK, expect outpainting, got ",
|
||||||
|
get_canvas_generation_mode(
|
||||||
|
init_img_partial_transparency, init_mask_has_mask
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"FULLY TRANSPARENT IMAGE WITH MASK, expect txt2img, got ",
|
||||||
|
get_canvas_generation_mode(init_img_full_transparency, init_mask_has_mask),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
BIN
backend/modules/test_images/init-img_full_transparency.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
backend/modules/test_images/init-img_opaque.png
Normal file
After Width: | Height: | Size: 292 KiB |
BIN
backend/modules/test_images/init-img_partial_transparency.png
Normal file
After Width: | Height: | Size: 164 KiB |
BIN
backend/modules/test_images/init-mask_has_mask.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
backend/modules/test_images/init-mask_no_mask.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
@ -303,6 +303,8 @@ The WebGUI is only rapid development. Check back regularly for updates!
|
|||||||
| `--cors [CORS ...]` | Additional allowed origins, comma-separated |
|
| `--cors [CORS ...]` | Additional allowed origins, comma-separated |
|
||||||
| `--host HOST` | Web server: Host or IP to listen on. Set to 0.0.0.0 to accept traffic from other devices on your network. |
|
| `--host HOST` | Web server: Host or IP to listen on. Set to 0.0.0.0 to accept traffic from other devices on your network. |
|
||||||
| `--port PORT` | Web server: Port to listen on |
|
| `--port PORT` | Web server: Port to listen on |
|
||||||
|
| `--certfile CERTFILE` | Web server: Path to certificate file to use for SSL. Use together with --keyfile |
|
||||||
|
| `--keyfile KEYFILE` | Web server: Path to private key file to use for SSL. Use together with --certfile' |
|
||||||
| `--gui` | Start InvokeAI GUI - This is the "desktop mode" version of the web app. It uses Flask to create a desktop app experience of the webserver. |
|
| `--gui` | Start InvokeAI GUI - This is the "desktop mode" version of the web app. It uses Flask to create a desktop app experience of the webserver. |
|
||||||
|
|
||||||
### Web Specific Features
|
### Web Specific Features
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
name: invokeai
|
|
||||||
channels:
|
|
||||||
- pytorch
|
|
||||||
- conda-forge
|
|
||||||
- defaults
|
|
||||||
dependencies:
|
|
||||||
- python>=3.9
|
|
||||||
- pip=22.2.2
|
|
||||||
- numpy=1.23.3
|
|
||||||
- pip:
|
|
||||||
- --extra-index-url https://download.pytorch.org/whl/rocm5.2/
|
|
||||||
- albumentations==0.4.3
|
|
||||||
- dependency_injector==4.40.0
|
|
||||||
- diffusers==0.6.0
|
|
||||||
- einops==0.3.0
|
|
||||||
- eventlet
|
|
||||||
- flask==2.1.3
|
|
||||||
- flask_cors==3.0.10
|
|
||||||
- flask_socketio==5.3.0
|
|
||||||
- getpass_asterisk
|
|
||||||
- imageio-ffmpeg==0.4.2
|
|
||||||
- imageio==2.9.0
|
|
||||||
- kornia==0.6.0
|
|
||||||
- omegaconf==2.2.3
|
|
||||||
- opencv-python==4.5.5.64
|
|
||||||
- pillow==9.2.0
|
|
||||||
- pudb==2019.2
|
|
||||||
- pyreadline3
|
|
||||||
- pytorch-lightning==1.7.7
|
|
||||||
- send2trash==1.8.0
|
|
||||||
- streamlit==1.12.0
|
|
||||||
- taming-transformers-rom1504
|
|
||||||
- test-tube>=0.7.5
|
|
||||||
- torch
|
|
||||||
- torch-fidelity==0.3.0
|
|
||||||
- torchaudio
|
|
||||||
- torchmetrics==0.7.0
|
|
||||||
- torchvision
|
|
||||||
- transformers==4.21.3
|
|
||||||
- git+https://github.com/openai/CLIP.git@main#egg=clip
|
|
||||||
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k_diffusion
|
|
||||||
- git+https://github.com/invoke-ai/Real-ESRGAN.git#egg=realesrgan
|
|
||||||
- git+https://github.com/invoke-ai/GFPGAN.git#egg=gfpgan
|
|
||||||
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
|
||||||
- -e .
|
|
@ -42,4 +42,5 @@ dependencies:
|
|||||||
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k_diffusion
|
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k_diffusion
|
||||||
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
||||||
- git+https://github.com/invoke-ai/GFPGAN#egg=gfpgan
|
- git+https://github.com/invoke-ai/GFPGAN#egg=gfpgan
|
||||||
|
- git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
||||||
- -e .
|
- -e .
|
||||||
|
@ -44,4 +44,5 @@ dependencies:
|
|||||||
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k-diffusion
|
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k-diffusion
|
||||||
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
||||||
- git+https://github.com/invoke-ai/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
- git+https://github.com/invoke-ai/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
||||||
|
- git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
||||||
- -e .
|
- -e .
|
||||||
|
@ -43,4 +43,5 @@ dependencies:
|
|||||||
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k-diffusion
|
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k-diffusion
|
||||||
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
||||||
- git+https://github.com/invoke-ai/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
- git+https://github.com/invoke-ai/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
||||||
|
- git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
||||||
- -e .
|
- -e .
|
||||||
|
@ -59,6 +59,7 @@ dependencies:
|
|||||||
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k-diffusion
|
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k-diffusion
|
||||||
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
||||||
- git+https://github.com/invoke-ai/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
- git+https://github.com/invoke-ai/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
||||||
|
- git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
||||||
- -e .
|
- -e .
|
||||||
variables:
|
variables:
|
||||||
PYTORCH_ENABLE_MPS_FALLBACK: 1
|
PYTORCH_ENABLE_MPS_FALLBACK: 1
|
||||||
|
@ -44,4 +44,5 @@ dependencies:
|
|||||||
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k_diffusion
|
- git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k_diffusion
|
||||||
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
- git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
||||||
- git+https://github.com/invoke-ai/GFPGAN#egg=gfpgan
|
- git+https://github.com/invoke-ai/GFPGAN#egg=gfpgan
|
||||||
|
- git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
||||||
- -e .
|
- -e .
|
||||||
|
@ -36,3 +36,4 @@ git+https://github.com/openai/CLIP.git@main#egg=clip
|
|||||||
git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k-diffusion
|
git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k-diffusion
|
||||||
git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
git+https://github.com/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
||||||
git+https://github.com/invoke-ai/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
git+https://github.com/invoke-ai/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
||||||
|
git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
@ -5,7 +5,7 @@
|
|||||||
- `python scripts/dream.py --web` serves both frontend and backend at
|
- `python scripts/dream.py --web` serves both frontend and backend at
|
||||||
http://localhost:9090
|
http://localhost:9090
|
||||||
|
|
||||||
## Environment
|
## Evironment
|
||||||
|
|
||||||
Install [node](https://nodejs.org/en/download/) (includes npm) and optionally
|
Install [node](https://nodejs.org/en/download/) (includes npm) and optionally
|
||||||
[yarn](https://yarnpkg.com/getting-started/install).
|
[yarn](https://yarnpkg.com/getting-started/install).
|
||||||
@ -15,7 +15,7 @@ packages.
|
|||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
1. From `frontend/`, run `npm run dev` / `yarn dev` to start the dev server.
|
1. From `frontend/`, run `npm dev` / `yarn dev` to start the dev server.
|
||||||
2. Run `python scripts/dream.py --web`.
|
2. Run `python scripts/dream.py --web`.
|
||||||
3. Navigate to the dev server address e.g. `http://localhost:5173/`.
|
3. Navigate to the dev server address e.g. `http://localhost:5173/`.
|
||||||
|
|
||||||
|
623
frontend/dist/assets/index.2b7cd976.js
vendored
Normal file
1
frontend/dist/assets/index.40a72c80.css
vendored
501
frontend/dist/assets/index.a8ba2a6c.js
vendored
1
frontend/dist/assets/index.f999e69e.css
vendored
Normal file
4
frontend/dist/index.html
vendored
@ -6,8 +6,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||||
<script type="module" crossorigin src="./assets/index.a8ba2a6c.js"></script>
|
<script type="module" crossorigin src="./assets/index.2b7cd976.js"></script>
|
||||||
<link rel="stylesheet" href="./assets/index.40a72c80.css">
|
<link rel="stylesheet" href="./assets/index.f999e69e.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
23
frontend/eslintconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint", "eslint-plugin-react-hooks"],
|
||||||
|
"root": true,
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"node": {
|
||||||
|
"paths": ["src"],
|
||||||
|
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"react/jsx-filename-extension": [1, { "extensions": [".tsx", ".ts"] }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,11 +7,13 @@
|
|||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"build-dev": "tsc && vite build -m development",
|
"build-dev": "tsc && vite build -m development",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.10",
|
"@chakra-ui/icons": "^2.0.10",
|
||||||
"@chakra-ui/react": "^2.3.1",
|
"@chakra-ui/react": "^2.3.1",
|
||||||
|
"@emotion/cache": "^11.10.5",
|
||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.10.4",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@radix-ui/react-context-menu": "^2.0.1",
|
"@radix-ui/react-context-menu": "^2.0.1",
|
||||||
@ -29,14 +31,18 @@
|
|||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.2",
|
"react-dropzone": "^14.2.2",
|
||||||
"react-hotkeys-hook": "^3.4.7",
|
"react-hotkeys-hook": "4.0.2",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
"react-konva": "^18.2.3",
|
"react-konva": "^18.2.3",
|
||||||
|
"react-konva-utils": "^0.3.0",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
|
"react-zoom-pan-pinch": "^2.1.3",
|
||||||
|
"redux-deep-persist": "^1.0.6",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"socket.io": "^4.5.2",
|
"socket.io": "^4.5.2",
|
||||||
"socket.io-client": "^4.5.2",
|
"socket.io-client": "^4.5.2",
|
||||||
|
"use-image": "^1.1.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"yarn": "^1.22.19"
|
"yarn": "^1.22.19"
|
||||||
},
|
},
|
||||||
@ -51,10 +57,13 @@
|
|||||||
"eslint": "^8.23.0",
|
"eslint": "^8.23.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"patch-package": "^6.5.0",
|
||||||
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"sass": "^1.55.0",
|
"sass": "^1.55.0",
|
||||||
"tsc-watch": "^5.0.3",
|
"tsc-watch": "^5.0.3",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^4.6.4",
|
||||||
"vite": "^3.0.7",
|
"vite": "^3.0.7",
|
||||||
"vite-plugin-eslint": "^1.8.1"
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-tsconfig-paths": "^3.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
frontend/patches/redux-deep-persist+1.0.6.patch
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
diff --git a/node_modules/redux-deep-persist/lib/types.d.ts b/node_modules/redux-deep-persist/lib/types.d.ts
|
||||||
|
index b67b8c2..7fc0fa1 100644
|
||||||
|
--- a/node_modules/redux-deep-persist/lib/types.d.ts
|
||||||
|
+++ b/node_modules/redux-deep-persist/lib/types.d.ts
|
||||||
|
@@ -35,6 +35,7 @@ export interface PersistConfig<S, RS = any, HSS = any, ESS = any> {
|
||||||
|
whitelist?: Array<string>;
|
||||||
|
transforms?: Array<Transform<HSS, ESS, S, RS>>;
|
||||||
|
throttle?: number;
|
||||||
|
+ debounce?: number;
|
||||||
|
migrate?: PersistMigrate;
|
||||||
|
stateReconciler?: false | StateReconciler<S>;
|
||||||
|
getStoredState?: (config: PersistConfig<S, RS, HSS, ESS>) => Promise<PersistedState>;
|
||||||
|
diff --git a/node_modules/redux-deep-persist/src/types.ts b/node_modules/redux-deep-persist/src/types.ts
|
||||||
|
index 398ac19..cbc5663 100644
|
||||||
|
--- a/node_modules/redux-deep-persist/src/types.ts
|
||||||
|
+++ b/node_modules/redux-deep-persist/src/types.ts
|
||||||
|
@@ -91,6 +91,7 @@ export interface PersistConfig<S, RS = any, HSS = any, ESS = any> {
|
||||||
|
whitelist?: Array<string>;
|
||||||
|
transforms?: Array<Transform<HSS, ESS, S, RS>>;
|
||||||
|
throttle?: number;
|
||||||
|
+ debounce?: number;
|
||||||
|
migrate?: PersistMigrate;
|
||||||
|
stateReconciler?: false | StateReconciler<S>;
|
||||||
|
/**
|
116
frontend/patches/redux-persist+6.0.0.patch
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
diff --git a/node_modules/redux-persist/es/createPersistoid.js b/node_modules/redux-persist/es/createPersistoid.js
|
||||||
|
index 8b43b9a..184faab 100644
|
||||||
|
--- a/node_modules/redux-persist/es/createPersistoid.js
|
||||||
|
+++ b/node_modules/redux-persist/es/createPersistoid.js
|
||||||
|
@@ -6,6 +6,7 @@ export default function createPersistoid(config) {
|
||||||
|
var whitelist = config.whitelist || null;
|
||||||
|
var transforms = config.transforms || [];
|
||||||
|
var throttle = config.throttle || 0;
|
||||||
|
+ var debounce = config.debounce || 0;
|
||||||
|
var storageKey = "".concat(config.keyPrefix !== undefined ? config.keyPrefix : KEY_PREFIX).concat(config.key);
|
||||||
|
var storage = config.storage;
|
||||||
|
var serialize;
|
||||||
|
@@ -28,30 +29,37 @@ export default function createPersistoid(config) {
|
||||||
|
var timeIterator = null;
|
||||||
|
var writePromise = null;
|
||||||
|
|
||||||
|
- var update = function update(state) {
|
||||||
|
- // add any changed keys to the queue
|
||||||
|
- Object.keys(state).forEach(function (key) {
|
||||||
|
- if (!passWhitelistBlacklist(key)) return; // is keyspace ignored? noop
|
||||||
|
+ // Timer for debounced `update()`
|
||||||
|
+ let timer = 0;
|
||||||
|
|
||||||
|
- if (lastState[key] === state[key]) return; // value unchanged? noop
|
||||||
|
+ function update(state) {
|
||||||
|
+ // Debounce the update
|
||||||
|
+ clearTimeout(timer);
|
||||||
|
+ timer = setTimeout(() => {
|
||||||
|
+ // add any changed keys to the queue
|
||||||
|
+ Object.keys(state).forEach(function (key) {
|
||||||
|
+ if (!passWhitelistBlacklist(key)) return; // is keyspace ignored? noop
|
||||||
|
|
||||||
|
- if (keysToProcess.indexOf(key) !== -1) return; // is key already queued? noop
|
||||||
|
+ if (lastState[key] === state[key]) return; // value unchanged? noop
|
||||||
|
|
||||||
|
- keysToProcess.push(key); // add key to queue
|
||||||
|
- }); //if any key is missing in the new state which was present in the lastState,
|
||||||
|
- //add it for processing too
|
||||||
|
+ if (keysToProcess.indexOf(key) !== -1) return; // is key already queued? noop
|
||||||
|
|
||||||
|
- Object.keys(lastState).forEach(function (key) {
|
||||||
|
- if (state[key] === undefined && passWhitelistBlacklist(key) && keysToProcess.indexOf(key) === -1 && lastState[key] !== undefined) {
|
||||||
|
- keysToProcess.push(key);
|
||||||
|
- }
|
||||||
|
- }); // start the time iterator if not running (read: throttle)
|
||||||
|
+ keysToProcess.push(key); // add key to queue
|
||||||
|
+ }); //if any key is missing in the new state which was present in the lastState,
|
||||||
|
+ //add it for processing too
|
||||||
|
|
||||||
|
- if (timeIterator === null) {
|
||||||
|
- timeIterator = setInterval(processNextKey, throttle);
|
||||||
|
- }
|
||||||
|
+ Object.keys(lastState).forEach(function (key) {
|
||||||
|
+ if (state[key] === undefined && passWhitelistBlacklist(key) && keysToProcess.indexOf(key) === -1 && lastState[key] !== undefined) {
|
||||||
|
+ keysToProcess.push(key);
|
||||||
|
+ }
|
||||||
|
+ }); // start the time iterator if not running (read: throttle)
|
||||||
|
+
|
||||||
|
+ if (timeIterator === null) {
|
||||||
|
+ timeIterator = setInterval(processNextKey, throttle);
|
||||||
|
+ }
|
||||||
|
|
||||||
|
- lastState = state;
|
||||||
|
+ lastState = state;
|
||||||
|
+ }, debounce)
|
||||||
|
};
|
||||||
|
|
||||||
|
function processNextKey() {
|
||||||
|
diff --git a/node_modules/redux-persist/es/types.js.flow b/node_modules/redux-persist/es/types.js.flow
|
||||||
|
index c50d3cd..39d8be2 100644
|
||||||
|
--- a/node_modules/redux-persist/es/types.js.flow
|
||||||
|
+++ b/node_modules/redux-persist/es/types.js.flow
|
||||||
|
@@ -19,6 +19,7 @@ export type PersistConfig = {
|
||||||
|
whitelist?: Array<string>,
|
||||||
|
transforms?: Array<Transform>,
|
||||||
|
throttle?: number,
|
||||||
|
+ debounce?: number,
|
||||||
|
migrate?: (PersistedState, number) => Promise<PersistedState>,
|
||||||
|
stateReconciler?: false | Function,
|
||||||
|
getStoredState?: PersistConfig => Promise<PersistedState>, // used for migrations
|
||||||
|
diff --git a/node_modules/redux-persist/lib/types.js.flow b/node_modules/redux-persist/lib/types.js.flow
|
||||||
|
index c50d3cd..39d8be2 100644
|
||||||
|
--- a/node_modules/redux-persist/lib/types.js.flow
|
||||||
|
+++ b/node_modules/redux-persist/lib/types.js.flow
|
||||||
|
@@ -19,6 +19,7 @@ export type PersistConfig = {
|
||||||
|
whitelist?: Array<string>,
|
||||||
|
transforms?: Array<Transform>,
|
||||||
|
throttle?: number,
|
||||||
|
+ debounce?: number,
|
||||||
|
migrate?: (PersistedState, number) => Promise<PersistedState>,
|
||||||
|
stateReconciler?: false | Function,
|
||||||
|
getStoredState?: PersistConfig => Promise<PersistedState>, // used for migrations
|
||||||
|
diff --git a/node_modules/redux-persist/src/types.js b/node_modules/redux-persist/src/types.js
|
||||||
|
index c50d3cd..39d8be2 100644
|
||||||
|
--- a/node_modules/redux-persist/src/types.js
|
||||||
|
+++ b/node_modules/redux-persist/src/types.js
|
||||||
|
@@ -19,6 +19,7 @@ export type PersistConfig = {
|
||||||
|
whitelist?: Array<string>,
|
||||||
|
transforms?: Array<Transform>,
|
||||||
|
throttle?: number,
|
||||||
|
+ debounce?: number,
|
||||||
|
migrate?: (PersistedState, number) => Promise<PersistedState>,
|
||||||
|
stateReconciler?: false | Function,
|
||||||
|
getStoredState?: PersistConfig => Promise<PersistedState>, // used for migrations
|
||||||
|
diff --git a/node_modules/redux-persist/types/types.d.ts b/node_modules/redux-persist/types/types.d.ts
|
||||||
|
index b3733bc..2a1696c 100644
|
||||||
|
--- a/node_modules/redux-persist/types/types.d.ts
|
||||||
|
+++ b/node_modules/redux-persist/types/types.d.ts
|
||||||
|
@@ -35,6 +35,7 @@ declare module "redux-persist/es/types" {
|
||||||
|
whitelist?: Array<string>;
|
||||||
|
transforms?: Array<Transform<HSS, ESS, S, RS>>;
|
||||||
|
throttle?: number;
|
||||||
|
+ debounce?: number;
|
||||||
|
migrate?: PersistMigrate;
|
||||||
|
stateReconciler?: false | StateReconciler<S>;
|
||||||
|
/**
|
@ -1,5 +1,9 @@
|
|||||||
@use '../styles/Mixins/' as *;
|
@use '../styles/Mixins/' as *;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--svg-color);
|
||||||
|
}
|
||||||
|
|
||||||
.App {
|
.App {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
@ -1,89 +1,19 @@
|
|||||||
import { useEffect } from 'react';
|
import ProgressBar from 'features/system/components/ProgressBar';
|
||||||
import ProgressBar from '../features/system/ProgressBar';
|
import SiteHeader from 'features/system/components/SiteHeader';
|
||||||
import SiteHeader from '../features/system/SiteHeader';
|
import Console from 'features/system/components/Console';
|
||||||
import Console from '../features/system/Console';
|
|
||||||
import { useAppDispatch } from './store';
|
|
||||||
import { requestSystemConfig } from './socketio/actions';
|
|
||||||
import { keepGUIAlive } from './utils';
|
import { keepGUIAlive } from './utils';
|
||||||
import InvokeTabs from '../features/tabs/InvokeTabs';
|
import InvokeTabs from 'features/tabs/components/InvokeTabs';
|
||||||
import ImageUploader from '../common/components/ImageUploader';
|
import ImageUploader from 'common/components/ImageUploader';
|
||||||
import { RootState, useAppSelector } from '../app/store';
|
|
||||||
|
|
||||||
import FloatingGalleryButton from '../features/tabs/FloatingGalleryButton';
|
import useToastWatcher from 'features/system/hooks/useToastWatcher';
|
||||||
import FloatingOptionsPanelButtons from '../features/tabs/FloatingOptionsPanelButtons';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import FloatingOptionsPanelButtons from 'features/tabs/components/FloatingOptionsPanelButtons';
|
||||||
import { GalleryState } from '../features/gallery/gallerySlice';
|
import FloatingGalleryButton from 'features/tabs/components/FloatingGalleryButton';
|
||||||
import { OptionsState } from '../features/options/optionsSlice';
|
|
||||||
import { activeTabNameSelector } from '../features/options/optionsSelectors';
|
|
||||||
import { SystemState } from '../features/system/systemSlice';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { Model } from './invokeai';
|
|
||||||
|
|
||||||
keepGUIAlive();
|
keepGUIAlive();
|
||||||
|
|
||||||
const appSelector = createSelector(
|
|
||||||
[
|
|
||||||
(state: RootState) => state.gallery,
|
|
||||||
(state: RootState) => state.options,
|
|
||||||
(state: RootState) => state.system,
|
|
||||||
activeTabNameSelector,
|
|
||||||
],
|
|
||||||
(
|
|
||||||
gallery: GalleryState,
|
|
||||||
options: OptionsState,
|
|
||||||
system: SystemState,
|
|
||||||
activeTabName
|
|
||||||
) => {
|
|
||||||
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
|
|
||||||
gallery;
|
|
||||||
const {
|
|
||||||
shouldShowOptionsPanel,
|
|
||||||
shouldHoldOptionsPanelOpen,
|
|
||||||
shouldPinOptionsPanel,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const modelStatusText = _.reduce(
|
|
||||||
system.model_list,
|
|
||||||
(acc: string, cur: Model, key: string) => {
|
|
||||||
if (cur.status === 'active') acc = key;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldShowGalleryButton = !(
|
|
||||||
shouldShowGallery ||
|
|
||||||
(shouldHoldGalleryOpen && !shouldPinGallery)
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldShowOptionsPanelButton =
|
|
||||||
!(
|
|
||||||
shouldShowOptionsPanel ||
|
|
||||||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
|
|
||||||
) && ['txt2img', 'img2img', 'inpainting'].includes(activeTabName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
modelStatusText,
|
|
||||||
shouldShowGalleryButton,
|
|
||||||
shouldShowOptionsPanelButton,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: _.isEqual,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const dispatch = useAppDispatch();
|
useToastWatcher();
|
||||||
|
|
||||||
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
|
|
||||||
useAppSelector(appSelector);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(requestSystemConfig());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
@ -96,9 +26,9 @@ const App = () => {
|
|||||||
<div className="app-console">
|
<div className="app-console">
|
||||||
<Console />
|
<Console />
|
||||||
</div>
|
</div>
|
||||||
{shouldShowGalleryButton && <FloatingGalleryButton />}
|
|
||||||
{shouldShowOptionsPanelButton && <FloatingOptionsPanelButtons />}
|
|
||||||
</ImageUploader>
|
</ImageUploader>
|
||||||
|
<FloatingOptionsPanelButtons />
|
||||||
|
<FloatingGalleryButton />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// TODO: use Enums?
|
// TODO: use Enums?
|
||||||
|
|
||||||
import { InProgressImageType } from '../features/system/systemSlice';
|
import { InProgressImageType } from 'features/system/store/systemSlice';
|
||||||
|
|
||||||
// Valid samplers
|
// Valid samplers
|
||||||
export const SAMPLERS: Array<string> = [
|
export const SAMPLERS: Array<string> = [
|
||||||
|
@ -13,10 +13,13 @@ export enum Feature {
|
|||||||
UPSCALE,
|
UPSCALE,
|
||||||
FACE_CORRECTION,
|
FACE_CORRECTION,
|
||||||
IMAGE_TO_IMAGE,
|
IMAGE_TO_IMAGE,
|
||||||
|
BOUNDING_BOX,
|
||||||
|
SEAM_CORRECTION,
|
||||||
|
INFILL_AND_SCALING,
|
||||||
}
|
}
|
||||||
/** For each tooltip in the UI, the below feature definitions & props will pull relevant information into the tooltip.
|
/** For each tooltip in the UI, the below feature definitions & props will pull relevant information into the tooltip.
|
||||||
*
|
*
|
||||||
* To-do: href & GuideImages are placeholders, and are not currently utilized, but will be updated (along with the tooltip UI) as feature and UI development and we get a better idea on where things "forever homes" will be .
|
* To-do: href & GuideImages are placeholders, and are not currently utilized, but will be updated (along with the tooltip UI) as feature and UI develop and we get a better idea on where things "forever homes" will be .
|
||||||
*/
|
*/
|
||||||
export const FEATURES: Record<Feature, FeatureHelpInfo> = {
|
export const FEATURES: Record<Feature, FeatureHelpInfo> = {
|
||||||
[Feature.PROMPT]: {
|
[Feature.PROMPT]: {
|
||||||
@ -55,7 +58,22 @@ export const FEATURES: Record<Feature, FeatureHelpInfo> = {
|
|||||||
guideImage: 'asset/path.gif',
|
guideImage: 'asset/path.gif',
|
||||||
},
|
},
|
||||||
[Feature.IMAGE_TO_IMAGE]: {
|
[Feature.IMAGE_TO_IMAGE]: {
|
||||||
text: 'ImageToImage allows the upload of an initial image, which InvokeAI will use to guide the generation process, along with a prompt. A lower value for this setting will more closely resemble the original image. Values between 0-1 are accepted, and a range of .25-.75 is recommended ',
|
text: 'Image to Image allows the upload of an initial image, which InvokeAI will use to guide the generation process, along with a prompt. A lower value for this setting will more closely resemble the original image. Values between 0-1 are accepted, and a range of .25-.75 is recommended ',
|
||||||
|
href: 'link/to/docs/feature3.html',
|
||||||
|
guideImage: 'asset/path.gif',
|
||||||
|
},
|
||||||
|
[Feature.BOUNDING_BOX]: {
|
||||||
|
text: 'The bounding box is analogous to the Width and Height settings for Text to Image or Image to Image. Only the area in the box will be processed.',
|
||||||
|
href: 'link/to/docs/feature3.html',
|
||||||
|
guideImage: 'asset/path.gif',
|
||||||
|
},
|
||||||
|
[Feature.SEAM_CORRECTION]: {
|
||||||
|
text: 'Control the handling of visible seams which may occur when a generated image is pasted back onto the canvas.',
|
||||||
|
href: 'link/to/docs/feature3.html',
|
||||||
|
guideImage: 'asset/path.gif',
|
||||||
|
},
|
||||||
|
[Feature.INFILL_AND_SCALING]: {
|
||||||
|
text: 'Manage infill methods (used on masked or erased areas of the canvas) and scaling (useful for small bounding box sizes).',
|
||||||
href: 'link/to/docs/feature3.html',
|
href: 'link/to/docs/feature3.html',
|
||||||
guideImage: 'asset/path.gif',
|
guideImage: 'asset/path.gif',
|
||||||
},
|
},
|
||||||
|
43
frontend/src/app/invokeai.d.ts
vendored
@ -12,7 +12,9 @@
|
|||||||
* 'gfpgan'.
|
* 'gfpgan'.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Category as GalleryCategory } from '../features/gallery/gallerySlice';
|
import { Category as GalleryCategory } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { InvokeTabName } from 'features/tabs/components/InvokeTabs';
|
||||||
|
import { IRect } from 'konva/lib/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
@ -103,7 +105,7 @@ export declare type PostProcessedImageMetadata =
|
|||||||
| FacetoolMetadata;
|
| FacetoolMetadata;
|
||||||
|
|
||||||
// Metadata includes the system config and image metadata.
|
// Metadata includes the system config and image metadata.
|
||||||
export declare type Metadata = SystemConfig & {
|
export declare type Metadata = SystemGenerationMetadata & {
|
||||||
image: GeneratedImageMetadata | PostProcessedImageMetadata;
|
image: GeneratedImageMetadata | PostProcessedImageMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -111,12 +113,14 @@ export declare type Metadata = SystemConfig & {
|
|||||||
export declare type Image = {
|
export declare type Image = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
thumbnail: string;
|
||||||
mtime: number;
|
mtime: number;
|
||||||
metadata?: Metadata;
|
metadata?: Metadata;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
category: GalleryCategory;
|
category: GalleryCategory;
|
||||||
isBase64: boolean;
|
isBase64?: boolean;
|
||||||
|
dreamPrompt?: 'string';
|
||||||
};
|
};
|
||||||
|
|
||||||
// GalleryImages is an array of Image.
|
// GalleryImages is an array of Image.
|
||||||
@ -140,13 +144,18 @@ export declare type SystemStatus = {
|
|||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type SystemConfig = {
|
export declare type SystemGenerationMetadata = {
|
||||||
model: string;
|
model: string;
|
||||||
model_id: string;
|
model_weights?: string;
|
||||||
|
model_id?: string;
|
||||||
model_hash: string;
|
model_hash: string;
|
||||||
app_id: string;
|
app_id: string;
|
||||||
app_version: string;
|
app_version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export declare type SystemConfig = SystemGenerationMetadata & {
|
||||||
model_list: ModelList;
|
model_list: ModelList;
|
||||||
|
infill_methods: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ModelStatus = 'active' | 'cached' | 'not loaded';
|
export declare type ModelStatus = 'active' | 'cached' | 'not loaded';
|
||||||
@ -171,10 +180,19 @@ export declare type SystemStatusResponse = SystemStatus;
|
|||||||
|
|
||||||
export declare type SystemConfigResponse = SystemConfig;
|
export declare type SystemConfigResponse = SystemConfig;
|
||||||
|
|
||||||
export declare type ImageResultResponse = Omit<Image, 'uuid'>;
|
export declare type ImageResultResponse = Omit<Image, 'uuid'> & {
|
||||||
|
boundingBox?: IRect;
|
||||||
|
generationMode: InvokeTabName;
|
||||||
|
};
|
||||||
|
|
||||||
export declare type ImageUploadResponse = Omit<Image, 'uuid' | 'metadata'> & {
|
export declare type ImageUploadResponse = {
|
||||||
destination: 'img2img' | 'inpainting';
|
// image: Omit<Image, 'uuid' | 'metadata' | 'category'>;
|
||||||
|
url: string;
|
||||||
|
mtime: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
thumbnail: string;
|
||||||
|
// bbox: [number, number, number, number];
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ErrorResponse = {
|
export declare type ErrorResponse = {
|
||||||
@ -198,9 +216,12 @@ export declare type ImageUrlResponse = {
|
|||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ImageUploadDestination = 'img2img' | 'inpainting';
|
|
||||||
|
|
||||||
export declare type UploadImagePayload = {
|
export declare type UploadImagePayload = {
|
||||||
file: File;
|
file: File;
|
||||||
destination?: ImageUploadDestination;
|
destination?: ImageUploadDestination;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export declare type UploadOutpaintingMergeImagePayload = {
|
||||||
|
dataURL: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
@ -1,39 +1,35 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { RootState } from '../store';
|
import { RootState } from 'app/store';
|
||||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
|
||||||
import { OptionsState } from '../../features/options/optionsSlice';
|
import { OptionsState } from 'features/options/store/optionsSlice';
|
||||||
|
import { SystemState } from 'features/system/store/systemSlice';
|
||||||
import { SystemState } from '../../features/system/systemSlice';
|
import { validateSeedWeights } from 'common/util/seedWeightPairs';
|
||||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
import { initialCanvasImageSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { validateSeedWeights } from '../../common/util/seedWeightPairs';
|
|
||||||
|
|
||||||
export const readinessSelector = createSelector(
|
export const readinessSelector = createSelector(
|
||||||
[
|
[
|
||||||
(state: RootState) => state.options,
|
(state: RootState) => state.options,
|
||||||
(state: RootState) => state.system,
|
(state: RootState) => state.system,
|
||||||
(state: RootState) => state.inpainting,
|
initialCanvasImageSelector,
|
||||||
activeTabNameSelector,
|
activeTabNameSelector,
|
||||||
],
|
],
|
||||||
(
|
(
|
||||||
options: OptionsState,
|
options: OptionsState,
|
||||||
system: SystemState,
|
system: SystemState,
|
||||||
inpainting: InpaintingState,
|
initialCanvasImage,
|
||||||
activeTabName
|
activeTabName
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
prompt,
|
prompt,
|
||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
seedWeights,
|
seedWeights,
|
||||||
// maskPath,
|
|
||||||
initialImage,
|
initialImage,
|
||||||
seed,
|
seed,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const { isProcessing, isConnected } = system;
|
const { isProcessing, isConnected } = system;
|
||||||
|
|
||||||
const { imageToInpaint } = inpainting;
|
|
||||||
|
|
||||||
let isReady = true;
|
let isReady = true;
|
||||||
const reasonsWhyNotReady: string[] = [];
|
const reasonsWhyNotReady: string[] = [];
|
||||||
|
|
||||||
@ -48,20 +44,6 @@ export const readinessSelector = createSelector(
|
|||||||
reasonsWhyNotReady.push('No initial image selected');
|
reasonsWhyNotReady.push('No initial image selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTabName === 'inpainting' && !imageToInpaint) {
|
|
||||||
isReady = false;
|
|
||||||
reasonsWhyNotReady.push('No inpainting image selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// // We don't use mask paths now.
|
|
||||||
// // Cannot generate with a mask without img2img
|
|
||||||
// if (maskPath && !initialImage) {
|
|
||||||
// isReady = false;
|
|
||||||
// reasonsWhyNotReady.push(
|
|
||||||
// 'On ImageToImage tab, but no mask is provided.'
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: job queue
|
// TODO: job queue
|
||||||
// Cannot generate if already processing an image
|
// Cannot generate if already processing an image
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { GalleryCategory } from '../../features/gallery/gallerySlice';
|
import { GalleryCategory } from 'features/gallery/store/gallerySlice';
|
||||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
import { InvokeTabName } from 'features/tabs/components/InvokeTabs';
|
||||||
import * as InvokeAI from '../invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We can't use redux-toolkit's createSlice() to make these actions,
|
* We can't use redux-toolkit's createSlice() to make these actions,
|
||||||
@ -26,8 +25,6 @@ export const requestNewImages = createAction<GalleryCategory>(
|
|||||||
export const cancelProcessing = createAction<undefined>(
|
export const cancelProcessing = createAction<undefined>(
|
||||||
'socketio/cancelProcessing'
|
'socketio/cancelProcessing'
|
||||||
);
|
);
|
||||||
export const uploadImage = createAction<InvokeAI.UploadImagePayload>('socketio/uploadImage');
|
|
||||||
export const uploadMaskImage = createAction<File>('socketio/uploadMaskImage');
|
|
||||||
|
|
||||||
export const requestSystemConfig = createAction<undefined>(
|
export const requestSystemConfig = createAction<undefined>(
|
||||||
'socketio/requestSystemConfig'
|
'socketio/requestSystemConfig'
|
||||||
@ -36,3 +33,11 @@ export const requestSystemConfig = createAction<undefined>(
|
|||||||
export const requestModelChange = createAction<string>(
|
export const requestModelChange = createAction<string>(
|
||||||
'socketio/requestModelChange'
|
'socketio/requestModelChange'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const saveStagingAreaImageToGallery = createAction<string>(
|
||||||
|
'socketio/saveStagingAreaImageToGallery'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const emptyTempFolder = createAction<undefined>(
|
||||||
|
'socketio/requestEmptyTempFolder'
|
||||||
|
);
|
||||||
|
@ -4,23 +4,22 @@ import { Socket } from 'socket.io-client';
|
|||||||
import {
|
import {
|
||||||
frontendToBackendParameters,
|
frontendToBackendParameters,
|
||||||
FrontendToBackendParametersConfig,
|
FrontendToBackendParametersConfig,
|
||||||
} from '../../common/util/parameterTranslation';
|
} from 'common/util/parameterTranslation';
|
||||||
import {
|
import {
|
||||||
GalleryCategory,
|
GalleryCategory,
|
||||||
GalleryState,
|
GalleryState,
|
||||||
removeImage,
|
removeImage,
|
||||||
} from '../../features/gallery/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
import { OptionsState } from '../../features/options/optionsSlice';
|
import { OptionsState } from 'features/options/store/optionsSlice';
|
||||||
import {
|
import {
|
||||||
addLogEntry,
|
addLogEntry,
|
||||||
errorOccurred,
|
generationRequested,
|
||||||
modelChangeRequested,
|
modelChangeRequested,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
} from '../../features/system/systemSlice';
|
} from 'features/system/store/systemSlice';
|
||||||
import { inpaintingImageElementRef } from '../../features/tabs/Inpainting/InpaintingCanvas';
|
import { InvokeTabName } from 'features/tabs/components/InvokeTabs';
|
||||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
import * as InvokeAI from '../invokeai';
|
import { RootState } from 'app/store';
|
||||||
import { RootState } from '../store';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object containing all functions which use `socketio.emit()`.
|
* Returns an object containing all functions which use `socketio.emit()`.
|
||||||
@ -42,7 +41,7 @@ const makeSocketIOEmitters = (
|
|||||||
const {
|
const {
|
||||||
options: optionsState,
|
options: optionsState,
|
||||||
system: systemState,
|
system: systemState,
|
||||||
inpainting: inpaintingState,
|
canvas: canvasState,
|
||||||
gallery: galleryState,
|
gallery: galleryState,
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
@ -50,32 +49,13 @@ const makeSocketIOEmitters = (
|
|||||||
{
|
{
|
||||||
generationMode,
|
generationMode,
|
||||||
optionsState,
|
optionsState,
|
||||||
inpaintingState,
|
canvasState,
|
||||||
systemState,
|
systemState,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (generationMode === 'inpainting') {
|
dispatch(generationRequested());
|
||||||
if (
|
|
||||||
!inpaintingImageElementRef.current ||
|
|
||||||
!inpaintingState.imageToInpaint?.url
|
|
||||||
) {
|
|
||||||
dispatch(
|
|
||||||
addLogEntry({
|
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
message: 'Inpainting image not loaded, cannot generate image.',
|
|
||||||
level: 'error',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(errorOccurred());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frontendToBackendParametersConfig.imageToProcessUrl =
|
if (!['txt2img', 'img2img'].includes(generationMode)) {
|
||||||
inpaintingState.imageToInpaint.url;
|
|
||||||
|
|
||||||
frontendToBackendParametersConfig.maskImageElement =
|
|
||||||
inpaintingImageElementRef.current;
|
|
||||||
} else if (!['txt2img', 'img2img'].includes(generationMode)) {
|
|
||||||
if (!galleryState.currentImage?.url) return;
|
if (!galleryState.currentImage?.url) return;
|
||||||
|
|
||||||
frontendToBackendParametersConfig.imageToProcessUrl =
|
frontendToBackendParametersConfig.imageToProcessUrl =
|
||||||
@ -96,7 +76,12 @@ const makeSocketIOEmitters = (
|
|||||||
// TODO: handle maintaining masks for reproducibility in future
|
// TODO: handle maintaining masks for reproducibility in future
|
||||||
if (generationParameters.init_mask) {
|
if (generationParameters.init_mask) {
|
||||||
generationParameters.init_mask = generationParameters.init_mask
|
generationParameters.init_mask = generationParameters.init_mask
|
||||||
.substr(0, 20)
|
.substr(0, 64)
|
||||||
|
.concat('...');
|
||||||
|
}
|
||||||
|
if (generationParameters.init_img) {
|
||||||
|
generationParameters.init_img = generationParameters.init_img
|
||||||
|
.substr(0, 64)
|
||||||
.concat('...');
|
.concat('...');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,9 +147,9 @@ const makeSocketIOEmitters = (
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
||||||
const { url, uuid, category } = imageToDelete;
|
const { url, uuid, category, thumbnail } = imageToDelete;
|
||||||
dispatch(removeImage(imageToDelete));
|
dispatch(removeImage(imageToDelete));
|
||||||
socketio.emit('deleteImage', url, uuid, category);
|
socketio.emit('deleteImage', url, thumbnail, uuid, category);
|
||||||
},
|
},
|
||||||
emitRequestImages: (category: GalleryCategory) => {
|
emitRequestImages: (category: GalleryCategory) => {
|
||||||
const gallery: GalleryState = getState().gallery;
|
const gallery: GalleryState = getState().gallery;
|
||||||
@ -179,13 +164,6 @@ const makeSocketIOEmitters = (
|
|||||||
emitCancelProcessing: () => {
|
emitCancelProcessing: () => {
|
||||||
socketio.emit('cancel');
|
socketio.emit('cancel');
|
||||||
},
|
},
|
||||||
emitUploadImage: (payload: InvokeAI.UploadImagePayload) => {
|
|
||||||
const { file, destination } = payload;
|
|
||||||
socketio.emit('uploadImage', file, file.name, destination);
|
|
||||||
},
|
|
||||||
emitUploadMaskImage: (file: File) => {
|
|
||||||
socketio.emit('uploadMaskImage', file, file.name);
|
|
||||||
},
|
|
||||||
emitRequestSystemConfig: () => {
|
emitRequestSystemConfig: () => {
|
||||||
socketio.emit('requestSystemConfig');
|
socketio.emit('requestSystemConfig');
|
||||||
},
|
},
|
||||||
@ -193,6 +171,12 @@ const makeSocketIOEmitters = (
|
|||||||
dispatch(modelChangeRequested());
|
dispatch(modelChangeRequested());
|
||||||
socketio.emit('requestModelChange', modelName);
|
socketio.emit('requestModelChange', modelName);
|
||||||
},
|
},
|
||||||
|
emitSaveStagingAreaImageToGallery: (url: string) => {
|
||||||
|
socketio.emit('requestSaveStagingAreaImageToGallery', url);
|
||||||
|
},
|
||||||
|
emitRequestEmptyTempFolder: () => {
|
||||||
|
socketio.emit('requestEmptyTempFolder');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { AnyAction, MiddlewareAPI, Dispatch } from '@reduxjs/toolkit';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import dateFormat from 'dateformat';
|
import dateFormat from 'dateformat';
|
||||||
|
|
||||||
import * as InvokeAI from '../invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addLogEntry,
|
addLogEntry,
|
||||||
@ -15,7 +15,8 @@ import {
|
|||||||
errorOccurred,
|
errorOccurred,
|
||||||
setModelList,
|
setModelList,
|
||||||
setIsCancelable,
|
setIsCancelable,
|
||||||
} from '../../features/system/systemSlice';
|
addToast,
|
||||||
|
} from 'features/system/store/systemSlice';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addGalleryImages,
|
addGalleryImages,
|
||||||
@ -23,21 +24,22 @@ import {
|
|||||||
clearIntermediateImage,
|
clearIntermediateImage,
|
||||||
GalleryState,
|
GalleryState,
|
||||||
removeImage,
|
removeImage,
|
||||||
setCurrentImage,
|
|
||||||
setIntermediateImage,
|
setIntermediateImage,
|
||||||
} from '../../features/gallery/gallerySlice';
|
} from 'features/gallery/store/gallerySlice';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clearInitialImage,
|
clearInitialImage,
|
||||||
|
setInfillMethod,
|
||||||
setInitialImage,
|
setInitialImage,
|
||||||
setMaskPath,
|
setMaskPath,
|
||||||
} from '../../features/options/optionsSlice';
|
} from 'features/options/store/optionsSlice';
|
||||||
import { requestImages, requestNewImages } from './actions';
|
|
||||||
import {
|
import {
|
||||||
clearImageToInpaint,
|
requestImages,
|
||||||
setImageToInpaint,
|
requestNewImages,
|
||||||
} from '../../features/tabs/Inpainting/inpaintingSlice';
|
requestSystemConfig,
|
||||||
import { tabMap } from '../../features/tabs/InvokeTabs';
|
} from './actions';
|
||||||
|
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
||||||
|
import { tabMap } from 'features/tabs/components/InvokeTabs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object containing listener callbacks for socketio events.
|
* Returns an object containing listener callbacks for socketio events.
|
||||||
@ -56,6 +58,7 @@ const makeSocketIOListeners = (
|
|||||||
try {
|
try {
|
||||||
dispatch(setIsConnected(true));
|
dispatch(setIsConnected(true));
|
||||||
dispatch(setCurrentStatus('Connected'));
|
dispatch(setCurrentStatus('Connected'));
|
||||||
|
dispatch(requestSystemConfig());
|
||||||
const gallery: GalleryState = getState().gallery;
|
const gallery: GalleryState = getState().gallery;
|
||||||
|
|
||||||
if (gallery.categories.user.latest_mtime) {
|
if (gallery.categories.user.latest_mtime) {
|
||||||
@ -97,19 +100,42 @@ const makeSocketIOListeners = (
|
|||||||
*/
|
*/
|
||||||
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
||||||
try {
|
try {
|
||||||
const { shouldLoopback, activeTab } = getState().options;
|
const state = getState();
|
||||||
|
const { shouldLoopback, activeTab } = state.options;
|
||||||
|
const { boundingBox: _, generationMode, ...rest } = data;
|
||||||
|
|
||||||
const newImage = {
|
const newImage = {
|
||||||
uuid: uuidv4(),
|
uuid: uuidv4(),
|
||||||
...data,
|
...rest,
|
||||||
category: 'result',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch(
|
if (['txt2img', 'img2img'].includes(generationMode)) {
|
||||||
addImage({
|
dispatch(
|
||||||
category: 'result',
|
addImage({
|
||||||
image: newImage,
|
category: 'result',
|
||||||
})
|
image: { ...newImage, category: 'result' },
|
||||||
);
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generationMode === 'unifiedCanvas' && data.boundingBox) {
|
||||||
|
const { boundingBox } = data;
|
||||||
|
dispatch(
|
||||||
|
addImageToStagingArea({
|
||||||
|
image: { ...newImage, category: 'temp' },
|
||||||
|
boundingBox,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state.canvas.shouldAutoSave) {
|
||||||
|
dispatch(
|
||||||
|
addImage({
|
||||||
|
image: { ...newImage, category: 'result' },
|
||||||
|
category: 'result',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldLoopback) {
|
if (shouldLoopback) {
|
||||||
const activeTabName = tabMap[activeTab];
|
const activeTabName = tabMap[activeTab];
|
||||||
@ -118,13 +144,11 @@ const makeSocketIOListeners = (
|
|||||||
dispatch(setInitialImage(newImage));
|
dispatch(setInitialImage(newImage));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'inpainting': {
|
|
||||||
dispatch(setImageToInpaint(newImage));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch(clearIntermediateImage());
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
@ -144,6 +168,7 @@ const makeSocketIOListeners = (
|
|||||||
setIntermediateImage({
|
setIntermediateImage({
|
||||||
uuid: uuidv4(),
|
uuid: uuidv4(),
|
||||||
...data,
|
...data,
|
||||||
|
category: 'result',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (!data.isBase64) {
|
if (!data.isBase64) {
|
||||||
@ -299,16 +324,11 @@ const makeSocketIOListeners = (
|
|||||||
|
|
||||||
// remove references to image in options
|
// remove references to image in options
|
||||||
const { initialImage, maskPath } = getState().options;
|
const { initialImage, maskPath } = getState().options;
|
||||||
const { imageToInpaint } = getState().inpainting;
|
|
||||||
|
|
||||||
if (initialImage?.url === url || initialImage === url) {
|
if (initialImage?.url === url || initialImage === url) {
|
||||||
dispatch(clearInitialImage());
|
dispatch(clearInitialImage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageToInpaint?.url === url) {
|
|
||||||
dispatch(clearImageToInpaint());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maskPath === url) {
|
if (maskPath === url) {
|
||||||
dispatch(setMaskPath(''));
|
dispatch(setMaskPath(''));
|
||||||
}
|
}
|
||||||
@ -320,56 +340,11 @@ const makeSocketIOListeners = (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onImageUploaded: (data: InvokeAI.ImageUploadResponse) => {
|
|
||||||
const { destination, ...rest } = data;
|
|
||||||
const image = {
|
|
||||||
uuid: uuidv4(),
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
dispatch(addImage({ image, category: 'user' }));
|
|
||||||
|
|
||||||
switch (destination) {
|
|
||||||
case 'img2img': {
|
|
||||||
dispatch(setInitialImage(image));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'inpainting': {
|
|
||||||
dispatch(setImageToInpaint(image));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
dispatch(setCurrentImage(image));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
addLogEntry({
|
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
message: `Image uploaded: ${data.url}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Callback to run when we receive a 'maskImageUploaded' event.
|
|
||||||
*/
|
|
||||||
onMaskImageUploaded: (data: InvokeAI.ImageUrlResponse) => {
|
|
||||||
const { url } = data;
|
|
||||||
dispatch(setMaskPath(url));
|
|
||||||
dispatch(
|
|
||||||
addLogEntry({
|
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
|
||||||
message: `Mask image uploaded: ${url}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSystemConfig: (data: InvokeAI.SystemConfig) => {
|
onSystemConfig: (data: InvokeAI.SystemConfig) => {
|
||||||
dispatch(setSystemConfig(data));
|
dispatch(setSystemConfig(data));
|
||||||
|
if (!data.infill_methods.includes('patchmatch')) {
|
||||||
|
dispatch(setInfillMethod(data.infill_methods[0]));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onModelChanged: (data: InvokeAI.ModelChangeResponse) => {
|
onModelChanged: (data: InvokeAI.ModelChangeResponse) => {
|
||||||
const { model_name, model_list } = data;
|
const { model_name, model_list } = data;
|
||||||
@ -399,6 +374,16 @@ const makeSocketIOListeners = (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
onTempFolderEmptied: () => {
|
||||||
|
dispatch(
|
||||||
|
addToast({
|
||||||
|
title: 'Temp Folder Emptied',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2500,
|
||||||
|
isClosable: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { io } from 'socket.io-client';
|
|||||||
import makeSocketIOListeners from './listeners';
|
import makeSocketIOListeners from './listeners';
|
||||||
import makeSocketIOEmitters from './emitters';
|
import makeSocketIOEmitters from './emitters';
|
||||||
|
|
||||||
import * as InvokeAI from '../invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a socketio middleware to handle communication with server.
|
* Creates a socketio middleware to handle communication with server.
|
||||||
@ -43,11 +43,10 @@ export const socketioMiddleware = () => {
|
|||||||
onGalleryImages,
|
onGalleryImages,
|
||||||
onProcessingCanceled,
|
onProcessingCanceled,
|
||||||
onImageDeleted,
|
onImageDeleted,
|
||||||
onImageUploaded,
|
|
||||||
onMaskImageUploaded,
|
|
||||||
onSystemConfig,
|
onSystemConfig,
|
||||||
onModelChanged,
|
onModelChanged,
|
||||||
onModelChangeFailed,
|
onModelChangeFailed,
|
||||||
|
onTempFolderEmptied,
|
||||||
} = makeSocketIOListeners(store);
|
} = makeSocketIOListeners(store);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -58,10 +57,10 @@ export const socketioMiddleware = () => {
|
|||||||
emitRequestImages,
|
emitRequestImages,
|
||||||
emitRequestNewImages,
|
emitRequestNewImages,
|
||||||
emitCancelProcessing,
|
emitCancelProcessing,
|
||||||
emitUploadImage,
|
|
||||||
emitUploadMaskImage,
|
|
||||||
emitRequestSystemConfig,
|
emitRequestSystemConfig,
|
||||||
emitRequestModelChange,
|
emitRequestModelChange,
|
||||||
|
emitSaveStagingAreaImageToGallery,
|
||||||
|
emitRequestEmptyTempFolder,
|
||||||
} = makeSocketIOEmitters(store, socketio);
|
} = makeSocketIOEmitters(store, socketio);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,17 +103,6 @@ export const socketioMiddleware = () => {
|
|||||||
onImageDeleted(data);
|
onImageDeleted(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socketio.on(
|
|
||||||
'imageUploaded',
|
|
||||||
(data: InvokeAI.ImageUploadResponse) => {
|
|
||||||
onImageUploaded(data);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
socketio.on('maskImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
|
||||||
onMaskImageUploaded(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => {
|
socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => {
|
||||||
onSystemConfig(data);
|
onSystemConfig(data);
|
||||||
});
|
});
|
||||||
@ -127,6 +115,10 @@ export const socketioMiddleware = () => {
|
|||||||
onModelChangeFailed(data);
|
onModelChangeFailed(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socketio.on('tempFolderEmptied', () => {
|
||||||
|
onTempFolderEmptied();
|
||||||
|
});
|
||||||
|
|
||||||
areListenersSet = true;
|
areListenersSet = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,16 +161,6 @@ export const socketioMiddleware = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'socketio/uploadImage': {
|
|
||||||
emitUploadImage(action.payload);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'socketio/uploadMaskImage': {
|
|
||||||
emitUploadMaskImage(action.payload);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'socketio/requestSystemConfig': {
|
case 'socketio/requestSystemConfig': {
|
||||||
emitRequestSystemConfig();
|
emitRequestSystemConfig();
|
||||||
break;
|
break;
|
||||||
@ -188,6 +170,16 @@ export const socketioMiddleware = () => {
|
|||||||
emitRequestModelChange(action.payload);
|
emitRequestModelChange(action.payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'socketio/saveStagingAreaImageToGallery': {
|
||||||
|
emitSaveStagingAreaImageToGallery(action.payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'socketio/requestEmptyTempFolder': {
|
||||||
|
emitRequestEmptyTempFolder();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next(action);
|
next(action);
|
||||||
|
@ -5,16 +5,14 @@ import type { TypedUseSelectorHook } from 'react-redux';
|
|||||||
import { persistReducer } from 'redux-persist';
|
import { persistReducer } from 'redux-persist';
|
||||||
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
||||||
|
|
||||||
import optionsReducer, { OptionsState } from '../features/options/optionsSlice';
|
import { getPersistConfig } from 'redux-deep-persist';
|
||||||
import galleryReducer, { GalleryState } from '../features/gallery/gallerySlice';
|
|
||||||
import inpaintingReducer, {
|
import optionsReducer from 'features/options/store/optionsSlice';
|
||||||
InpaintingState,
|
import galleryReducer from 'features/gallery/store/gallerySlice';
|
||||||
} from '../features/tabs/Inpainting/inpaintingSlice';
|
import systemReducer from 'features/system/store/systemSlice';
|
||||||
|
import canvasReducer from 'features/canvas/store/canvasSlice';
|
||||||
|
|
||||||
import systemReducer, { SystemState } from '../features/system/systemSlice';
|
|
||||||
import { socketioMiddleware } from './socketio/middleware';
|
import { socketioMiddleware } from './socketio/middleware';
|
||||||
import autoMergeLevel2 from 'redux-persist/es/stateReconciler/autoMergeLevel2';
|
|
||||||
import { PersistPartial } from 'redux-persist/es/persistReducer';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* redux-persist provides an easy and reliable way to persist state across reloads.
|
* redux-persist provides an easy and reliable way to persist state across reloads.
|
||||||
@ -28,87 +26,79 @@ import { PersistPartial } from 'redux-persist/es/persistReducer';
|
|||||||
* These can be blacklisted in redux-persist.
|
* These can be blacklisted in redux-persist.
|
||||||
*
|
*
|
||||||
* The necesssary nested persistors with blacklists are configured below.
|
* The necesssary nested persistors with blacklists are configured below.
|
||||||
*
|
|
||||||
* TODO: Do we blacklist initialImagePath? If the image is deleted from disk we get an
|
|
||||||
* ugly 404. But if we blacklist it, then this is a valuable parameter that is lost
|
|
||||||
* on reload. Need to figure out a good way to handle this.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const rootPersistConfig = {
|
const canvasBlacklist = [
|
||||||
key: 'root',
|
'cursorPosition',
|
||||||
storage,
|
'isCanvasInitialized',
|
||||||
stateReconciler: autoMergeLevel2,
|
'doesCanvasNeedScaling',
|
||||||
blacklist: ['gallery', 'system', 'inpainting'],
|
].map((blacklistItem) => `canvas.${blacklistItem}`);
|
||||||
};
|
|
||||||
|
|
||||||
const systemPersistConfig = {
|
const systemBlacklist = [
|
||||||
key: 'system',
|
'currentIteration',
|
||||||
storage,
|
'currentStatus',
|
||||||
stateReconciler: autoMergeLevel2,
|
'currentStep',
|
||||||
blacklist: [
|
'isCancelable',
|
||||||
'isCancelable',
|
'isConnected',
|
||||||
'isConnected',
|
'isESRGANAvailable',
|
||||||
'isProcessing',
|
'isGFPGANAvailable',
|
||||||
'currentStep',
|
'isProcessing',
|
||||||
'socketId',
|
'socketId',
|
||||||
'isESRGANAvailable',
|
'totalIterations',
|
||||||
'isGFPGANAvailable',
|
'totalSteps',
|
||||||
'currentStep',
|
].map((blacklistItem) => `system.${blacklistItem}`);
|
||||||
'totalSteps',
|
|
||||||
'currentIteration',
|
|
||||||
'totalIterations',
|
|
||||||
'currentStatus',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const galleryPersistConfig = {
|
const galleryBlacklist = [
|
||||||
key: 'gallery',
|
'categories',
|
||||||
storage,
|
'currentCategory',
|
||||||
stateReconciler: autoMergeLevel2,
|
'currentImage',
|
||||||
whitelist: [
|
'currentImageUuid',
|
||||||
'galleryWidth',
|
'shouldAutoSwitchToNewImages',
|
||||||
'shouldPinGallery',
|
'shouldHoldGalleryOpen',
|
||||||
'shouldShowGallery',
|
'intermediateImage',
|
||||||
'galleryScrollPosition',
|
].map((blacklistItem) => `gallery.${blacklistItem}`);
|
||||||
'galleryImageMinimumWidth',
|
|
||||||
'galleryImageObjectFit',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const inpaintingPersistConfig = {
|
const rootReducer = combineReducers({
|
||||||
key: 'inpainting',
|
|
||||||
storage,
|
|
||||||
stateReconciler: autoMergeLevel2,
|
|
||||||
blacklist: ['pastLines', 'futuresLines', 'cursorPosition'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const reducers = combineReducers({
|
|
||||||
options: optionsReducer,
|
options: optionsReducer,
|
||||||
gallery: persistReducer<GalleryState>(galleryPersistConfig, galleryReducer),
|
gallery: galleryReducer,
|
||||||
system: persistReducer<SystemState>(systemPersistConfig, systemReducer),
|
system: systemReducer,
|
||||||
inpainting: persistReducer<InpaintingState>(
|
canvas: canvasReducer,
|
||||||
inpaintingPersistConfig,
|
|
||||||
inpaintingReducer
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistedReducer = persistReducer<{
|
const rootPersistConfig = getPersistConfig({
|
||||||
options: OptionsState;
|
key: 'root',
|
||||||
gallery: GalleryState & PersistPartial;
|
storage,
|
||||||
system: SystemState & PersistPartial;
|
rootReducer,
|
||||||
inpainting: InpaintingState & PersistPartial;
|
blacklist: [...canvasBlacklist, ...systemBlacklist, ...galleryBlacklist],
|
||||||
}>(rootPersistConfig, reducers);
|
debounce: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
|
||||||
|
|
||||||
// Continue with store setup
|
// Continue with store setup
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: persistedReducer,
|
reducer: persistedReducer,
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware({
|
getDefaultMiddleware({
|
||||||
// redux-persist sometimes needs to temporarily put a function in redux state, need to disable this check
|
immutableCheck: false,
|
||||||
serializableCheck: false,
|
serializableCheck: false,
|
||||||
}).concat(socketioMiddleware()),
|
}).concat(socketioMiddleware()),
|
||||||
|
devTools: {
|
||||||
|
// Uncommenting these very rapidly called actions makes the redux dev tools output much more readable
|
||||||
|
actionsDenylist: [
|
||||||
|
'canvas/setCursorPosition',
|
||||||
|
'canvas/setStageCoordinates',
|
||||||
|
'canvas/setStageScale',
|
||||||
|
'canvas/setIsDrawing',
|
||||||
|
// 'canvas/setBoundingBoxCoordinates',
|
||||||
|
// 'canvas/setBoundingBoxDimensions',
|
||||||
|
'canvas/setIsDrawing',
|
||||||
|
'canvas/addPointToCurrentLine',
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type AppGetState = typeof store.getState;
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
export type AppDispatch = typeof store.dispatch;
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
BIN
frontend/src/assets/images/mask.afdesign
Normal file
77
frontend/src/assets/images/mask.svg
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 60 60" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
<g transform="matrix(1,0,0,1,0,5)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,10)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,15)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,20)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,25)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,30)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,35)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,40)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,45)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,50)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,55)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,60)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-5)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-10)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-15)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-20)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-25)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-30)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-35)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-40)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-45)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-50)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-55)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,0,-60)">
|
||||||
|
<path d="M-3.5,63.5L64,-4" style="fill:none;stroke:black;stroke-width:1px;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
@ -1,7 +1,7 @@
|
|||||||
import { Box, forwardRef, Icon } from '@chakra-ui/react';
|
import { Box, forwardRef, Icon } from '@chakra-ui/react';
|
||||||
import { IconType } from 'react-icons';
|
import { IconType } from 'react-icons';
|
||||||
import { MdHelp } from 'react-icons/md';
|
import { MdHelp } from 'react-icons/md';
|
||||||
import { Feature } from '../../app/features';
|
import { Feature } from 'app/features';
|
||||||
import GuidePopover from './GuidePopover';
|
import GuidePopover from './GuidePopover';
|
||||||
|
|
||||||
type GuideIconProps = {
|
type GuideIconProps = {
|
||||||
@ -13,7 +13,7 @@ const GuideIcon = forwardRef(
|
|||||||
({ feature, icon = MdHelp }: GuideIconProps, ref) => (
|
({ feature, icon = MdHelp }: GuideIconProps, ref) => (
|
||||||
<GuidePopover feature={feature}>
|
<GuidePopover feature={feature}>
|
||||||
<Box ref={ref}>
|
<Box ref={ref}>
|
||||||
<Icon as={icon} />
|
<Icon marginBottom={'-.15rem'} as={icon} />
|
||||||
</Box>
|
</Box>
|
||||||
</GuidePopover>
|
</GuidePopover>
|
||||||
)
|
)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
.guide-popover-arrow {
|
.guide-popover-arrow {
|
||||||
background-color: var(--tab-panel-bg) !important;
|
background-color: var(--tab-panel-bg);
|
||||||
box-shadow: none !important;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-popover-content {
|
.guide-popover-content {
|
||||||
background-color: var(--background-color-secondary) !important;
|
background-color: var(--background-color-secondary);
|
||||||
border: none !important;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-popover-guide-content {
|
.guide-popover-guide-content {
|
||||||
|
@ -5,12 +5,12 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
Box,
|
Box,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { SystemState } from '../../features/system/systemSlice';
|
import { SystemState } from 'features/system/store/systemSlice';
|
||||||
import { useAppSelector } from '../../app/store';
|
import { useAppSelector } from 'app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from 'app/store';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
import { Feature, FEATURES } from '../../app/features';
|
import { Feature, FEATURES } from 'app/features';
|
||||||
|
|
||||||
type GuideProps = {
|
type GuideProps = {
|
||||||
children: ReactElement;
|
children: ReactElement;
|
||||||
|
86
frontend/src/common/components/IAIAlertDialog.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
Button,
|
||||||
|
forwardRef,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { cloneElement, ReactElement, ReactNode, useRef } from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
acceptButtonText?: string;
|
||||||
|
acceptCallback: () => void;
|
||||||
|
cancelButtonText?: string;
|
||||||
|
cancelCallback?: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
title: string;
|
||||||
|
triggerComponent: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAIAlertDialog = forwardRef((props: Props, ref) => {
|
||||||
|
const {
|
||||||
|
acceptButtonText = 'Accept',
|
||||||
|
acceptCallback,
|
||||||
|
cancelButtonText = 'Cancel',
|
||||||
|
cancelCallback,
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
triggerComponent,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
const handleAccept = () => {
|
||||||
|
acceptCallback();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
cancelCallback && cancelCallback();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{cloneElement(triggerComponent, {
|
||||||
|
onClick: onOpen,
|
||||||
|
ref: ref,
|
||||||
|
})}
|
||||||
|
|
||||||
|
<AlertDialog
|
||||||
|
isOpen={isOpen}
|
||||||
|
leastDestructiveRef={cancelRef}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent className="modal">
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
{title}
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<AlertDialogBody>{children}</AlertDialogBody>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button
|
||||||
|
ref={cancelRef}
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="modal-close-btn"
|
||||||
|
>
|
||||||
|
{cancelButtonText}
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="red" onClick={handleAccept} ml={3}>
|
||||||
|
{acceptButtonText}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
export default IAIAlertDialog;
|
@ -1,3 +1,8 @@
|
|||||||
.invokeai__button {
|
.invokeai__button {
|
||||||
justify-content: space-between;
|
background-color: var(--btn-base-color);
|
||||||
|
place-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-base-color-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
svg {
|
svg {
|
||||||
width: 0.6rem;
|
width: 0.6rem;
|
||||||
height: 0.6rem;
|
height: 0.6rem;
|
||||||
stroke-width: 3px !important;
|
stroke-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-checked] {
|
&[data-checked] {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.invokeai__icon-button {
|
.invokeai__icon-button {
|
||||||
background-color: var(--btn-grey);
|
background: var(--btn-base-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--btn-grey-hover);
|
background-color: var(--btn-base-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-selected='true'] {
|
&[data-selected='true'] {
|
||||||
@ -20,16 +20,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-variant='link'] {
|
&[data-variant='link'] {
|
||||||
background: none !important;
|
background: none;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: none !important;
|
background: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-selected='true'] {
|
// Check Box Style
|
||||||
border-color: var(--accent-color);
|
&[data-as-checkbox='true'] {
|
||||||
|
background-color: var(--btn-base-color);
|
||||||
|
border: 3px solid var(--btn-base-color);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--accent-color-hover);
|
background-color: var(--btn-base-color);
|
||||||
|
border-color: var(--btn-checkbox-border-hover);
|
||||||
|
svg {
|
||||||
|
fill: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-selected='true'] {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
svg {
|
||||||
|
fill: var(--accent-color-hover);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
svg {
|
||||||
|
fill: var(--accent-color-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,28 +61,12 @@
|
|||||||
animation-duration: 1s;
|
animation-duration: 1s;
|
||||||
animation-timing-function: ease-in-out;
|
animation-timing-function: ease-in-out;
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
animation: none;
|
animation: none;
|
||||||
background-color: var(--accent-color-hover);
|
background-color: var(--accent-color-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-as-checkbox='true'] {
|
|
||||||
background-color: var(--btn-grey);
|
|
||||||
border: 3px solid var(--btn-grey);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--btn-grey);
|
|
||||||
border-color: var(--btn-checkbox-border-hover);
|
|
||||||
svg {
|
|
||||||
fill: var(--text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulseColor {
|
@keyframes pulseColor {
|
||||||
|
@ -25,13 +25,23 @@ const IAIIconButton = forwardRef((props: IAIIconButtonProps, forwardedRef) => {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={tooltip} hasArrow {...tooltipProps}>
|
<Tooltip
|
||||||
|
label={tooltip}
|
||||||
|
hasArrow
|
||||||
|
{...tooltipProps}
|
||||||
|
{...(tooltipProps?.placement
|
||||||
|
? { placement: tooltipProps.placement }
|
||||||
|
: { placement: 'top' })}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={`invokeai__icon-button ${styleClass}`}
|
className={
|
||||||
|
styleClass
|
||||||
|
? `invokeai__icon-button ${styleClass}`
|
||||||
|
: `invokeai__icon-button`
|
||||||
|
}
|
||||||
data-as-checkbox={asCheckbox}
|
data-as-checkbox={asCheckbox}
|
||||||
data-selected={isChecked !== undefined ? isChecked : undefined}
|
data-selected={isChecked !== undefined ? isChecked : undefined}
|
||||||
style={props.onClick ? { cursor: 'pointer' } : {}}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
.invokeai__number-input-form-control {
|
.invokeai__number-input-form-control {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: max-content auto;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
column-gap: 1rem;
|
||||||
|
|
||||||
.invokeai__number-input-form-label {
|
.invokeai__number-input-form-label {
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
flex-grow: 2;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-right: 1rem;
|
|
||||||
|
|
||||||
&[data-focus] + .invokeai__number-input-root {
|
&[data-focus] + .invokeai__number-input-root {
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -33,7 +31,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
border: 2px solid var(--border-color);
|
border: 2px solid var(--border-color);
|
||||||
border-radius: 0.2rem;
|
border-radius: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invokeai__number-input-field {
|
.invokeai__number-input-field {
|
||||||
@ -41,10 +39,8 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 0;
|
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding-left: 0.5rem;
|
padding: 0 0.5rem;
|
||||||
padding-right: 0.5rem;
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@ -21,6 +21,7 @@ const numberStringRegex = /^-?(0\.)?\.?$/;
|
|||||||
interface Props extends Omit<NumberInputProps, 'onChange'> {
|
interface Props extends Omit<NumberInputProps, 'onChange'> {
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
labelFontSize?: string | number;
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
showStepper?: boolean;
|
showStepper?: boolean;
|
||||||
value: number;
|
value: number;
|
||||||
@ -43,6 +44,7 @@ interface Props extends Omit<NumberInputProps, 'onChange'> {
|
|||||||
const IAINumberInput = (props: Props) => {
|
const IAINumberInput = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
|
labelFontSize = '1rem',
|
||||||
styleClass,
|
styleClass,
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
showStepper = true,
|
showStepper = true,
|
||||||
@ -127,6 +129,7 @@ const IAINumberInput = (props: Props) => {
|
|||||||
<FormLabel
|
<FormLabel
|
||||||
className="invokeai__number-input-form-label"
|
className="invokeai__number-input-form-label"
|
||||||
style={{ display: label ? 'block' : 'none' }}
|
style={{ display: label ? 'block' : 'none' }}
|
||||||
|
fontSize={labelFontSize}
|
||||||
{...formLabelProps}
|
{...formLabelProps}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
.invokeai__popover-content {
|
.invokeai__popover-content {
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
width: unset !important;
|
width: unset;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 0.5rem !important;
|
border-radius: 0.5rem;
|
||||||
background-color: var(--background-color) !important;
|
background-color: var(--background-color);
|
||||||
border: 2px solid var(--border-color) !important;
|
border: 2px solid var(--border-color);
|
||||||
|
|
||||||
.invokeai__popover-arrow {
|
.invokeai__popover-arrow {
|
||||||
background-color: var(--background-color) !important;
|
background-color: var(--background-color) !important;
|
||||||
|
@ -29,7 +29,7 @@ const IAIPopover = (props: IAIPopoverProps) => {
|
|||||||
<Popover {...rest}>
|
<Popover {...rest}>
|
||||||
<PopoverTrigger>{triggerComponent}</PopoverTrigger>
|
<PopoverTrigger>{triggerComponent}</PopoverTrigger>
|
||||||
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
||||||
{hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />}
|
{hasArrow && <PopoverArrow className="invokeai__popover-arrow" />}
|
||||||
{children}
|
{children}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: max-content;
|
|
||||||
|
|
||||||
.invokeai__select-label {
|
.invokeai__select-label {
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
@ -15,6 +14,7 @@
|
|||||||
border: 2px solid var(--border-color);
|
border: 2px solid var(--border-color);
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
border-radius: 0.2rem;
|
border-radius: 0.2rem;
|
||||||
|
|
||||||
@ -27,5 +27,6 @@
|
|||||||
|
|
||||||
.invokeai__select-option {
|
.invokeai__select-option {
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
|
color: var(--text-color-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Select,
|
||||||
|
SelectProps,
|
||||||
|
Tooltip,
|
||||||
|
TooltipProps,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
type IAISelectProps = SelectProps & {
|
type IAISelectProps = SelectProps & {
|
||||||
label: string;
|
label?: string;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||||
validValues:
|
validValues:
|
||||||
| Array<number | string>
|
| Array<number | string>
|
||||||
| Array<{ key: string; value: string | number }>;
|
| Array<{ key: string; value: string | number }>;
|
||||||
@ -16,6 +25,8 @@ const IAISelect = (props: IAISelectProps) => {
|
|||||||
label,
|
label,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
validValues,
|
validValues,
|
||||||
|
tooltip,
|
||||||
|
tooltipProps,
|
||||||
size = 'sm',
|
size = 'sm',
|
||||||
fontSize = 'md',
|
fontSize = 'md',
|
||||||
styleClass,
|
styleClass,
|
||||||
@ -32,37 +43,41 @@ const IAISelect = (props: IAISelectProps) => {
|
|||||||
e.nativeEvent.cancelBubble = true;
|
e.nativeEvent.cancelBubble = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormLabel
|
{label && (
|
||||||
className="invokeai__select-label"
|
<FormLabel
|
||||||
fontSize={fontSize}
|
className="invokeai__select-label"
|
||||||
marginBottom={1}
|
fontSize={fontSize}
|
||||||
flexGrow={2}
|
marginBottom={1}
|
||||||
whiteSpace="nowrap"
|
flexGrow={2}
|
||||||
>
|
whiteSpace="nowrap"
|
||||||
{label}
|
>
|
||||||
</FormLabel>
|
{label}
|
||||||
<Select
|
</FormLabel>
|
||||||
className="invokeai__select-picker"
|
)}
|
||||||
fontSize={fontSize}
|
<Tooltip label={tooltip} {...tooltipProps}>
|
||||||
size={size}
|
<Select
|
||||||
{...rest}
|
className="invokeai__select-picker"
|
||||||
>
|
fontSize={fontSize}
|
||||||
{validValues.map((opt) => {
|
size={size}
|
||||||
return typeof opt === 'string' || typeof opt === 'number' ? (
|
{...rest}
|
||||||
<option key={opt} value={opt} className="invokeai__select-option">
|
>
|
||||||
{opt}
|
{validValues.map((opt) => {
|
||||||
</option>
|
return typeof opt === 'string' || typeof opt === 'number' ? (
|
||||||
) : (
|
<option key={opt} value={opt} className="invokeai__select-option">
|
||||||
<option
|
{opt}
|
||||||
key={opt.value}
|
</option>
|
||||||
value={opt.value}
|
) : (
|
||||||
className="invokeai__select-option"
|
<option
|
||||||
>
|
key={opt.value}
|
||||||
{opt.key}
|
value={opt.value}
|
||||||
</option>
|
className="invokeai__select-option"
|
||||||
);
|
>
|
||||||
})}
|
{opt.key}
|
||||||
</Select>
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</Tooltip>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,40 +1,62 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
.invokeai__slider-component {
|
||||||
|
|
||||||
.invokeai__slider-form-control {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 1rem;
|
gap: 1rem;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: max-content;
|
|
||||||
padding-right: 0.25rem;
|
|
||||||
|
|
||||||
.invokeai__slider-inner-container {
|
.invokeai__slider-component-label {
|
||||||
display: flex;
|
min-width: max-content;
|
||||||
column-gap: 0.5rem;
|
margin: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.invokeai__slider-form-label {
|
.invokeai__slider_track {
|
||||||
color: var(--text-color-secondary);
|
background-color: var(--tab-color);
|
||||||
margin: 0;
|
}
|
||||||
margin-right: 0.5rem;
|
|
||||||
margin-bottom: 0.1rem;
|
.invokeai__slider_track-filled {
|
||||||
|
background-color: var(--slider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-thumb {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-mark {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--slider-color);
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invokeai__slider-number-input {
|
||||||
|
border: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 2rem;
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 2px solid var(--input-border-color);
|
||||||
|
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.invokeai__slider-root {
|
&:disabled {
|
||||||
.invokeai__slider-filled-track {
|
opacity: 0.2;
|
||||||
background-color: var(--accent-color-hover);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.invokeai__slider-track {
|
.invokeai__slider-number-stepper {
|
||||||
background-color: var(--text-color-secondary);
|
border: none;
|
||||||
height: 5px;
|
}
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invokeai__slider-thumb {
|
&[data-markers='true'] {
|
||||||
}
|
.invokeai__slider_container {
|
||||||
|
margin-top: -1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.invokeai__slider-thumb-tooltip {
|
|
||||||
}
|
|
||||||
|
@ -1,87 +1,246 @@
|
|||||||
import {
|
import {
|
||||||
Slider,
|
|
||||||
SliderTrack,
|
|
||||||
SliderFilledTrack,
|
|
||||||
SliderThumb,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
|
||||||
Tooltip,
|
|
||||||
SliderProps,
|
|
||||||
FormControlProps,
|
FormControlProps,
|
||||||
|
FormLabel,
|
||||||
FormLabelProps,
|
FormLabelProps,
|
||||||
SliderTrackProps,
|
HStack,
|
||||||
|
NumberDecrementStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputFieldProps,
|
||||||
|
NumberInputProps,
|
||||||
|
NumberInputStepper,
|
||||||
|
NumberInputStepperProps,
|
||||||
|
Slider,
|
||||||
|
SliderFilledTrack,
|
||||||
|
SliderMark,
|
||||||
|
SliderMarkProps,
|
||||||
|
SliderThumb,
|
||||||
SliderThumbProps,
|
SliderThumbProps,
|
||||||
|
SliderTrack,
|
||||||
|
SliderTrackProps,
|
||||||
|
Tooltip,
|
||||||
TooltipProps,
|
TooltipProps,
|
||||||
SliderInnerTrackProps,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
import React, { FocusEvent, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { BiReset } from 'react-icons/bi';
|
||||||
|
import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
type IAISliderProps = SliderProps & {
|
export type IAIFullSliderProps = {
|
||||||
label?: string;
|
label: string;
|
||||||
|
value: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
onChange: (v: number) => void;
|
||||||
|
withSliderMarks?: boolean;
|
||||||
|
sliderMarkLeftOffset?: number;
|
||||||
|
sliderMarkRightOffset?: number;
|
||||||
|
withInput?: boolean;
|
||||||
|
isInteger?: boolean;
|
||||||
|
inputWidth?: string | number;
|
||||||
|
inputReadOnly?: boolean;
|
||||||
|
withReset?: boolean;
|
||||||
|
handleReset?: () => void;
|
||||||
|
isResetDisabled?: boolean;
|
||||||
|
isSliderDisabled?: boolean;
|
||||||
|
isInputDisabled?: boolean;
|
||||||
|
tooltipSuffix?: string;
|
||||||
|
hideTooltip?: boolean;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
formControlProps?: FormControlProps;
|
sliderFormControlProps?: FormControlProps;
|
||||||
formLabelProps?: FormLabelProps;
|
sliderFormLabelProps?: FormLabelProps;
|
||||||
|
sliderMarkProps?: Omit<SliderMarkProps, 'value'>;
|
||||||
sliderTrackProps?: SliderTrackProps;
|
sliderTrackProps?: SliderTrackProps;
|
||||||
sliderInnerTrackProps?: SliderInnerTrackProps;
|
|
||||||
sliderThumbProps?: SliderThumbProps;
|
sliderThumbProps?: SliderThumbProps;
|
||||||
sliderThumbTooltipProps?: Omit<TooltipProps, 'children'>;
|
sliderNumberInputProps?: NumberInputProps;
|
||||||
|
sliderNumberInputFieldProps?: NumberInputFieldProps;
|
||||||
|
sliderNumberInputStepperProps?: NumberInputStepperProps;
|
||||||
|
sliderTooltipProps?: Omit<TooltipProps, 'children'>;
|
||||||
|
sliderIAIIconButtonProps?: IAIIconButtonProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IAISlider = (props: IAISliderProps) => {
|
export default function IAISlider(props: IAIFullSliderProps) {
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
|
value,
|
||||||
|
min = 1,
|
||||||
|
max = 100,
|
||||||
|
step = 1,
|
||||||
|
onChange,
|
||||||
|
tooltipSuffix = '',
|
||||||
|
withSliderMarks = false,
|
||||||
|
sliderMarkLeftOffset = 0,
|
||||||
|
sliderMarkRightOffset = -7,
|
||||||
|
withInput = false,
|
||||||
|
isInteger = false,
|
||||||
|
inputWidth = '5rem',
|
||||||
|
inputReadOnly = true,
|
||||||
|
withReset = false,
|
||||||
|
hideTooltip = false,
|
||||||
|
handleReset,
|
||||||
|
isResetDisabled,
|
||||||
|
isSliderDisabled,
|
||||||
|
isInputDisabled,
|
||||||
styleClass,
|
styleClass,
|
||||||
formControlProps,
|
sliderFormControlProps,
|
||||||
formLabelProps,
|
sliderFormLabelProps,
|
||||||
|
sliderMarkProps,
|
||||||
sliderTrackProps,
|
sliderTrackProps,
|
||||||
sliderInnerTrackProps,
|
|
||||||
sliderThumbProps,
|
sliderThumbProps,
|
||||||
sliderThumbTooltipProps,
|
sliderNumberInputProps,
|
||||||
|
sliderNumberInputFieldProps,
|
||||||
|
sliderNumberInputStepperProps,
|
||||||
|
sliderTooltipProps,
|
||||||
|
sliderIAIIconButtonProps,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const [localInputValue, setLocalInputValue] = useState<string>(String(value));
|
||||||
|
|
||||||
|
const numberInputMax = useMemo(
|
||||||
|
() => (sliderNumberInputProps?.max ? sliderNumberInputProps.max : max),
|
||||||
|
[max, sliderNumberInputProps?.max]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (String(value) !== localInputValue && localInputValue !== '') {
|
||||||
|
setLocalInputValue(String(value));
|
||||||
|
}
|
||||||
|
}, [value, localInputValue, setLocalInputValue]);
|
||||||
|
|
||||||
|
const handleInputBlur = (e: FocusEvent<HTMLInputElement>) => {
|
||||||
|
const clamped = _.clamp(
|
||||||
|
isInteger ? Math.floor(Number(e.target.value)) : Number(e.target.value),
|
||||||
|
min,
|
||||||
|
numberInputMax
|
||||||
|
);
|
||||||
|
setLocalInputValue(String(clamped));
|
||||||
|
onChange(clamped);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (v: any) => {
|
||||||
|
setLocalInputValue(v);
|
||||||
|
onChange(Number(v));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetDisable = () => {
|
||||||
|
if (!handleReset) return;
|
||||||
|
handleReset();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl
|
||||||
className={`invokeai__slider-form-control ${styleClass}`}
|
className={
|
||||||
{...formControlProps}
|
styleClass
|
||||||
|
? `invokeai__slider-component ${styleClass}`
|
||||||
|
: `invokeai__slider-component`
|
||||||
|
}
|
||||||
|
data-markers={withSliderMarks}
|
||||||
|
{...sliderFormControlProps}
|
||||||
>
|
>
|
||||||
<div className="invokeai__slider-inner-container">
|
<FormLabel
|
||||||
<FormLabel
|
className="invokeai__slider-component-label"
|
||||||
className={`invokeai__slider-form-label`}
|
{...sliderFormLabelProps}
|
||||||
whiteSpace="nowrap"
|
>
|
||||||
{...formLabelProps}
|
{label}
|
||||||
>
|
</FormLabel>
|
||||||
{label}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
|
<HStack w={'100%'} gap={2}>
|
||||||
<Slider
|
<Slider
|
||||||
className={`invokeai__slider-root`}
|
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onMouseEnter={() => setShowTooltip(true)}
|
||||||
|
onMouseLeave={() => setShowTooltip(false)}
|
||||||
|
focusThumbOnChange={false}
|
||||||
|
isDisabled={isSliderDisabled}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<SliderTrack
|
{withSliderMarks && (
|
||||||
className={`invokeai__slider-track`}
|
<>
|
||||||
{...sliderTrackProps}
|
<SliderMark
|
||||||
>
|
value={min}
|
||||||
<SliderFilledTrack
|
className="invokeai__slider-mark invokeai__slider-mark-start"
|
||||||
className={`invokeai__slider-filled-track`}
|
ml={sliderMarkLeftOffset}
|
||||||
{...sliderInnerTrackProps}
|
{...sliderMarkProps}
|
||||||
/>
|
>
|
||||||
|
{min}
|
||||||
|
</SliderMark>
|
||||||
|
<SliderMark
|
||||||
|
value={max}
|
||||||
|
className="invokeai__slider-mark invokeai__slider-mark-end"
|
||||||
|
ml={sliderMarkRightOffset}
|
||||||
|
{...sliderMarkProps}
|
||||||
|
>
|
||||||
|
{max}
|
||||||
|
</SliderMark>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SliderTrack className="invokeai__slider_track" {...sliderTrackProps}>
|
||||||
|
<SliderFilledTrack className="invokeai__slider_track-filled" />
|
||||||
</SliderTrack>
|
</SliderTrack>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className={`invokeai__slider-thumb-tooltip`}
|
|
||||||
placement="top"
|
|
||||||
hasArrow
|
hasArrow
|
||||||
{...sliderThumbTooltipProps}
|
className="invokeai__slider-component-tooltip"
|
||||||
|
placement="top"
|
||||||
|
isOpen={showTooltip}
|
||||||
|
label={`${value}${tooltipSuffix}`}
|
||||||
|
hidden={hideTooltip}
|
||||||
|
{...sliderTooltipProps}
|
||||||
>
|
>
|
||||||
<SliderThumb
|
<SliderThumb
|
||||||
className={`invokeai__slider-thumb`}
|
className="invokeai__slider-thumb"
|
||||||
{...sliderThumbProps}
|
{...sliderThumbProps}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Slider>
|
</Slider>
|
||||||
</div>
|
|
||||||
|
{withInput && (
|
||||||
|
<NumberInput
|
||||||
|
min={min}
|
||||||
|
max={numberInputMax}
|
||||||
|
step={step}
|
||||||
|
value={localInputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
className="invokeai__slider-number-field"
|
||||||
|
isDisabled={isInputDisabled}
|
||||||
|
{...sliderNumberInputProps}
|
||||||
|
>
|
||||||
|
<NumberInputField
|
||||||
|
className="invokeai__slider-number-input"
|
||||||
|
width={inputWidth}
|
||||||
|
readOnly={inputReadOnly}
|
||||||
|
{...sliderNumberInputFieldProps}
|
||||||
|
/>
|
||||||
|
<NumberInputStepper {...sliderNumberInputStepperProps}>
|
||||||
|
<NumberIncrementStepper className="invokeai__slider-number-stepper" />
|
||||||
|
<NumberDecrementStepper className="invokeai__slider-number-stepper" />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{withReset && (
|
||||||
|
<IAIIconButton
|
||||||
|
size={'sm'}
|
||||||
|
aria-label={'Reset'}
|
||||||
|
tooltip={'Reset'}
|
||||||
|
icon={<BiReset />}
|
||||||
|
onClick={handleResetDisable}
|
||||||
|
isDisabled={isResetDisabled}
|
||||||
|
{...sliderIAIIconButtonProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default IAISlider;
|
|
||||||
|
@ -33,7 +33,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-uploader-button-outer {
|
.image-uploader-button-outer {
|
||||||
min-width: 20rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -42,10 +41,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
color: var(--tab-list-text-inactive);
|
color: var(--tab-list-text-inactive);
|
||||||
background-color: var(--btn-grey);
|
background-color: var(--background-color);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--btn-grey-hover);
|
background-color: var(--background-color-light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,10 +65,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 4rem !important;
|
width: 4rem;
|
||||||
height: 4rem !important;
|
height: 4rem;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.2rem !important;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import { useCallback, ReactNode, useState, useEffect } from 'react';
|
import {
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
useCallback,
|
||||||
|
ReactNode,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
KeyboardEvent,
|
||||||
|
} from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { uploadImage } from '../../app/socketio/actions';
|
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||||
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
|
||||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
import { tabDict } from 'features/tabs/components/InvokeTabs';
|
||||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
|
||||||
import { tabDict } from '../../features/tabs/InvokeTabs';
|
|
||||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||||
|
import { uploadImage } from 'features/gallery/store/thunks/uploadImage';
|
||||||
|
import useImageUploader from 'common/hooks/useImageUploader';
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
type ImageUploaderProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -19,6 +25,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const toast = useToast({});
|
const toast = useToast({});
|
||||||
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
||||||
|
const { setOpenUploader } = useImageUploader();
|
||||||
|
|
||||||
const fileRejectionCallback = useCallback(
|
const fileRejectionCallback = useCallback(
|
||||||
(rejection: FileRejection) => {
|
(rejection: FileRejection) => {
|
||||||
@ -38,15 +45,10 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fileAcceptedCallback = useCallback(
|
const fileAcceptedCallback = useCallback(
|
||||||
(file: File) => {
|
async (file: File) => {
|
||||||
setIsHandlingUpload(true);
|
dispatch(uploadImage({ imageFile: file }));
|
||||||
const payload: UploadImagePayload = { file };
|
|
||||||
if (['img2img', 'inpainting'].includes(activeTabName)) {
|
|
||||||
payload.destination = activeTabName as ImageUploadDestination;
|
|
||||||
}
|
|
||||||
dispatch(uploadImage(payload));
|
|
||||||
},
|
},
|
||||||
[dispatch, activeTabName]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
@ -77,6 +79,8 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
maxFiles: 1,
|
maxFiles: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setOpenUploader(open);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pasteImageListener = (e: ClipboardEvent) => {
|
const pasteImageListener = (e: ClipboardEvent) => {
|
||||||
const dataTransferItemList = e.clipboardData?.items;
|
const dataTransferItemList = e.clipboardData?.items;
|
||||||
@ -118,12 +122,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: UploadImagePayload = { file };
|
dispatch(uploadImage({ imageFile: file }));
|
||||||
if (['img2img', 'inpainting'].includes(activeTabName)) {
|
|
||||||
payload.destination = activeTabName as ImageUploadDestination;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(uploadImage(payload));
|
|
||||||
};
|
};
|
||||||
document.addEventListener('paste', pasteImageListener);
|
document.addEventListener('paste', pasteImageListener);
|
||||||
return () => {
|
return () => {
|
||||||
@ -131,13 +130,21 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
};
|
};
|
||||||
}, [dispatch, toast, activeTabName]);
|
}, [dispatch, toast, activeTabName]);
|
||||||
|
|
||||||
const overlaySecondaryText = ['img2img', 'inpainting'].includes(activeTabName)
|
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
|
||||||
|
activeTabName
|
||||||
|
)
|
||||||
? ` to ${tabDict[activeTabName as keyof typeof tabDict].tooltip}`
|
? ` to ${tabDict[activeTabName as keyof typeof tabDict].tooltip}`
|
||||||
: ``;
|
: ``;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageUploaderTriggerContext.Provider value={open}>
|
<ImageUploaderTriggerContext.Provider value={open}>
|
||||||
<div {...getRootProps({ style: {} })}>
|
<div
|
||||||
|
{...getRootProps({ style: {} })}
|
||||||
|
onKeyDown={(e: KeyboardEvent) => {
|
||||||
|
// Bail out if user hits spacebar - do not open the uploader
|
||||||
|
if (e.key === ' ') return;
|
||||||
|
}}
|
||||||
|
>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
{children}
|
{children}
|
||||||
{isDragActive && isHandlingUpload && (
|
{isDragActive && isHandlingUpload && (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Heading } from '@chakra-ui/react';
|
import { Heading } from '@chakra-ui/react';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { FaUpload } from 'react-icons/fa';
|
import { FaUpload } from 'react-icons/fa';
|
||||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||||
|
|
||||||
type ImageUploaderButtonProps = {
|
type ImageUploaderButtonProps = {
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { FaUpload } from 'react-icons/fa';
|
import { FaUpload } from 'react-icons/fa';
|
||||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||||
import IAIIconButton from './IAIIconButton';
|
import IAIIconButton from './IAIIconButton';
|
||||||
|
|
||||||
const ImageUploaderIconButton = () => {
|
const ImageUploaderIconButton = () => {
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Img2ImgPlaceHolder from '../../../assets/images/image2img.png';
|
|
||||||
|
|
||||||
export const ImageToImageWIP = () => {
|
|
||||||
return (
|
|
||||||
<div className="work-in-progress txt2img-work-in-progress">
|
|
||||||
<img src={Img2ImgPlaceHolder} alt="img2img_placeholder" />
|
|
||||||
<h1>Image To Image</h1>
|
|
||||||
<p>
|
|
||||||
Image to Image is already available in the WebUI. You can access it from
|
|
||||||
the Text to Image - Advanced Options menu. A dedicated UI for Image To
|
|
||||||
Image will be released soon.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,14 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function InpaintingWIP() {
|
|
||||||
return (
|
|
||||||
<div className="work-in-progress inpainting-work-in-progress">
|
|
||||||
<h1>Inpainting</h1>
|
|
||||||
<p>
|
|
||||||
Inpainting is available as a part of the Invoke AI Command Line
|
|
||||||
Interface. A dedicated WebUI interface will be released in the near
|
|
||||||
future.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function OutpaintingWIP() {
|
|
||||||
return (
|
|
||||||
<div className="work-in-progress outpainting-work-in-progress">
|
|
||||||
<h1>Outpainting</h1>
|
|
||||||
<p>
|
|
||||||
Outpainting is available as a part of the Invoke AI Command Line
|
|
||||||
Interface. A dedicated WebUI interface will be released in the near
|
|
||||||
future.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ export const PostProcessingWIP = () => {
|
|||||||
Upscaling and Face Restoration are already available in the WebUI. You
|
Upscaling and Face Restoration are already available in the WebUI. You
|
||||||
can access them from the Advanced Options menu of the Text To Image and
|
can access them from the Advanced Options menu of the Text To Image and
|
||||||
Image To Image tabs. You can also process images directly, using the
|
Image To Image tabs. You can also process images directly, using the
|
||||||
image action buttons above the main image display.
|
image action buttons above the current image display or in the viewer.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
A dedicated UI will be released soon to facilitate more advanced post
|
A dedicated UI will be released soon to facilitate more advanced post
|
||||||
|
16
frontend/src/common/components/WorkInProgress/Training.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function TrainingWIP() {
|
||||||
|
return (
|
||||||
|
<div className="work-in-progress nodes-work-in-progress">
|
||||||
|
<h1>Training</h1>
|
||||||
|
<p>
|
||||||
|
A dedicated workflow for training your own embeddings and checkpoints
|
||||||
|
using Textual Inversion and Dreambooth from the web interface. <br />
|
||||||
|
<br />
|
||||||
|
InvokeAI already supports training custom embeddings using Textual
|
||||||
|
Inversion using the main script.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,25 +1,37 @@
|
|||||||
import { RefObject, useEffect } from 'react';
|
import { RefObject, useEffect, useRef } from 'react';
|
||||||
|
import { Rect } from 'react-konva';
|
||||||
|
|
||||||
const useClickOutsideWatcher = (
|
const watchers: {
|
||||||
ref: RefObject<HTMLElement>,
|
ref: RefObject<HTMLElement>;
|
||||||
callback: () => void,
|
enable: boolean;
|
||||||
req = true
|
callback: () => void;
|
||||||
) => {
|
}[] = [];
|
||||||
|
|
||||||
|
const useClickOutsideWatcher = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(e: MouseEvent) {
|
function handleClickOutside(e: MouseEvent) {
|
||||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
watchers.forEach(({ ref, enable, callback }) => {
|
||||||
callback();
|
if (enable && ref.current && !ref.current.contains(e.target as Node)) {
|
||||||
}
|
console.log('callback');
|
||||||
}
|
callback();
|
||||||
if (req) {
|
}
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
});
|
||||||
}
|
}
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
return () => {
|
return () => {
|
||||||
if (req) {
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [ref, req, callback]);
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
addWatcher: (watcher: {
|
||||||
|
ref: RefObject<HTMLElement>;
|
||||||
|
callback: () => void;
|
||||||
|
enable: boolean;
|
||||||
|
}) => {
|
||||||
|
watchers.push(watcher);
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useClickOutsideWatcher;
|
export default useClickOutsideWatcher;
|
||||||
|
14
frontend/src/common/hooks/useImageUploader.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
let openFunction: () => void;
|
||||||
|
|
||||||
|
const useImageUploader = () => {
|
||||||
|
return {
|
||||||
|
setOpenUploader: (open?: () => void) => {
|
||||||
|
if (open) {
|
||||||
|
openFunction = open;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openUploader: openFunction,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useImageUploader;
|
16
frontend/src/common/icons/TrainingIcon.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createIcon } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const TrainingIcon = createIcon({
|
||||||
|
displayName: 'TrainingIcon',
|
||||||
|
viewBox: '0 0 3544 3544',
|
||||||
|
path: (
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M0,768.593L0,2774.71C0,2930.6 78.519,3068.3 198.135,3150.37C273.059,3202.68 364.177,3233.38 462.407,3233.38C462.407,3233.38 3080.9,3233.38 3080.9,3233.38C3179.13,3233.38 3270.25,3202.68 3345.17,3150.37C3464.79,3068.3 3543.31,2930.6 3543.31,2774.71L3543.31,768.593C3543.31,517.323 3339.31,313.324 3088.04,313.324L455.269,313.324C203.999,313.324 0,517.323 0,768.593ZM3427.88,775.73L3427.88,2770.97C3427.88,2962.47 3272.4,3117.95 3080.9,3117.95L462.407,3117.95C270.906,3117.95 115.431,2962.47 115.431,2770.97C115.431,2770.97 115.431,775.73 115.431,775.73C115.431,584.229 270.906,428.755 462.407,428.755C462.407,428.755 3080.9,428.755 3080.9,428.755C3272.4,428.755 3427.88,584.229 3427.88,775.73ZM796.24,1322.76L796.24,1250.45C796.24,1199.03 836.16,1157.27 885.331,1157.27C885.331,1157.27 946.847,1157.27 946.847,1157.27C996.017,1157.27 1035.94,1199.03 1035.94,1250.45L1035.94,1644.81L2507.37,1644.81L2507.37,1250.45C2507.37,1199.03 2547.29,1157.27 2596.46,1157.27C2596.46,1157.27 2657.98,1157.27 2657.98,1157.27C2707.15,1157.27 2747.07,1199.03 2747.07,1250.45L2747.07,1322.76C2756.66,1319.22 2767.02,1317.29 2777.83,1317.29C2777.83,1317.29 2839.34,1317.29 2839.34,1317.29C2888.51,1317.29 2928.43,1357.21 2928.43,1406.38L2928.43,1527.32C2933.51,1526.26 2938.77,1525.71 2944.16,1525.71L2995.3,1525.71C3036.18,1525.71 3069.37,1557.59 3069.37,1596.86C3069.37,1596.86 3069.37,1946.44 3069.37,1946.44C3069.37,1985.72 3036.18,2017.6 2995.3,2017.6C2995.3,2017.6 2944.16,2017.6 2944.16,2017.6C2938.77,2017.6 2933.51,2017.04 2928.43,2015.99L2928.43,2136.92C2928.43,2186.09 2888.51,2226.01 2839.34,2226.01L2777.83,2226.01C2767.02,2226.01 2756.66,2224.08 2747.07,2220.55L2747.07,2292.85C2747.07,2344.28 2707.15,2386.03 2657.98,2386.03C2657.98,2386.03 2596.46,2386.03 2596.46,2386.03C2547.29,2386.03 2507.37,2344.28 2507.37,2292.85L2507.37,1898.5L1035.94,1898.5L1035.94,2292.85C1035.94,2344.28 996.017,2386.03 946.847,2386.03C946.847,2386.03 885.331,2386.03 885.331,2386.03C836.16,2386.03 796.24,2344.28 796.24,2292.85L796.24,2220.55C786.651,2224.08 776.29,2226.01 765.482,2226.01L703.967,2226.01C654.796,2226.01 614.876,2186.09 614.876,2136.92L614.876,2015.99C609.801,2017.04 604.539,2017.6 599.144,2017.6C599.144,2017.6 548.003,2017.6 548.003,2017.6C507.125,2017.6 473.937,1985.72 473.937,1946.44C473.937,1946.44 473.937,1596.86 473.937,1596.86C473.937,1557.59 507.125,1525.71 548.003,1525.71L599.144,1525.71C604.539,1525.71 609.801,1526.26 614.876,1527.32L614.876,1406.38C614.876,1357.21 654.796,1317.29 703.967,1317.29C703.967,1317.29 765.482,1317.29 765.482,1317.29C776.29,1317.29 786.651,1319.22 796.24,1322.76ZM977.604,1250.45C977.604,1232.7 963.822,1218.29 946.847,1218.29L885.331,1218.29C868.355,1218.29 854.573,1232.7 854.573,1250.45L854.573,2292.85C854.573,2310.61 868.355,2325.02 885.331,2325.02L946.847,2325.02C963.822,2325.02 977.604,2310.61 977.604,2292.85L977.604,1250.45ZM2565.7,1250.45C2565.7,1232.7 2579.49,1218.29 2596.46,1218.29L2657.98,1218.29C2674.95,1218.29 2688.73,1232.7 2688.73,1250.45L2688.73,2292.85C2688.73,2310.61 2674.95,2325.02 2657.98,2325.02L2596.46,2325.02C2579.49,2325.02 2565.7,2310.61 2565.7,2292.85L2565.7,1250.45ZM673.209,1406.38L673.209,2136.92C673.209,2153.9 686.991,2167.68 703.967,2167.68L765.482,2167.68C782.458,2167.68 796.24,2153.9 796.24,2136.92L796.24,1406.38C796.24,1389.41 782.458,1375.63 765.482,1375.63L703.967,1375.63C686.991,1375.63 673.209,1389.41 673.209,1406.38ZM2870.1,1406.38L2870.1,2136.92C2870.1,2153.9 2856.32,2167.68 2839.34,2167.68L2777.83,2167.68C2760.85,2167.68 2747.07,2153.9 2747.07,2136.92L2747.07,1406.38C2747.07,1389.41 2760.85,1375.63 2777.83,1375.63L2839.34,1375.63C2856.32,1375.63 2870.1,1389.41 2870.1,1406.38ZM614.876,1577.5C610.535,1574.24 605.074,1572.3 599.144,1572.3L548.003,1572.3C533.89,1572.3 522.433,1583.3 522.433,1596.86L522.433,1946.44C522.433,1960 533.89,1971.01 548.003,1971.01L599.144,1971.01C605.074,1971.01 610.535,1969.07 614.876,1965.81L614.876,1577.5ZM2928.43,1965.81L2928.43,1577.5C2932.77,1574.24 2938.23,1572.3 2944.16,1572.3L2995.3,1572.3C3009.42,1572.3 3020.87,1583.3 3020.87,1596.86L3020.87,1946.44C3020.87,1960 3009.42,1971.01 2995.3,1971.01L2944.16,1971.01C2938.23,1971.01 2932.77,1969.07 2928.43,1965.81ZM2507.37,1703.14L1035.94,1703.14L1035.94,1840.16L2507.37,1840.16L2507.37,1898.38L2507.37,1659.46L2507.37,1703.14Z"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TrainingIcon;
|
BIN
frontend/src/common/icons/UnifiedCanvas.afdesign
Normal file
16
frontend/src/common/icons/UnifiedCanvasIcon.tsx
Normal file
BIN
frontend/src/common/icons/design_files/Training.afdesign
Normal file
5
frontend/src/common/icons/design_files/Training.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 3544 3544" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<path d="M0,768.593L0,2774.71C0,2930.6 78.519,3068.3 198.135,3150.37C273.059,3202.68 364.177,3233.38 462.407,3233.38C462.407,3233.38 3080.9,3233.38 3080.9,3233.38C3179.13,3233.38 3270.25,3202.68 3345.17,3150.37C3464.79,3068.3 3543.31,2930.6 3543.31,2774.71L3543.31,768.593C3543.31,517.323 3339.31,313.324 3088.04,313.324L455.269,313.324C203.999,313.324 0,517.323 0,768.593ZM3427.88,775.73L3427.88,2770.97C3427.88,2962.47 3272.4,3117.95 3080.9,3117.95L462.407,3117.95C270.906,3117.95 115.431,2962.47 115.431,2770.97C115.431,2770.97 115.431,775.73 115.431,775.73C115.431,584.229 270.906,428.755 462.407,428.755C462.407,428.755 3080.9,428.755 3080.9,428.755C3272.4,428.755 3427.88,584.229 3427.88,775.73ZM796.24,1322.76L796.24,1250.45C796.24,1199.03 836.16,1157.27 885.331,1157.27C885.331,1157.27 946.847,1157.27 946.847,1157.27C996.017,1157.27 1035.94,1199.03 1035.94,1250.45L1035.94,1644.81L2507.37,1644.81L2507.37,1250.45C2507.37,1199.03 2547.29,1157.27 2596.46,1157.27C2596.46,1157.27 2657.98,1157.27 2657.98,1157.27C2707.15,1157.27 2747.07,1199.03 2747.07,1250.45L2747.07,1322.76C2756.66,1319.22 2767.02,1317.29 2777.83,1317.29C2777.83,1317.29 2839.34,1317.29 2839.34,1317.29C2888.51,1317.29 2928.43,1357.21 2928.43,1406.38L2928.43,1527.32C2933.51,1526.26 2938.77,1525.71 2944.16,1525.71L2995.3,1525.71C3036.18,1525.71 3069.37,1557.59 3069.37,1596.86C3069.37,1596.86 3069.37,1946.44 3069.37,1946.44C3069.37,1985.72 3036.18,2017.6 2995.3,2017.6C2995.3,2017.6 2944.16,2017.6 2944.16,2017.6C2938.77,2017.6 2933.51,2017.04 2928.43,2015.99L2928.43,2136.92C2928.43,2186.09 2888.51,2226.01 2839.34,2226.01L2777.83,2226.01C2767.02,2226.01 2756.66,2224.08 2747.07,2220.55L2747.07,2292.85C2747.07,2344.28 2707.15,2386.03 2657.98,2386.03C2657.98,2386.03 2596.46,2386.03 2596.46,2386.03C2547.29,2386.03 2507.37,2344.28 2507.37,2292.85L2507.37,1898.5L1035.94,1898.5L1035.94,2292.85C1035.94,2344.28 996.017,2386.03 946.847,2386.03C946.847,2386.03 885.331,2386.03 885.331,2386.03C836.16,2386.03 796.24,2344.28 796.24,2292.85L796.24,2220.55C786.651,2224.08 776.29,2226.01 765.482,2226.01L703.967,2226.01C654.796,2226.01 614.876,2186.09 614.876,2136.92L614.876,2015.99C609.801,2017.04 604.539,2017.6 599.144,2017.6C599.144,2017.6 548.003,2017.6 548.003,2017.6C507.125,2017.6 473.937,1985.72 473.937,1946.44C473.937,1946.44 473.937,1596.86 473.937,1596.86C473.937,1557.59 507.125,1525.71 548.003,1525.71L599.144,1525.71C604.539,1525.71 609.801,1526.26 614.876,1527.32L614.876,1406.38C614.876,1357.21 654.796,1317.29 703.967,1317.29C703.967,1317.29 765.482,1317.29 765.482,1317.29C776.29,1317.29 786.651,1319.22 796.24,1322.76ZM977.604,1250.45C977.604,1232.7 963.822,1218.29 946.847,1218.29L885.331,1218.29C868.355,1218.29 854.573,1232.7 854.573,1250.45L854.573,2292.85C854.573,2310.61 868.355,2325.02 885.331,2325.02L946.847,2325.02C963.822,2325.02 977.604,2310.61 977.604,2292.85L977.604,1250.45ZM2565.7,1250.45C2565.7,1232.7 2579.49,1218.29 2596.46,1218.29L2657.98,1218.29C2674.95,1218.29 2688.73,1232.7 2688.73,1250.45L2688.73,2292.85C2688.73,2310.61 2674.95,2325.02 2657.98,2325.02L2596.46,2325.02C2579.49,2325.02 2565.7,2310.61 2565.7,2292.85L2565.7,1250.45ZM673.209,1406.38L673.209,2136.92C673.209,2153.9 686.991,2167.68 703.967,2167.68L765.482,2167.68C782.458,2167.68 796.24,2153.9 796.24,2136.92L796.24,1406.38C796.24,1389.41 782.458,1375.63 765.482,1375.63L703.967,1375.63C686.991,1375.63 673.209,1389.41 673.209,1406.38ZM2870.1,1406.38L2870.1,2136.92C2870.1,2153.9 2856.32,2167.68 2839.34,2167.68L2777.83,2167.68C2760.85,2167.68 2747.07,2153.9 2747.07,2136.92L2747.07,1406.38C2747.07,1389.41 2760.85,1375.63 2777.83,1375.63L2839.34,1375.63C2856.32,1375.63 2870.1,1389.41 2870.1,1406.38ZM614.876,1577.5C610.535,1574.24 605.074,1572.3 599.144,1572.3L548.003,1572.3C533.89,1572.3 522.433,1583.3 522.433,1596.86L522.433,1946.44C522.433,1960 533.89,1971.01 548.003,1971.01L599.144,1971.01C605.074,1971.01 610.535,1969.07 614.876,1965.81L614.876,1577.5ZM2928.43,1965.81L2928.43,1577.5C2932.77,1574.24 2938.23,1572.3 2944.16,1572.3L2995.3,1572.3C3009.42,1572.3 3020.87,1583.3 3020.87,1596.86L3020.87,1946.44C3020.87,1960 3009.42,1971.01 2995.3,1971.01L2944.16,1971.01C2938.23,1971.01 2932.77,1969.07 2928.43,1965.81ZM2507.37,1703.14L1035.94,1703.14L1035.94,1840.16L2507.37,1840.16L2507.37,1898.38L2507.37,1659.46L2507.37,1703.14Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.7 KiB |
BIN
frontend/src/common/icons/design_files/UnifiedCanvas.afdesign
Normal file
7
frontend/src/common/icons/design_files/UnifiedCanvas.svg
Normal file
After Width: | Height: | Size: 9.8 KiB |
21
frontend/src/common/util/openBase64ImageInTab.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
type Base64AndCaption = {
|
||||||
|
base64: string;
|
||||||
|
caption: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openBase64ImageInTab = (images: Base64AndCaption[]) => {
|
||||||
|
const w = window.open('');
|
||||||
|
if (!w) return;
|
||||||
|
|
||||||
|
images.forEach((i) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.src = i.base64;
|
||||||
|
|
||||||
|
w.document.write(i.caption);
|
||||||
|
w.document.write('</br>');
|
||||||
|
w.document.write(image.outerHTML);
|
||||||
|
w.document.write('</br></br>');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default openBase64ImageInTab;
|
@ -1,20 +1,24 @@
|
|||||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants';
|
||||||
import { OptionsState } from '../../features/options/optionsSlice';
|
import { OptionsState } from 'features/options/store/optionsSlice';
|
||||||
import { SystemState } from '../../features/system/systemSlice';
|
import { SystemState } from 'features/system/store/systemSlice';
|
||||||
|
|
||||||
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
||||||
import randomInt from './randomInt';
|
import randomInt from './randomInt';
|
||||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
import { InvokeTabName } from 'features/tabs/components/InvokeTabs';
|
||||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
import {
|
||||||
import generateMask from '../../features/tabs/Inpainting/util/generateMask';
|
CanvasState,
|
||||||
|
isCanvasMaskLine,
|
||||||
|
} from 'features/canvas/store/canvasTypes';
|
||||||
|
import generateMask from 'features/canvas/util/generateMask';
|
||||||
|
import openBase64ImageInTab from './openBase64ImageInTab';
|
||||||
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
|
|
||||||
export type FrontendToBackendParametersConfig = {
|
export type FrontendToBackendParametersConfig = {
|
||||||
generationMode: InvokeTabName;
|
generationMode: InvokeTabName;
|
||||||
optionsState: OptionsState;
|
optionsState: OptionsState;
|
||||||
inpaintingState: InpaintingState;
|
canvasState: CanvasState;
|
||||||
systemState: SystemState;
|
systemState: SystemState;
|
||||||
imageToProcessUrl?: string;
|
imageToProcessUrl?: string;
|
||||||
maskImageElement?: HTMLImageElement;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,46 +28,56 @@ export type FrontendToBackendParametersConfig = {
|
|||||||
export const frontendToBackendParameters = (
|
export const frontendToBackendParameters = (
|
||||||
config: FrontendToBackendParametersConfig
|
config: FrontendToBackendParametersConfig
|
||||||
): { [key: string]: any } => {
|
): { [key: string]: any } => {
|
||||||
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
generationMode,
|
generationMode,
|
||||||
optionsState,
|
optionsState,
|
||||||
inpaintingState,
|
canvasState,
|
||||||
systemState,
|
systemState,
|
||||||
imageToProcessUrl,
|
imageToProcessUrl,
|
||||||
maskImageElement,
|
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
prompt,
|
|
||||||
iterations,
|
|
||||||
steps,
|
|
||||||
cfgScale,
|
cfgScale,
|
||||||
threshold,
|
codeformerFidelity,
|
||||||
perlin,
|
facetoolStrength,
|
||||||
|
facetoolType,
|
||||||
height,
|
height,
|
||||||
width,
|
|
||||||
sampler,
|
|
||||||
seed,
|
|
||||||
seamless,
|
|
||||||
hiresFix,
|
hiresFix,
|
||||||
img2imgStrength,
|
img2imgStrength,
|
||||||
|
infillMethod,
|
||||||
initialImage,
|
initialImage,
|
||||||
|
iterations,
|
||||||
|
perlin,
|
||||||
|
prompt,
|
||||||
|
sampler,
|
||||||
|
seamBlur,
|
||||||
|
seamless,
|
||||||
|
seamSize,
|
||||||
|
seamSteps,
|
||||||
|
seamStrength,
|
||||||
|
seed,
|
||||||
|
seedWeights,
|
||||||
shouldFitToWidthHeight,
|
shouldFitToWidthHeight,
|
||||||
shouldGenerateVariations,
|
shouldGenerateVariations,
|
||||||
variationAmount,
|
shouldRandomizeSeed,
|
||||||
seedWeights,
|
|
||||||
shouldRunESRGAN,
|
shouldRunESRGAN,
|
||||||
|
shouldRunFacetool,
|
||||||
|
steps,
|
||||||
|
threshold,
|
||||||
|
tileSize,
|
||||||
upscalingLevel,
|
upscalingLevel,
|
||||||
upscalingStrength,
|
upscalingStrength,
|
||||||
shouldRunFacetool,
|
variationAmount,
|
||||||
facetoolStrength,
|
width,
|
||||||
codeformerFidelity,
|
|
||||||
facetoolType,
|
|
||||||
shouldRandomizeSeed,
|
|
||||||
} = optionsState;
|
} = optionsState;
|
||||||
|
|
||||||
const { shouldDisplayInProgressType, saveIntermediatesInterval } =
|
const {
|
||||||
systemState;
|
shouldDisplayInProgressType,
|
||||||
|
saveIntermediatesInterval,
|
||||||
|
enableImageDebugging,
|
||||||
|
} = systemState;
|
||||||
|
|
||||||
const generationParameters: { [k: string]: any } = {
|
const generationParameters: { [k: string]: any } = {
|
||||||
prompt,
|
prompt,
|
||||||
@ -80,6 +94,8 @@ export const frontendToBackendParameters = (
|
|||||||
progress_images: shouldDisplayInProgressType === 'full-res',
|
progress_images: shouldDisplayInProgressType === 'full-res',
|
||||||
progress_latents: shouldDisplayInProgressType === 'latents',
|
progress_latents: shouldDisplayInProgressType === 'latents',
|
||||||
save_intermediates: saveIntermediatesInterval,
|
save_intermediates: saveIntermediatesInterval,
|
||||||
|
generation_mode: generationMode,
|
||||||
|
init_mask: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
generationParameters.seed = shouldRandomizeSeed
|
generationParameters.seed = shouldRandomizeSeed
|
||||||
@ -101,35 +117,38 @@ export const frontendToBackendParameters = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// inpainting exclusive parameters
|
// inpainting exclusive parameters
|
||||||
if (generationMode === 'inpainting' && maskImageElement) {
|
if (generationMode === 'unifiedCanvas' && canvasBaseLayer) {
|
||||||
const {
|
const {
|
||||||
lines,
|
layerState: { objects },
|
||||||
boundingBoxCoordinate,
|
boundingBoxCoordinates,
|
||||||
boundingBoxDimensions,
|
boundingBoxDimensions,
|
||||||
inpaintReplace,
|
inpaintReplace,
|
||||||
shouldUseInpaintReplace,
|
shouldUseInpaintReplace,
|
||||||
} = inpaintingState;
|
stageScale,
|
||||||
|
isMaskEnabled,
|
||||||
|
shouldPreserveMaskedArea,
|
||||||
|
boundingBoxScaleMethod: boundingBoxScale,
|
||||||
|
scaledBoundingBoxDimensions,
|
||||||
|
} = canvasState;
|
||||||
|
|
||||||
const boundingBox = {
|
const boundingBox = {
|
||||||
...boundingBoxCoordinate,
|
...boundingBoxCoordinates,
|
||||||
...boundingBoxDimensions,
|
...boundingBoxDimensions,
|
||||||
};
|
};
|
||||||
|
|
||||||
generationParameters.init_img = imageToProcessUrl;
|
const maskDataURL = generateMask(
|
||||||
generationParameters.strength = img2imgStrength;
|
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
|
||||||
generationParameters.fit = false;
|
|
||||||
|
|
||||||
const { maskDataURL, isMaskEmpty } = generateMask(
|
|
||||||
maskImageElement,
|
|
||||||
lines,
|
|
||||||
boundingBox
|
boundingBox
|
||||||
);
|
);
|
||||||
|
|
||||||
generationParameters.is_mask_empty = isMaskEmpty;
|
generationParameters.init_mask = maskDataURL;
|
||||||
|
|
||||||
generationParameters.init_mask = maskDataURL.split(
|
generationParameters.fit = false;
|
||||||
'data:image/png;base64,'
|
|
||||||
)[1];
|
generationParameters.init_img = imageToProcessUrl;
|
||||||
|
generationParameters.strength = img2imgStrength;
|
||||||
|
|
||||||
|
generationParameters.invert_mask = shouldPreserveMaskedArea;
|
||||||
|
|
||||||
if (shouldUseInpaintReplace) {
|
if (shouldUseInpaintReplace) {
|
||||||
generationParameters.inpaint_replace = inpaintReplace;
|
generationParameters.inpaint_replace = inpaintReplace;
|
||||||
@ -137,8 +156,47 @@ export const frontendToBackendParameters = (
|
|||||||
|
|
||||||
generationParameters.bounding_box = boundingBox;
|
generationParameters.bounding_box = boundingBox;
|
||||||
|
|
||||||
// TODO: The server metadata generation needs to be changed to fix this.
|
const tempScale = canvasBaseLayer.scale();
|
||||||
|
|
||||||
|
canvasBaseLayer.scale({
|
||||||
|
x: 1 / stageScale,
|
||||||
|
y: 1 / stageScale,
|
||||||
|
});
|
||||||
|
|
||||||
|
const absPos = canvasBaseLayer.getAbsolutePosition();
|
||||||
|
|
||||||
|
const imageDataURL = canvasBaseLayer.toDataURL({
|
||||||
|
x: boundingBox.x + absPos.x,
|
||||||
|
y: boundingBox.y + absPos.y,
|
||||||
|
width: boundingBox.width,
|
||||||
|
height: boundingBox.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (enableImageDebugging) {
|
||||||
|
openBase64ImageInTab([
|
||||||
|
{ base64: maskDataURL, caption: 'mask sent as init_mask' },
|
||||||
|
{ base64: imageDataURL, caption: 'image sent as init_img' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasBaseLayer.scale(tempScale);
|
||||||
|
|
||||||
|
generationParameters.init_img = imageDataURL;
|
||||||
|
|
||||||
generationParameters.progress_images = false;
|
generationParameters.progress_images = false;
|
||||||
|
|
||||||
|
if (boundingBoxScale !== 'none') {
|
||||||
|
generationParameters.inpaint_width = scaledBoundingBoxDimensions.width;
|
||||||
|
generationParameters.inpaint_height = scaledBoundingBoxDimensions.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
generationParameters.seam_size = seamSize;
|
||||||
|
generationParameters.seam_blur = seamBlur;
|
||||||
|
generationParameters.seam_strength = seamStrength;
|
||||||
|
generationParameters.seam_steps = seamSteps;
|
||||||
|
generationParameters.tile_size = tileSize;
|
||||||
|
generationParameters.infill_method = infillMethod;
|
||||||
|
generationParameters.force_outpaint = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldGenerateVariations) {
|
if (shouldGenerateVariations) {
|
||||||
@ -171,6 +229,10 @@ export const frontendToBackendParameters = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableImageDebugging) {
|
||||||
|
generationParameters.enable_image_debugging = enableImageDebugging;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generationParameters,
|
generationParameters,
|
||||||
esrganParameters,
|
esrganParameters,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
const promptToString = (prompt: InvokeAI.Prompt): string => {
|
const promptToString = (prompt: InvokeAI.Prompt): string => {
|
||||||
if (prompt.length === 1) {
|
if (prompt.length === 1) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
|
||||||
export const stringToSeedWeights = (
|
export const stringToSeedWeights = (
|
||||||
string: string
|
string: string
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import { useAppDispatch } from 'app/store';
|
||||||
|
import IAIAlertDialog from 'common/components/IAIAlertDialog';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import { clearCanvasHistory } from 'features/canvas/store/canvasSlice';
|
||||||
|
import { FaTrash } from 'react-icons/fa';
|
||||||
|
|
||||||
|
const ClearCanvasHistoryButtonModal = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIAlertDialog
|
||||||
|
title={'Clear Canvas History'}
|
||||||
|
acceptCallback={() => dispatch(clearCanvasHistory())}
|
||||||
|
acceptButtonText={'Clear History'}
|
||||||
|
triggerComponent={
|
||||||
|
<IAIButton size={'sm'} leftIcon={<FaTrash />}>
|
||||||
|
Clear Canvas History
|
||||||
|
</IAIButton>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Clearing the canvas history leaves your current canvas intact, but
|
||||||
|
irreversibly clears the undo and redo history.
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<p>Are you sure you want to clear the canvas history?</p>
|
||||||
|
</IAIAlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ClearCanvasHistoryButtonModal;
|
205
frontend/src/features/canvas/components/IAICanvas.tsx
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { Layer, Stage } from 'react-konva';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import {
|
||||||
|
canvasSelector,
|
||||||
|
isStagingSelector,
|
||||||
|
} from 'features/canvas/store/canvasSelectors';
|
||||||
|
import IAICanvasMaskLines from './IAICanvasMaskLines';
|
||||||
|
import IAICanvasToolPreview from './IAICanvasToolPreview';
|
||||||
|
import { Vector2d } from 'konva/lib/types';
|
||||||
|
import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox';
|
||||||
|
import useCanvasHotkeys from '../hooks/useCanvasHotkeys';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import IAICanvasMaskCompositer from './IAICanvasMaskCompositer';
|
||||||
|
import useCanvasWheel from '../hooks/useCanvasZoom';
|
||||||
|
import useCanvasMouseDown from '../hooks/useCanvasMouseDown';
|
||||||
|
import useCanvasMouseUp from '../hooks/useCanvasMouseUp';
|
||||||
|
import useCanvasMouseMove from '../hooks/useCanvasMouseMove';
|
||||||
|
import useCanvasMouseEnter from '../hooks/useCanvasMouseEnter';
|
||||||
|
import useCanvasMouseOut from '../hooks/useCanvasMouseOut';
|
||||||
|
import useCanvasDragMove from '../hooks/useCanvasDragMove';
|
||||||
|
import IAICanvasObjectRenderer from './IAICanvasObjectRenderer';
|
||||||
|
import IAICanvasGrid from './IAICanvasGrid';
|
||||||
|
import IAICanvasIntermediateImage from './IAICanvasIntermediateImage';
|
||||||
|
import IAICanvasStatusText from './IAICanvasStatusText';
|
||||||
|
import IAICanvasStagingArea from './IAICanvasStagingArea';
|
||||||
|
import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar';
|
||||||
|
import {
|
||||||
|
setCanvasBaseLayer,
|
||||||
|
setCanvasStage,
|
||||||
|
} from '../util/konvaInstanceProvider';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[canvasSelector, isStagingSelector],
|
||||||
|
(canvas, isStaging) => {
|
||||||
|
const {
|
||||||
|
isMaskEnabled,
|
||||||
|
stageScale,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
stageDimensions,
|
||||||
|
stageCoordinates,
|
||||||
|
tool,
|
||||||
|
isMovingStage,
|
||||||
|
shouldShowIntermediates,
|
||||||
|
shouldShowGrid,
|
||||||
|
} = canvas;
|
||||||
|
|
||||||
|
let stageCursor: string | undefined = '';
|
||||||
|
|
||||||
|
if (tool === 'move' || isStaging) {
|
||||||
|
if (isMovingStage) {
|
||||||
|
stageCursor = 'grabbing';
|
||||||
|
} else {
|
||||||
|
stageCursor = 'grab';
|
||||||
|
}
|
||||||
|
} else if (isTransformingBoundingBox) {
|
||||||
|
stageCursor = undefined;
|
||||||
|
} else if (isMouseOverBoundingBox) {
|
||||||
|
stageCursor = 'move';
|
||||||
|
} else {
|
||||||
|
stageCursor = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isMaskEnabled,
|
||||||
|
isModifyingBoundingBox: isTransformingBoundingBox || isMovingBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
shouldShowGrid,
|
||||||
|
stageCoordinates,
|
||||||
|
stageCursor,
|
||||||
|
stageDimensions,
|
||||||
|
stageScale,
|
||||||
|
tool,
|
||||||
|
isStaging,
|
||||||
|
shouldShowIntermediates,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvas = () => {
|
||||||
|
const {
|
||||||
|
isMaskEnabled,
|
||||||
|
isModifyingBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
shouldShowGrid,
|
||||||
|
stageCoordinates,
|
||||||
|
stageCursor,
|
||||||
|
stageDimensions,
|
||||||
|
stageScale,
|
||||||
|
tool,
|
||||||
|
isStaging,
|
||||||
|
shouldShowIntermediates,
|
||||||
|
} = useAppSelector(selector);
|
||||||
|
useCanvasHotkeys();
|
||||||
|
|
||||||
|
const stageRef = useRef<Konva.Stage | null>(null);
|
||||||
|
const canvasBaseLayerRef = useRef<Konva.Layer | null>(null);
|
||||||
|
|
||||||
|
const canvasStageRefCallback = useCallback((el: Konva.Stage) => {
|
||||||
|
setCanvasStage(el as Konva.Stage);
|
||||||
|
stageRef.current = el;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const canvasBaseLayerRefCallback = useCallback((el: Konva.Layer) => {
|
||||||
|
setCanvasBaseLayer(el as Konva.Layer);
|
||||||
|
canvasBaseLayerRef.current = el;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const lastCursorPositionRef = useRef<Vector2d>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// Use refs for values that do not affect rendering, other values in redux
|
||||||
|
const didMouseMoveRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const handleWheel = useCanvasWheel(stageRef);
|
||||||
|
const handleMouseDown = useCanvasMouseDown(stageRef);
|
||||||
|
const handleMouseUp = useCanvasMouseUp(stageRef, didMouseMoveRef);
|
||||||
|
const handleMouseMove = useCanvasMouseMove(
|
||||||
|
stageRef,
|
||||||
|
didMouseMoveRef,
|
||||||
|
lastCursorPositionRef
|
||||||
|
);
|
||||||
|
const handleMouseEnter = useCanvasMouseEnter(stageRef);
|
||||||
|
const handleMouseOut = useCanvasMouseOut();
|
||||||
|
const { handleDragStart, handleDragMove, handleDragEnd } =
|
||||||
|
useCanvasDragMove();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inpainting-canvas-container">
|
||||||
|
<div className="inpainting-canvas-wrapper">
|
||||||
|
<Stage
|
||||||
|
tabIndex={-1}
|
||||||
|
ref={canvasStageRefCallback}
|
||||||
|
className={'inpainting-canvas-stage'}
|
||||||
|
style={{
|
||||||
|
...(stageCursor ? { cursor: stageCursor } : {}),
|
||||||
|
}}
|
||||||
|
x={stageCoordinates.x}
|
||||||
|
y={stageCoordinates.y}
|
||||||
|
width={stageDimensions.width}
|
||||||
|
height={stageDimensions.height}
|
||||||
|
scale={{ x: stageScale, y: stageScale }}
|
||||||
|
onTouchStart={handleMouseDown}
|
||||||
|
onTouchMove={handleMouseMove}
|
||||||
|
onTouchEnd={handleMouseUp}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseOut}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragMove={handleDragMove}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onWheel={handleWheel}
|
||||||
|
listening={(tool === 'move' || isStaging) && !isModifyingBoundingBox}
|
||||||
|
draggable={(tool === 'move' || isStaging) && !isModifyingBoundingBox}
|
||||||
|
>
|
||||||
|
<Layer id={'grid'} visible={shouldShowGrid}>
|
||||||
|
<IAICanvasGrid />
|
||||||
|
</Layer>
|
||||||
|
|
||||||
|
<Layer
|
||||||
|
id={'base'}
|
||||||
|
ref={canvasBaseLayerRefCallback}
|
||||||
|
listening={false}
|
||||||
|
imageSmoothingEnabled={false}
|
||||||
|
>
|
||||||
|
<IAICanvasObjectRenderer />
|
||||||
|
</Layer>
|
||||||
|
<Layer id={'mask'} visible={isMaskEnabled} listening={false}>
|
||||||
|
<IAICanvasMaskLines visible={true} listening={false} />
|
||||||
|
<IAICanvasMaskCompositer listening={false} />
|
||||||
|
</Layer>
|
||||||
|
<Layer id="preview" imageSmoothingEnabled={false}>
|
||||||
|
{!isStaging && (
|
||||||
|
<IAICanvasToolPreview
|
||||||
|
visible={tool !== 'move'}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IAICanvasStagingArea visible={isStaging} />
|
||||||
|
{shouldShowIntermediates && <IAICanvasIntermediateImage />}
|
||||||
|
<IAICanvasBoundingBox
|
||||||
|
visible={shouldShowBoundingBox && !isStaging}
|
||||||
|
/>
|
||||||
|
</Layer>
|
||||||
|
</Stage>
|
||||||
|
<IAICanvasStatusText />
|
||||||
|
<IAICanvasStagingAreaToolbar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvas;
|
115
frontend/src/features/canvas/components/IAICanvasGrid.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/
|
||||||
|
|
||||||
|
import { useColorMode } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { ReactNode, useCallback, useLayoutEffect, useState } from 'react';
|
||||||
|
import { Group, Line as KonvaLine } from 'react-konva';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[canvasSelector],
|
||||||
|
(canvas) => {
|
||||||
|
const { stageScale, stageCoordinates, stageDimensions } = canvas;
|
||||||
|
return { stageScale, stageCoordinates, stageDimensions };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridLinesColor = {
|
||||||
|
dark: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
green: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
light: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAICanvasGrid = () => {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
const { stageScale, stageCoordinates, stageDimensions } =
|
||||||
|
useAppSelector(selector);
|
||||||
|
const [gridLines, setGridLines] = useState<ReactNode[]>([]);
|
||||||
|
|
||||||
|
const unscale = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
return value / stageScale;
|
||||||
|
},
|
||||||
|
[stageScale]
|
||||||
|
);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const gridLineColor = gridLinesColor[colorMode];
|
||||||
|
|
||||||
|
const { width, height } = stageDimensions;
|
||||||
|
const { x, y } = stageCoordinates;
|
||||||
|
|
||||||
|
const stageRect = {
|
||||||
|
x1: 0,
|
||||||
|
y1: 0,
|
||||||
|
x2: width,
|
||||||
|
y2: height,
|
||||||
|
offset: {
|
||||||
|
x: unscale(x),
|
||||||
|
y: unscale(y),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridOffset = {
|
||||||
|
x: Math.ceil(unscale(x) / 64) * 64,
|
||||||
|
y: Math.ceil(unscale(y) / 64) * 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridRect = {
|
||||||
|
x1: -gridOffset.x,
|
||||||
|
y1: -gridOffset.y,
|
||||||
|
x2: unscale(width) - gridOffset.x + 64,
|
||||||
|
y2: unscale(height) - gridOffset.y + 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridFullRect = {
|
||||||
|
x1: Math.min(stageRect.x1, gridRect.x1),
|
||||||
|
y1: Math.min(stageRect.y1, gridRect.y1),
|
||||||
|
x2: Math.max(stageRect.x2, gridRect.x2),
|
||||||
|
y2: Math.max(stageRect.y2, gridRect.y2),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fullRect = gridFullRect;
|
||||||
|
|
||||||
|
const // find the x & y size of the grid
|
||||||
|
xSize = fullRect.x2 - fullRect.x1,
|
||||||
|
ySize = fullRect.y2 - fullRect.y1,
|
||||||
|
// compute the number of steps required on each axis.
|
||||||
|
xSteps = Math.round(xSize / 64) + 1,
|
||||||
|
ySteps = Math.round(ySize / 64) + 1;
|
||||||
|
|
||||||
|
const xLines = _.range(0, xSteps).map((i) => (
|
||||||
|
<KonvaLine
|
||||||
|
key={`x_${i}`}
|
||||||
|
x={fullRect.x1 + i * 64}
|
||||||
|
y={fullRect.y1}
|
||||||
|
points={[0, 0, 0, ySize]}
|
||||||
|
stroke={gridLineColor}
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
const yLines = _.range(0, ySteps).map((i) => (
|
||||||
|
<KonvaLine
|
||||||
|
key={`y_${i}`}
|
||||||
|
x={fullRect.x1}
|
||||||
|
y={fullRect.y1 + i * 64}
|
||||||
|
points={[0, 0, xSize, 0]}
|
||||||
|
stroke={gridLineColor}
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
setGridLines(xLines.concat(yLines));
|
||||||
|
}, [stageScale, stageCoordinates, stageDimensions, colorMode, unscale]);
|
||||||
|
|
||||||
|
return <Group>{gridLines}</Group>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasGrid;
|
15
frontend/src/features/canvas/components/IAICanvasImage.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Image } from 'react-konva';
|
||||||
|
import useImage from 'use-image';
|
||||||
|
|
||||||
|
type IAICanvasImageProps = {
|
||||||
|
url: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
const IAICanvasImage = (props: IAICanvasImageProps) => {
|
||||||
|
const { url, x, y } = props;
|
||||||
|
const [image] = useImage(url);
|
||||||
|
return <Image x={x} y={y} image={image} listening={false} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasImage;
|
@ -0,0 +1,59 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { RootState, useAppSelector } from 'app/store';
|
||||||
|
import { GalleryState } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { ImageConfig } from 'konva/lib/shapes/Image';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Image as KonvaImage } from 'react-konva';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[(state: RootState) => state.gallery],
|
||||||
|
(gallery: GalleryState) => {
|
||||||
|
return gallery.intermediateImage ? gallery.intermediateImage : null;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type Props = Omit<ImageConfig, 'image'>;
|
||||||
|
|
||||||
|
const IAICanvasIntermediateImage = (props: Props) => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
const intermediateImage = useAppSelector(selector);
|
||||||
|
|
||||||
|
const [loadedImageElement, setLoadedImageElement] =
|
||||||
|
useState<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!intermediateImage) return;
|
||||||
|
const tempImage = new Image();
|
||||||
|
|
||||||
|
tempImage.onload = () => {
|
||||||
|
setLoadedImageElement(tempImage);
|
||||||
|
};
|
||||||
|
tempImage.src = intermediateImage.url;
|
||||||
|
}, [intermediateImage]);
|
||||||
|
|
||||||
|
if (!intermediateImage?.boundingBox) return null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
boundingBox: { x, y, width, height },
|
||||||
|
} = intermediateImage;
|
||||||
|
|
||||||
|
return loadedImageElement ? (
|
||||||
|
<KonvaImage
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
image={loadedImageElement}
|
||||||
|
listening={false}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasIntermediateImage;
|
@ -0,0 +1,175 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import { RectConfig } from 'konva/lib/shapes/Rect';
|
||||||
|
import { Rect } from 'react-konva';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
|
||||||
|
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
|
export const canvasMaskCompositerSelector = createSelector(
|
||||||
|
canvasSelector,
|
||||||
|
(canvas) => {
|
||||||
|
const { maskColor, stageCoordinates, stageDimensions, stageScale } = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
stageCoordinates,
|
||||||
|
stageDimensions,
|
||||||
|
stageScale,
|
||||||
|
maskColorString: rgbaColorToString(maskColor),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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 [fillPatternImage, setFillPatternImage] =
|
||||||
|
useState<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
|
const [offset, setOffset] = useState<number>(0);
|
||||||
|
|
||||||
|
const rectRef = useRef<Konva.Rect>(null);
|
||||||
|
const incrementOffset = useCallback(() => {
|
||||||
|
setOffset(offset + 1);
|
||||||
|
setTimeout(incrementOffset, 500);
|
||||||
|
}, [offset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fillPatternImage) return;
|
||||||
|
const image = new Image();
|
||||||
|
|
||||||
|
image.onload = () => {
|
||||||
|
setFillPatternImage(image);
|
||||||
|
};
|
||||||
|
image.src = getColoredSVG(maskColorString);
|
||||||
|
}, [fillPatternImage, maskColorString]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fillPatternImage) return;
|
||||||
|
fillPatternImage.src = getColoredSVG(maskColorString);
|
||||||
|
}, [fillPatternImage, maskColorString]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => setOffset((i) => (i + 1) % 5), 50);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!fillPatternImage ||
|
||||||
|
!isNumber(stageCoordinates.x) ||
|
||||||
|
!isNumber(stageCoordinates.y) ||
|
||||||
|
!isNumber(stageScale) ||
|
||||||
|
!isNumber(stageDimensions.width) ||
|
||||||
|
!isNumber(stageDimensions.height)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Rect
|
||||||
|
ref={rectRef}
|
||||||
|
offsetX={stageCoordinates.x / stageScale}
|
||||||
|
offsetY={stageCoordinates.y / stageScale}
|
||||||
|
height={stageDimensions.height / stageScale}
|
||||||
|
width={stageDimensions.width / stageScale}
|
||||||
|
fillPatternImage={fillPatternImage}
|
||||||
|
fillPatternOffsetY={!isNumber(offset) ? 0 : offset}
|
||||||
|
fillPatternRepeat={'repeat'}
|
||||||
|
fillPatternScale={{ x: 1 / stageScale, y: 1 / stageScale }}
|
||||||
|
listening={true}
|
||||||
|
globalCompositeOperation={'source-in'}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasMaskCompositer;
|
@ -0,0 +1,54 @@
|
|||||||
|
import { GroupConfig } from 'konva/lib/Group';
|
||||||
|
import { Group, Line } from 'react-konva';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import { isCanvasMaskLine } from '../store/canvasTypes';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export const canvasLinesSelector = createSelector(
|
||||||
|
[canvasSelector],
|
||||||
|
(canvas) => {
|
||||||
|
return { objects: canvas.layerState.objects };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type InpaintingCanvasLinesProps = GroupConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the lines which comprise the mask.
|
||||||
|
*
|
||||||
|
* Uses globalCompositeOperation to handle the brush and eraser tools.
|
||||||
|
*/
|
||||||
|
const IAICanvasLines = (props: InpaintingCanvasLinesProps) => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
const { objects } = useAppSelector(canvasLinesSelector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group listening={false} {...rest}>
|
||||||
|
{objects.filter(isCanvasMaskLine).map((line, i) => (
|
||||||
|
<Line
|
||||||
|
key={i}
|
||||||
|
points={line.points}
|
||||||
|
stroke={'rgb(0,0,0)'} // The lines can be any color, just need alpha > 0
|
||||||
|
strokeWidth={line.strokeWidth * 2}
|
||||||
|
tension={0}
|
||||||
|
lineCap="round"
|
||||||
|
lineJoin="round"
|
||||||
|
shadowForStrokeEnabled={false}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation={
|
||||||
|
line.tool === 'brush' ? 'source-over' : 'destination-out'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasLines;
|
@ -0,0 +1,62 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { Group, Line } from 'react-konva';
|
||||||
|
import { isCanvasBaseImage, isCanvasBaseLine } from '../store/canvasTypes';
|
||||||
|
import IAICanvasImage from './IAICanvasImage';
|
||||||
|
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[canvasSelector],
|
||||||
|
(canvas) => {
|
||||||
|
const {
|
||||||
|
layerState: { objects },
|
||||||
|
} = canvas;
|
||||||
|
return {
|
||||||
|
objects,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasObjectRenderer = () => {
|
||||||
|
const { objects } = useAppSelector(selector);
|
||||||
|
|
||||||
|
if (!objects) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group name="outpainting-objects" listening={false}>
|
||||||
|
{objects.map((obj, i) => {
|
||||||
|
if (isCanvasBaseImage(obj)) {
|
||||||
|
return (
|
||||||
|
<IAICanvasImage key={i} x={obj.x} y={obj.y} url={obj.image.url} />
|
||||||
|
);
|
||||||
|
} else if (isCanvasBaseLine(obj)) {
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
key={i}
|
||||||
|
points={obj.points}
|
||||||
|
stroke={obj.color ? rgbaColorToString(obj.color) : 'rgb(0,0,0)'} // The lines can be any color, just need alpha > 0
|
||||||
|
strokeWidth={obj.strokeWidth * 2}
|
||||||
|
tension={0}
|
||||||
|
lineCap="round"
|
||||||
|
lineJoin="round"
|
||||||
|
shadowForStrokeEnabled={false}
|
||||||
|
listening={false}
|
||||||
|
globalCompositeOperation={
|
||||||
|
obj.tool === 'brush' ? 'source-over' : 'destination-out'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasObjectRenderer;
|
79
frontend/src/features/canvas/components/IAICanvasResizer.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Spinner } from '@chakra-ui/react';
|
||||||
|
import { useLayoutEffect, useRef } from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
|
||||||
|
import {
|
||||||
|
resizeAndScaleCanvas,
|
||||||
|
resizeCanvas,
|
||||||
|
setCanvasContainerDimensions,
|
||||||
|
setDoesCanvasNeedScaling,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
canvasSelector,
|
||||||
|
initialCanvasImageSelector,
|
||||||
|
} from 'features/canvas/store/canvasSelectors';
|
||||||
|
|
||||||
|
const canvasResizerSelector = createSelector(
|
||||||
|
canvasSelector,
|
||||||
|
initialCanvasImageSelector,
|
||||||
|
activeTabNameSelector,
|
||||||
|
(canvas, initialCanvasImage, activeTabName) => {
|
||||||
|
const { doesCanvasNeedScaling, isCanvasInitialized } = canvas;
|
||||||
|
return {
|
||||||
|
doesCanvasNeedScaling,
|
||||||
|
activeTabName,
|
||||||
|
initialCanvasImage,
|
||||||
|
isCanvasInitialized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasResizer = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const {
|
||||||
|
doesCanvasNeedScaling,
|
||||||
|
activeTabName,
|
||||||
|
initialCanvasImage,
|
||||||
|
isCanvasInitialized,
|
||||||
|
} = useAppSelector(canvasResizerSelector);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
|
||||||
|
const { clientWidth, clientHeight } = ref.current;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setCanvasContainerDimensions({
|
||||||
|
width: clientWidth,
|
||||||
|
height: clientHeight,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isCanvasInitialized) {
|
||||||
|
dispatch(resizeAndScaleCanvas());
|
||||||
|
} else {
|
||||||
|
dispatch(resizeCanvas());
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setDoesCanvasNeedScaling(false));
|
||||||
|
}, 0);
|
||||||
|
}, [
|
||||||
|
dispatch,
|
||||||
|
initialCanvasImage,
|
||||||
|
doesCanvasNeedScaling,
|
||||||
|
activeTabName,
|
||||||
|
isCanvasInitialized,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="inpainting-canvas-area">
|
||||||
|
<Spinner thickness="2px" speed="1s" size="xl" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasResizer;
|
@ -0,0 +1,84 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import { GroupConfig } from 'konva/lib/Group';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { Group, Rect } from 'react-konva';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import IAICanvasImage from './IAICanvasImage';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[canvasSelector],
|
||||||
|
(canvas) => {
|
||||||
|
const {
|
||||||
|
layerState: {
|
||||||
|
stagingArea: { images, selectedImageIndex },
|
||||||
|
},
|
||||||
|
shouldShowStagingImage,
|
||||||
|
shouldShowStagingOutline,
|
||||||
|
} = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentStagingAreaImage:
|
||||||
|
images.length > 0 ? images[selectedImageIndex] : undefined,
|
||||||
|
isOnFirstImage: selectedImageIndex === 0,
|
||||||
|
isOnLastImage: selectedImageIndex === images.length - 1,
|
||||||
|
shouldShowStagingImage,
|
||||||
|
shouldShowStagingOutline,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type Props = GroupConfig;
|
||||||
|
|
||||||
|
const IAICanvasStagingArea = (props: Props) => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
const {
|
||||||
|
currentStagingAreaImage,
|
||||||
|
shouldShowStagingImage,
|
||||||
|
shouldShowStagingOutline,
|
||||||
|
} = useAppSelector(selector);
|
||||||
|
|
||||||
|
if (!currentStagingAreaImage) return null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
image: { width, height, url },
|
||||||
|
} = currentStagingAreaImage;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group {...rest}>
|
||||||
|
{shouldShowStagingImage && <IAICanvasImage url={url} x={x} y={y} />}
|
||||||
|
{shouldShowStagingOutline && (
|
||||||
|
<Group>
|
||||||
|
<Rect
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
strokeWidth={1}
|
||||||
|
stroke={'black'}
|
||||||
|
strokeScaleEnabled={false}
|
||||||
|
/>
|
||||||
|
<Rect
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
dash={[4, 4]}
|
||||||
|
strokeWidth={1}
|
||||||
|
stroke={'white'}
|
||||||
|
strokeScaleEnabled={false}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasStagingArea;
|
@ -0,0 +1,180 @@
|
|||||||
|
import { ButtonGroup, Flex } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
FaArrowLeft,
|
||||||
|
FaArrowRight,
|
||||||
|
FaCheck,
|
||||||
|
FaEye,
|
||||||
|
FaEyeSlash,
|
||||||
|
FaSave,
|
||||||
|
FaTrash,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import {
|
||||||
|
commitStagingAreaImage,
|
||||||
|
discardStagedImages,
|
||||||
|
nextStagingAreaImage,
|
||||||
|
prevStagingAreaImage,
|
||||||
|
setShouldShowStagingImage,
|
||||||
|
setShouldShowStagingOutline,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { saveStagingAreaImageToGallery } from 'app/socketio/actions';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[canvasSelector],
|
||||||
|
(canvas) => {
|
||||||
|
const {
|
||||||
|
layerState: {
|
||||||
|
stagingArea: { images, selectedImageIndex },
|
||||||
|
},
|
||||||
|
shouldShowStagingOutline,
|
||||||
|
shouldShowStagingImage,
|
||||||
|
} = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentStagingAreaImage:
|
||||||
|
images.length > 0 ? images[selectedImageIndex] : undefined,
|
||||||
|
isOnFirstImage: selectedImageIndex === 0,
|
||||||
|
isOnLastImage: selectedImageIndex === images.length - 1,
|
||||||
|
shouldShowStagingImage,
|
||||||
|
shouldShowStagingOutline,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasStagingAreaToolbar = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const {
|
||||||
|
isOnFirstImage,
|
||||||
|
isOnLastImage,
|
||||||
|
currentStagingAreaImage,
|
||||||
|
shouldShowStagingImage,
|
||||||
|
} = useAppSelector(selector);
|
||||||
|
|
||||||
|
const handleMouseOver = useCallback(() => {
|
||||||
|
dispatch(setShouldShowStagingOutline(false));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleMouseOut = useCallback(() => {
|
||||||
|
dispatch(setShouldShowStagingOutline(true));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['left'],
|
||||||
|
() => {
|
||||||
|
handlePrevImage();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['right'],
|
||||||
|
() => {
|
||||||
|
handleNextImage();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['enter'],
|
||||||
|
() => {
|
||||||
|
handleAccept();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePrevImage = () => dispatch(prevStagingAreaImage());
|
||||||
|
const handleNextImage = () => dispatch(nextStagingAreaImage());
|
||||||
|
const handleAccept = () => dispatch(commitStagingAreaImage());
|
||||||
|
|
||||||
|
if (!currentStagingAreaImage) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
pos={'absolute'}
|
||||||
|
bottom={'1rem'}
|
||||||
|
w={'100%'}
|
||||||
|
align={'center'}
|
||||||
|
justify={'center'}
|
||||||
|
filter="drop-shadow(0 0.5rem 1rem rgba(0,0,0))"
|
||||||
|
onMouseOver={handleMouseOver}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
|
>
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip="Previous (Left)"
|
||||||
|
aria-label="Previous (Left)"
|
||||||
|
icon={<FaArrowLeft />}
|
||||||
|
onClick={handlePrevImage}
|
||||||
|
data-selected={true}
|
||||||
|
isDisabled={isOnFirstImage}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip="Next (Right)"
|
||||||
|
aria-label="Next (Right)"
|
||||||
|
icon={<FaArrowRight />}
|
||||||
|
onClick={handleNextImage}
|
||||||
|
data-selected={true}
|
||||||
|
isDisabled={isOnLastImage}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip="Accept (Enter)"
|
||||||
|
aria-label="Accept (Enter)"
|
||||||
|
icon={<FaCheck />}
|
||||||
|
onClick={handleAccept}
|
||||||
|
data-selected={true}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip="Show/Hide"
|
||||||
|
aria-label="Show/Hide"
|
||||||
|
data-alert={!shouldShowStagingImage}
|
||||||
|
icon={shouldShowStagingImage ? <FaEye /> : <FaEyeSlash />}
|
||||||
|
onClick={() =>
|
||||||
|
dispatch(setShouldShowStagingImage(!shouldShowStagingImage))
|
||||||
|
}
|
||||||
|
data-selected={true}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip="Save to Gallery"
|
||||||
|
aria-label="Save to Gallery"
|
||||||
|
icon={<FaSave />}
|
||||||
|
onClick={() =>
|
||||||
|
dispatch(
|
||||||
|
saveStagingAreaImageToGallery(currentStagingAreaImage.image.url)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data-selected={true}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip="Discard All"
|
||||||
|
aria-label="Discard All"
|
||||||
|
icon={<FaTrash />}
|
||||||
|
onClick={() => dispatch(discardStagedImages())}
|
||||||
|
data-selected={true}
|
||||||
|
style={{ backgroundColor: 'var(--btn-delete-image)' }}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasStagingAreaToolbar;
|
116
frontend/src/features/canvas/components/IAICanvasStatusText.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import IAICanvasStatusTextCursorPos from './IAICanvasStatusText/IAICanvasStatusTextCursorPos';
|
||||||
|
import roundToHundreth from '../util/roundToHundreth';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[canvasSelector],
|
||||||
|
(canvas) => {
|
||||||
|
const {
|
||||||
|
stageDimensions: { width: stageWidth, height: stageHeight },
|
||||||
|
stageCoordinates: { x: stageX, y: stageY },
|
||||||
|
boundingBoxDimensions: { width: boxWidth, height: boxHeight },
|
||||||
|
scaledBoundingBoxDimensions: {
|
||||||
|
width: scaledBoxWidth,
|
||||||
|
height: scaledBoxHeight,
|
||||||
|
},
|
||||||
|
boundingBoxCoordinates: { x: boxX, y: boxY },
|
||||||
|
stageScale,
|
||||||
|
shouldShowCanvasDebugInfo,
|
||||||
|
layer,
|
||||||
|
boundingBoxScaleMethod,
|
||||||
|
} = canvas;
|
||||||
|
|
||||||
|
let boundingBoxColor = 'inherit';
|
||||||
|
|
||||||
|
if (
|
||||||
|
(boundingBoxScaleMethod === 'none' &&
|
||||||
|
(boxWidth < 512 || boxHeight < 512)) ||
|
||||||
|
(boundingBoxScaleMethod === 'manual' &&
|
||||||
|
scaledBoxWidth * scaledBoxHeight < 512 * 512)
|
||||||
|
) {
|
||||||
|
boundingBoxColor = 'var(--status-working-color)';
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeLayerColor =
|
||||||
|
layer === 'mask' ? 'var(--status-working-color)' : 'inherit';
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeLayerColor,
|
||||||
|
activeLayerString: layer.charAt(0).toUpperCase() + layer.slice(1),
|
||||||
|
boundingBoxColor,
|
||||||
|
boundingBoxCoordinatesString: `(${roundToHundreth(
|
||||||
|
boxX
|
||||||
|
)}, ${roundToHundreth(boxY)})`,
|
||||||
|
boundingBoxDimensionsString: `${boxWidth}×${boxHeight}`,
|
||||||
|
scaledBoundingBoxDimensionsString: `${scaledBoxWidth}×${scaledBoxHeight}`,
|
||||||
|
canvasCoordinatesString: `${roundToHundreth(stageX)}×${roundToHundreth(
|
||||||
|
stageY
|
||||||
|
)}`,
|
||||||
|
canvasDimensionsString: `${stageWidth}×${stageHeight}`,
|
||||||
|
canvasScaleString: Math.round(stageScale * 100),
|
||||||
|
shouldShowCanvasDebugInfo,
|
||||||
|
shouldShowBoundingBox: boundingBoxScaleMethod !== 'auto',
|
||||||
|
shouldShowScaledBoundingBox: boundingBoxScaleMethod !== 'none',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasStatusText = () => {
|
||||||
|
const {
|
||||||
|
activeLayerColor,
|
||||||
|
activeLayerString,
|
||||||
|
boundingBoxColor,
|
||||||
|
boundingBoxCoordinatesString,
|
||||||
|
boundingBoxDimensionsString,
|
||||||
|
scaledBoundingBoxDimensionsString,
|
||||||
|
shouldShowScaledBoundingBox,
|
||||||
|
canvasCoordinatesString,
|
||||||
|
canvasDimensionsString,
|
||||||
|
canvasScaleString,
|
||||||
|
shouldShowCanvasDebugInfo,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
} = useAppSelector(selector);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="canvas-status-text">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: activeLayerColor,
|
||||||
|
}}
|
||||||
|
>{`Active Layer: ${activeLayerString}`}</div>
|
||||||
|
<div>{`Canvas Scale: ${canvasScaleString}%`}</div>
|
||||||
|
{shouldShowBoundingBox && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: boundingBoxColor,
|
||||||
|
}}
|
||||||
|
>{`Bounding Box: ${boundingBoxDimensionsString}`}</div>
|
||||||
|
)}
|
||||||
|
{shouldShowScaledBoundingBox && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: boundingBoxColor,
|
||||||
|
}}
|
||||||
|
>{`Scaled Bounding Box: ${scaledBoundingBoxDimensionsString}`}</div>
|
||||||
|
)}
|
||||||
|
{shouldShowCanvasDebugInfo && (
|
||||||
|
<>
|
||||||
|
<div>{`Bounding Box Position: ${boundingBoxCoordinatesString}`}</div>
|
||||||
|
<div>{`Canvas Dimensions: ${canvasDimensionsString}`}</div>
|
||||||
|
<div>{`Canvas Position: ${canvasCoordinatesString}`}</div>
|
||||||
|
<IAICanvasStatusTextCursorPos />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasStatusText;
|
@ -0,0 +1,34 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import roundToHundreth from 'features/canvas/util/roundToHundreth';
|
||||||
|
|
||||||
|
const cursorPositionSelector = createSelector(
|
||||||
|
[canvasSelector],
|
||||||
|
(canvas) => {
|
||||||
|
const { cursorPosition } = canvas;
|
||||||
|
|
||||||
|
const { cursorX, cursorY } = cursorPosition
|
||||||
|
? { cursorX: cursorPosition.x, cursorY: cursorPosition.y }
|
||||||
|
: { cursorX: -1, cursorY: -1 };
|
||||||
|
|
||||||
|
return {
|
||||||
|
cursorCoordinatesString: `(${roundToHundreth(cursorX)}, ${roundToHundreth(
|
||||||
|
cursorY
|
||||||
|
)})`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function IAICanvasStatusTextCursorPos() {
|
||||||
|
const { cursorCoordinatesString } = useAppSelector(cursorPositionSelector);
|
||||||
|
|
||||||
|
return <div>{`Cursor Position: ${cursorCoordinatesString}`}</div>;
|
||||||
|
}
|
156
frontend/src/features/canvas/components/IAICanvasToolPreview.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { GroupConfig } from 'konva/lib/Group';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { Circle, Group } from 'react-konva';
|
||||||
|
import { useAppSelector } from 'app/store';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||||
|
import {
|
||||||
|
COLOR_PICKER_SIZE,
|
||||||
|
COLOR_PICKER_STROKE_RADIUS,
|
||||||
|
} from '../util/constants';
|
||||||
|
|
||||||
|
const canvasBrushPreviewSelector = createSelector(
|
||||||
|
canvasSelector,
|
||||||
|
(canvas) => {
|
||||||
|
const {
|
||||||
|
cursorPosition,
|
||||||
|
stageDimensions: { width, height },
|
||||||
|
brushSize,
|
||||||
|
colorPickerColor,
|
||||||
|
maskColor,
|
||||||
|
brushColor,
|
||||||
|
tool,
|
||||||
|
layer,
|
||||||
|
shouldShowBrush,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
stageScale,
|
||||||
|
} = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
cursorPosition,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
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,
|
||||||
|
shouldShowBrush,
|
||||||
|
shouldDrawBrushPreview:
|
||||||
|
!(
|
||||||
|
isMovingBoundingBox ||
|
||||||
|
isTransformingBoundingBox ||
|
||||||
|
!cursorPosition
|
||||||
|
) && shouldShowBrush,
|
||||||
|
strokeWidth: 1.5 / stageScale,
|
||||||
|
dotRadius: 1.5 / stageScale,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a black circle around the canvas brush preview.
|
||||||
|
*/
|
||||||
|
const IAICanvasToolPreview = (props: GroupConfig) => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
const {
|
||||||
|
cursorPosition,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
radius,
|
||||||
|
maskColorString,
|
||||||
|
tool,
|
||||||
|
layer,
|
||||||
|
shouldDrawBrushPreview,
|
||||||
|
dotRadius,
|
||||||
|
strokeWidth,
|
||||||
|
brushColorString,
|
||||||
|
colorPickerColorString,
|
||||||
|
colorPickerInnerRadius,
|
||||||
|
colorPickerOuterRadius,
|
||||||
|
} = useAppSelector(canvasBrushPreviewSelector);
|
||||||
|
|
||||||
|
if (!shouldDrawBrushPreview) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group listening={false} {...rest}>
|
||||||
|
{tool === 'colorPicker' ? (
|
||||||
|
<>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={colorPickerOuterRadius}
|
||||||
|
stroke={brushColorString}
|
||||||
|
strokeWidth={COLOR_PICKER_STROKE_RADIUS}
|
||||||
|
strokeScaleEnabled={false}
|
||||||
|
/>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={colorPickerInnerRadius}
|
||||||
|
stroke={colorPickerColorString}
|
||||||
|
strokeWidth={COLOR_PICKER_STROKE_RADIUS}
|
||||||
|
strokeScaleEnabled={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={radius}
|
||||||
|
fill={layer === 'mask' ? maskColorString : brushColorString}
|
||||||
|
globalCompositeOperation={
|
||||||
|
tool === 'eraser' ? 'destination-out' : 'source-over'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={radius}
|
||||||
|
stroke={'rgba(255,255,255,0.4)'}
|
||||||
|
strokeWidth={strokeWidth * 2}
|
||||||
|
strokeEnabled={true}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={radius}
|
||||||
|
stroke={'rgba(0,0,0,1)'}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeEnabled={true}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={dotRadius * 2}
|
||||||
|
fill={'rgba(255,255,255,0.4)'}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
<Circle
|
||||||
|
x={cursorPosition ? cursorPosition.x : width / 2}
|
||||||
|
y={cursorPosition ? cursorPosition.y : height / 2}
|
||||||
|
radius={dotRadius}
|
||||||
|
fill={'rgba(0,0,0,1)'}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasToolPreview;
|
@ -0,0 +1,299 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
|
import { Vector2d } from 'konva/lib/types';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { Group, Rect, Transformer } from 'react-konva';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import {
|
||||||
|
roundDownToMultiple,
|
||||||
|
roundToMultiple,
|
||||||
|
} from 'common/util/roundDownToMultiple';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import {
|
||||||
|
setBoundingBoxCoordinates,
|
||||||
|
setBoundingBoxDimensions,
|
||||||
|
setIsMouseOverBoundingBox,
|
||||||
|
setIsMovingBoundingBox,
|
||||||
|
setIsTransformingBoundingBox,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import { GroupConfig } from 'konva/lib/Group';
|
||||||
|
|
||||||
|
const boundingBoxPreviewSelector = createSelector(
|
||||||
|
canvasSelector,
|
||||||
|
(canvas) => {
|
||||||
|
const {
|
||||||
|
boundingBoxCoordinates,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
stageDimensions,
|
||||||
|
stageScale,
|
||||||
|
isDrawing,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
tool,
|
||||||
|
stageCoordinates,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
} = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
boundingBoxCoordinates,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
isDrawing,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
stageDimensions,
|
||||||
|
stageScale,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
tool,
|
||||||
|
stageCoordinates,
|
||||||
|
boundingBoxStrokeWidth: (isMouseOverBoundingBox ? 8 : 1) / stageScale,
|
||||||
|
hitStrokeWidth: 20 / stageScale,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type IAICanvasBoundingBoxPreviewProps = GroupConfig;
|
||||||
|
|
||||||
|
const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const {
|
||||||
|
boundingBoxCoordinates,
|
||||||
|
boundingBoxDimensions,
|
||||||
|
isDrawing,
|
||||||
|
isMouseOverBoundingBox,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
isMovingBoundingBox,
|
||||||
|
isTransformingBoundingBox,
|
||||||
|
stageCoordinates,
|
||||||
|
stageDimensions,
|
||||||
|
stageScale,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
tool,
|
||||||
|
boundingBoxStrokeWidth,
|
||||||
|
hitStrokeWidth,
|
||||||
|
} = useAppSelector(boundingBoxPreviewSelector);
|
||||||
|
|
||||||
|
const transformerRef = useRef<Konva.Transformer>(null);
|
||||||
|
const shapeRef = useRef<Konva.Rect>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!transformerRef.current || !shapeRef.current) return;
|
||||||
|
transformerRef.current.nodes([shapeRef.current]);
|
||||||
|
transformerRef.current.getLayer()?.batchDraw();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scaledStep = 64 * stageScale;
|
||||||
|
|
||||||
|
const handleOnDragMove = useCallback(
|
||||||
|
(e: KonvaEventObject<DragEvent>) => {
|
||||||
|
if (!shouldSnapToGrid) {
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxCoordinates({
|
||||||
|
x: Math.floor(e.target.x()),
|
||||||
|
y: Math.floor(e.target.y()),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragX = e.target.x();
|
||||||
|
const dragY = e.target.y();
|
||||||
|
|
||||||
|
const newX = roundToMultiple(dragX, 64);
|
||||||
|
const newY = roundToMultiple(dragY, 64);
|
||||||
|
|
||||||
|
e.target.x(newX);
|
||||||
|
e.target.y(newY);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxCoordinates({
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, 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 rect = shapeRef.current;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const x = Math.round(rect.x());
|
||||||
|
const y = Math.round(rect.y());
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxDimensions({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setBoundingBoxCoordinates({
|
||||||
|
x: shouldSnapToGrid ? roundDownToMultiple(x, 64) : x,
|
||||||
|
y: shouldSnapToGrid ? roundDownToMultiple(y, 64) : y,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset the scale now that the coords/dimensions have been un-scaled
|
||||||
|
rect.scaleX(1);
|
||||||
|
rect.scaleY(1);
|
||||||
|
}, [dispatch, shouldSnapToGrid]);
|
||||||
|
|
||||||
|
const anchorDragBoundFunc = useCallback(
|
||||||
|
(
|
||||||
|
oldPos: Vector2d, // old absolute position of anchor point
|
||||||
|
newPos: Vector2d, // new absolute position (potentially) of anchor point
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_e: MouseEvent
|
||||||
|
) => {
|
||||||
|
/**
|
||||||
|
* Konva does not transform with width or height. It transforms the anchor point
|
||||||
|
* and scale factor. This is then sent to the shape's onTransform listeners.
|
||||||
|
*
|
||||||
|
* We need to snap the new dimensions to steps of 64. But because the whole
|
||||||
|
* stage is scaled, our actual desired step is actually 64 * the stage scale.
|
||||||
|
*
|
||||||
|
* Additionally, we need to ensure we offset the position so that we snap to a
|
||||||
|
* multiple of 64 that is aligned with the grid, and not from the absolute zero
|
||||||
|
* coordinate.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Calculate the offset of the grid.
|
||||||
|
const offsetX = oldPos.x % scaledStep;
|
||||||
|
const offsetY = oldPos.y % scaledStep;
|
||||||
|
|
||||||
|
const newCoordinates = {
|
||||||
|
x: roundDownToMultiple(newPos.x, scaledStep) + offsetX,
|
||||||
|
y: roundDownToMultiple(newPos.y, scaledStep) + offsetY,
|
||||||
|
};
|
||||||
|
|
||||||
|
return newCoordinates;
|
||||||
|
},
|
||||||
|
[scaledStep]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStartedTransforming = () => {
|
||||||
|
dispatch(setIsTransformingBoundingBox(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndedTransforming = () => {
|
||||||
|
dispatch(setIsTransformingBoundingBox(false));
|
||||||
|
dispatch(setIsMouseOverBoundingBox(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartedMoving = () => {
|
||||||
|
dispatch(setIsMovingBoundingBox(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndedModifying = () => {
|
||||||
|
dispatch(setIsTransformingBoundingBox(false));
|
||||||
|
dispatch(setIsMovingBoundingBox(false));
|
||||||
|
dispatch(setIsMouseOverBoundingBox(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseOver = () => {
|
||||||
|
dispatch(setIsMouseOverBoundingBox(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseOut = () => {
|
||||||
|
!isTransformingBoundingBox &&
|
||||||
|
!isMovingBoundingBox &&
|
||||||
|
dispatch(setIsMouseOverBoundingBox(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group {...rest}>
|
||||||
|
<Rect
|
||||||
|
offsetX={stageCoordinates.x / stageScale}
|
||||||
|
offsetY={stageCoordinates.y / stageScale}
|
||||||
|
height={stageDimensions.height / stageScale}
|
||||||
|
width={stageDimensions.width / stageScale}
|
||||||
|
fill={'rgba(0,0,0,0.4)'}
|
||||||
|
listening={false}
|
||||||
|
visible={shouldDarkenOutsideBoundingBox}
|
||||||
|
/>
|
||||||
|
<Rect
|
||||||
|
x={boundingBoxCoordinates.x}
|
||||||
|
y={boundingBoxCoordinates.y}
|
||||||
|
width={boundingBoxDimensions.width}
|
||||||
|
height={boundingBoxDimensions.height}
|
||||||
|
fill={'rgb(255,255,255)'}
|
||||||
|
listening={false}
|
||||||
|
visible={shouldDarkenOutsideBoundingBox}
|
||||||
|
globalCompositeOperation={'destination-out'}
|
||||||
|
/>
|
||||||
|
<Rect
|
||||||
|
draggable={true}
|
||||||
|
fillEnabled={false}
|
||||||
|
height={boundingBoxDimensions.height}
|
||||||
|
hitStrokeWidth={hitStrokeWidth}
|
||||||
|
listening={!isDrawing && tool === 'move'}
|
||||||
|
onDragEnd={handleEndedModifying}
|
||||||
|
onDragMove={handleOnDragMove}
|
||||||
|
onMouseDown={handleStartedMoving}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
|
onMouseOver={handleMouseOver}
|
||||||
|
onMouseUp={handleEndedModifying}
|
||||||
|
onTransform={handleOnTransform}
|
||||||
|
onTransformEnd={handleEndedTransforming}
|
||||||
|
ref={shapeRef}
|
||||||
|
stroke={isMouseOverBoundingBox ? 'rgba(255,255,255,0.7)' : 'white'}
|
||||||
|
strokeWidth={boundingBoxStrokeWidth}
|
||||||
|
width={boundingBoxDimensions.width}
|
||||||
|
x={boundingBoxCoordinates.x}
|
||||||
|
y={boundingBoxCoordinates.y}
|
||||||
|
/>
|
||||||
|
<Transformer
|
||||||
|
anchorCornerRadius={3}
|
||||||
|
anchorDragBoundFunc={anchorDragBoundFunc}
|
||||||
|
anchorFill={'rgba(212,216,234,1)'}
|
||||||
|
anchorSize={15}
|
||||||
|
anchorStroke={'rgb(42,42,42)'}
|
||||||
|
borderDash={[4, 4]}
|
||||||
|
borderEnabled={true}
|
||||||
|
borderStroke={'black'}
|
||||||
|
draggable={false}
|
||||||
|
enabledAnchors={tool === 'move' ? undefined : []}
|
||||||
|
flipEnabled={false}
|
||||||
|
ignoreStroke={true}
|
||||||
|
keepRatio={false}
|
||||||
|
listening={!isDrawing && tool === 'move'}
|
||||||
|
onDragEnd={handleEndedModifying}
|
||||||
|
onMouseDown={handleStartedTransforming}
|
||||||
|
onMouseUp={handleEndedTransforming}
|
||||||
|
onTransformEnd={handleEndedTransforming}
|
||||||
|
ref={transformerRef}
|
||||||
|
rotateEnabled={false}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasBoundingBox;
|
@ -0,0 +1,136 @@
|
|||||||
|
import { ButtonGroup, Flex } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
clearMask,
|
||||||
|
setIsMaskEnabled,
|
||||||
|
setLayer,
|
||||||
|
setMaskColor,
|
||||||
|
setShouldPreserveMaskedArea,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { FaMask, FaTrash } from 'react-icons/fa';
|
||||||
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
|
import IAICheckbox from 'common/components/IAICheckbox';
|
||||||
|
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||||
|
import IAIButton from 'common/components/IAIButton';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { rgbaColorToString } from 'features/canvas/util/colorToString';
|
||||||
|
|
||||||
|
export const selector = createSelector(
|
||||||
|
[canvasSelector],
|
||||||
|
(canvas) => {
|
||||||
|
const { maskColor, layer, isMaskEnabled, shouldPreserveMaskedArea } =
|
||||||
|
canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
layer,
|
||||||
|
maskColor,
|
||||||
|
maskColorString: rgbaColorToString(maskColor),
|
||||||
|
isMaskEnabled,
|
||||||
|
shouldPreserveMaskedArea,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const IAICanvasMaskOptions = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { layer, maskColor, isMaskEnabled, shouldPreserveMaskedArea } =
|
||||||
|
useAppSelector(selector);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['q'],
|
||||||
|
() => {
|
||||||
|
handleToggleMaskLayer();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[layer]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['shift+c'],
|
||||||
|
() => {
|
||||||
|
handleClearMask();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['h'],
|
||||||
|
() => {
|
||||||
|
handleToggleEnableMask();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[isMaskEnabled]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleMaskLayer = () => {
|
||||||
|
dispatch(setLayer(layer === 'mask' ? 'base' : 'mask'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearMask = () => dispatch(clearMask());
|
||||||
|
|
||||||
|
const handleToggleEnableMask = () =>
|
||||||
|
dispatch(setIsMaskEnabled(!isMaskEnabled));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIPopover
|
||||||
|
trigger="hover"
|
||||||
|
triggerComponent={
|
||||||
|
<ButtonGroup>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Masking Options"
|
||||||
|
tooltip="Masking Options"
|
||||||
|
icon={<FaMask />}
|
||||||
|
style={
|
||||||
|
layer === 'mask'
|
||||||
|
? { backgroundColor: 'var(--accent-color)' }
|
||||||
|
: { backgroundColor: 'var(--btn-base-color)' }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flex direction={'column'} gap={'0.5rem'}>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Enable Mask (H)"
|
||||||
|
isChecked={isMaskEnabled}
|
||||||
|
onChange={handleToggleEnableMask}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Preserve Masked Area"
|
||||||
|
isChecked={shouldPreserveMaskedArea}
|
||||||
|
onChange={(e) =>
|
||||||
|
dispatch(setShouldPreserveMaskedArea(e.target.checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IAIColorPicker
|
||||||
|
style={{ paddingTop: '0.5rem', paddingBottom: '0.5rem' }}
|
||||||
|
color={maskColor}
|
||||||
|
onChange={(newColor) => dispatch(setMaskColor(newColor))}
|
||||||
|
/>
|
||||||
|
<IAIButton size={'sm'} leftIcon={<FaTrash />} onClick={handleClearMask}>
|
||||||
|
Clear Mask (Shift+C)
|
||||||
|
</IAIButton>
|
||||||
|
</Flex>
|
||||||
|
</IAIPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasMaskOptions;
|
@ -0,0 +1,58 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { FaRedo } from 'react-icons/fa';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { redo } from 'features/canvas/store/canvasSlice';
|
||||||
|
|
||||||
|
const canvasRedoSelector = createSelector(
|
||||||
|
[canvasSelector, activeTabNameSelector],
|
||||||
|
(canvas, activeTabName) => {
|
||||||
|
const { futureLayerStates } = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
canRedo: futureLayerStates.length > 0,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function IAICanvasRedoButton() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { canRedo, activeTabName } = useAppSelector(canvasRedoSelector);
|
||||||
|
|
||||||
|
const handleRedo = () => {
|
||||||
|
dispatch(redo());
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['meta+shift+z', 'ctrl+shift+z', 'control+y', 'meta+y'],
|
||||||
|
() => {
|
||||||
|
handleRedo();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => canRedo,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[activeTabName, canRedo]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Redo (Ctrl+Shift+Z)"
|
||||||
|
tooltip="Redo (Ctrl+Shift+Z)"
|
||||||
|
icon={<FaRedo />}
|
||||||
|
onClick={handleRedo}
|
||||||
|
isDisabled={!canRedo}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
setShouldAutoSave,
|
||||||
|
setShouldCropToBoundingBoxOnSave,
|
||||||
|
setShouldDarkenOutsideBoundingBox,
|
||||||
|
setShouldShowCanvasDebugInfo,
|
||||||
|
setShouldShowGrid,
|
||||||
|
setShouldShowIntermediates,
|
||||||
|
setShouldSnapToGrid,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { FaWrench } from 'react-icons/fa';
|
||||||
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
|
import IAICheckbox from 'common/components/IAICheckbox';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import EmptyTempFolderButtonModal from 'features/system/components/ClearTempFolderButtonModal';
|
||||||
|
import ClearCanvasHistoryButtonModal from '../ClearCanvasHistoryButtonModal';
|
||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
|
export const canvasControlsSelector = createSelector(
|
||||||
|
[canvasSelector],
|
||||||
|
(canvas) => {
|
||||||
|
const {
|
||||||
|
shouldAutoSave,
|
||||||
|
shouldCropToBoundingBoxOnSave,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
shouldShowCanvasDebugInfo,
|
||||||
|
shouldShowGrid,
|
||||||
|
shouldShowIntermediates,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
} = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldAutoSave,
|
||||||
|
shouldCropToBoundingBoxOnSave,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
shouldShowCanvasDebugInfo,
|
||||||
|
shouldShowGrid,
|
||||||
|
shouldShowIntermediates,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasSettingsButtonPopover = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const {
|
||||||
|
shouldAutoSave,
|
||||||
|
shouldCropToBoundingBoxOnSave,
|
||||||
|
shouldDarkenOutsideBoundingBox,
|
||||||
|
shouldShowCanvasDebugInfo,
|
||||||
|
shouldShowGrid,
|
||||||
|
shouldShowIntermediates,
|
||||||
|
shouldSnapToGrid,
|
||||||
|
} = useAppSelector(canvasControlsSelector);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['n'],
|
||||||
|
() => {
|
||||||
|
dispatch(setShouldSnapToGrid(!shouldSnapToGrid));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[shouldSnapToGrid]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangeShouldSnapToGrid = (e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
dispatch(setShouldSnapToGrid(e.target.checked));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIPopover
|
||||||
|
trigger="hover"
|
||||||
|
triggerComponent={
|
||||||
|
<IAIIconButton
|
||||||
|
tooltip="Canvas Settings"
|
||||||
|
aria-label="Canvas Settings"
|
||||||
|
icon={<FaWrench />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flex direction={'column'} gap={'0.5rem'}>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Show Intermediates"
|
||||||
|
isChecked={shouldShowIntermediates}
|
||||||
|
onChange={(e) =>
|
||||||
|
dispatch(setShouldShowIntermediates(e.target.checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Show Grid"
|
||||||
|
isChecked={shouldShowGrid}
|
||||||
|
onChange={(e) => dispatch(setShouldShowGrid(e.target.checked))}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Snap to Grid"
|
||||||
|
isChecked={shouldSnapToGrid}
|
||||||
|
onChange={handleChangeShouldSnapToGrid}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Darken Outside Selection"
|
||||||
|
isChecked={shouldDarkenOutsideBoundingBox}
|
||||||
|
onChange={(e) =>
|
||||||
|
dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Auto Save to Gallery"
|
||||||
|
isChecked={shouldAutoSave}
|
||||||
|
onChange={(e) => dispatch(setShouldAutoSave(e.target.checked))}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Save Box Region Only"
|
||||||
|
isChecked={shouldCropToBoundingBoxOnSave}
|
||||||
|
onChange={(e) =>
|
||||||
|
dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IAICheckbox
|
||||||
|
label="Show Canvas Debug Info"
|
||||||
|
isChecked={shouldShowCanvasDebugInfo}
|
||||||
|
onChange={(e) =>
|
||||||
|
dispatch(setShouldShowCanvasDebugInfo(e.target.checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ClearCanvasHistoryButtonModal />
|
||||||
|
<EmptyTempFolderButtonModal />
|
||||||
|
</Flex>
|
||||||
|
</IAIPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasSettingsButtonPopover;
|
@ -0,0 +1,217 @@
|
|||||||
|
import { ButtonGroup, Flex } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
setBrushColor,
|
||||||
|
setBrushSize,
|
||||||
|
setTool,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import {
|
||||||
|
FaEraser,
|
||||||
|
FaEyeDropper,
|
||||||
|
FaPaintBrush,
|
||||||
|
FaSlidersH,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import {
|
||||||
|
canvasSelector,
|
||||||
|
isStagingSelector,
|
||||||
|
} from 'features/canvas/store/canvasSelectors';
|
||||||
|
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import IAIPopover from 'common/components/IAIPopover';
|
||||||
|
import IAISlider from 'common/components/IAISlider';
|
||||||
|
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||||
|
|
||||||
|
export const selector = createSelector(
|
||||||
|
[canvasSelector, isStagingSelector, systemSelector],
|
||||||
|
(canvas, isStaging, system) => {
|
||||||
|
const { isProcessing } = system;
|
||||||
|
const { tool, brushColor, brushSize } = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
isStaging,
|
||||||
|
isProcessing,
|
||||||
|
brushColor,
|
||||||
|
brushSize,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasToolChooserOptions = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { tool, brushColor, brushSize, isStaging } = useAppSelector(selector);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['b'],
|
||||||
|
() => {
|
||||||
|
handleSelectBrushTool();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['e'],
|
||||||
|
() => {
|
||||||
|
handleSelectEraserTool();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[tool]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['c'],
|
||||||
|
() => {
|
||||||
|
handleSelectColorPickerTool();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[tool]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['BracketLeft'],
|
||||||
|
() => {
|
||||||
|
dispatch(setBrushSize(Math.max(brushSize - 5, 5)));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[brushSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['BracketRight'],
|
||||||
|
() => {
|
||||||
|
dispatch(setBrushSize(Math.min(brushSize + 5, 500)));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[brushSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['shift+BracketLeft'],
|
||||||
|
() => {
|
||||||
|
dispatch(
|
||||||
|
setBrushColor({
|
||||||
|
...brushColor,
|
||||||
|
a: _.clamp(brushColor.a - 0.05, 0.05, 1),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[brushColor]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['shift+BracketRight'],
|
||||||
|
() => {
|
||||||
|
dispatch(
|
||||||
|
setBrushColor({
|
||||||
|
...brushColor,
|
||||||
|
a: _.clamp(brushColor.a + 0.05, 0.05, 1),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[brushColor]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectBrushTool = () => dispatch(setTool('brush'));
|
||||||
|
const handleSelectEraserTool = () => dispatch(setTool('eraser'));
|
||||||
|
const handleSelectColorPickerTool = () => dispatch(setTool('colorPicker'));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Brush Tool (B)"
|
||||||
|
tooltip="Brush Tool (B)"
|
||||||
|
icon={<FaPaintBrush />}
|
||||||
|
data-selected={tool === 'brush' && !isStaging}
|
||||||
|
onClick={handleSelectBrushTool}
|
||||||
|
isDisabled={isStaging}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Eraser Tool (E)"
|
||||||
|
tooltip="Eraser Tool (E)"
|
||||||
|
icon={<FaEraser />}
|
||||||
|
data-selected={tool === 'eraser' && !isStaging}
|
||||||
|
isDisabled={isStaging}
|
||||||
|
onClick={handleSelectEraserTool}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Color Picker (C)"
|
||||||
|
tooltip="Color Picker (C)"
|
||||||
|
icon={<FaEyeDropper />}
|
||||||
|
data-selected={tool === 'colorPicker' && !isStaging}
|
||||||
|
isDisabled={isStaging}
|
||||||
|
onClick={handleSelectColorPickerTool}
|
||||||
|
/>
|
||||||
|
<IAIPopover
|
||||||
|
trigger="hover"
|
||||||
|
triggerComponent={
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Brush Options"
|
||||||
|
tooltip="Brush Options"
|
||||||
|
icon={<FaSlidersH />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
minWidth={'15rem'}
|
||||||
|
direction={'column'}
|
||||||
|
gap={'1rem'}
|
||||||
|
width={'100%'}
|
||||||
|
>
|
||||||
|
<Flex gap={'1rem'} justifyContent="space-between">
|
||||||
|
<IAISlider
|
||||||
|
label="Size"
|
||||||
|
value={brushSize}
|
||||||
|
withInput
|
||||||
|
onChange={(newSize) => dispatch(setBrushSize(newSize))}
|
||||||
|
sliderNumberInputProps={{ max: 500 }}
|
||||||
|
inputReadOnly={false}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<IAIColorPicker
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
paddingTop: '0.5rem',
|
||||||
|
paddingBottom: '0.5rem',
|
||||||
|
}}
|
||||||
|
color={brushColor}
|
||||||
|
onChange={(newColor) => dispatch(setBrushColor(newColor))}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</IAIPopover>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasToolChooserOptions;
|
@ -0,0 +1,307 @@
|
|||||||
|
import { ButtonGroup } from '@chakra-ui/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
resetCanvas,
|
||||||
|
resetCanvasView,
|
||||||
|
resizeAndScaleCanvas,
|
||||||
|
setIsMaskEnabled,
|
||||||
|
setLayer,
|
||||||
|
setTool,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import {
|
||||||
|
FaArrowsAlt,
|
||||||
|
FaCopy,
|
||||||
|
FaCrosshairs,
|
||||||
|
FaDownload,
|
||||||
|
FaLayerGroup,
|
||||||
|
FaSave,
|
||||||
|
FaTrash,
|
||||||
|
FaUpload,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import IAICanvasUndoButton from './IAICanvasUndoButton';
|
||||||
|
import IAICanvasRedoButton from './IAICanvasRedoButton';
|
||||||
|
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
||||||
|
import IAICanvasMaskOptions from './IAICanvasMaskOptions';
|
||||||
|
import { mergeAndUploadCanvas } from 'features/canvas/store/thunks/mergeAndUploadCanvas';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
||||||
|
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||||
|
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
|
||||||
|
import useImageUploader from 'common/hooks/useImageUploader';
|
||||||
|
import {
|
||||||
|
canvasSelector,
|
||||||
|
isStagingSelector,
|
||||||
|
} from 'features/canvas/store/canvasSelectors';
|
||||||
|
import IAISelect from 'common/components/IAISelect';
|
||||||
|
import {
|
||||||
|
CanvasLayer,
|
||||||
|
LAYER_NAMES_DICT,
|
||||||
|
} from 'features/canvas/store/canvasTypes';
|
||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
|
||||||
|
export const selector = createSelector(
|
||||||
|
[systemSelector, canvasSelector, isStagingSelector],
|
||||||
|
(system, canvas, isStaging) => {
|
||||||
|
const { isProcessing } = system;
|
||||||
|
const { tool, shouldCropToBoundingBoxOnSave, layer, isMaskEnabled } =
|
||||||
|
canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isProcessing,
|
||||||
|
isStaging,
|
||||||
|
isMaskEnabled,
|
||||||
|
tool,
|
||||||
|
layer,
|
||||||
|
shouldCropToBoundingBoxOnSave,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const IAICanvasOutpaintingControls = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const {
|
||||||
|
isProcessing,
|
||||||
|
isStaging,
|
||||||
|
isMaskEnabled,
|
||||||
|
layer,
|
||||||
|
tool,
|
||||||
|
shouldCropToBoundingBoxOnSave,
|
||||||
|
} = useAppSelector(selector);
|
||||||
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
|
||||||
|
const { openUploader } = useImageUploader();
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['v'],
|
||||||
|
() => {
|
||||||
|
handleSelectMoveTool();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['r'],
|
||||||
|
() => {
|
||||||
|
handleResetCanvasView();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[canvasBaseLayer]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['shift+m'],
|
||||||
|
() => {
|
||||||
|
handleMergeVisible();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => !isProcessing,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[canvasBaseLayer, isProcessing]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['shift+s'],
|
||||||
|
() => {
|
||||||
|
handleSaveToGallery();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => !isProcessing,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[canvasBaseLayer, isProcessing]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['meta+c', 'ctrl+c'],
|
||||||
|
() => {
|
||||||
|
handleCopyImageToClipboard();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => !isProcessing,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[canvasBaseLayer, isProcessing]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['shift+d'],
|
||||||
|
() => {
|
||||||
|
handleDownloadAsImage();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => !isProcessing,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[canvasBaseLayer, isProcessing]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectMoveTool = () => dispatch(setTool('move'));
|
||||||
|
|
||||||
|
const handleResetCanvasView = () => {
|
||||||
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
if (!canvasBaseLayer) return;
|
||||||
|
const clientRect = canvasBaseLayer.getClientRect({
|
||||||
|
skipTransform: true,
|
||||||
|
});
|
||||||
|
dispatch(
|
||||||
|
resetCanvasView({
|
||||||
|
contentRect: clientRect,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetCanvas = () => {
|
||||||
|
dispatch(resetCanvas());
|
||||||
|
dispatch(resizeAndScaleCanvas());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMergeVisible = () => {
|
||||||
|
dispatch(
|
||||||
|
mergeAndUploadCanvas({
|
||||||
|
cropVisible: false,
|
||||||
|
shouldSetAsInitialImage: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveToGallery = () => {
|
||||||
|
dispatch(
|
||||||
|
mergeAndUploadCanvas({
|
||||||
|
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
||||||
|
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
||||||
|
shouldSaveToGallery: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyImageToClipboard = () => {
|
||||||
|
dispatch(
|
||||||
|
mergeAndUploadCanvas({
|
||||||
|
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
||||||
|
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
||||||
|
shouldCopy: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadAsImage = () => {
|
||||||
|
dispatch(
|
||||||
|
mergeAndUploadCanvas({
|
||||||
|
cropVisible: shouldCropToBoundingBoxOnSave ? false : true,
|
||||||
|
cropToBoundingBox: shouldCropToBoundingBoxOnSave,
|
||||||
|
shouldDownload: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeLayer = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const newLayer = e.target.value as CanvasLayer;
|
||||||
|
dispatch(setLayer(newLayer));
|
||||||
|
if (newLayer === 'mask' && !isMaskEnabled) {
|
||||||
|
dispatch(setIsMaskEnabled(true));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inpainting-settings">
|
||||||
|
<IAISelect
|
||||||
|
tooltip={'Layer (Q)'}
|
||||||
|
tooltipProps={{ hasArrow: true, placement: 'top' }}
|
||||||
|
value={layer}
|
||||||
|
validValues={LAYER_NAMES_DICT}
|
||||||
|
onChange={handleChangeLayer}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IAICanvasMaskOptions />
|
||||||
|
<IAICanvasToolChooserOptions />
|
||||||
|
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Move Tool (V)"
|
||||||
|
tooltip="Move Tool (V)"
|
||||||
|
icon={<FaArrowsAlt />}
|
||||||
|
data-selected={tool === 'move' || isStaging}
|
||||||
|
onClick={handleSelectMoveTool}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Reset View (R)"
|
||||||
|
tooltip="Reset View (R)"
|
||||||
|
icon={<FaCrosshairs />}
|
||||||
|
onClick={handleResetCanvasView}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Merge Visible (Shift+M)"
|
||||||
|
tooltip="Merge Visible (Shift+M)"
|
||||||
|
icon={<FaLayerGroup />}
|
||||||
|
onClick={handleMergeVisible}
|
||||||
|
isDisabled={isProcessing}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Save to Gallery (Shift+S)"
|
||||||
|
tooltip="Save to Gallery (Shift+S)"
|
||||||
|
icon={<FaSave />}
|
||||||
|
onClick={handleSaveToGallery}
|
||||||
|
isDisabled={isProcessing}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Copy to Clipboard (Cmd/Ctrl+C)"
|
||||||
|
tooltip="Copy to Clipboard (Cmd/Ctrl+C)"
|
||||||
|
icon={<FaCopy />}
|
||||||
|
onClick={handleCopyImageToClipboard}
|
||||||
|
isDisabled={isProcessing}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Download as Image (Shift+D)"
|
||||||
|
tooltip="Download as Image (Shift+D)"
|
||||||
|
icon={<FaDownload />}
|
||||||
|
onClick={handleDownloadAsImage}
|
||||||
|
isDisabled={isProcessing}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAICanvasUndoButton />
|
||||||
|
<IAICanvasRedoButton />
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Upload"
|
||||||
|
tooltip="Upload"
|
||||||
|
icon={<FaUpload />}
|
||||||
|
onClick={openUploader}
|
||||||
|
/>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Clear Canvas"
|
||||||
|
tooltip="Clear Canvas"
|
||||||
|
icon={<FaTrash />}
|
||||||
|
onClick={handleResetCanvas}
|
||||||
|
style={{ backgroundColor: 'var(--btn-delete-image)' }}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
<ButtonGroup isAttached>
|
||||||
|
<IAICanvasSettingsButtonPopover />
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IAICanvasOutpaintingControls;
|
@ -0,0 +1,59 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { FaUndo } from 'react-icons/fa';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
|
||||||
|
import { undo } from 'features/canvas/store/canvasSlice';
|
||||||
|
|
||||||
|
const canvasUndoSelector = createSelector(
|
||||||
|
[canvasSelector, activeTabNameSelector],
|
||||||
|
(canvas, activeTabName) => {
|
||||||
|
const { pastLayerStates } = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
canUndo: pastLayerStates.length > 0,
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function IAICanvasUndoButton() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { canUndo, activeTabName } = useAppSelector(canvasUndoSelector);
|
||||||
|
|
||||||
|
const handleUndo = () => {
|
||||||
|
dispatch(undo());
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['meta+z', 'ctrl+z'],
|
||||||
|
() => {
|
||||||
|
handleUndo();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => canUndo,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[activeTabName, canUndo]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Undo (Ctrl+Z)"
|
||||||
|
tooltip="Undo (Ctrl+Z)"
|
||||||
|
icon={<FaUndo />}
|
||||||
|
onClick={handleUndo}
|
||||||
|
isDisabled={!canUndo}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
52
frontend/src/features/canvas/hooks/useCanvasDragMove.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { KonvaEventObject } from 'konva/lib/Node';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { canvasSelector, isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import {
|
||||||
|
setIsMovingStage,
|
||||||
|
setStageCoordinates,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[canvasSelector, isStagingSelector],
|
||||||
|
(canvas, isStaging) => {
|
||||||
|
const { tool } = canvas;
|
||||||
|
return {
|
||||||
|
tool,
|
||||||
|
isStaging,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ memoizeOptions: { resultEqualityCheck: _.isEqual } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const useCanvasDrag = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { tool, isStaging } = useAppSelector(selector);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleDragStart: useCallback(() => {
|
||||||
|
if (!(tool === 'move' || isStaging)) return;
|
||||||
|
dispatch(setIsMovingStage(true));
|
||||||
|
}, [dispatch, isStaging, tool]),
|
||||||
|
|
||||||
|
handleDragMove: useCallback(
|
||||||
|
(e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (!(tool === 'move' || isStaging)) return;
|
||||||
|
|
||||||
|
const newCoordinates = { x: e.target.x(), y: e.target.y() };
|
||||||
|
|
||||||
|
dispatch(setStageCoordinates(newCoordinates));
|
||||||
|
},
|
||||||
|
[dispatch, isStaging, tool]
|
||||||
|
),
|
||||||
|
|
||||||
|
handleDragEnd: useCallback(() => {
|
||||||
|
if (!(tool === 'move' || isStaging)) return;
|
||||||
|
dispatch(setIsMovingStage(false));
|
||||||
|
}, [dispatch, isStaging, tool]),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCanvasDrag;
|
102
frontend/src/features/canvas/hooks/useCanvasHotkeys.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
|
||||||
|
import {
|
||||||
|
resetCanvasInteractionState,
|
||||||
|
setShouldShowBoundingBox,
|
||||||
|
setTool,
|
||||||
|
} from 'features/canvas/store/canvasSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
|
import { CanvasTool } from '../store/canvasTypes';
|
||||||
|
import { getCanvasStage } from '../util/konvaInstanceProvider';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
[canvasSelector, activeTabNameSelector],
|
||||||
|
(canvas, activeTabName) => {
|
||||||
|
const {
|
||||||
|
cursorPosition,
|
||||||
|
shouldLockBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
tool,
|
||||||
|
} = canvas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeTabName,
|
||||||
|
isCursorOnCanvas: Boolean(cursorPosition),
|
||||||
|
shouldLockBoundingBox,
|
||||||
|
shouldShowBoundingBox,
|
||||||
|
tool,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memoizeOptions: {
|
||||||
|
resultEqualityCheck: _.isEqual,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const useInpaintingCanvasHotkeys = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { activeTabName, shouldShowBoundingBox, tool } =
|
||||||
|
useAppSelector(selector);
|
||||||
|
|
||||||
|
const previousToolRef = useRef<CanvasTool | null>(null);
|
||||||
|
|
||||||
|
const canvasStage = getCanvasStage();
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'esc',
|
||||||
|
() => {
|
||||||
|
dispatch(resetCanvasInteractionState());
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: () => true,
|
||||||
|
preventDefault: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'shift+h',
|
||||||
|
() => {
|
||||||
|
dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[activeTabName, shouldShowBoundingBox]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
['space'],
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (e.repeat) return;
|
||||||
|
|
||||||
|
canvasStage?.container().focus();
|
||||||
|
|
||||||
|
if (tool !== 'move') {
|
||||||
|
previousToolRef.current = tool;
|
||||||
|
dispatch(setTool('move'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
tool === 'move' &&
|
||||||
|
previousToolRef.current &&
|
||||||
|
previousToolRef.current !== 'move'
|
||||||
|
) {
|
||||||
|
dispatch(setTool(previousToolRef.current));
|
||||||
|
previousToolRef.current = 'move';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keyup: true,
|
||||||
|
keydown: true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[tool, previousToolRef]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useInpaintingCanvasHotkeys;
|