Merge branch 'development' into backend-can-find-frontend
4
.gitignore
vendored
@ -194,10 +194,6 @@ checkpoints
|
||||
|
||||
# Let the frontend manage its own gitignore
|
||||
!frontend/*
|
||||
frontend/apt-get
|
||||
frontend/dist
|
||||
frontend/sudo
|
||||
frontend/update
|
||||
|
||||
# Scratch folder
|
||||
.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 |
|
||||
| `--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 |
|
||||
| `--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. |
|
||||
|
||||
### 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/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
||||
- git+https://github.com/invoke-ai/GFPGAN#egg=gfpgan
|
||||
- git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
||||
- -e .
|
||||
|
@ -44,4 +44,5 @@ dependencies:
|
||||
- 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/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
||||
- git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
||||
- -e .
|
||||
|
@ -43,4 +43,5 @@ dependencies:
|
||||
- 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/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
||||
- git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
||||
- -e .
|
||||
|
@ -59,6 +59,7 @@ dependencies:
|
||||
- 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/GFPGAN@basicsr-1.4.2#egg=gfpgan
|
||||
- git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
||||
- -e .
|
||||
variables:
|
||||
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/invoke-ai/clipseg.git@relaxed-python-requirement#egg=clipseg
|
||||
- git+https://github.com/invoke-ai/GFPGAN#egg=gfpgan
|
||||
- git+https://github.com/invoke-ai/PyPatchMatch@0.1.1#egg=pypatchmatch
|
||||
- -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/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/PyPatchMatch@0.1.1#egg=pypatchmatch
|
@ -5,7 +5,7 @@
|
||||
- `python scripts/dream.py --web` serves both frontend and backend at
|
||||
http://localhost:9090
|
||||
|
||||
## Environment
|
||||
## Evironment
|
||||
|
||||
Install [node](https://nodejs.org/en/download/) (includes npm) and optionally
|
||||
[yarn](https://yarnpkg.com/getting-started/install).
|
||||
@ -15,7 +15,7 @@ packages.
|
||||
|
||||
## 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`.
|
||||
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" />
|
||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||
<script type="module" crossorigin src="./assets/index.a8ba2a6c.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.40a72c80.css">
|
||||
<script type="module" crossorigin src="./assets/index.2b7cd976.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.f999e69e.css">
|
||||
</head>
|
||||
|
||||
<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",
|
||||
"build": "tsc && vite build",
|
||||
"build-dev": "tsc && vite build -m development",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.10",
|
||||
"@chakra-ui/react": "^2.3.1",
|
||||
"@emotion/cache": "^11.10.5",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@radix-ui/react-context-menu": "^2.0.1",
|
||||
@ -29,14 +31,18 @@
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-hotkeys-hook": "4.0.2",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-konva": "^18.2.3",
|
||||
"react-konva-utils": "^0.3.0",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"redux-deep-persist": "^1.0.6",
|
||||
"redux-persist": "^6.0.0",
|
||||
"socket.io": "^4.5.2",
|
||||
"socket.io-client": "^4.5.2",
|
||||
"use-image": "^1.1.0",
|
||||
"uuid": "^9.0.0",
|
||||
"yarn": "^1.22.19"
|
||||
},
|
||||
@ -51,10 +57,13 @@
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"patch-package": "^6.5.0",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"sass": "^1.55.0",
|
||||
"tsc-watch": "^5.0.3",
|
||||
"typescript": "^4.6.4",
|
||||
"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 *;
|
||||
|
||||
svg {
|
||||
fill: var(--svg-color);
|
||||
}
|
||||
|
||||
.App {
|
||||
display: grid;
|
||||
width: 100vw;
|
||||
|
@ -1,89 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
import ProgressBar from '../features/system/ProgressBar';
|
||||
import SiteHeader from '../features/system/SiteHeader';
|
||||
import Console from '../features/system/Console';
|
||||
import { useAppDispatch } from './store';
|
||||
import { requestSystemConfig } from './socketio/actions';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import SiteHeader from 'features/system/components/SiteHeader';
|
||||
import Console from 'features/system/components/Console';
|
||||
import { keepGUIAlive } from './utils';
|
||||
import InvokeTabs from '../features/tabs/InvokeTabs';
|
||||
import ImageUploader from '../common/components/ImageUploader';
|
||||
import { RootState, useAppSelector } from '../app/store';
|
||||
import InvokeTabs from 'features/tabs/components/InvokeTabs';
|
||||
import ImageUploader from 'common/components/ImageUploader';
|
||||
|
||||
import FloatingGalleryButton from '../features/tabs/FloatingGalleryButton';
|
||||
import FloatingOptionsPanelButtons from '../features/tabs/FloatingOptionsPanelButtons';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { GalleryState } from '../features/gallery/gallerySlice';
|
||||
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';
|
||||
import useToastWatcher from 'features/system/hooks/useToastWatcher';
|
||||
|
||||
import FloatingOptionsPanelButtons from 'features/tabs/components/FloatingOptionsPanelButtons';
|
||||
import FloatingGalleryButton from 'features/tabs/components/FloatingGalleryButton';
|
||||
|
||||
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 dispatch = useAppDispatch();
|
||||
|
||||
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
|
||||
useAppSelector(appSelector);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(requestSystemConfig());
|
||||
}, [dispatch]);
|
||||
useToastWatcher();
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
@ -96,9 +26,9 @@ const App = () => {
|
||||
<div className="app-console">
|
||||
<Console />
|
||||
</div>
|
||||
{shouldShowGalleryButton && <FloatingGalleryButton />}
|
||||
{shouldShowOptionsPanelButton && <FloatingOptionsPanelButtons />}
|
||||
</ImageUploader>
|
||||
<FloatingOptionsPanelButtons />
|
||||
<FloatingGalleryButton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
// TODO: use Enums?
|
||||
|
||||
import { InProgressImageType } from '../features/system/systemSlice';
|
||||
import { InProgressImageType } from 'features/system/store/systemSlice';
|
||||
|
||||
// Valid samplers
|
||||
export const SAMPLERS: Array<string> = [
|
||||
|
@ -13,10 +13,13 @@ export enum Feature {
|
||||
UPSCALE,
|
||||
FACE_CORRECTION,
|
||||
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.
|
||||
*
|
||||
* 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> = {
|
||||
[Feature.PROMPT]: {
|
||||
@ -55,7 +58,22 @@ export const FEATURES: Record<Feature, FeatureHelpInfo> = {
|
||||
guideImage: 'asset/path.gif',
|
||||
},
|
||||
[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',
|
||||
guideImage: 'asset/path.gif',
|
||||
},
|
||||
|
41
frontend/src/app/invokeai.d.ts
vendored
@ -12,7 +12,9 @@
|
||||
* '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:
|
||||
@ -103,7 +105,7 @@ export declare type PostProcessedImageMetadata =
|
||||
| FacetoolMetadata;
|
||||
|
||||
// Metadata includes the system config and image metadata.
|
||||
export declare type Metadata = SystemConfig & {
|
||||
export declare type Metadata = SystemGenerationMetadata & {
|
||||
image: GeneratedImageMetadata | PostProcessedImageMetadata;
|
||||
};
|
||||
|
||||
@ -111,12 +113,14 @@ export declare type Metadata = SystemConfig & {
|
||||
export declare type Image = {
|
||||
uuid: string;
|
||||
url: string;
|
||||
thumbnail: string;
|
||||
mtime: number;
|
||||
metadata?: Metadata;
|
||||
width: number;
|
||||
height: number;
|
||||
category: GalleryCategory;
|
||||
isBase64: boolean;
|
||||
isBase64?: boolean;
|
||||
dreamPrompt?: 'string';
|
||||
};
|
||||
|
||||
// GalleryImages is an array of Image.
|
||||
@ -140,13 +144,18 @@ export declare type SystemStatus = {
|
||||
hasError: boolean;
|
||||
};
|
||||
|
||||
export declare type SystemConfig = {
|
||||
export declare type SystemGenerationMetadata = {
|
||||
model: string;
|
||||
model_id: string;
|
||||
model_weights?: string;
|
||||
model_id?: string;
|
||||
model_hash: string;
|
||||
app_id: string;
|
||||
app_version: string;
|
||||
};
|
||||
|
||||
export declare type SystemConfig = SystemGenerationMetadata & {
|
||||
model_list: ModelList;
|
||||
infill_methods: string[];
|
||||
};
|
||||
|
||||
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 ImageResultResponse = Omit<Image, 'uuid'>;
|
||||
export declare type ImageResultResponse = Omit<Image, 'uuid'> & {
|
||||
boundingBox?: IRect;
|
||||
generationMode: InvokeTabName;
|
||||
};
|
||||
|
||||
export declare type ImageUploadResponse = Omit<Image, 'uuid' | 'metadata'> & {
|
||||
destination: 'img2img' | 'inpainting';
|
||||
export declare type ImageUploadResponse = {
|
||||
// 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 = {
|
||||
@ -198,9 +216,12 @@ export declare type ImageUrlResponse = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export declare type ImageUploadDestination = 'img2img' | 'inpainting';
|
||||
|
||||
export declare type UploadImagePayload = {
|
||||
file: File;
|
||||
destination?: ImageUploadDestination;
|
||||
};
|
||||
|
||||
export declare type UploadOutpaintingMergeImagePayload = {
|
||||
dataURL: string;
|
||||
name: string;
|
||||
};
|
||||
|
@ -1,39 +1,35 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { RootState } from '../store';
|
||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import { validateSeedWeights } from '../../common/util/seedWeightPairs';
|
||||
import { RootState } from 'app/store';
|
||||
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
|
||||
import { OptionsState } from 'features/options/store/optionsSlice';
|
||||
import { SystemState } from 'features/system/store/systemSlice';
|
||||
import { validateSeedWeights } from 'common/util/seedWeightPairs';
|
||||
import { initialCanvasImageSelector } from 'features/canvas/store/canvasSelectors';
|
||||
|
||||
export const readinessSelector = createSelector(
|
||||
[
|
||||
(state: RootState) => state.options,
|
||||
(state: RootState) => state.system,
|
||||
(state: RootState) => state.inpainting,
|
||||
initialCanvasImageSelector,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(
|
||||
options: OptionsState,
|
||||
system: SystemState,
|
||||
inpainting: InpaintingState,
|
||||
initialCanvasImage,
|
||||
activeTabName
|
||||
) => {
|
||||
const {
|
||||
prompt,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
// maskPath,
|
||||
initialImage,
|
||||
seed,
|
||||
} = options;
|
||||
|
||||
const { isProcessing, isConnected } = system;
|
||||
|
||||
const { imageToInpaint } = inpainting;
|
||||
|
||||
let isReady = true;
|
||||
const reasonsWhyNotReady: string[] = [];
|
||||
|
||||
@ -48,20 +44,6 @@ export const readinessSelector = createSelector(
|
||||
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
|
||||
// Cannot generate if already processing an image
|
||||
if (isProcessing) {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { GalleryCategory } from '../../features/gallery/gallerySlice';
|
||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||
import * as InvokeAI from '../invokeai';
|
||||
|
||||
import { GalleryCategory } from 'features/gallery/store/gallerySlice';
|
||||
import { InvokeTabName } from 'features/tabs/components/InvokeTabs';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
|
||||
/**
|
||||
* 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>(
|
||||
'socketio/cancelProcessing'
|
||||
);
|
||||
export const uploadImage = createAction<InvokeAI.UploadImagePayload>('socketio/uploadImage');
|
||||
export const uploadMaskImage = createAction<File>('socketio/uploadMaskImage');
|
||||
|
||||
export const requestSystemConfig = createAction<undefined>(
|
||||
'socketio/requestSystemConfig'
|
||||
@ -36,3 +33,11 @@ export const requestSystemConfig = createAction<undefined>(
|
||||
export const requestModelChange = createAction<string>(
|
||||
'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 {
|
||||
frontendToBackendParameters,
|
||||
FrontendToBackendParametersConfig,
|
||||
} from '../../common/util/parameterTranslation';
|
||||
} from 'common/util/parameterTranslation';
|
||||
import {
|
||||
GalleryCategory,
|
||||
GalleryState,
|
||||
removeImage,
|
||||
} from '../../features/gallery/gallerySlice';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { OptionsState } from 'features/options/store/optionsSlice';
|
||||
import {
|
||||
addLogEntry,
|
||||
errorOccurred,
|
||||
generationRequested,
|
||||
modelChangeRequested,
|
||||
setIsProcessing,
|
||||
} from '../../features/system/systemSlice';
|
||||
import { inpaintingImageElementRef } from '../../features/tabs/Inpainting/InpaintingCanvas';
|
||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||
import * as InvokeAI from '../invokeai';
|
||||
import { RootState } from '../store';
|
||||
} from 'features/system/store/systemSlice';
|
||||
import { InvokeTabName } from 'features/tabs/components/InvokeTabs';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
import { RootState } from 'app/store';
|
||||
|
||||
/**
|
||||
* Returns an object containing all functions which use `socketio.emit()`.
|
||||
@ -42,7 +41,7 @@ const makeSocketIOEmitters = (
|
||||
const {
|
||||
options: optionsState,
|
||||
system: systemState,
|
||||
inpainting: inpaintingState,
|
||||
canvas: canvasState,
|
||||
gallery: galleryState,
|
||||
} = state;
|
||||
|
||||
@ -50,32 +49,13 @@ const makeSocketIOEmitters = (
|
||||
{
|
||||
generationMode,
|
||||
optionsState,
|
||||
inpaintingState,
|
||||
canvasState,
|
||||
systemState,
|
||||
};
|
||||
|
||||
if (generationMode === 'inpainting') {
|
||||
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;
|
||||
}
|
||||
dispatch(generationRequested());
|
||||
|
||||
frontendToBackendParametersConfig.imageToProcessUrl =
|
||||
inpaintingState.imageToInpaint.url;
|
||||
|
||||
frontendToBackendParametersConfig.maskImageElement =
|
||||
inpaintingImageElementRef.current;
|
||||
} else if (!['txt2img', 'img2img'].includes(generationMode)) {
|
||||
if (!['txt2img', 'img2img'].includes(generationMode)) {
|
||||
if (!galleryState.currentImage?.url) return;
|
||||
|
||||
frontendToBackendParametersConfig.imageToProcessUrl =
|
||||
@ -96,7 +76,12 @@ const makeSocketIOEmitters = (
|
||||
// TODO: handle maintaining masks for reproducibility in future
|
||||
if (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('...');
|
||||
}
|
||||
|
||||
@ -162,9 +147,9 @@ const makeSocketIOEmitters = (
|
||||
);
|
||||
},
|
||||
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
||||
const { url, uuid, category } = imageToDelete;
|
||||
const { url, uuid, category, thumbnail } = imageToDelete;
|
||||
dispatch(removeImage(imageToDelete));
|
||||
socketio.emit('deleteImage', url, uuid, category);
|
||||
socketio.emit('deleteImage', url, thumbnail, uuid, category);
|
||||
},
|
||||
emitRequestImages: (category: GalleryCategory) => {
|
||||
const gallery: GalleryState = getState().gallery;
|
||||
@ -179,13 +164,6 @@ const makeSocketIOEmitters = (
|
||||
emitCancelProcessing: () => {
|
||||
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: () => {
|
||||
socketio.emit('requestSystemConfig');
|
||||
},
|
||||
@ -193,6 +171,12 @@ const makeSocketIOEmitters = (
|
||||
dispatch(modelChangeRequested());
|
||||
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 dateFormat from 'dateformat';
|
||||
|
||||
import * as InvokeAI from '../invokeai';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
|
||||
import {
|
||||
addLogEntry,
|
||||
@ -15,7 +15,8 @@ import {
|
||||
errorOccurred,
|
||||
setModelList,
|
||||
setIsCancelable,
|
||||
} from '../../features/system/systemSlice';
|
||||
addToast,
|
||||
} from 'features/system/store/systemSlice';
|
||||
|
||||
import {
|
||||
addGalleryImages,
|
||||
@ -23,21 +24,22 @@ import {
|
||||
clearIntermediateImage,
|
||||
GalleryState,
|
||||
removeImage,
|
||||
setCurrentImage,
|
||||
setIntermediateImage,
|
||||
} from '../../features/gallery/gallerySlice';
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
|
||||
import {
|
||||
clearInitialImage,
|
||||
setInfillMethod,
|
||||
setInitialImage,
|
||||
setMaskPath,
|
||||
} from '../../features/options/optionsSlice';
|
||||
import { requestImages, requestNewImages } from './actions';
|
||||
} from 'features/options/store/optionsSlice';
|
||||
import {
|
||||
clearImageToInpaint,
|
||||
setImageToInpaint,
|
||||
} from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import { tabMap } from '../../features/tabs/InvokeTabs';
|
||||
requestImages,
|
||||
requestNewImages,
|
||||
requestSystemConfig,
|
||||
} 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.
|
||||
@ -56,6 +58,7 @@ const makeSocketIOListeners = (
|
||||
try {
|
||||
dispatch(setIsConnected(true));
|
||||
dispatch(setCurrentStatus('Connected'));
|
||||
dispatch(requestSystemConfig());
|
||||
const gallery: GalleryState = getState().gallery;
|
||||
|
||||
if (gallery.categories.user.latest_mtime) {
|
||||
@ -97,19 +100,42 @@ const makeSocketIOListeners = (
|
||||
*/
|
||||
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
||||
try {
|
||||
const { shouldLoopback, activeTab } = getState().options;
|
||||
const state = getState();
|
||||
const { shouldLoopback, activeTab } = state.options;
|
||||
const { boundingBox: _, generationMode, ...rest } = data;
|
||||
|
||||
const newImage = {
|
||||
uuid: uuidv4(),
|
||||
...data,
|
||||
category: 'result',
|
||||
...rest,
|
||||
};
|
||||
|
||||
if (['txt2img', 'img2img'].includes(generationMode)) {
|
||||
dispatch(
|
||||
addImage({
|
||||
category: 'result',
|
||||
image: newImage,
|
||||
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) {
|
||||
const activeTabName = tabMap[activeTab];
|
||||
@ -118,13 +144,11 @@ const makeSocketIOListeners = (
|
||||
dispatch(setInitialImage(newImage));
|
||||
break;
|
||||
}
|
||||
case 'inpainting': {
|
||||
dispatch(setImageToInpaint(newImage));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(clearIntermediateImage());
|
||||
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
@ -144,6 +168,7 @@ const makeSocketIOListeners = (
|
||||
setIntermediateImage({
|
||||
uuid: uuidv4(),
|
||||
...data,
|
||||
category: 'result',
|
||||
})
|
||||
);
|
||||
if (!data.isBase64) {
|
||||
@ -299,16 +324,11 @@ const makeSocketIOListeners = (
|
||||
|
||||
// remove references to image in options
|
||||
const { initialImage, maskPath } = getState().options;
|
||||
const { imageToInpaint } = getState().inpainting;
|
||||
|
||||
if (initialImage?.url === url || initialImage === url) {
|
||||
dispatch(clearInitialImage());
|
||||
}
|
||||
|
||||
if (imageToInpaint?.url === url) {
|
||||
dispatch(clearImageToInpaint());
|
||||
}
|
||||
|
||||
if (maskPath === url) {
|
||||
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) => {
|
||||
dispatch(setSystemConfig(data));
|
||||
if (!data.infill_methods.includes('patchmatch')) {
|
||||
dispatch(setInfillMethod(data.infill_methods[0]));
|
||||
}
|
||||
},
|
||||
onModelChanged: (data: InvokeAI.ModelChangeResponse) => {
|
||||
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 makeSocketIOEmitters from './emitters';
|
||||
|
||||
import * as InvokeAI from '../invokeai';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
|
||||
/**
|
||||
* Creates a socketio middleware to handle communication with server.
|
||||
@ -43,11 +43,10 @@ export const socketioMiddleware = () => {
|
||||
onGalleryImages,
|
||||
onProcessingCanceled,
|
||||
onImageDeleted,
|
||||
onImageUploaded,
|
||||
onMaskImageUploaded,
|
||||
onSystemConfig,
|
||||
onModelChanged,
|
||||
onModelChangeFailed,
|
||||
onTempFolderEmptied,
|
||||
} = makeSocketIOListeners(store);
|
||||
|
||||
const {
|
||||
@ -58,10 +57,10 @@ export const socketioMiddleware = () => {
|
||||
emitRequestImages,
|
||||
emitRequestNewImages,
|
||||
emitCancelProcessing,
|
||||
emitUploadImage,
|
||||
emitUploadMaskImage,
|
||||
emitRequestSystemConfig,
|
||||
emitRequestModelChange,
|
||||
emitSaveStagingAreaImageToGallery,
|
||||
emitRequestEmptyTempFolder,
|
||||
} = makeSocketIOEmitters(store, socketio);
|
||||
|
||||
/**
|
||||
@ -104,17 +103,6 @@ export const socketioMiddleware = () => {
|
||||
onImageDeleted(data);
|
||||
});
|
||||
|
||||
socketio.on(
|
||||
'imageUploaded',
|
||||
(data: InvokeAI.ImageUploadResponse) => {
|
||||
onImageUploaded(data);
|
||||
}
|
||||
);
|
||||
|
||||
socketio.on('maskImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
||||
onMaskImageUploaded(data);
|
||||
});
|
||||
|
||||
socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => {
|
||||
onSystemConfig(data);
|
||||
});
|
||||
@ -127,6 +115,10 @@ export const socketioMiddleware = () => {
|
||||
onModelChangeFailed(data);
|
||||
});
|
||||
|
||||
socketio.on('tempFolderEmptied', () => {
|
||||
onTempFolderEmptied();
|
||||
});
|
||||
|
||||
areListenersSet = true;
|
||||
}
|
||||
|
||||
@ -169,16 +161,6 @@ export const socketioMiddleware = () => {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/uploadImage': {
|
||||
emitUploadImage(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/uploadMaskImage': {
|
||||
emitUploadMaskImage(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/requestSystemConfig': {
|
||||
emitRequestSystemConfig();
|
||||
break;
|
||||
@ -188,6 +170,16 @@ export const socketioMiddleware = () => {
|
||||
emitRequestModelChange(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/saveStagingAreaImageToGallery': {
|
||||
emitSaveStagingAreaImageToGallery(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/requestEmptyTempFolder': {
|
||||
emitRequestEmptyTempFolder();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
next(action);
|
||||
|
@ -5,16 +5,14 @@ import type { TypedUseSelectorHook } from 'react-redux';
|
||||
import { persistReducer } from 'redux-persist';
|
||||
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
|
||||
|
||||
import optionsReducer, { OptionsState } from '../features/options/optionsSlice';
|
||||
import galleryReducer, { GalleryState } from '../features/gallery/gallerySlice';
|
||||
import inpaintingReducer, {
|
||||
InpaintingState,
|
||||
} from '../features/tabs/Inpainting/inpaintingSlice';
|
||||
import { getPersistConfig } from 'redux-deep-persist';
|
||||
|
||||
import optionsReducer from 'features/options/store/optionsSlice';
|
||||
import galleryReducer from 'features/gallery/store/gallerySlice';
|
||||
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 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.
|
||||
@ -28,87 +26,79 @@ import { PersistPartial } from 'redux-persist/es/persistReducer';
|
||||
* These can be blacklisted in redux-persist.
|
||||
*
|
||||
* 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 = {
|
||||
key: 'root',
|
||||
storage,
|
||||
stateReconciler: autoMergeLevel2,
|
||||
blacklist: ['gallery', 'system', 'inpainting'],
|
||||
};
|
||||
const canvasBlacklist = [
|
||||
'cursorPosition',
|
||||
'isCanvasInitialized',
|
||||
'doesCanvasNeedScaling',
|
||||
].map((blacklistItem) => `canvas.${blacklistItem}`);
|
||||
|
||||
const systemPersistConfig = {
|
||||
key: 'system',
|
||||
storage,
|
||||
stateReconciler: autoMergeLevel2,
|
||||
blacklist: [
|
||||
const systemBlacklist = [
|
||||
'currentIteration',
|
||||
'currentStatus',
|
||||
'currentStep',
|
||||
'isCancelable',
|
||||
'isConnected',
|
||||
'isProcessing',
|
||||
'currentStep',
|
||||
'socketId',
|
||||
'isESRGANAvailable',
|
||||
'isGFPGANAvailable',
|
||||
'currentStep',
|
||||
'totalSteps',
|
||||
'currentIteration',
|
||||
'isProcessing',
|
||||
'socketId',
|
||||
'totalIterations',
|
||||
'currentStatus',
|
||||
],
|
||||
};
|
||||
'totalSteps',
|
||||
].map((blacklistItem) => `system.${blacklistItem}`);
|
||||
|
||||
const galleryPersistConfig = {
|
||||
key: 'gallery',
|
||||
storage,
|
||||
stateReconciler: autoMergeLevel2,
|
||||
whitelist: [
|
||||
'galleryWidth',
|
||||
'shouldPinGallery',
|
||||
'shouldShowGallery',
|
||||
'galleryScrollPosition',
|
||||
'galleryImageMinimumWidth',
|
||||
'galleryImageObjectFit',
|
||||
],
|
||||
};
|
||||
const galleryBlacklist = [
|
||||
'categories',
|
||||
'currentCategory',
|
||||
'currentImage',
|
||||
'currentImageUuid',
|
||||
'shouldAutoSwitchToNewImages',
|
||||
'shouldHoldGalleryOpen',
|
||||
'intermediateImage',
|
||||
].map((blacklistItem) => `gallery.${blacklistItem}`);
|
||||
|
||||
const inpaintingPersistConfig = {
|
||||
key: 'inpainting',
|
||||
storage,
|
||||
stateReconciler: autoMergeLevel2,
|
||||
blacklist: ['pastLines', 'futuresLines', 'cursorPosition'],
|
||||
};
|
||||
|
||||
const reducers = combineReducers({
|
||||
const rootReducer = combineReducers({
|
||||
options: optionsReducer,
|
||||
gallery: persistReducer<GalleryState>(galleryPersistConfig, galleryReducer),
|
||||
system: persistReducer<SystemState>(systemPersistConfig, systemReducer),
|
||||
inpainting: persistReducer<InpaintingState>(
|
||||
inpaintingPersistConfig,
|
||||
inpaintingReducer
|
||||
),
|
||||
gallery: galleryReducer,
|
||||
system: systemReducer,
|
||||
canvas: canvasReducer,
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer<{
|
||||
options: OptionsState;
|
||||
gallery: GalleryState & PersistPartial;
|
||||
system: SystemState & PersistPartial;
|
||||
inpainting: InpaintingState & PersistPartial;
|
||||
}>(rootPersistConfig, reducers);
|
||||
const rootPersistConfig = getPersistConfig({
|
||||
key: 'root',
|
||||
storage,
|
||||
rootReducer,
|
||||
blacklist: [...canvasBlacklist, ...systemBlacklist, ...galleryBlacklist],
|
||||
debounce: 300,
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
|
||||
|
||||
// Continue with store setup
|
||||
export const store = configureStore({
|
||||
reducer: persistedReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
// redux-persist sometimes needs to temporarily put a function in redux state, need to disable this check
|
||||
immutableCheck: false,
|
||||
serializableCheck: false,
|
||||
}).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 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 { IconType } from 'react-icons';
|
||||
import { MdHelp } from 'react-icons/md';
|
||||
import { Feature } from '../../app/features';
|
||||
import { Feature } from 'app/features';
|
||||
import GuidePopover from './GuidePopover';
|
||||
|
||||
type GuideIconProps = {
|
||||
@ -13,7 +13,7 @@ const GuideIcon = forwardRef(
|
||||
({ feature, icon = MdHelp }: GuideIconProps, ref) => (
|
||||
<GuidePopover feature={feature}>
|
||||
<Box ref={ref}>
|
||||
<Icon as={icon} />
|
||||
<Icon marginBottom={'-.15rem'} as={icon} />
|
||||
</Box>
|
||||
</GuidePopover>
|
||||
)
|
||||
|
@ -1,11 +1,11 @@
|
||||
.guide-popover-arrow {
|
||||
background-color: var(--tab-panel-bg) !important;
|
||||
box-shadow: none !important;
|
||||
background-color: var(--tab-panel-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.guide-popover-content {
|
||||
background-color: var(--background-color-secondary) !important;
|
||||
border: none !important;
|
||||
background-color: var(--background-color-secondary);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.guide-popover-guide-content {
|
||||
|
@ -5,12 +5,12 @@ import {
|
||||
PopoverTrigger,
|
||||
Box,
|
||||
} from '@chakra-ui/react';
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { SystemState } from 'features/system/store/systemSlice';
|
||||
import { useAppSelector } from 'app/store';
|
||||
import { RootState } from 'app/store';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { ReactElement } from 'react';
|
||||
import { Feature, FEATURES } from '../../app/features';
|
||||
import { Feature, FEATURES } from 'app/features';
|
||||
|
||||
type GuideProps = {
|
||||
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 {
|
||||
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 {
|
||||
width: 0.6rem;
|
||||
height: 0.6rem;
|
||||
stroke-width: 3px !important;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
&[data-checked] {
|
||||
|
@ -1,11 +1,11 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.invokeai__icon-button {
|
||||
background-color: var(--btn-grey);
|
||||
background: var(--btn-base-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-grey-hover);
|
||||
background-color: var(--btn-base-color-hover);
|
||||
}
|
||||
|
||||
&[data-selected='true'] {
|
||||
@ -20,16 +20,39 @@
|
||||
}
|
||||
|
||||
&[data-variant='link'] {
|
||||
background: none !important;
|
||||
background: none;
|
||||
&:hover {
|
||||
background: none !important;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Check Box Style
|
||||
&[data-as-checkbox='true'] {
|
||||
background-color: var(--btn-base-color);
|
||||
border: 3px solid var(--btn-base-color);
|
||||
|
||||
svg {
|
||||
fill: var(--text-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 {
|
||||
border-color: var(--accent-color-hover);
|
||||
svg {
|
||||
fill: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,28 +61,12 @@
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
&:hover {
|
||||
animation: none;
|
||||
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 {
|
||||
|
@ -25,13 +25,23 @@ const IAIIconButton = forwardRef((props: IAIIconButtonProps, forwardedRef) => {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltip} hasArrow {...tooltipProps}>
|
||||
<Tooltip
|
||||
label={tooltip}
|
||||
hasArrow
|
||||
{...tooltipProps}
|
||||
{...(tooltipProps?.placement
|
||||
? { placement: tooltipProps.placement }
|
||||
: { placement: 'top' })}
|
||||
>
|
||||
<IconButton
|
||||
ref={forwardedRef}
|
||||
className={`invokeai__icon-button ${styleClass}`}
|
||||
className={
|
||||
styleClass
|
||||
? `invokeai__icon-button ${styleClass}`
|
||||
: `invokeai__icon-button`
|
||||
}
|
||||
data-as-checkbox={asCheckbox}
|
||||
data-selected={isChecked !== undefined ? isChecked : undefined}
|
||||
style={props.onClick ? { cursor: 'pointer' } : {}}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
@ -1,16 +1,14 @@
|
||||
.invokeai__number-input-form-control {
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 1rem;
|
||||
|
||||
.invokeai__number-input-form-label {
|
||||
color: var(--text-color-secondary);
|
||||
margin-right: 0;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
flex-grow: 2;
|
||||
white-space: nowrap;
|
||||
padding-right: 1rem;
|
||||
|
||||
&[data-focus] + .invokeai__number-input-root {
|
||||
outline: none;
|
||||
@ -33,7 +31,7 @@
|
||||
align-items: center;
|
||||
background-color: var(--background-color-secondary);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.2rem;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.invokeai__number-input-field {
|
||||
@ -41,10 +39,8 @@
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
font-size: 0.9rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
@ -21,6 +21,7 @@ const numberStringRegex = /^-?(0\.)?\.?$/;
|
||||
interface Props extends Omit<NumberInputProps, 'onChange'> {
|
||||
styleClass?: string;
|
||||
label?: string;
|
||||
labelFontSize?: string | number;
|
||||
width?: string | number;
|
||||
showStepper?: boolean;
|
||||
value: number;
|
||||
@ -43,6 +44,7 @@ interface Props extends Omit<NumberInputProps, 'onChange'> {
|
||||
const IAINumberInput = (props: Props) => {
|
||||
const {
|
||||
label,
|
||||
labelFontSize = '1rem',
|
||||
styleClass,
|
||||
isDisabled = false,
|
||||
showStepper = true,
|
||||
@ -127,6 +129,7 @@ const IAINumberInput = (props: Props) => {
|
||||
<FormLabel
|
||||
className="invokeai__number-input-form-label"
|
||||
style={{ display: label ? 'block' : 'none' }}
|
||||
fontSize={labelFontSize}
|
||||
{...formLabelProps}
|
||||
>
|
||||
{label}
|
||||
|
@ -1,10 +1,10 @@
|
||||
.invokeai__popover-content {
|
||||
min-width: unset;
|
||||
width: unset !important;
|
||||
width: unset;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem !important;
|
||||
background-color: var(--background-color) !important;
|
||||
border: 2px solid var(--border-color) !important;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--background-color);
|
||||
border: 2px solid var(--border-color);
|
||||
|
||||
.invokeai__popover-arrow {
|
||||
background-color: var(--background-color) !important;
|
||||
|
@ -29,7 +29,7 @@ const IAIPopover = (props: IAIPopoverProps) => {
|
||||
<Popover {...rest}>
|
||||
<PopoverTrigger>{triggerComponent}</PopoverTrigger>
|
||||
<PopoverContent className={`invokeai__popover-content ${styleClass}`}>
|
||||
{hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />}
|
||||
{hasArrow && <PopoverArrow className="invokeai__popover-arrow" />}
|
||||
{children}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
@ -4,7 +4,6 @@
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
|
||||
.invokeai__select-label {
|
||||
color: var(--text-color-secondary);
|
||||
@ -15,6 +14,7 @@
|
||||
border: 2px solid var(--border-color);
|
||||
background-color: var(--background-color-secondary);
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.2rem;
|
||||
|
||||
@ -27,5 +27,6 @@
|
||||
|
||||
.invokeai__select-option {
|
||||
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';
|
||||
|
||||
type IAISelectProps = SelectProps & {
|
||||
label: string;
|
||||
label?: string;
|
||||
styleClass?: string;
|
||||
tooltip?: string;
|
||||
tooltipProps?: Omit<TooltipProps, 'children'>;
|
||||
validValues:
|
||||
| Array<number | string>
|
||||
| Array<{ key: string; value: string | number }>;
|
||||
@ -16,6 +25,8 @@ const IAISelect = (props: IAISelectProps) => {
|
||||
label,
|
||||
isDisabled,
|
||||
validValues,
|
||||
tooltip,
|
||||
tooltipProps,
|
||||
size = 'sm',
|
||||
fontSize = 'md',
|
||||
styleClass,
|
||||
@ -32,6 +43,7 @@ const IAISelect = (props: IAISelectProps) => {
|
||||
e.nativeEvent.cancelBubble = true;
|
||||
}}
|
||||
>
|
||||
{label && (
|
||||
<FormLabel
|
||||
className="invokeai__select-label"
|
||||
fontSize={fontSize}
|
||||
@ -41,6 +53,8 @@ const IAISelect = (props: IAISelectProps) => {
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
<Tooltip label={tooltip} {...tooltipProps}>
|
||||
<Select
|
||||
className="invokeai__select-picker"
|
||||
fontSize={fontSize}
|
||||
@ -63,6 +77,7 @@ const IAISelect = (props: IAISelectProps) => {
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
@ -1,40 +1,62 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.invokeai__slider-form-control {
|
||||
.invokeai__slider-component {
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
padding-right: 0.25rem;
|
||||
|
||||
.invokeai__slider-inner-container {
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
.invokeai__slider-form-label {
|
||||
color: var(--text-color-secondary);
|
||||
.invokeai__slider-component-label {
|
||||
min-width: max-content;
|
||||
margin: 0;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.1rem;
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.invokeai__slider-root {
|
||||
.invokeai__slider-filled-track {
|
||||
background-color: var(--accent-color-hover);
|
||||
.invokeai__slider_track {
|
||||
background-color: var(--tab-color);
|
||||
}
|
||||
|
||||
.invokeai__slider-track {
|
||||
background-color: var(--text-color-secondary);
|
||||
height: 5px;
|
||||
border-radius: 9999px;
|
||||
.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);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
.invokeai__slider-thumb-tooltip {
|
||||
.invokeai__slider-number-stepper {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&[data-markers='true'] {
|
||||
.invokeai__slider_container {
|
||||
margin-top: -1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,87 +1,246 @@
|
||||
import {
|
||||
Slider,
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Tooltip,
|
||||
SliderProps,
|
||||
FormControlProps,
|
||||
FormLabel,
|
||||
FormLabelProps,
|
||||
SliderTrackProps,
|
||||
HStack,
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputFieldProps,
|
||||
NumberInputProps,
|
||||
NumberInputStepper,
|
||||
NumberInputStepperProps,
|
||||
Slider,
|
||||
SliderFilledTrack,
|
||||
SliderMark,
|
||||
SliderMarkProps,
|
||||
SliderThumb,
|
||||
SliderThumbProps,
|
||||
SliderTrack,
|
||||
SliderTrackProps,
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
SliderInnerTrackProps,
|
||||
} 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 & {
|
||||
label?: string;
|
||||
export type IAIFullSliderProps = {
|
||||
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;
|
||||
formControlProps?: FormControlProps;
|
||||
formLabelProps?: FormLabelProps;
|
||||
sliderFormControlProps?: FormControlProps;
|
||||
sliderFormLabelProps?: FormLabelProps;
|
||||
sliderMarkProps?: Omit<SliderMarkProps, 'value'>;
|
||||
sliderTrackProps?: SliderTrackProps;
|
||||
sliderInnerTrackProps?: SliderInnerTrackProps;
|
||||
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 {
|
||||
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,
|
||||
formControlProps,
|
||||
formLabelProps,
|
||||
sliderFormControlProps,
|
||||
sliderFormLabelProps,
|
||||
sliderMarkProps,
|
||||
sliderTrackProps,
|
||||
sliderInnerTrackProps,
|
||||
sliderThumbProps,
|
||||
sliderThumbTooltipProps,
|
||||
sliderNumberInputProps,
|
||||
sliderNumberInputFieldProps,
|
||||
sliderNumberInputStepperProps,
|
||||
sliderTooltipProps,
|
||||
sliderIAIIconButtonProps,
|
||||
...rest
|
||||
} = 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 (
|
||||
<FormControl
|
||||
className={`invokeai__slider-form-control ${styleClass}`}
|
||||
{...formControlProps}
|
||||
className={
|
||||
styleClass
|
||||
? `invokeai__slider-component ${styleClass}`
|
||||
: `invokeai__slider-component`
|
||||
}
|
||||
data-markers={withSliderMarks}
|
||||
{...sliderFormControlProps}
|
||||
>
|
||||
<div className="invokeai__slider-inner-container">
|
||||
<FormLabel
|
||||
className={`invokeai__slider-form-label`}
|
||||
whiteSpace="nowrap"
|
||||
{...formLabelProps}
|
||||
className="invokeai__slider-component-label"
|
||||
{...sliderFormLabelProps}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
|
||||
<HStack w={'100%'} gap={2}>
|
||||
<Slider
|
||||
className={`invokeai__slider-root`}
|
||||
aria-label={label}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onChange={handleInputChange}
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
focusThumbOnChange={false}
|
||||
isDisabled={isSliderDisabled}
|
||||
{...rest}
|
||||
>
|
||||
<SliderTrack
|
||||
className={`invokeai__slider-track`}
|
||||
{...sliderTrackProps}
|
||||
{withSliderMarks && (
|
||||
<>
|
||||
<SliderMark
|
||||
value={min}
|
||||
className="invokeai__slider-mark invokeai__slider-mark-start"
|
||||
ml={sliderMarkLeftOffset}
|
||||
{...sliderMarkProps}
|
||||
>
|
||||
<SliderFilledTrack
|
||||
className={`invokeai__slider-filled-track`}
|
||||
{...sliderInnerTrackProps}
|
||||
/>
|
||||
{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>
|
||||
|
||||
<Tooltip
|
||||
className={`invokeai__slider-thumb-tooltip`}
|
||||
placement="top"
|
||||
hasArrow
|
||||
{...sliderThumbTooltipProps}
|
||||
className="invokeai__slider-component-tooltip"
|
||||
placement="top"
|
||||
isOpen={showTooltip}
|
||||
label={`${value}${tooltipSuffix}`}
|
||||
hidden={hideTooltip}
|
||||
{...sliderTooltipProps}
|
||||
>
|
||||
<SliderThumb
|
||||
className={`invokeai__slider-thumb`}
|
||||
className="invokeai__slider-thumb"
|
||||
{...sliderThumbProps}
|
||||
/>
|
||||
</Tooltip>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default IAISlider;
|
||||
}
|
||||
|
@ -33,7 +33,6 @@
|
||||
}
|
||||
|
||||
.image-uploader-button-outer {
|
||||
min-width: 20rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@ -42,10 +41,10 @@
|
||||
cursor: pointer;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--tab-list-text-inactive);
|
||||
background-color: var(--btn-grey);
|
||||
background-color: var(--background-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-grey-hover);
|
||||
background-color: var(--background-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,10 +65,10 @@
|
||||
text-align: center;
|
||||
|
||||
svg {
|
||||
width: 4rem !important;
|
||||
height: 4rem !important;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.2rem !important;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { useCallback, ReactNode, useState, useEffect } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import {
|
||||
useCallback,
|
||||
ReactNode,
|
||||
useState,
|
||||
useEffect,
|
||||
KeyboardEvent,
|
||||
} from 'react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { uploadImage } from '../../app/socketio/actions';
|
||||
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
||||
import { tabDict } from '../../features/tabs/InvokeTabs';
|
||||
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||
import { activeTabNameSelector } from 'features/options/store/optionsSelectors';
|
||||
import { tabDict } from 'features/tabs/components/InvokeTabs';
|
||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||
import { uploadImage } from 'features/gallery/store/thunks/uploadImage';
|
||||
import useImageUploader from 'common/hooks/useImageUploader';
|
||||
|
||||
type ImageUploaderProps = {
|
||||
children: ReactNode;
|
||||
@ -19,6 +25,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const toast = useToast({});
|
||||
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
||||
const { setOpenUploader } = useImageUploader();
|
||||
|
||||
const fileRejectionCallback = useCallback(
|
||||
(rejection: FileRejection) => {
|
||||
@ -38,15 +45,10 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
);
|
||||
|
||||
const fileAcceptedCallback = useCallback(
|
||||
(file: File) => {
|
||||
setIsHandlingUpload(true);
|
||||
const payload: UploadImagePayload = { file };
|
||||
if (['img2img', 'inpainting'].includes(activeTabName)) {
|
||||
payload.destination = activeTabName as ImageUploadDestination;
|
||||
}
|
||||
dispatch(uploadImage(payload));
|
||||
async (file: File) => {
|
||||
dispatch(uploadImage({ imageFile: file }));
|
||||
},
|
||||
[dispatch, activeTabName]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
@ -77,6 +79,8 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
maxFiles: 1,
|
||||
});
|
||||
|
||||
setOpenUploader(open);
|
||||
|
||||
useEffect(() => {
|
||||
const pasteImageListener = (e: ClipboardEvent) => {
|
||||
const dataTransferItemList = e.clipboardData?.items;
|
||||
@ -118,12 +122,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: UploadImagePayload = { file };
|
||||
if (['img2img', 'inpainting'].includes(activeTabName)) {
|
||||
payload.destination = activeTabName as ImageUploadDestination;
|
||||
}
|
||||
|
||||
dispatch(uploadImage(payload));
|
||||
dispatch(uploadImage({ imageFile: file }));
|
||||
};
|
||||
document.addEventListener('paste', pasteImageListener);
|
||||
return () => {
|
||||
@ -131,13 +130,21 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
||||
};
|
||||
}, [dispatch, toast, activeTabName]);
|
||||
|
||||
const overlaySecondaryText = ['img2img', 'inpainting'].includes(activeTabName)
|
||||
const overlaySecondaryText = ['img2img', 'unifiedCanvas'].includes(
|
||||
activeTabName
|
||||
)
|
||||
? ` to ${tabDict[activeTabName as keyof typeof tabDict].tooltip}`
|
||||
: ``;
|
||||
|
||||
return (
|
||||
<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()} />
|
||||
{children}
|
||||
{isDragActive && isHandlingUpload && (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Heading } from '@chakra-ui/react';
|
||||
import { useContext } from 'react';
|
||||
import { FaUpload } from 'react-icons/fa';
|
||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||
|
||||
type ImageUploaderButtonProps = {
|
||||
styleClass?: string;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
import { FaUpload } from 'react-icons/fa';
|
||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||
import IAIIconButton from './IAIIconButton';
|
||||
|
||||
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
|
||||
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 action buttons above the main image display.
|
||||
image action buttons above the current image display or in the viewer.
|
||||
</p>
|
||||
<p>
|
||||
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 = (
|
||||
ref: RefObject<HTMLElement>,
|
||||
callback: () => void,
|
||||
req = true
|
||||
) => {
|
||||
const watchers: {
|
||||
ref: RefObject<HTMLElement>;
|
||||
enable: boolean;
|
||||
callback: () => void;
|
||||
}[] = [];
|
||||
|
||||
const useClickOutsideWatcher = () => {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
watchers.forEach(({ ref, enable, callback }) => {
|
||||
if (enable && ref.current && !ref.current.contains(e.target as Node)) {
|
||||
console.log('callback');
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (req) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
return () => {
|
||||
if (req) {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
};
|
||||
}, [ref, req, callback]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
addWatcher: (watcher: {
|
||||
ref: RefObject<HTMLElement>;
|
||||
callback: () => void;
|
||||
enable: boolean;
|
||||
}) => {
|
||||
watchers.push(watcher);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
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 { OptionsState } from '../../features/options/optionsSlice';
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants';
|
||||
import { OptionsState } from 'features/options/store/optionsSlice';
|
||||
import { SystemState } from 'features/system/store/systemSlice';
|
||||
|
||||
import { stringToSeedWeightsArray } from './seedWeightPairs';
|
||||
import randomInt from './randomInt';
|
||||
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
|
||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import generateMask from '../../features/tabs/Inpainting/util/generateMask';
|
||||
import { InvokeTabName } from 'features/tabs/components/InvokeTabs';
|
||||
import {
|
||||
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 = {
|
||||
generationMode: InvokeTabName;
|
||||
optionsState: OptionsState;
|
||||
inpaintingState: InpaintingState;
|
||||
canvasState: CanvasState;
|
||||
systemState: SystemState;
|
||||
imageToProcessUrl?: string;
|
||||
maskImageElement?: HTMLImageElement;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -24,46 +28,56 @@ export type FrontendToBackendParametersConfig = {
|
||||
export const frontendToBackendParameters = (
|
||||
config: FrontendToBackendParametersConfig
|
||||
): { [key: string]: any } => {
|
||||
const canvasBaseLayer = getCanvasBaseLayer();
|
||||
|
||||
const {
|
||||
generationMode,
|
||||
optionsState,
|
||||
inpaintingState,
|
||||
canvasState,
|
||||
systemState,
|
||||
imageToProcessUrl,
|
||||
maskImageElement,
|
||||
} = config;
|
||||
|
||||
const {
|
||||
prompt,
|
||||
iterations,
|
||||
steps,
|
||||
cfgScale,
|
||||
threshold,
|
||||
perlin,
|
||||
codeformerFidelity,
|
||||
facetoolStrength,
|
||||
facetoolType,
|
||||
height,
|
||||
width,
|
||||
sampler,
|
||||
seed,
|
||||
seamless,
|
||||
hiresFix,
|
||||
img2imgStrength,
|
||||
infillMethod,
|
||||
initialImage,
|
||||
iterations,
|
||||
perlin,
|
||||
prompt,
|
||||
sampler,
|
||||
seamBlur,
|
||||
seamless,
|
||||
seamSize,
|
||||
seamSteps,
|
||||
seamStrength,
|
||||
seed,
|
||||
seedWeights,
|
||||
shouldFitToWidthHeight,
|
||||
shouldGenerateVariations,
|
||||
variationAmount,
|
||||
seedWeights,
|
||||
shouldRandomizeSeed,
|
||||
shouldRunESRGAN,
|
||||
shouldRunFacetool,
|
||||
steps,
|
||||
threshold,
|
||||
tileSize,
|
||||
upscalingLevel,
|
||||
upscalingStrength,
|
||||
shouldRunFacetool,
|
||||
facetoolStrength,
|
||||
codeformerFidelity,
|
||||
facetoolType,
|
||||
shouldRandomizeSeed,
|
||||
variationAmount,
|
||||
width,
|
||||
} = optionsState;
|
||||
|
||||
const { shouldDisplayInProgressType, saveIntermediatesInterval } =
|
||||
systemState;
|
||||
const {
|
||||
shouldDisplayInProgressType,
|
||||
saveIntermediatesInterval,
|
||||
enableImageDebugging,
|
||||
} = systemState;
|
||||
|
||||
const generationParameters: { [k: string]: any } = {
|
||||
prompt,
|
||||
@ -80,6 +94,8 @@ export const frontendToBackendParameters = (
|
||||
progress_images: shouldDisplayInProgressType === 'full-res',
|
||||
progress_latents: shouldDisplayInProgressType === 'latents',
|
||||
save_intermediates: saveIntermediatesInterval,
|
||||
generation_mode: generationMode,
|
||||
init_mask: '',
|
||||
};
|
||||
|
||||
generationParameters.seed = shouldRandomizeSeed
|
||||
@ -101,35 +117,38 @@ export const frontendToBackendParameters = (
|
||||
}
|
||||
|
||||
// inpainting exclusive parameters
|
||||
if (generationMode === 'inpainting' && maskImageElement) {
|
||||
if (generationMode === 'unifiedCanvas' && canvasBaseLayer) {
|
||||
const {
|
||||
lines,
|
||||
boundingBoxCoordinate,
|
||||
layerState: { objects },
|
||||
boundingBoxCoordinates,
|
||||
boundingBoxDimensions,
|
||||
inpaintReplace,
|
||||
shouldUseInpaintReplace,
|
||||
} = inpaintingState;
|
||||
stageScale,
|
||||
isMaskEnabled,
|
||||
shouldPreserveMaskedArea,
|
||||
boundingBoxScaleMethod: boundingBoxScale,
|
||||
scaledBoundingBoxDimensions,
|
||||
} = canvasState;
|
||||
|
||||
const boundingBox = {
|
||||
...boundingBoxCoordinate,
|
||||
...boundingBoxCoordinates,
|
||||
...boundingBoxDimensions,
|
||||
};
|
||||
|
||||
generationParameters.init_img = imageToProcessUrl;
|
||||
generationParameters.strength = img2imgStrength;
|
||||
generationParameters.fit = false;
|
||||
|
||||
const { maskDataURL, isMaskEmpty } = generateMask(
|
||||
maskImageElement,
|
||||
lines,
|
||||
const maskDataURL = generateMask(
|
||||
isMaskEnabled ? objects.filter(isCanvasMaskLine) : [],
|
||||
boundingBox
|
||||
);
|
||||
|
||||
generationParameters.is_mask_empty = isMaskEmpty;
|
||||
generationParameters.init_mask = maskDataURL;
|
||||
|
||||
generationParameters.init_mask = maskDataURL.split(
|
||||
'data:image/png;base64,'
|
||||
)[1];
|
||||
generationParameters.fit = false;
|
||||
|
||||
generationParameters.init_img = imageToProcessUrl;
|
||||
generationParameters.strength = img2imgStrength;
|
||||
|
||||
generationParameters.invert_mask = shouldPreserveMaskedArea;
|
||||
|
||||
if (shouldUseInpaintReplace) {
|
||||
generationParameters.inpaint_replace = inpaintReplace;
|
||||
@ -137,8 +156,47 @@ export const frontendToBackendParameters = (
|
||||
|
||||
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;
|
||||
|
||||
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) {
|
||||
@ -171,6 +229,10 @@ export const frontendToBackendParameters = (
|
||||
}
|
||||
}
|
||||
|
||||
if (enableImageDebugging) {
|
||||
generationParameters.enable_image_debugging = enableImageDebugging;
|
||||
}
|
||||
|
||||
return {
|
||||
generationParameters,
|
||||
esrganParameters,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
|
||||
const promptToString = (prompt: InvokeAI.Prompt): string => {
|
||||
if (prompt.length === 1) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
|
||||
export const stringToSeedWeights = (
|
||||
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;
|