Merge branch 'development' into backend-can-find-frontend

This commit is contained in:
Lincoln Stein 2022-11-26 14:01:23 -05:00 committed by GitHub
commit 4ae1df5b5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
309 changed files with 12219 additions and 7675 deletions

4
.gitignore vendored
View File

@ -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/

File diff suppressed because it is too large Load Diff

View 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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -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

View File

@ -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 .

View File

@ -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 .

View File

@ -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 .

View File

@ -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 .

View File

@ -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

View File

@ -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 .

View File

@ -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

View File

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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>

View 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"] }]
}
}
}

View File

@ -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"
}
}

View 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>;
/**

View 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>;
/**

View File

@ -1,5 +1,9 @@
@use '../styles/Mixins/' as *;
svg {
fill: var(--svg-color);
}
.App {
display: grid;
width: 100vw;

View File

@ -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>
);
};

View File

@ -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> = [

View File

@ -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',
},

View File

@ -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;
};

View File

@ -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) {

View File

@ -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'
);

View File

@ -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');
},
};
};

View File

@ -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,
};
dispatch(
addImage({
category: 'result',
image: newImage,
})
);
if (['txt2img', 'img2img'].includes(generationMode)) {
dispatch(
addImage({
category: 'result',
image: { ...newImage, category: 'result' },
})
);
}
if (generationMode === 'unifiedCanvas' && data.boundingBox) {
const { boundingBox } = data;
dispatch(
addImageToStagingArea({
image: { ...newImage, category: 'temp' },
boundingBox,
})
);
if (state.canvas.shouldAutoSave) {
dispatch(
addImage({
image: { ...newImage, category: 'result' },
category: 'result',
})
);
}
}
if (shouldLoopback) {
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,
})
);
},
};
};

View File

@ -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);

View File

@ -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: [
'isCancelable',
'isConnected',
'isProcessing',
'currentStep',
'socketId',
'isESRGANAvailable',
'isGFPGANAvailable',
'currentStep',
'totalSteps',
'currentIteration',
'totalIterations',
'currentStatus',
],
};
const systemBlacklist = [
'currentIteration',
'currentStatus',
'currentStep',
'isCancelable',
'isConnected',
'isESRGANAvailable',
'isGFPGANAvailable',
'isProcessing',
'socketId',
'totalIterations',
'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;

Binary file not shown.

View 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

View File

@ -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>
)

View File

@ -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 {

View File

@ -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;

View 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;

View File

@ -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);
}
}

View File

@ -15,7 +15,7 @@
svg {
width: 0.6rem;
height: 0.6rem;
stroke-width: 3px !important;
stroke-width: 3px;
}
&[data-checked] {

View File

@ -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;
}
}
&[data-selected='true'] {
border-color: var(--accent-color);
// 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 {
border-color: var(--accent-color-hover);
background-color: var(--btn-base-color);
border-color: var(--btn-checkbox-border-hover);
svg {
fill: var(--text-color);
}
}
&[data-selected='true'] {
border-color: var(--accent-color);
svg {
fill: var(--accent-color-hover);
}
&:hover {
svg {
fill: var(--accent-color-hover);
}
}
}
}
@ -38,28 +61,12 @@
animation-duration: 1s;
animation-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 {

View File

@ -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>

View File

@ -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;

View File

@ -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}

View File

@ -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;

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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,37 +43,41 @@ const IAISelect = (props: IAISelectProps) => {
e.nativeEvent.cancelBubble = true;
}}
>
<FormLabel
className="invokeai__select-label"
fontSize={fontSize}
marginBottom={1}
flexGrow={2}
whiteSpace="nowrap"
>
{label}
</FormLabel>
<Select
className="invokeai__select-picker"
fontSize={fontSize}
size={size}
{...rest}
>
{validValues.map((opt) => {
return typeof opt === 'string' || typeof opt === 'number' ? (
<option key={opt} value={opt} className="invokeai__select-option">
{opt}
</option>
) : (
<option
key={opt.value}
value={opt.value}
className="invokeai__select-option"
>
{opt.key}
</option>
);
})}
</Select>
{label && (
<FormLabel
className="invokeai__select-label"
fontSize={fontSize}
marginBottom={1}
flexGrow={2}
whiteSpace="nowrap"
>
{label}
</FormLabel>
)}
<Tooltip label={tooltip} {...tooltipProps}>
<Select
className="invokeai__select-picker"
fontSize={fontSize}
size={size}
{...rest}
>
{validValues.map((opt) => {
return typeof opt === 'string' || typeof opt === 'number' ? (
<option key={opt} value={opt} className="invokeai__select-option">
{opt}
</option>
) : (
<option
key={opt.value}
value={opt.value}
className="invokeai__select-option"
>
{opt.key}
</option>
);
})}
</Select>
</Tooltip>
</FormControl>
);
};

View File

@ -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-component-label {
min-width: max-content;
margin: 0;
font-weight: bold;
font-size: 0.9rem;
color: var(--text-color-secondary);
}
.invokeai__slider-form-label {
color: var(--text-color-secondary);
margin: 0;
margin-right: 0.5rem;
margin-bottom: 0.1rem;
.invokeai__slider_track {
background-color: var(--tab-color);
}
.invokeai__slider_track-filled {
background-color: var(--slider-color);
}
.invokeai__slider-thumb {
width: 4px;
}
.invokeai__slider-mark {
font-size: 0.75rem;
font-weight: bold;
color: var(--slider-color);
margin-top: 0.3rem;
}
.invokeai__slider-number-input {
border: none;
font-size: 0.9rem;
font-weight: bold;
height: 2rem;
background-color: var(--background-color-secondary);
border: 2px solid var(--border-color);
&:focus {
outline: none;
box-shadow: none;
border: 2px solid var(--input-border-color);
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
}
.invokeai__slider-root {
.invokeai__slider-filled-track {
background-color: var(--accent-color-hover);
}
&:disabled {
opacity: 0.2;
}
}
.invokeai__slider-track {
background-color: var(--text-color-secondary);
height: 5px;
border-radius: 9999px;
}
.invokeai__slider-number-stepper {
border: none;
}
.invokeai__slider-thumb {
}
&[data-markers='true'] {
.invokeai__slider_container {
margin-top: -1rem;
}
}
}
.invokeai__slider-thumb-tooltip {
}

View File

@ -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}
>
{label}
</FormLabel>
<FormLabel
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}
>
<SliderFilledTrack
className={`invokeai__slider-filled-track`}
{...sliderInnerTrackProps}
/>
{withSliderMarks && (
<>
<SliderMark
value={min}
className="invokeai__slider-mark invokeai__slider-mark-start"
ml={sliderMarkLeftOffset}
{...sliderMarkProps}
>
{min}
</SliderMark>
<SliderMark
value={max}
className="invokeai__slider-mark invokeai__slider-mark-end"
ml={sliderMarkRightOffset}
{...sliderMarkProps}
>
{max}
</SliderMark>
</>
)}
<SliderTrack className="invokeai__slider_track" {...sliderTrackProps}>
<SliderFilledTrack className="invokeai__slider_track-filled" />
</SliderTrack>
<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;
}

View File

@ -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;
}
}

View File

@ -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 && (

View File

@ -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;

View File

@ -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 = () => {

View File

@ -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>
);
};

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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

View 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>
);
}

View File

@ -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)) {
callback();
}
}
if (req) {
document.addEventListener('mousedown', handleClickOutside);
watchers.forEach(({ ref, enable, callback }) => {
if (enable && ref.current && !ref.current.contains(e.target as Node)) {
console.log('callback');
callback();
}
});
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
if (req) {
document.removeEventListener('mousedown', handleClickOutside);
}
document.removeEventListener('mousedown', handleClickOutside);
};
}, [ref, req, callback]);
}, []);
return {
addWatcher: (watcher: {
ref: RefObject<HTMLElement>;
callback: () => void;
enable: boolean;
}) => {
watchers.push(watcher);
},
};
};
export default useClickOutsideWatcher;

View File

@ -0,0 +1,14 @@
let openFunction: () => void;
const useImageUploader = () => {
return {
setOpenUploader: (open?: () => void) => {
if (open) {
openFunction = open;
}
},
openUploader: openFunction,
};
};
export default useImageUploader;

View 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;

Binary file not shown.

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

View 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;

View File

@ -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,

View File

@ -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) {

View File

@ -1,4 +1,4 @@
import * as InvokeAI from '../../app/invokeai';
import * as InvokeAI from 'app/invokeai';
export const stringToSeedWeights = (
string: string

View File

@ -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;

View 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;

View 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;

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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;

View File

@ -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;

View File

@ -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;

View 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;

View File

@ -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>;
}

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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}
/>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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}
/>
);
}

View 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;

View 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;

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