From 73ad173c74f10a9ddc81e3efb9d79a7f5db4af4b Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 16 Apr 2024 16:21:12 -0400 Subject: [PATCH 001/219] update labels for Style Only and CompositionOnly to be designated as beta --- invokeai/frontend/web/public/locales/en.json | 1 + .../components/parameters/ParamControlAdapterIPMethod.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index f69f09552a..87ea9de27d 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -69,6 +69,7 @@ "auto": "Auto", "back": "Back", "batch": "Batch Manager", + "beta": "Beta", "cancel": "Cancel", "copy": "Copy", "copyError": "$t(gallery.copy) Error", diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx index 7385997804..c7aaa9f26c 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx @@ -23,8 +23,8 @@ const ParamControlAdapterIPMethod = ({ id }: Props) => { const options: { label: string; value: IPMethod }[] = useMemo( () => [ { label: t('controlnet.full'), value: 'full' }, - { label: t('controlnet.style'), value: 'style' }, - { label: t('controlnet.composition'), value: 'composition' }, + { label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' }, + { label: `${t('controlnet.composition')} (${t('common.beta')})`, value: 'composition' }, ], [t] ); From 0c7283c82d2190508c8f9e1d1fbb10ca37a35466 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 15 Apr 2024 14:49:34 +0200 Subject: [PATCH 002/219] translationBot(ui): update translation (Turkish) Currently translated at 50.8% (580 of 1140 strings) translationBot(ui): update translation (Korean) Currently translated at 43.3% (494 of 1140 strings) translationBot(ui): update translation (Chinese (Simplified)) Currently translated at 80.9% (923 of 1140 strings) translationBot(ui): update translation (Russian) Currently translated at 98.8% (1127 of 1140 strings) translationBot(ui): update translation (Dutch) Currently translated at 63.7% (727 of 1140 strings) translationBot(ui): update translation (Japanese) Currently translated at 50.4% (575 of 1140 strings) translationBot(ui): update translation (Italian) Currently translated at 98.3% (1121 of 1140 strings) translationBot(ui): update translation (Spanish) Currently translated at 27.8% (317 of 1140 strings) translationBot(ui): update translation (German) Currently translated at 72.2% (824 of 1140 strings) Co-authored-by: Anonymous Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/ Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/es/ Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/ Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ko/ Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/nl/ Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/ Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/tr/ Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 3 ++- invokeai/frontend/web/public/locales/es.json | 4 +++- invokeai/frontend/web/public/locales/it.json | 4 +++- invokeai/frontend/web/public/locales/ja.json | 2 +- invokeai/frontend/web/public/locales/ko.json | 2 +- invokeai/frontend/web/public/locales/nl.json | 3 ++- invokeai/frontend/web/public/locales/ru.json | 4 +++- invokeai/frontend/web/public/locales/tr.json | 3 ++- invokeai/frontend/web/public/locales/zh_CN.json | 2 +- 9 files changed, 18 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 033dffdc44..0a104c083b 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -85,7 +85,8 @@ "loadMore": "Mehr laden", "noImagesInGallery": "Keine Bilder in der Galerie", "loading": "Lade", - "deleteImage": "Lösche Bild", + "deleteImage_one": "Lösche Bild", + "deleteImage_other": "", "copy": "Kopieren", "download": "Runterladen", "setCurrentImage": "Setze aktuelle Bild", diff --git a/invokeai/frontend/web/public/locales/es.json b/invokeai/frontend/web/public/locales/es.json index 3037045db5..6b410cd0bf 100644 --- a/invokeai/frontend/web/public/locales/es.json +++ b/invokeai/frontend/web/public/locales/es.json @@ -33,7 +33,9 @@ "autoSwitchNewImages": "Auto seleccionar Imágenes nuevas", "loadMore": "Cargar más", "noImagesInGallery": "No hay imágenes para mostrar", - "deleteImage": "Eliminar Imagen", + "deleteImage_one": "Eliminar Imagen", + "deleteImage_many": "", + "deleteImage_other": "", "deleteImageBin": "Las imágenes eliminadas se enviarán a la papelera de tu sistema operativo.", "deleteImagePermanent": "Las imágenes eliminadas no se pueden restaurar.", "assets": "Activos", diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index db01e0af4b..28ff066c1c 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -82,7 +82,9 @@ "autoSwitchNewImages": "Passaggio automatico a nuove immagini", "loadMore": "Carica altro", "noImagesInGallery": "Nessuna immagine da visualizzare", - "deleteImage": "Elimina l'immagine", + "deleteImage_one": "Elimina l'immagine", + "deleteImage_many": "", + "deleteImage_other": "", "deleteImagePermanent": "Le immagini eliminate non possono essere ripristinate.", "deleteImageBin": "Le immagini eliminate verranno spostate nel cestino del tuo sistema operativo.", "assets": "Risorse", diff --git a/invokeai/frontend/web/public/locales/ja.json b/invokeai/frontend/web/public/locales/ja.json index d13b1e4cb0..264593153a 100644 --- a/invokeai/frontend/web/public/locales/ja.json +++ b/invokeai/frontend/web/public/locales/ja.json @@ -90,7 +90,7 @@ "problemDeletingImages": "画像の削除中に問題が発生", "drop": "ドロップ", "dropOrUpload": "$t(gallery.drop) またはアップロード", - "deleteImage": "画像を削除", + "deleteImage_other": "画像を削除", "deleteImageBin": "削除された画像はOSのゴミ箱に送られます。", "deleteImagePermanent": "削除された画像は復元できません。", "download": "ダウンロード", diff --git a/invokeai/frontend/web/public/locales/ko.json b/invokeai/frontend/web/public/locales/ko.json index 44f0f5eac6..1c02d86105 100644 --- a/invokeai/frontend/web/public/locales/ko.json +++ b/invokeai/frontend/web/public/locales/ko.json @@ -82,7 +82,7 @@ "drop": "드랍", "problemDeletingImages": "이미지 삭제 중 발생한 문제", "downloadSelection": "선택 항목 다운로드", - "deleteImage": "이미지 삭제", + "deleteImage_other": "이미지 삭제", "currentlyInUse": "이 이미지는 현재 다음 기능에서 사용되고 있습니다:", "dropOrUpload": "$t(gallery.drop) 또는 업로드", "copy": "복사", diff --git a/invokeai/frontend/web/public/locales/nl.json b/invokeai/frontend/web/public/locales/nl.json index 70adbb371d..29ceb3227b 100644 --- a/invokeai/frontend/web/public/locales/nl.json +++ b/invokeai/frontend/web/public/locales/nl.json @@ -42,7 +42,8 @@ "autoSwitchNewImages": "Wissel autom. naar nieuwe afbeeldingen", "loadMore": "Laad meer", "noImagesInGallery": "Geen afbeeldingen om te tonen", - "deleteImage": "Verwijder afbeelding", + "deleteImage_one": "Verwijder afbeelding", + "deleteImage_other": "", "deleteImageBin": "Verwijderde afbeeldingen worden naar de prullenbak van je besturingssysteem gestuurd.", "deleteImagePermanent": "Verwijderde afbeeldingen kunnen niet worden hersteld.", "assets": "Eigen onderdelen", diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index 8ac36ef2de..f254b7faa5 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -86,7 +86,9 @@ "noImagesInGallery": "Изображений нет", "deleteImagePermanent": "Удаленные изображения невозможно восстановить.", "deleteImageBin": "Удаленные изображения будут отправлены в корзину вашей операционной системы.", - "deleteImage": "Удалить изображение", + "deleteImage_one": "Удалить изображение", + "deleteImage_few": "", + "deleteImage_many": "", "assets": "Ресурсы", "autoAssignBoardOnClick": "Авто-назначение доски по клику", "deleteSelection": "Удалить выделенное", diff --git a/invokeai/frontend/web/public/locales/tr.json b/invokeai/frontend/web/public/locales/tr.json index 2a666a128c..415bd2d744 100644 --- a/invokeai/frontend/web/public/locales/tr.json +++ b/invokeai/frontend/web/public/locales/tr.json @@ -298,7 +298,8 @@ "noImagesInGallery": "Gösterilecek Görsel Yok", "autoSwitchNewImages": "Yeni Görseli Biter Bitmez Gör", "currentlyInUse": "Bu görsel şurada kullanımda:", - "deleteImage": "Görseli Sil", + "deleteImage_one": "Görseli Sil", + "deleteImage_other": "", "loadMore": "Daha Getir", "setCurrentImage": "Çalışma Görseli Yap", "unableToLoad": "Galeri Yüklenemedi", diff --git a/invokeai/frontend/web/public/locales/zh_CN.json b/invokeai/frontend/web/public/locales/zh_CN.json index e2cb35af74..8aff73d2a1 100644 --- a/invokeai/frontend/web/public/locales/zh_CN.json +++ b/invokeai/frontend/web/public/locales/zh_CN.json @@ -78,7 +78,7 @@ "autoSwitchNewImages": "自动切换到新图像", "loadMore": "加载更多", "noImagesInGallery": "无图像可用于显示", - "deleteImage": "删除图片", + "deleteImage_other": "删除图片", "deleteImageBin": "被删除的图片会发送到你操作系统的回收站。", "deleteImagePermanent": "删除的图片无法被恢复。", "assets": "素材", From 5295a398f34668710186c77fac760cae00e6a5d8 Mon Sep 17 00:00:00 2001 From: Riccardo Giovanetti Date: Mon, 15 Apr 2024 14:49:34 +0200 Subject: [PATCH 003/219] translationBot(ui): update translation (Italian) Currently translated at 98.4% (1122 of 1140 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 28ff066c1c..491b31907b 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -83,8 +83,8 @@ "loadMore": "Carica altro", "noImagesInGallery": "Nessuna immagine da visualizzare", "deleteImage_one": "Elimina l'immagine", - "deleteImage_many": "", - "deleteImage_other": "", + "deleteImage_many": "Elimina {{count}} immagini", + "deleteImage_other": "Elimina {{count}} immagini", "deleteImagePermanent": "Le immagini eliminate non possono essere ripristinate.", "deleteImageBin": "Le immagini eliminate verranno spostate nel cestino del tuo sistema operativo.", "assets": "Risorse", From ac1071a5e515840077e496cb043841a22b1364f5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 18 Apr 2024 06:57:58 +1000 Subject: [PATCH 004/219] chore: v4.1.0 --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index 4b56dfc53e..7039708762 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "4.0.4" +__version__ = "4.1.0" From a35386f24c020b219f1097acfa1e05e5ddc11f20 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Thu, 18 Apr 2024 20:59:09 +0530 Subject: [PATCH 005/219] fix: IP Adapter Method having incorrect informational popover --- invokeai/frontend/web/public/locales/en.json | 4 ++++ .../src/common/components/InformationalPopover/constants.ts | 1 + .../components/parameters/ParamControlAdapterIPMethod.tsx | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 87ea9de27d..7ac20649b0 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1183,6 +1183,10 @@ "heading": "Resize Mode", "paragraphs": ["Method to fit Control Adapter's input image size to the output generation size."] }, + "ipAdapterMethod": { + "heading": "Method", + "paragraphs": ["Method by which to apply the current IP Adapter."] + }, "controlNetWeight": { "heading": "Weight", "paragraphs": [ diff --git a/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts b/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts index b556816502..f973d98f1a 100644 --- a/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts +++ b/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts @@ -24,6 +24,7 @@ export type Feature = | 'dynamicPromptsSeedBehaviour' | 'imageFit' | 'infillMethod' + | 'ipAdapterMethod' | 'lora' | 'loraWeight' | 'noiseUseCPU' diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx index c7aaa9f26c..7d531e2106 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx @@ -52,7 +52,7 @@ const ParamControlAdapterIPMethod = ({ id }: Props) => { return ( - + {t('controlnet.ipAdapterMethod')} From 2b9f06dc4cca07aaf5e9ede9ab7221a3c623691b Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 19 Apr 2024 06:45:42 -0400 Subject: [PATCH 006/219] Re-enable app shutdown actions (#6244) * closes #6242 * only override sigINT during slow model scanning * fix ruff formatting --------- Co-authored-by: Lincoln Stein --- .../model_install/model_install_default.py | 16 ++-------- invokeai/backend/util/catch_sigint.py | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 invokeai/backend/util/catch_sigint.py diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index 6a3117bcb8..6eb9549ef0 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -3,7 +3,6 @@ import locale import os import re -import signal import threading import time from hashlib import sha256 @@ -43,6 +42,7 @@ from invokeai.backend.model_manager.metadata.metadata_base import HuggingFaceMet from invokeai.backend.model_manager.probe import ModelProbe from invokeai.backend.model_manager.search import ModelSearch from invokeai.backend.util import InvokeAILogger +from invokeai.backend.util.catch_sigint import catch_sigint from invokeai.backend.util.devices import TorchDevice from .model_install_base import ( @@ -112,17 +112,6 @@ class ModelInstallService(ModelInstallServiceBase): def start(self, invoker: Optional[Invoker] = None) -> None: """Start the installer thread.""" - # Yes, this is weird. When the installer thread is running, the - # thread masks the ^C signal. When we receive a - # sigINT, we stop the thread, reset sigINT, and send a new - # sigINT to the parent process. - def sigint_handler(signum, frame): - self.stop() - signal.signal(signal.SIGINT, signal.SIG_DFL) - signal.raise_signal(signal.SIGINT) - - signal.signal(signal.SIGINT, sigint_handler) - with self._lock: if self._running: raise Exception("Attempt to start the installer service twice") @@ -132,7 +121,8 @@ class ModelInstallService(ModelInstallServiceBase): # In normal use, we do not want to scan the models directory - it should never have orphaned models. # We should only do the scan when the flag is set (which should only be set when testing). if self.app_config.scan_models_on_startup: - self._register_orphaned_models() + with catch_sigint(): + self._register_orphaned_models() # Check all models' paths and confirm they exist. A model could be missing if it was installed on a volume # that isn't currently mounted. In this case, we don't want to delete the model from the database, but we do diff --git a/invokeai/backend/util/catch_sigint.py b/invokeai/backend/util/catch_sigint.py new file mode 100644 index 0000000000..b9735d94f9 --- /dev/null +++ b/invokeai/backend/util/catch_sigint.py @@ -0,0 +1,29 @@ +""" +This module defines a context manager `catch_sigint()` which temporarily replaces +the sigINT handler defined by the ASGI in order to allow the user to ^C the application +and shut it down immediately. This was implemented in order to allow the user to interrupt +slow model hashing during startup. + +Use like this: + + from invokeai.backend.util.catch_sigint import catch_sigint + with catch_sigint(): + run_some_hard_to_interrupt_process() +""" + +import signal +from contextlib import contextmanager +from typing import Generator + + +def sigint_handler(signum, frame): # type: ignore + signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.raise_signal(signal.SIGINT) + + +@contextmanager +def catch_sigint() -> Generator[None, None, None]: + original_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, sigint_handler) + yield + signal.signal(signal.SIGINT, original_handler) From 1d1e4d02dc547975316a87f28bd6a2911aa61b52 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:03:29 +1100 Subject: [PATCH 007/219] feat(ui): rough out regional prompts store --- invokeai/frontend/web/src/app/store/store.ts | 6 + .../store/regionalPromptsSlice.ts | 126 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index b538a3eaeb..4626f4e36b 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -21,6 +21,10 @@ import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workf import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice'; import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice'; import { queueSlice } from 'features/queue/store/queueSlice'; +import { + regionalPromptsPersistConfig, + regionalPromptsSlice, +} from 'features/regionalPrompts/store/regionalPromptsSlice'; import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice'; import { configSlice } from 'features/system/store/configSlice'; import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice'; @@ -59,6 +63,7 @@ const allReducers = { [queueSlice.name]: queueSlice.reducer, [workflowSlice.name]: workflowSlice.reducer, [hrfSlice.name]: hrfSlice.reducer, + [regionalPromptsSlice.name]: regionalPromptsSlice.reducer, [api.reducerPath]: api.reducer, }; @@ -103,6 +108,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [loraPersistConfig.name]: loraPersistConfig, [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, [hrfPersistConfig.name]: hrfPersistConfig, + [regionalPromptsPersistConfig.name]: regionalPromptsPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts new file mode 100644 index 0000000000..6a248ea527 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts @@ -0,0 +1,126 @@ +import type { EntityState } from '@reduxjs/toolkit'; +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { getSelectorsOptions } from 'app/store/createMemoizedSelector'; +import type { PersistConfig, RootState } from 'app/store/store'; +import type { RgbaColor } from 'react-colorful'; +import { v4 as uuidv4 } from 'uuid'; + +type LayerObjectBase = { + id: string; + isSelected: boolean; +}; + +export type ImageObject = LayerObjectBase & { + kind: 'image'; + imageName: string; + x: number; + y: number; + width: number; + height: number; +}; + +export type LineObject = LayerObjectBase & { + kind: 'line'; + strokeWidth: number; + points: number[]; + color: RgbaColor; +}; + +export type FillRectObject = LayerObjectBase & { + kind: 'fillRect'; + x: number; + y: number; + width: number; + height: number; + color: RgbaColor; +}; + +export type LayerObject = ImageObject | LineObject | FillRectObject; + +export type PromptRegionLayer = { + id: string; + objects: EntityState; + prompt: string; +}; + +export const layersAdapter = createEntityAdapter({ + selectId: (layer) => layer.id, +}); +export const layersSelectors = layersAdapter.getSelectors(undefined, getSelectorsOptions); + +export const layerObjectsAdapter = createEntityAdapter({ + selectId: (obj) => obj.id, +}); +export const layerObjectsSelectors = layerObjectsAdapter.getSelectors(undefined, getSelectorsOptions); + +const getMockState = () => { + // Mock data + const layer1ID = uuidv4(); + const obj1ID = uuidv4(); + const obj2ID = uuidv4(); + + const objectEntities: Record = { + [obj1ID]: { + id: obj1ID, + kind: 'line', + isSelected: false, + color: { r: 255, g: 0, b: 0, a: 1 }, + strokeWidth: 5, + points: [20, 20, 100, 100], + }, + [obj2ID]: { + id: obj2ID, + kind: 'fillRect', + isSelected: false, + color: { r: 0, g: 255, b: 0, a: 1 }, + x: 150, + y: 150, + width: 100, + height: 100, + }, + }; + const objectsInitialState = layerObjectsAdapter.getInitialState(undefined, objectEntities); + const entities: Record = { + [layer1ID]: { + id: layer1ID, + prompt: 'strawberries', + objects: objectsInitialState, + }, + }; + + return entities; +}; + +export const initialRegionalPromptsState = layersAdapter.getInitialState( + { _version: 1, selectedID: null }, + getMockState() +); + +export type RegionalPromptsState = typeof initialRegionalPromptsState; + +export const regionalPromptsSlice = createSlice({ + name: 'regionalPrompts', + initialState: initialRegionalPromptsState, + reducers: { + layerAdded: layersAdapter.addOne, + layerRemoved: layersAdapter.removeOne, + layerUpdated: layersAdapter.updateOne, + layersReset: layersAdapter.removeAll, + }, +}); + +export const { layerAdded, layerRemoved, layerUpdated, layersReset } = regionalPromptsSlice.actions; + +export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrateRegionalPromptsState = (state: any): any => { + return state; +}; + +export const regionalPromptsPersistConfig: PersistConfig = { + name: regionalPromptsSlice.name, + initialState: initialRegionalPromptsState, + migrate: migrateRegionalPromptsState, + persistDenylist: [], +}; From f87eee810bc540c336713018f7f0f447c1b35caf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:03:37 +1100 Subject: [PATCH 008/219] feat(ui): rough out regional prompts components --- .../components/LineComponent.tsx | 23 +++++++++++ .../components/RectComponent.tsx | 25 ++++++++++++ .../RegionalPromptingEditor.stories.tsx | 19 +++++++++ .../components/RegionalPromptsEditor.tsx | 25 ++++++++++++ .../components/RegionalPromptsStage.tsx | 39 +++++++++++++++++++ .../regionalPrompts/hooks/useTransform.ts | 30 ++++++++++++++ 6 files changed, 161 insertions(+) create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/RectComponent.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptingEditor.stories.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsStage.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/hooks/useTransform.ts diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx new file mode 100644 index 0000000000..86785a4219 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx @@ -0,0 +1,23 @@ +import { rgbaColorToString } from 'features/canvas/util/colorToString'; +import { useTransform } from 'features/regionalPrompts/hooks/useTransform'; +import type { LineObject } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { Line } from 'react-konva'; + +type Props = { + line: LineObject; +}; + +export const LineComponent = ({ line }: Props) => { + const { shapeRef } = useTransform(line); + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RectComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RectComponent.tsx new file mode 100644 index 0000000000..e2b83a47e7 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RectComponent.tsx @@ -0,0 +1,25 @@ +import { rgbaColorToString } from 'features/canvas/util/colorToString'; +import { useTransform } from 'features/regionalPrompts/hooks/useTransform'; +import type { FillRectObject } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { Rect } from 'react-konva'; + +type Props = { + rect: FillRectObject; +}; + +export const RectComponent = ({ rect }: Props) => { + const { shapeRef } = useTransform(rect); + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptingEditor.stories.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptingEditor.stories.tsx new file mode 100644 index 0000000000..a67e9f58f7 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptingEditor.stories.tsx @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { RegionalPromptsEditor } from 'features/regionalPrompts/components/RegionalPromptsEditor'; + +const meta: Meta = { + title: 'Feature/RegionalPrompts', + tags: ['autodocs'], + component: RegionalPromptsEditor, +}; + +export default meta; +type Story = StoryObj; + +const Component = () => { + return ; +}; + +export const Default: Story = { + render: Component, +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx new file mode 100644 index 0000000000..907f8a2c69 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx @@ -0,0 +1,25 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage'; +import { layersSelectors, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice'; + +const selectLayers = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => + layersSelectors.selectAll(regionalPrompts) +); + +export const RegionalPromptsEditor = () => { + const layers = useAppSelector(selectLayers); + return ( + + + {layers.map((layer) => ( + {layer.prompt} + ))} + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsStage.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsStage.tsx new file mode 100644 index 0000000000..b80d142a84 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsStage.tsx @@ -0,0 +1,39 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { LineComponent } from 'features/regionalPrompts/components/LineComponent'; +import { RectComponent } from 'features/regionalPrompts/components/RectComponent'; +import { + layerObjectsSelectors, + layersSelectors, + selectRegionalPromptsSlice, +} from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { memo } from 'react'; +import { Group, Layer, Stage } from 'react-konva'; + +const selectLayers = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => + layersSelectors.selectAll(regionalPrompts) +); + +export const RegionalPromptsStage: React.FC = memo(() => { + const layers = useAppSelector(selectLayers); + return ( + + + {layers.map((layer) => ( + + {layerObjectsSelectors.selectAll(layer.objects).map((obj) => { + if (obj.kind === 'line') { + return ; + } + if (obj.kind === 'fillRect') { + return ; + } + })} + + ))} + + + ); +}); + +RegionalPromptsStage.displayName = 'RegionalPromptingEditor'; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/hooks/useTransform.ts b/invokeai/frontend/web/src/features/regionalPrompts/hooks/useTransform.ts new file mode 100644 index 0000000000..d04cbb8c55 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/hooks/useTransform.ts @@ -0,0 +1,30 @@ +import type { FillRectObject, LayerObject, LineObject } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import type { Image } from 'konva/lib/shapes/Image'; +import type { Line } from 'konva/lib/shapes/Line'; +import type { Rect } from 'konva/lib/shapes/Rect'; +import type { Transformer } from 'konva/lib/shapes/Transformer'; +import { useEffect, useRef } from 'react'; + +type ShapeType = T extends LineObject ? Line : T extends FillRectObject ? Rect : Image; + +export const useTransform = (object: TObject) => { + const shapeRef = useRef>(null); + const transformerRef = useRef(null); + + useEffect(() => { + if (!object.isSelected) { + return; + } + + if (!transformerRef.current || !shapeRef.current) { + return; + } + + if (object.isSelected) { + transformerRef.current.nodes([shapeRef.current]); + transformerRef.current.getLayer()?.batchDraw(); + } + }, [object.isSelected]); + + return { shapeRef, transformerRef }; +}; From 83d359b6818c366b750c881145659a55aacea0d3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 8 Apr 2024 17:06:08 +1000 Subject: [PATCH 009/219] feat(ui): wip regional prompting UI --- .../src/common/components/ColorPreview.tsx | 19 ++ .../src/common/components/RgbColorPicker.tsx | 84 ++++++++ .../src/features/canvas/util/colorToString.ts | 7 +- .../components/AddLayerButton.tsx | 13 ++ .../components/BrushPreview.tsx | 23 ++ .../regionalPrompts/components/BrushSize.tsx | 22 ++ .../components/DeleteLayerButton.tsx | 17 ++ .../components/LayerColorPicker.tsx | 35 ++++ .../components/LayerListItem.tsx | 32 +++ .../components/LineComponent.tsx | 14 +- .../components/RectComponent.tsx | 10 +- .../components/RegionalPromptsEditor.tsx | 23 +- .../components/RegionalPromptsPrompt.tsx | 73 +++++++ .../components/RegionalPromptsStage.tsx | 61 ++++-- .../components/ResetLayerButton.tsx | 17 ++ .../regionalPrompts/hooks/useLayer.ts | 18 ++ .../regionalPrompts/hooks/useMouseDown.ts | 139 ++++++++++++ .../store/regionalPromptsSlice.ts | 197 ++++++++++++------ 18 files changed, 712 insertions(+), 92 deletions(-) create mode 100644 invokeai/frontend/web/src/common/components/ColorPreview.tsx create mode 100644 invokeai/frontend/web/src/common/components/RgbColorPicker.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/BrushPreview.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/BrushSize.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/DeleteLayerButton.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/LayerColorPicker.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/LayerListItem.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPrompt.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/components/ResetLayerButton.tsx create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/hooks/useLayer.ts create mode 100644 invokeai/frontend/web/src/features/regionalPrompts/hooks/useMouseDown.ts diff --git a/invokeai/frontend/web/src/common/components/ColorPreview.tsx b/invokeai/frontend/web/src/common/components/ColorPreview.tsx new file mode 100644 index 0000000000..a52c98ef19 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/ColorPreview.tsx @@ -0,0 +1,19 @@ +import type { FlexProps } from '@invoke-ai/ui-library'; +import { Flex, forwardRef } from '@invoke-ai/ui-library'; +import { useMemo } from 'react'; +import type { RgbaColor, RgbColor } from 'react-colorful'; + +type Props = FlexProps & { + previewColor: RgbColor | RgbaColor; +}; + +export const ColorPreview = forwardRef((props: Props, ref) => { + const { previewColor, ...rest } = props; + const colorString = useMemo(() => { + if ('a' in previewColor) { + return `rgba(${previewColor.r}, ${previewColor.g}, ${previewColor.b}, ${previewColor.a ?? 1})`; + } + return `rgba(${previewColor.r}, ${previewColor.g}, ${previewColor.b}, 1)`; + }, [previewColor]); + return ; +}); diff --git a/invokeai/frontend/web/src/common/components/RgbColorPicker.tsx b/invokeai/frontend/web/src/common/components/RgbColorPicker.tsx new file mode 100644 index 0000000000..2b7ddf0cd9 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/RgbColorPicker.tsx @@ -0,0 +1,84 @@ +import type { ChakraProps } from '@invoke-ai/ui-library'; +import { CompositeNumberInput, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import type { CSSProperties } from 'react'; +import { memo, useCallback } from 'react'; +import { RgbColorPicker as ColorfulRgbColorPicker } from 'react-colorful'; +import type { ColorPickerBaseProps, RgbColor } from 'react-colorful/dist/types'; +import { useTranslation } from 'react-i18next'; + +type RgbColorPickerProps = ColorPickerBaseProps & { + withNumberInput?: boolean; +}; + +const colorPickerPointerStyles: NonNullable = { + width: 6, + height: 6, + borderColor: 'base.100', +}; + +const sx: ChakraProps['sx'] = { + '.react-colorful__hue-pointer': colorPickerPointerStyles, + '.react-colorful__saturation-pointer': colorPickerPointerStyles, + '.react-colorful__alpha-pointer': colorPickerPointerStyles, + gap: 5, + flexDir: 'column', +}; + +const colorPickerStyles: CSSProperties = { width: '100%' }; + +const numberInputWidth: ChakraProps['w'] = '4.2rem'; + +const RgbColorPicker = (props: RgbColorPickerProps) => { + const { color, onChange, withNumberInput, ...rest } = props; + const { t } = useTranslation(); + const handleChangeR = useCallback((r: number) => onChange({ ...color, r }), [color, onChange]); + const handleChangeG = useCallback((g: number) => onChange({ ...color, g }), [color, onChange]); + const handleChangeB = useCallback((b: number) => onChange({ ...color, b }), [color, onChange]); + return ( + + + {withNumberInput && ( + + + {t('common.red')} + + + + {t('common.green')} + + + + {t('common.blue')} + + + + )} + + ); +}; + +export default memo(RgbColorPicker); diff --git a/invokeai/frontend/web/src/features/canvas/util/colorToString.ts b/invokeai/frontend/web/src/features/canvas/util/colorToString.ts index a4b619c5de..25d79fed5a 100644 --- a/invokeai/frontend/web/src/features/canvas/util/colorToString.ts +++ b/invokeai/frontend/web/src/features/canvas/util/colorToString.ts @@ -1,6 +1,11 @@ -import type { RgbaColor } from 'react-colorful'; +import type { RgbaColor, RgbColor } from 'react-colorful'; export const rgbaColorToString = (color: RgbaColor): string => { const { r, g, b, a } = color; return `rgba(${r}, ${g}, ${b}, ${a})`; }; + +export const rgbColorToString = (color: RgbColor): string => { + const { r, g, b } = color; + return `rgba(${r}, ${g}, ${b})`; +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx new file mode 100644 index 0000000000..2b10cb9676 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/AddLayerButton.tsx @@ -0,0 +1,13 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { layerAdded } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { useCallback } from 'react'; + +export const AddLayerButton = () => { + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + dispatch(layerAdded('promptRegionLayer')); + }, [dispatch]); + + return ; +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/BrushPreview.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/BrushPreview.tsx new file mode 100644 index 0000000000..4796e4a170 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/BrushPreview.tsx @@ -0,0 +1,23 @@ +import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { rgbColorToString } from 'features/canvas/util/colorToString'; +import { $cursorPosition } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { Circle } from 'react-konva'; + +export const BrushPreview = () => { + const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize); + const color = useAppSelector((s) => { + const _color = s.regionalPrompts.layers.find((l) => l.id === s.regionalPrompts.selectedLayer)?.color; + if (!_color) { + return null; + } + return rgbColorToString(_color); + }); + const pos = useStore($cursorPosition); + + if (!brushSize || !color || !pos) { + return null; + } + + return ; +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/BrushSize.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/BrushSize.tsx new file mode 100644 index 0000000000..be9e5a3b27 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/BrushSize.tsx @@ -0,0 +1,22 @@ +import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { brushSizeChanged } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { useCallback } from 'react'; + +export const BrushSize = () => { + const dispatch = useAppDispatch(); + const brushSize = useAppSelector((s) => s.regionalPrompts.brushSize); + const onChange = useCallback( + (v: number) => { + dispatch(brushSizeChanged(v)); + }, + [dispatch] + ); + return ( + + Brush Size + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/DeleteLayerButton.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/DeleteLayerButton.tsx new file mode 100644 index 0000000000..dd858aba8c --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/DeleteLayerButton.tsx @@ -0,0 +1,17 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { layerDeleted } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { useCallback } from 'react'; + +type Props = { + id: string; +}; + +export const DeleteLayerButton = ({ id }: Props) => { + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + dispatch(layerDeleted(id)); + }, [dispatch, id]); + + return ; +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerColorPicker.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerColorPicker.tsx new file mode 100644 index 0000000000..1edab785f1 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerColorPicker.tsx @@ -0,0 +1,35 @@ +import { Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { ColorPreview } from 'common/components/ColorPreview'; +import RgbColorPicker from 'common/components/RgbColorPicker'; +import { useLayer } from 'features/regionalPrompts/hooks/useLayer'; +import { promptRegionLayerColorChanged } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { useCallback } from 'react'; +import type { RgbColor } from 'react-colorful'; + +type Props = { + id: string; +}; + +export const LayerColorPicker = ({ id }: Props) => { + const layer = useLayer(id); + const dispatch = useAppDispatch(); + const onColorChange = useCallback( + (color: RgbColor) => { + dispatch(promptRegionLayerColorChanged({ layerId: id, color })); + }, + [dispatch, id] + ); + return ( + + + + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/LayerListItem.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerListItem.tsx new file mode 100644 index 0000000000..d1b6aa03be --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/LayerListItem.tsx @@ -0,0 +1,32 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { DeleteLayerButton } from 'features/regionalPrompts/components/DeleteLayerButton'; +import { LayerColorPicker } from 'features/regionalPrompts/components/LayerColorPicker'; +import { RegionalPromptsPrompt } from 'features/regionalPrompts/components/RegionalPromptsPrompt'; +import { ResetLayerButton } from 'features/regionalPrompts/components/ResetLayerButton'; +import { layerSelected } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { useCallback, useMemo } from 'react'; + +type Props = { + id: string; +}; + +export const LayerListItem = ({ id }: Props) => { + const dispatch = useAppDispatch(); + const selectedLayer = useAppSelector((s) => s.regionalPrompts.selectedLayer); + const border = useMemo(() => (selectedLayer === id ? '1px solid red' : 'none'), [selectedLayer, id]); + const onClickCapture = useCallback(() => { + // Must be capture so that the layer is selected before deleting/resetting/etc + dispatch(layerSelected(id)); + }, [dispatch, id]); + return ( + + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx index 86785a4219..df50d07cda 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/LineComponent.tsx @@ -1,13 +1,15 @@ -import { rgbaColorToString } from 'features/canvas/util/colorToString'; +import { rgbColorToString } from 'features/canvas/util/colorToString'; import { useTransform } from 'features/regionalPrompts/hooks/useTransform'; import type { LineObject } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import type { RgbColor } from 'react-colorful'; import { Line } from 'react-konva'; type Props = { line: LineObject; + color: RgbColor; }; -export const LineComponent = ({ line }: Props) => { +export const LineComponent = ({ line, color }: Props) => { const { shapeRef } = useTransform(line); return ( @@ -15,9 +17,13 @@ export const LineComponent = ({ line }: Props) => { ref={shapeRef} key={line.id} points={line.points} - stroke={rgbaColorToString(line.color)} strokeWidth={line.strokeWidth} - draggable + stroke={rgbColorToString(color)} + tension={0} + lineCap="round" + lineJoin="round" + shadowForStrokeEnabled={false} + listening={false} /> ); }; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RectComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RectComponent.tsx index e2b83a47e7..1401731dd2 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/RectComponent.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RectComponent.tsx @@ -1,13 +1,15 @@ -import { rgbaColorToString } from 'features/canvas/util/colorToString'; +import { rgbColorToString } from 'features/canvas/util/colorToString'; import { useTransform } from 'features/regionalPrompts/hooks/useTransform'; import type { FillRectObject } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import type { RgbColor } from 'react-colorful'; import { Rect } from 'react-konva'; type Props = { rect: FillRectObject; + color: RgbColor; }; -export const RectComponent = ({ rect }: Props) => { +export const RectComponent = ({ rect, color }: Props) => { const { shapeRef } = useTransform(rect); return ( @@ -18,8 +20,8 @@ export const RectComponent = ({ rect }: Props) => { y={rect.y} width={rect.width} height={rect.height} - fill={rgbaColorToString(rect.color)} - draggable + fill={rgbColorToString(color)} + listening={false} /> ); }; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx index 907f8a2c69..525aee02d3 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx @@ -1,23 +1,28 @@ import { Flex } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; +import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton'; +import { BrushSize } from 'features/regionalPrompts/components/BrushSize'; +import { LayerListItem } from 'features/regionalPrompts/components/LayerListItem'; import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage'; -import { layersSelectors, selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice'; -const selectLayers = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => - layersSelectors.selectAll(regionalPrompts) +const selectLayerIds = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => + regionalPrompts.layers.map((l) => l.id) ); export const RegionalPromptsEditor = () => { - const layers = useAppSelector(selectLayers); + const layerIds = useAppSelector(selectLayerIds); return ( - - - {layers.map((layer) => ( - {layer.prompt} + + + + + {layerIds.map((id) => ( + ))} - + diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPrompt.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPrompt.tsx new file mode 100644 index 0000000000..1ae1f55f67 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsPrompt.tsx @@ -0,0 +1,73 @@ +import { Box, Textarea } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton'; +import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; +import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; +import { PromptPopover } from 'features/prompt/PromptPopover'; +import { usePrompt } from 'features/prompt/usePrompt'; +import { useLayer } from 'features/regionalPrompts/hooks/useLayer'; +import { promptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { SDXLConcatButton } from 'features/sdxl/components/SDXLPrompts/SDXLConcatButton'; +import { memo, useCallback, useRef } from 'react'; +import type { HotkeyCallback } from 'react-hotkeys-hook'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; + +type Props = { + layerId: string; +}; + +export const RegionalPromptsPrompt = memo((props: Props) => { + const layer = useLayer(props.layerId); + const dispatch = useAppDispatch(); + const baseModel = useAppSelector((s) => s.generation.model)?.base; + const textareaRef = useRef(null); + const { t } = useTranslation(); + const handleChange = useCallback( + (v: string) => { + dispatch(promptChanged({ layerId: props.layerId, prompt: v })); + }, + [dispatch, props.layerId] + ); + const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown, onFocus } = usePrompt({ + prompt: layer.prompt, + textareaRef: textareaRef, + onChange: handleChange, + }); + const focus: HotkeyCallback = useCallback( + (e) => { + onFocus(); + e.preventDefault(); + }, + [onFocus] + ); + + useHotkeys('alt+a', focus, []); + + return ( + + +