Merge branch 'main' into lstein/feat/simple-mm2-api

This commit is contained in:
Lincoln Stein
2024-05-04 17:01:15 -04:00
committed by GitHub
163 changed files with 3124 additions and 3148 deletions

View File

@ -58,7 +58,7 @@
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/inter": "^5.0.17",
"@invoke-ai/ui-library": "^0.0.21",
"@invoke-ai/ui-library": "^0.0.25",
"@nanostores/react": "^0.7.2",
"@reduxjs/toolkit": "2.2.2",
"@roarr/browser-log-writer": "^1.3.0",

View File

@ -7,7 +7,7 @@ settings:
dependencies:
'@chakra-ui/react':
specifier: ^2.8.2
version: 2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0)
version: 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/react-use-size':
specifier: ^2.1.0
version: 2.1.0(react@18.2.0)
@ -30,8 +30,8 @@ dependencies:
specifier: ^5.0.17
version: 5.0.17
'@invoke-ai/ui-library':
specifier: ^0.0.21
version: 0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.2)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0)
specifier: ^0.0.25
version: 0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.3)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0)
'@nanostores/react':
specifier: ^0.7.2
version: 0.7.2(nanostores@0.10.0)(react@18.2.0)
@ -306,7 +306,7 @@ packages:
'@jridgewell/trace-mapping': 0.3.25
dev: true
/@ark-ui/anatomy@1.3.0(@internationalized/date@3.5.2):
/@ark-ui/anatomy@1.3.0(@internationalized/date@3.5.3):
resolution: {integrity: sha512-1yG2MrzUlix6KthjQMCNiHnkXrWwEdFAX6D+HqGJaNu0XvaGul2J+wDNtjsdX+gxiWu1nXXEEOAWlFVYMUf65w==}
dependencies:
'@zag-js/accordion': 0.32.1
@ -318,7 +318,7 @@ packages:
'@zag-js/color-utils': 0.32.1
'@zag-js/combobox': 0.32.1
'@zag-js/date-picker': 0.32.1
'@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2)
'@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3)
'@zag-js/dialog': 0.32.1
'@zag-js/editable': 0.32.1
'@zag-js/file-upload': 0.32.1
@ -345,13 +345,13 @@ packages:
- '@internationalized/date'
dev: false
/@ark-ui/react@1.3.0(@internationalized/date@3.5.2)(react-dom@18.2.0)(react@18.2.0):
/@ark-ui/react@1.3.0(@internationalized/date@3.5.3)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-JHjNoIX50+mUCTaEGMjfGQWGGi31pKsV646jZJlR/1xohpYJigzg8BvO97cTsVk8fwtur+cm11gz3Nf7f5QUnA==}
peerDependencies:
react: '>=18.0.0'
react-dom: '>=18.0.0'
dependencies:
'@ark-ui/anatomy': 1.3.0(@internationalized/date@3.5.2)
'@ark-ui/anatomy': 1.3.0(@internationalized/date@3.5.3)
'@zag-js/accordion': 0.32.1
'@zag-js/avatar': 0.32.1
'@zag-js/carousel': 0.32.1
@ -361,7 +361,7 @@ packages:
'@zag-js/combobox': 0.32.1
'@zag-js/core': 0.32.1
'@zag-js/date-picker': 0.32.1
'@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2)
'@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3)
'@zag-js/dialog': 0.32.1
'@zag-js/editable': 0.32.1
'@zag-js/file-upload': 0.32.1
@ -1681,7 +1681,7 @@ packages:
react: 18.2.0
dev: false
/@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0):
/@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0):
resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==}
peerDependencies:
'@chakra-ui/system': '>=2.0.0'
@ -1694,9 +1694,9 @@ packages:
'@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0)
framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0)
framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
@ -1848,16 +1848,6 @@ packages:
react: 18.2.0
dev: false
/@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.3)(react@18.2.0):
resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==}
peerDependencies:
'@emotion/react': '>=10.0.35'
react: '>=18'
dependencies:
'@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0)
react: 18.2.0
dev: false
/@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.4)(react@18.2.0):
resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==}
peerDependencies:
@ -1905,18 +1895,6 @@ packages:
resolution: {integrity: sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==}
dev: false
/@chakra-ui/focus-lock@2.1.0(@types/react@18.2.59)(react@18.2.0):
resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==}
peerDependencies:
react: '>=18'
dependencies:
'@chakra-ui/dom-utils': 2.1.0
react: 18.2.0
react-focus-lock: 2.11.1(@types/react@18.2.59)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@chakra-ui/focus-lock@2.1.0(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==}
peerDependencies:
@ -1924,7 +1902,7 @@ packages:
dependencies:
'@chakra-ui/dom-utils': 2.1.0
react: 18.2.0
react-focus-lock: 2.11.2(@types/react@18.2.73)(react@18.2.0)
react-focus-lock: 2.11.1(@types/react@18.2.73)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
@ -2100,59 +2078,6 @@ packages:
react: 18.2.0
dev: false
/@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0):
resolution: {integrity: sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==}
peerDependencies:
'@chakra-ui/system': '>=2.0.0'
framer-motion: '>=4.0.0'
react: '>=18'
dependencies:
'@chakra-ui/clickable': 2.1.0(react@18.2.0)
'@chakra-ui/descendant': 3.1.0(react@18.2.0)
'@chakra-ui/lazy-utils': 2.0.5
'@chakra-ui/popper': 3.1.0(react@18.2.0)
'@chakra-ui/react-children-utils': 2.0.6(react@18.2.0)
'@chakra-ui/react-context': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-outside-click': 2.2.0(react@18.2.0)
'@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0)
framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
/@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==}
peerDependencies:
'@chakra-ui/system': '>=2.0.0'
framer-motion: '>=4.0.0'
react: '>=18'
react-dom: '>=18'
dependencies:
'@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.59)(react@18.2.0)
'@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/react-context': 2.1.0(react@18.2.0)
'@chakra-ui/react-types': 2.0.7(react@18.2.0)
'@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0)
aria-hidden: 1.2.3
framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.7(@types/react@18.2.59)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==}
peerDependencies:
@ -2170,11 +2095,37 @@ packages:
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0)
aria-hidden: 1.2.4
aria-hidden: 1.2.3
framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.9(@types/react@18.2.73)(react@18.2.0)
react-remove-scroll: 2.5.7(@types/react@18.2.73)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==}
peerDependencies:
'@chakra-ui/system': '>=2.0.0'
framer-motion: '>=4.0.0'
react: '>=18'
react-dom: '>=18'
dependencies:
'@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0)
'@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/react-context': 2.1.0(react@18.2.0)
'@chakra-ui/react-types': 2.0.7(react@18.2.0)
'@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0)
aria-hidden: 1.2.3
framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.7(@types/react@18.2.73)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
@ -2248,7 +2199,7 @@ packages:
react: 18.2.0
dev: false
/@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0):
/@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0):
resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==}
peerDependencies:
'@chakra-ui/system': '>=2.0.0'
@ -2266,8 +2217,8 @@ packages:
'@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0)
framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
@ -2305,25 +2256,6 @@ packages:
react: 18.2.0
dev: false
/@chakra-ui/provider@2.4.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==}
peerDependencies:
'@emotion/react': ^11.0.0
'@emotion/styled': ^11.0.0
react: '>=18'
react-dom: '>=18'
dependencies:
'@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.3)(react@18.2.0)
'@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/react-env': 3.1.0(react@18.2.0)
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/utils': 2.0.15
'@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@chakra-ui/provider@2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==}
peerDependencies:
@ -2554,77 +2486,6 @@ packages:
react: 18.2.0
dev: false
/@chakra-ui/react@2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==}
peerDependencies:
'@emotion/react': ^11.0.0
'@emotion/styled': ^11.0.0
framer-motion: '>=4.0.0'
react: '>=18'
react-dom: '>=18'
dependencies:
'@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0)
'@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/counter': 2.1.0(react@18.2.0)
'@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.3)(react@18.2.0)
'@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.59)(react@18.2.0)
'@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/hooks': 2.2.1(react@18.2.0)
'@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/live-region': 2.1.0(react@18.2.0)
'@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0)
'@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.59)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0)
'@chakra-ui/popper': 3.1.0(react@18.2.0)
'@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/provider': 2.4.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/react-env': 3.1.0(react@18.2.0)
'@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/styled-system': 2.9.2
'@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0)
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2)
'@chakra-ui/theme-utils': 2.0.21
'@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/transition': 2.1.0(framer-motion@11.0.6)(react@18.2.0)
'@chakra-ui/utils': 2.0.15
'@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0)
framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==}
peerDependencies:
@ -2696,6 +2557,77 @@ packages:
- '@types/react'
dev: false
/@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==}
peerDependencies:
'@emotion/react': ^11.0.0
'@emotion/styled': ^11.0.0
framer-motion: '>=4.0.0'
react: '>=18'
react-dom: '>=18'
dependencies:
'@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0)
'@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/counter': 2.1.0(react@18.2.0)
'@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.2.0)
'@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0)
'@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/hooks': 2.2.1(react@18.2.0)
'@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/live-region': 2.1.0(react@18.2.0)
'@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0)
'@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0)
'@chakra-ui/popper': 3.1.0(react@18.2.0)
'@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/react-env': 3.1.0(react@18.2.0)
'@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/styled-system': 2.9.2
'@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0)
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2)
'@chakra-ui/theme-utils': 2.0.21
'@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0)
'@chakra-ui/utils': 2.0.15
'@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0)
'@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0)
framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0):
resolution: {integrity: sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==}
peerDependencies:
@ -2814,7 +2746,7 @@ packages:
react: 18.2.0
dev: false
/@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react@18.2.0):
/@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0):
resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==}
peerDependencies:
'@chakra-ui/system': '>=2.0.0'
@ -2823,30 +2755,11 @@ packages:
dependencies:
'@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0)
framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
/@chakra-ui/system@2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0):
resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==}
peerDependencies:
'@emotion/react': ^11.0.0
'@emotion/styled': ^11.0.0
react: '>=18'
dependencies:
'@chakra-ui/color-mode': 2.2.0(react@18.2.0)
'@chakra-ui/object-utils': 2.1.0
'@chakra-ui/react-utils': 2.0.12(react@18.2.0)
'@chakra-ui/styled-system': 2.9.2
'@chakra-ui/theme-utils': 2.0.21
'@chakra-ui/utils': 2.0.15
'@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0)
react: 18.2.0
react-fast-compare: 3.2.2
dev: false
/@chakra-ui/system@2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0):
resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==}
peerDependencies:
@ -2975,7 +2888,7 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0):
/@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==}
peerDependencies:
'@chakra-ui/system': 2.6.2
@ -2991,9 +2904,9 @@ packages:
'@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/styled-system': 2.9.2
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
'@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2)
framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0)
framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
@ -3020,7 +2933,7 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.6)(react-dom@18.2.0)(react@18.2.0):
/@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==}
peerDependencies:
'@chakra-ui/system': '>=2.0.0'
@ -3036,8 +2949,8 @@ packages:
'@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0)
'@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0)
'@chakra-ui/shared-utils': 2.0.5
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0)
framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)
framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
@ -3064,17 +2977,6 @@ packages:
react: 18.2.0
dev: false
/@chakra-ui/transition@2.1.0(framer-motion@11.0.6)(react@18.2.0):
resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==}
peerDependencies:
framer-motion: '>=4.0.0'
react: '>=18'
dependencies:
'@chakra-ui/shared-utils': 2.0.5
framer-motion: 11.0.6(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
/@chakra-ui/utils@2.0.15:
resolution: {integrity: sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==}
dependencies:
@ -3198,12 +3100,6 @@ packages:
dev: false
optional: true
/@emotion/is-prop-valid@1.2.1:
resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==}
dependencies:
'@emotion/memoize': 0.8.1
dev: false
/@emotion/is-prop-valid@1.2.2:
resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==}
dependencies:
@ -3220,27 +3116,6 @@ packages:
resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==}
dev: false
/@emotion/react@11.11.3(@types/react@18.2.59)(react@18.2.0):
resolution: {integrity: sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==}
peerDependencies:
'@types/react': '*'
react: '>=16.8.0'
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@emotion/babel-plugin': 11.11.0
'@emotion/cache': 11.11.0
'@emotion/serialize': 1.1.3
'@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0)
'@emotion/utils': 1.2.1
'@emotion/weak-memoize': 0.3.1
'@types/react': 18.2.59
hoist-non-react-statics: 3.3.2
react: 18.2.0
dev: false
/@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==}
peerDependencies:
@ -3276,27 +3151,6 @@ packages:
resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==}
dev: false
/@emotion/styled@11.11.0(@emotion/react@11.11.3)(@types/react@18.2.59)(react@18.2.0):
resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==}
peerDependencies:
'@emotion/react': ^11.0.0-rc.0
'@types/react': '*'
react: '>=16.8.0'
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@emotion/babel-plugin': 11.11.0
'@emotion/is-prop-valid': 1.2.1
'@emotion/react': 11.11.3(@types/react@18.2.59)(react@18.2.0)
'@emotion/serialize': 1.1.3
'@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0)
'@emotion/utils': 1.2.1
'@types/react': 18.2.59
react: 18.2.0
dev: false
/@emotion/styled@11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==}
peerDependencies:
@ -3663,16 +3517,16 @@ packages:
resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==}
dev: true
/@internationalized/date@3.5.2:
resolution: {integrity: sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==}
/@internationalized/date@3.5.3:
resolution: {integrity: sha512-X9bi8NAEHAjD8yzmPYT2pdJsbe+tYSEBAfowtlxJVJdZR3aK8Vg7ZUT1Fm5M47KLzp/M1p1VwAaeSma3RT7biw==}
dependencies:
'@swc/helpers': 0.5.7
'@swc/helpers': 0.5.11
dev: false
/@internationalized/number@3.5.1:
resolution: {integrity: sha512-N0fPU/nz15SwR9IbfJ5xaS9Ss/O5h1sVXMZf43vc9mxEG48ovglvvzBjF53aHlq20uoR6c+88CrIXipU/LSzwg==}
dependencies:
'@swc/helpers': 0.5.7
'@swc/helpers': 0.5.11
dev: false
/@invoke-ai/eslint-config-react@0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.3):
@ -3709,14 +3563,14 @@ packages:
prettier: 3.2.5
dev: true
/@invoke-ai/ui-library@0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.2)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-tCvgkBPDt0gNq+8IcR03e/Mw7R8Mb/SMXTqx3FEIxlTQEo93A/D38dKXeDCzTdx4sQ+sknfB+JLBbHs6sg5hhQ==}
/@invoke-ai/ui-library@0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.3)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Fmjdlu62NXHgairYXGjcuCrxPEAl1G6Q6ban8g3excF6pDDdBeS7CmSNCyEDMxnSIOZrQlI04OhaMB17Imi9Uw==}
peerDependencies:
'@fontsource-variable/inter': ^5.0.16
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
'@ark-ui/react': 1.3.0(@internationalized/date@3.5.2)(react-dom@18.2.0)(react@18.2.0)
'@ark-ui/react': 1.3.0(@internationalized/date@3.5.3)(react-dom@18.2.0)(react@18.2.0)
'@chakra-ui/anatomy': 2.2.2
'@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0)
'@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0)
@ -5381,8 +5235,8 @@ packages:
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
dev: true
/@swc/helpers@0.5.7:
resolution: {integrity: sha512-BVvNZhx362+l2tSwSuyEUV4h7+jk9raNdoTSdLfwTshXJSaGmYKluGRJznziCI3KX02Z19DdsQrdfrpXAU3Hfg==}
/@swc/helpers@0.5.11:
resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==}
dependencies:
tslib: 2.6.2
dev: false
@ -5844,10 +5698,6 @@ packages:
resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==}
dev: true
/@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
dev: false
/@types/prop-types@15.7.12:
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
@ -5877,14 +5727,6 @@ packages:
'@types/react': 18.2.73
dev: false
/@types/react@18.2.59:
resolution: {integrity: sha512-DE+F6BYEC8VtajY85Qr7mmhTd/79rJKIHCg99MU9SWPB4xvLb6D1za2vYflgZfmPqQVEr6UqJTnLXEwzpVPuOg==}
dependencies:
'@types/prop-types': 15.7.11
'@types/scheduler': 0.16.8
csstype: 3.1.3
dev: false
/@types/react@18.2.73:
resolution: {integrity: sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==}
dependencies:
@ -5895,10 +5737,6 @@ packages:
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==}
dev: true
/@types/scheduler@0.16.8:
resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==}
dev: false
/@types/semver@7.5.8:
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
dev: true
@ -6405,10 +6243,10 @@ packages:
/@zag-js/date-picker@0.32.1:
resolution: {integrity: sha512-n/hYmF+/R4+NuyfPRzCgeuLT6LJihKSuKzK29STPWy3sC/tBBHiqhNv1/4UKbatHUJXdBW2XF+N8Rw08RffcFQ==}
dependencies:
'@internationalized/date': 3.5.2
'@internationalized/date': 3.5.3
'@zag-js/anatomy': 0.32.1
'@zag-js/core': 0.32.1
'@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2)
'@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3)
'@zag-js/dismissable': 0.32.1
'@zag-js/dom-event': 0.32.1
'@zag-js/dom-query': 0.32.1
@ -6420,12 +6258,12 @@ packages:
'@zag-js/utils': 0.32.1
dev: false
/@zag-js/date-utils@0.32.1(@internationalized/date@3.5.2):
/@zag-js/date-utils@0.32.1(@internationalized/date@3.5.3):
resolution: {integrity: sha512-dbBDRSVr5pRUw3rXndyGuSshZiWqQI5JQO4D2KIFGkXzorj6WzoOpcO910Z7AdM/9cCAMpCjUrka8d8o9BpJBg==}
peerDependencies:
'@internationalized/date': '>=3.0.0'
dependencies:
'@internationalized/date': 3.5.2
'@internationalized/date': 3.5.3
dev: false
/@zag-js/dialog@0.32.1:
@ -6999,13 +6837,6 @@ packages:
tslib: 2.6.2
dev: false
/aria-hidden@1.2.4:
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
engines: {node: '>=10'}
dependencies:
tslib: 2.6.2
dev: false
/aria-query@5.1.3:
resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
dependencies:
@ -9026,13 +8857,6 @@ packages:
tslib: 2.6.2
dev: false
/focus-lock@1.3.4:
resolution: {integrity: sha512-Gv0N3mvej3pD+HWkNryrF8sExzEHqhQ6OSFxD4DPxm9n5HGCaHme98ZMBZroNEAJcsdtHxk+skvThGKyUeoEGA==}
engines: {node: '>=10'}
dependencies:
tslib: 2.6.2
dev: false
/focus-trap@7.5.4:
resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==}
dependencies:
@ -9095,24 +8919,6 @@ packages:
tslib: 2.6.2
dev: false
/framer-motion@11.0.6(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-BpO3mWF8UwxzO3Ca5AmSkrg14QYTeJa9vKgoLOoBdBdTPj0e81i1dMwnX6EQJXRieUx20uiDBXq8bA6y7N6b8Q==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.6.2
optionalDependencies:
'@emotion/is-prop-valid': 0.8.8
dev: false
/framesync@6.1.2:
resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==}
dependencies:
@ -11485,7 +11291,7 @@ packages:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
dev: false
/react-focus-lock@2.11.1(@types/react@18.2.59)(react@18.2.0):
/react-focus-lock@2.11.1(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
@ -11495,31 +11301,12 @@ packages:
optional: true
dependencies:
'@babel/runtime': 7.23.9
'@types/react': 18.2.59
'@types/react': 18.2.73
focus-lock: 1.3.3
prop-types: 15.8.1
react: 18.2.0
react-clientside-effect: 1.2.6(react@18.2.0)
use-callback-ref: 1.3.1(@types/react@18.2.59)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.2.59)(react@18.2.0)
dev: false
/react-focus-lock@2.11.2(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-DDTbEiov0+RthESPVSTIdAWPPKic+op3sCcP+icbMRobvQNt7LuAlJ3KoarqQv5sCgKArru3kXmlmFTa27/CdQ==}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.24.1
'@types/react': 18.2.73
focus-lock: 1.3.4
prop-types: 15.8.1
react: 18.2.0
react-clientside-effect: 1.2.6(react@18.2.0)
use-callback-ref: 1.3.2(@types/react@18.2.73)(react@18.2.0)
use-callback-ref: 1.3.1(@types/react@18.2.73)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0)
dev: false
@ -11634,25 +11421,9 @@ packages:
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/react-remove-scroll-bar@2.3.5(@types/react@18.2.59)(react@18.2.0):
/react-remove-scroll-bar@2.3.5(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.59
react: 18.2.0
react-style-singleton: 2.2.1(@types/react@18.2.59)(react@18.2.0)
tslib: 2.6.2
dev: false
/react-remove-scroll-bar@2.3.6(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
@ -11666,28 +11437,9 @@ packages:
tslib: 2.6.2
dev: false
/react-remove-scroll@2.5.7(@types/react@18.2.59)(react@18.2.0):
/react-remove-scroll@2.5.7(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.59
react: 18.2.0
react-remove-scroll-bar: 2.3.5(@types/react@18.2.59)(react@18.2.0)
react-style-singleton: 2.2.1(@types/react@18.2.59)(react@18.2.0)
tslib: 2.6.2
use-callback-ref: 1.3.1(@types/react@18.2.59)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.2.59)(react@18.2.0)
dev: false
/react-remove-scroll@2.5.9(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-bvHCLBrFfM2OgcrpPY2YW84sPdS2o2HKWJUf1xGyGLnSoEnOTOBpahIarjRuYtN0ryahCeP242yf+5TrBX/pZA==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
@ -11697,10 +11449,10 @@ packages:
dependencies:
'@types/react': 18.2.73
react: 18.2.0
react-remove-scroll-bar: 2.3.6(@types/react@18.2.73)(react@18.2.0)
react-remove-scroll-bar: 2.3.5(@types/react@18.2.73)(react@18.2.0)
react-style-singleton: 2.2.1(@types/react@18.2.73)(react@18.2.0)
tslib: 2.6.2
use-callback-ref: 1.3.2(@types/react@18.2.73)(react@18.2.0)
use-callback-ref: 1.3.1(@types/react@18.2.73)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0)
dev: false
@ -11756,23 +11508,6 @@ packages:
- '@types/react'
dev: false
/react-style-singleton@2.2.1(@types/react@18.2.59)(react@18.2.0):
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.59
get-nonce: 1.0.1
invariant: 2.2.4
react: 18.2.0
tslib: 2.6.2
dev: false
/react-style-singleton@2.2.1(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@ -13288,24 +13023,9 @@ packages:
punycode: 2.3.1
dev: true
/use-callback-ref@1.3.1(@types/react@18.2.59)(react@18.2.0):
/use-callback-ref@1.3.1(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.59
react: 18.2.0
tslib: 2.6.2
dev: false
/use-callback-ref@1.3.2(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
@ -13358,22 +13078,6 @@ packages:
react: 18.2.0
dev: false
/use-sidecar@1.1.2(@types/react@18.2.59)(react@18.2.0):
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.59
detect-node-es: 1.1.0
react: 18.2.0
tslib: 2.6.2
dev: false
/use-sidecar@1.1.2(@types/react@18.2.73)(react@18.2.0):
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}

View File

@ -88,11 +88,13 @@
"negativePrompt": "Negative Prompt",
"discordLabel": "Discord",
"dontAskMeAgain": "Don't ask me again",
"editor": "Editor",
"error": "Error",
"file": "File",
"folder": "Folder",
"format": "format",
"githubLabel": "Github",
"goTo": "Go to",
"hotkeysLabel": "Hotkeys",
"imageFailedToLoad": "Unable to Load Image",
"img2img": "Image To Image",
@ -140,7 +142,8 @@
"blue": "Blue",
"alpha": "Alpha",
"selected": "Selected",
"viewer": "Viewer"
"viewer": "Viewer",
"tab": "Tab"
},
"controlnet": {
"controlAdapter_one": "Control Adapter",
@ -361,7 +364,8 @@
"bulkDownloadRequestFailed": "Problem Preparing Download",
"bulkDownloadFailed": "Download Failed",
"problemDeletingImages": "Problem Deleting Images",
"problemDeletingImagesDesc": "One or more images could not be deleted"
"problemDeletingImagesDesc": "One or more images could not be deleted",
"switchTo": "Switch to {{ tab }} (Z)"
},
"hotkeys": {
"searchHotkeys": "Search Hotkeys",
@ -584,6 +588,14 @@
"upscale": {
"desc": "Upscale the current image",
"title": "Upscale"
},
"backToEditor": {
"desc": "Closes the Image Viewer and shows the Editor View (Text to Image tab only)",
"title": "Back to Editor"
},
"openImageViewer": {
"desc": "Opens the Image Viewer (Text to Image tab only)",
"title": "Open Image Viewer"
}
},
"metadata": {
@ -1522,7 +1534,7 @@
"moveForward": "Move Forward",
"moveBackward": "Move Backward",
"brushSize": "Brush Size",
"controlLayers": "Control Layers (BETA)",
"controlLayers": "Control Layers",
"globalMaskOpacity": "Global Mask Opacity",
"autoNegative": "Auto Negative",
"toggleVisibility": "Toggle Layer Visibility",
@ -1543,8 +1555,25 @@
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
"globalIPAdapter": "Global $t(common.ipAdapter)",
"globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)",
"globalInitialImage": "Global Initial Image",
"globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)",
"opacityFilter": "Opacity Filter",
"clearProcessor": "Clear Processor",
"resetProcessor": "Reset Processor to Defaults"
"resetProcessor": "Reset Processor to Defaults",
"noLayersAdded": "No Layers Added"
},
"ui": {
"tabs": {
"generation": "Generation",
"generationTab": "$t(ui.tabs.generation) $t(common.tab)",
"canvas": "Canvas",
"canvasTab": "$t(ui.tabs.canvas) $t(common.tab)",
"workflows": "Workflows",
"workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)",
"models": "Models",
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
"queue": "Queue",
"queueTab": "$t(ui.tabs.queue) $t(common.tab)"
}
}
}

View File

@ -20,8 +20,7 @@ export type LoggerNamespace =
| 'models'
| 'config'
| 'canvas'
| 'txt2img'
| 'img2img'
| 'generation'
| 'nodes'
| 'system'
| 'socketio'

View File

@ -32,7 +32,6 @@ import { addImagesStarredListener } from 'app/store/middleware/listenerMiddlewar
import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred';
import { addImageToDeleteSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected';
import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded';
import { addInitialImageSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/initialImageSelected';
import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected';
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged';
@ -73,9 +72,6 @@ const startAppListening = listenerMiddleware.startListening as AppStartListening
// Image uploaded
addImageUploadedFulfilledListener(startAppListening);
// Image selected
addInitialImageSelectedListener(startAppListening);
// Image deleted
addRequestedSingleImageDeletionListener(startAppListening);
addDeleteBoardAndImagesFulfilledListener(startAppListening);

View File

@ -1,9 +1,9 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { imagesApi } from 'services/api/endpoints/images';
export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => {
@ -14,19 +14,14 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
// Remove all deleted images from the UI
let wasInitialImageReset = false;
let wasCanvasReset = false;
let wasNodeEditorReset = false;
let wereControlAdaptersReset = false;
let wereControlLayersReset = false;
const { generation, canvas, nodes, controlAdapters } = getState();
const { canvas, nodes, controlAdapters, controlLayers } = getState();
deleted_images.forEach((image_name) => {
const imageUsage = getImageUsage(generation, canvas, nodes, controlAdapters, image_name);
if (imageUsage.isInitialImage && !wasInitialImageReset) {
dispatch(clearInitialImage());
wasInitialImageReset = true;
}
const imageUsage = getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name);
if (imageUsage.isCanvasImage && !wasCanvasReset) {
dispatch(resetCanvas());
@ -42,6 +37,11 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
dispatch(controlAdaptersReset());
wereControlAdaptersReset = true;
}
if (imageUsage.isControlLayerImage && !wereControlLayersReset) {
dispatch(allLayersDeleted());
wereControlLayersReset = true;
}
});
},
});

View File

@ -48,10 +48,12 @@ export const addCanvasImageToControlNetListener = (startAppListening: AppStartLi
})
).unwrap();
const { image_name } = imageDTO;
dispatch(
controlAdapterImageChanged({
id,
controlImage: imageDTO,
controlImage: image_name,
})
);
},

View File

@ -58,10 +58,12 @@ export const addCanvasMaskToControlNetListener = (startAppListening: AppStartLis
})
).unwrap();
const { image_name } = imageDTO;
dispatch(
controlAdapterImageChanged({
id,
controlImage: imageDTO,
controlImage: image_name,
})
);
},

View File

@ -10,7 +10,7 @@ import {
caLayerProcessorConfigChanged,
isControlAdapterLayer,
} from 'features/controlLayers/store/controlLayersSlice';
import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
import { isImageOutput } from 'features/nodes/types/common';
import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next';
@ -76,7 +76,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
}
// @ts-expect-error: TS isn't able to narrow the typing of buildNode and `config` will error...
const processorNode = CONTROLNET_PROCESSORS[config.type].buildNode(image, config);
const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config);
const enqueueBatchArg: BatchConfig = {
prepend: true,
batch: {

View File

@ -12,7 +12,6 @@ import {
selectControlAdapterById,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { isEqual } from 'lodash-es';
type AnyControlAdapterParamChangeAction =
| ReturnType<typeof controlAdapterProcessorParamsChanged>
@ -53,11 +52,6 @@ const predicate: AnyListenerPredicate<RootState> = (action, state, prevState) =>
return false;
}
if (prevCA.controlImage === ca.controlImage && isEqual(prevCA.processorNode, ca.processorNode)) {
// Don't re-process if the processor hasn't changed
return false;
}
const isProcessorSelected = processorType !== 'none';
const hasControlImage = Boolean(controlImage);

View File

@ -91,7 +91,7 @@ export const addControlNetImageProcessedListener = (startAppListening: AppStartL
dispatch(
controlAdapterProcessedImageChanged({
id,
processedControlImage,
processedControlImage: processedControlImage.image_name,
})
);
}

View File

@ -30,7 +30,7 @@ import type { ImageDTO } from 'services/api/types';
export const addEnqueueRequestedCanvasListener = (startAppListening: AppStartListening) => {
startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> =>
enqueueRequested.match(action) && action.payload.tabName === 'unifiedCanvas',
enqueueRequested.match(action) && action.payload.tabName === 'canvas',
effect: async (action, { getState, dispatch }) => {
const log = logger('queue');
const { prepend } = action.payload;

View File

@ -1,16 +1,14 @@
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph';
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildLinearImageToImageGraph } from 'features/nodes/util/graph/buildLinearImageToImageGraph';
import { buildLinearSDXLImageToImageGraph } from 'features/nodes/util/graph/buildLinearSDXLImageToImageGraph';
import { buildLinearSDXLTextToImageGraph } from 'features/nodes/util/graph/buildLinearSDXLTextToImageGraph';
import { buildLinearTextToImageGraph } from 'features/nodes/util/graph/buildLinearTextToImageGraph';
import { queueApi } from 'services/api/endpoints/queue';
export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => {
startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> =>
enqueueRequested.match(action) && (action.payload.tabName === 'txt2img' || action.payload.tabName === 'img2img'),
enqueueRequested.match(action) && action.payload.tabName === 'generation',
effect: async (action, { getState, dispatch }) => {
const state = getState();
const model = state.generation.model;
@ -19,17 +17,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
let graph;
if (model && model.base === 'sdxl') {
if (action.payload.tabName === 'txt2img') {
graph = await buildLinearSDXLTextToImageGraph(state);
} else {
graph = await buildLinearSDXLImageToImageGraph(state);
}
graph = await buildGenerationTabSDXLGraph(state);
} else {
if (action.payload.tabName === 'txt2img') {
graph = await buildLinearTextToImageGraph(state);
} else {
graph = await buildLinearImageToImageGraph(state);
}
graph = await buildGenerationTabGraph(state);
}
const batchConfig = prepareLinearUIBatch(state, graph, prepend);

View File

@ -8,7 +8,7 @@ import type { BatchConfig } from 'services/api/types';
export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) => {
startAppListening({
predicate: (action): action is ReturnType<typeof enqueueRequested> =>
enqueueRequested.match(action) && action.payload.tabName === 'nodes',
enqueueRequested.match(action) && action.payload.tabName === 'workflows',
effect: async (action, { getState, dispatch }) => {
const state = getState();
const { nodes, edges } = state.nodes;

View File

@ -1,7 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { selectionChanged } from 'features/gallery/store/gallerySlice';
import { isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import { imagesSelectors } from 'services/api/util';
@ -62,6 +62,7 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
} else {
dispatch(selectionChanged([imageDTO]));
}
dispatch(isImageViewerOpenChanged(true));
},
});
};

View File

@ -1,5 +1,6 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import {
controlAdapterImageChanged,
@ -7,6 +8,13 @@ import {
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import {
isControlAdapterLayer,
isInitialImageLayer,
isIPAdapterLayer,
isRegionalGuidanceLayer,
layerDeleted,
} from 'features/controlLayers/store/controlLayersSlice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
@ -14,12 +22,82 @@ import { imageSelected } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { isImageFieldInputInstance } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { clamp, forEach } from 'lodash-es';
import { api } from 'services/api';
import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import { imagesSelectors } from 'services/api/util';
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.nodes.nodes.forEach((node) => {
if (!isInvocationNode(node)) {
return;
}
forEach(node.data.inputs, (input) => {
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
dispatch(
fieldImageValueChanged({
nodeId: node.data.id,
fieldName: input.name,
value: undefined,
})
);
}
});
});
};
const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
forEach(selectControlAdapterAll(state.controlAdapters), (ca) => {
if (
ca.controlImage === imageDTO.image_name ||
(isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
) {
dispatch(
controlAdapterImageChanged({
id: ca.id,
controlImage: null,
})
);
dispatch(
controlAdapterProcessedImageChanged({
id: ca.id,
processedControlImage: null,
})
);
}
});
};
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.controlLayers.present.layers.forEach((l) => {
if (isRegionalGuidanceLayer(l)) {
if (l.ipAdapters.some((ipa) => ipa.image?.imageName === imageDTO.image_name)) {
dispatch(layerDeleted(l.id));
}
}
if (isControlAdapterLayer(l)) {
if (
l.controlAdapter.image?.imageName === imageDTO.image_name ||
l.controlAdapter.processedImage?.imageName === imageDTO.image_name
) {
dispatch(layerDeleted(l.id));
}
}
if (isIPAdapterLayer(l)) {
if (l.ipAdapter.image?.imageName === imageDTO.image_name) {
dispatch(layerDeleted(l.id));
}
}
if (isInitialImageLayer(l)) {
if (l.image?.imageName === imageDTO.image_name) {
dispatch(layerDeleted(l.id));
}
}
});
};
export const addRequestedSingleImageDeletionListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: imageDeletionConfirmed,
@ -73,50 +151,9 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
}
imageDTOs.forEach((imageDTO) => {
// reset init image if we deleted it
if (getState().generation.initialImage?.imageName === imageDTO.image_name) {
dispatch(clearInitialImage());
}
// reset control adapters that use the deleted images
forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => {
if (
ca.controlImage === imageDTO.image_name ||
(isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
) {
dispatch(
controlAdapterImageChanged({
id: ca.id,
controlImage: null,
})
);
dispatch(
controlAdapterProcessedImageChanged({
id: ca.id,
processedControlImage: null,
})
);
}
});
// reset nodes that use the deleted images
getState().nodes.nodes.forEach((node) => {
if (!isInvocationNode(node)) {
return;
}
forEach(node.data.inputs, (input) => {
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
dispatch(
fieldImageValueChanged({
nodeId: node.data.id,
fieldName: input.name,
value: undefined,
})
);
}
});
});
deleteControlAdapterImages(state, dispatch, imageDTO);
deleteNodesImages(state, dispatch, imageDTO);
deleteControlLayerImages(state, dispatch, imageDTO);
});
// Delete from server
@ -168,50 +205,9 @@ export const addRequestedSingleImageDeletionListener = (startAppListening: AppSt
}
imageDTOs.forEach((imageDTO) => {
// reset init image if we deleted it
if (getState().generation.initialImage?.imageName === imageDTO.image_name) {
dispatch(clearInitialImage());
}
// reset control adapters that use the deleted images
forEach(selectControlAdapterAll(getState().controlAdapters), (ca) => {
if (
ca.controlImage === imageDTO.image_name ||
(isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
) {
dispatch(
controlAdapterImageChanged({
id: ca.id,
controlImage: null,
})
);
dispatch(
controlAdapterProcessedImageChanged({
id: ca.id,
processedControlImage: null,
})
);
}
});
// reset nodes that use the deleted images
getState().nodes.nodes.forEach((node) => {
if (!isInvocationNode(node)) {
return;
}
forEach(node.data.inputs, (input) => {
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
dispatch(
fieldImageValueChanged({
nodeId: node.data.id,
fieldName: input.name,
value: undefined,
})
);
}
});
});
deleteControlAdapterImages(state, dispatch, imageDTO);
deleteNodesImages(state, dispatch, imageDTO);
deleteControlLayerImages(state, dispatch, imageDTO);
});
} catch {
// no-op

View File

@ -9,13 +9,14 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
caLayerImageChanged,
iiLayerImageChanged,
ipaLayerImageChanged,
rgLayerIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { imagesApi } from 'services/api/endpoints/images';
export const dndDropped = createAction<{
@ -52,18 +53,6 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
return;
}
/**
* Image dropped on initial image
*/
if (
overData.actionType === 'SET_INITIAL_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
dispatch(initialImageChanged(activeData.payload.imageDTO));
return;
}
/**
* Image dropped on ControlNet
*/
@ -76,7 +65,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
dispatch(
controlAdapterImageChanged({
id,
controlImage: activeData.payload.imageDTO,
controlImage: activeData.payload.imageDTO.image_name,
})
);
dispatch(
@ -143,6 +132,24 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
return;
}
/**
* Image dropped on II Layer Image
*/
if (
overData.actionType === 'SET_II_LAYER_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { layerId } = overData.context;
dispatch(
iiLayerImageChanged({
layerId,
imageDTO: activeData.payload.imageDTO,
})
);
return;
}
/**
* Image dropped on Canvas
*/

View File

@ -14,7 +14,6 @@ export const addImageToDeleteSelectedListener = (startAppListening: AppStartList
const isImageInUse =
imagesUsage.some((i) => i.isCanvasImage) ||
imagesUsage.some((i) => i.isInitialImage) ||
imagesUsage.some((i) => i.isControlImage) ||
imagesUsage.some((i) => i.isNodesImage);

View File

@ -8,11 +8,12 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
caLayerImageChanged,
iiLayerImageChanged,
ipaLayerImageChanged,
rgLayerIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { initialImageChanged, selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next';
import { omit } from 'lodash-es';
@ -101,7 +102,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
dispatch(
controlAdapterImageChanged({
id,
controlImage: imageDTO,
controlImage: imageDTO.image_name,
})
);
dispatch(
@ -146,15 +147,15 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
);
}
if (postUploadAction?.type === 'SET_INITIAL_IMAGE') {
dispatch(initialImageChanged(imageDTO));
if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(iiLayerImageChanged({ layerId, imageDTO }));
dispatch(
addToast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setInitialImage'),
description: t('toast.setControlImage'),
})
);
return;
}
if (postUploadAction?.type === 'SET_NODES_IMAGE') {

View File

@ -1,21 +0,0 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { initialImageSelected } from 'features/parameters/store/actions';
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next';
export const addInitialImageSelectedListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: initialImageSelected,
effect: (action, { dispatch }) => {
if (!action.payload) {
dispatch(addToast(makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' })));
return;
}
dispatch(initialImageChanged(action.payload));
dispatch(addToast(makeToast(t('toast.sentToImageToImage'))));
},
});
};

View File

@ -1,7 +1,7 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
controlAdapterModelChanged,
controlAdapterIsEnabledChanged,
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { loraRemoved } from 'features/lora/store/loraSlice';
@ -54,7 +54,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
// handle incompatible controlnets
selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
if (ca.model?.base !== newBaseModel) {
dispatch(controlAdapterModelChanged({ id: ca.id, modelConfig: null }));
dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false }));
modelsCleared += 1;
}
});

View File

@ -96,16 +96,16 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
dispatch(setScheduler(scheduler));
}
}
const setSizeOptions = { updateAspectRatio: true, clamp: true };
if (width) {
if (isParameterWidth(width)) {
dispatch(widthChanged({ width, updateAspectRatio: true }));
dispatch(widthChanged({ width, ...setSizeOptions }));
}
}
if (height) {
if (isParameterHeight(height)) {
dispatch(heightChanged({ height, updateAspectRatio: true }));
dispatch(heightChanged({ height, ...setSizeOptions }));
}
}

View File

@ -17,14 +17,10 @@ const accept: Accept = {
const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => {
let postUploadAction: PostUploadAction = { type: 'TOAST' };
if (activeTabName === 'unifiedCanvas') {
if (activeTabName === 'canvas') {
postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' };
}
if (activeTabName === 'img2img') {
postUploadAction = { type: 'SET_INITIAL_IMAGE' };
}
return postUploadAction;
});

View File

@ -67,7 +67,7 @@ export const useGlobalHotkeys = () => {
useHotkeys(
'1',
() => {
dispatch(setActiveTab('txt2img'));
dispatch(setActiveTab('generation'));
},
[dispatch]
);
@ -75,7 +75,7 @@ export const useGlobalHotkeys = () => {
useHotkeys(
'2',
() => {
dispatch(setActiveTab('img2img'));
dispatch(setActiveTab('canvas'));
},
[dispatch]
);
@ -83,31 +83,23 @@ export const useGlobalHotkeys = () => {
useHotkeys(
'3',
() => {
dispatch(setActiveTab('unifiedCanvas'));
dispatch(setActiveTab('workflows'));
},
[dispatch]
);
useHotkeys(
'4',
() => {
dispatch(setActiveTab('nodes'));
},
[dispatch]
);
useHotkeys(
'5',
() => {
if (isModelManagerEnabled) {
dispatch(setActiveTab('modelManager'));
dispatch(setActiveTab('models'));
}
},
[dispatch, isModelManagerEnabled]
);
useHotkeys(
isModelManagerEnabled ? '6' : '5',
isModelManagerEnabled ? '5' : '4',
() => {
dispatch(setActiveTab('queue'));
},

View File

@ -16,7 +16,6 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import i18n from 'i18next';
import { forEach } from 'lodash-es';
import { getConnectedEdges } from 'reactflow';
import { assert } from 'tsafe';
const selector = createMemoizedSelector(
[
@ -29,7 +28,7 @@ const selector = createMemoizedSelector(
activeTabNameSelector,
],
(controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => {
const { initialImage, model } = generation;
const { model } = generation;
const { positivePrompt } = controlLayers.present;
const { isConnected } = system;
@ -41,11 +40,7 @@ const selector = createMemoizedSelector(
reasons.push(i18n.t('parameters.invoke.systemDisconnected'));
}
if (activeTabName === 'img2img' && !initialImage) {
reasons.push(i18n.t('parameters.invoke.noInitialImageSelected'));
}
if (activeTabName === 'nodes') {
if (activeTabName === 'workflows') {
if (nodes.shouldValidateGraph) {
if (!nodes.nodes.length) {
reasons.push(i18n.t('parameters.invoke.noNodesInGraph'));
@ -98,8 +93,8 @@ const selector = createMemoizedSelector(
reasons.push(i18n.t('parameters.invoke.noModelSelected'));
}
if (activeTabName === 'txt2img') {
// Handling for Control Layers - only exists on txt2img tab now
if (activeTabName === 'generation') {
// Handling for generation tab
controlLayers.present.layers
.filter((l) => l.isEnabled)
.flatMap((l) => {
@ -110,7 +105,7 @@ const selector = createMemoizedSelector(
} else if (l.type === 'regional_guidance_layer') {
return l.ipAdapters;
}
assert(false);
return [];
})
.forEach((ca, i) => {
const hasNoModel = !ca.model;

View File

@ -22,6 +22,7 @@ import {
} from 'features/canvas/store/canvasSlice';
import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes';
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
import { memo, useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
@ -219,97 +220,107 @@ const IAICanvasToolbar = () => {
const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]);
return (
<Flex alignItems="center" gap={2} flexWrap="wrap">
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
<FormControl isDisabled={isStaging} w="5rem">
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} />
</FormControl>
</Tooltip>
<Flex w="full" gap={2} alignItems="center">
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineEnd="auto" />
</Flex>
<Flex flex={1} gap={2} justifyContent="center">
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
<FormControl isDisabled={isStaging} w="5rem">
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} />
</FormControl>
</Tooltip>
<IAICanvasMaskOptions />
<IAICanvasToolChooserOptions />
<IAICanvasMaskOptions />
<IAICanvasToolChooserOptions />
<ButtonGroup>
<IconButton
aria-label={`${t('unifiedCanvas.move')} (V)`}
tooltip={`${t('unifiedCanvas.move')} (V)`}
icon={<PiHandGrabbingBold />}
isChecked={tool === 'move' || isStaging}
onClick={handleSelectMoveTool}
/>
<IconButton
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
onClick={handleSetShouldShowBoundingBox}
isDisabled={isStaging}
/>
<IconButton
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
icon={<PiCrosshairSimpleBold />}
onClick={handleClickResetCanvasView}
/>
</ButtonGroup>
<ButtonGroup>
<IconButton
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
icon={<PiStackBold />}
onClick={handleMergeVisible}
isDisabled={isStaging}
/>
<IconButton
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
icon={<PiFloppyDiskBold />}
onClick={handleSaveToGallery}
isDisabled={isStaging}
/>
{isClipboardAPIAvailable && (
<ButtonGroup>
<IconButton
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
icon={<PiCopyBold />}
onClick={handleCopyImageToClipboard}
aria-label={`${t('unifiedCanvas.move')} (V)`}
tooltip={`${t('unifiedCanvas.move')} (V)`}
icon={<PiHandGrabbingBold />}
isChecked={tool === 'move' || isStaging}
onClick={handleSelectMoveTool}
/>
<IconButton
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
onClick={handleSetShouldShowBoundingBox}
isDisabled={isStaging}
/>
)}
<IconButton
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
icon={<PiDownloadSimpleBold />}
onClick={handleDownloadAsImage}
isDisabled={isStaging}
/>
</ButtonGroup>
<ButtonGroup>
<IAICanvasUndoButton />
<IAICanvasRedoButton />
</ButtonGroup>
<IconButton
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
icon={<PiCrosshairSimpleBold />}
onClick={handleClickResetCanvasView}
/>
</ButtonGroup>
<ButtonGroup>
<IconButton
aria-label={`${t('common.upload')}`}
tooltip={`${t('common.upload')}`}
icon={<PiUploadSimpleBold />}
isDisabled={isStaging}
{...getUploadButtonProps()}
/>
<input {...getUploadInputProps()} />
<IconButton
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
icon={<PiTrashSimpleBold />}
onClick={handleResetCanvas}
colorScheme="error"
isDisabled={isStaging}
/>
</ButtonGroup>
<ButtonGroup>
<IAICanvasSettingsButtonPopover />
</ButtonGroup>
<ButtonGroup>
<IconButton
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
icon={<PiStackBold />}
onClick={handleMergeVisible}
isDisabled={isStaging}
/>
<IconButton
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
icon={<PiFloppyDiskBold />}
onClick={handleSaveToGallery}
isDisabled={isStaging}
/>
{isClipboardAPIAvailable && (
<IconButton
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
icon={<PiCopyBold />}
onClick={handleCopyImageToClipboard}
isDisabled={isStaging}
/>
)}
<IconButton
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
icon={<PiDownloadSimpleBold />}
onClick={handleDownloadAsImage}
isDisabled={isStaging}
/>
</ButtonGroup>
<ButtonGroup>
<IAICanvasUndoButton />
<IAICanvasRedoButton />
</ButtonGroup>
<ButtonGroup>
<IconButton
aria-label={`${t('common.upload')}`}
tooltip={`${t('common.upload')}`}
icon={<PiUploadSimpleBold />}
isDisabled={isStaging}
{...getUploadButtonProps()}
/>
<input {...getUploadInputProps()} />
<IconButton
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
icon={<PiTrashSimpleBold />}
onClick={handleResetCanvas}
colorScheme="error"
isDisabled={isStaging}
/>
</ButtonGroup>
<ButtonGroup>
<IAICanvasSettingsButtonPopover />
</ButtonGroup>
</Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
<ViewerButton />
</Flex>
</Flex>
</Flex>
);
};

View File

@ -75,7 +75,7 @@ const useInpaintingCanvasHotkeys = () => {
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'unifiedCanvas') {
if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') {
return;
}
if ($toolStash.get() || $tool.get() === 'move') {
@ -90,7 +90,7 @@ const useInpaintingCanvasHotkeys = () => {
);
const onKeyUp = useCallback(
(e: KeyboardEvent) => {
if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'unifiedCanvas') {
if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') {
return;
}
if (!$toolStash.get() || $tool.get() !== 'move') {

View File

@ -76,7 +76,7 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => {
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<ParamControlAdapterModel id={id} />
</Box>
{activeTabName === 'unifiedCanvas' && <ControlNetCanvasImageImports id={id} />}
{activeTabName === 'canvas' && <ControlNetCanvasImageImports id={id} />}
<IconButton
size="sm"
tooltip={t('controlnet.duplicate')}
@ -113,7 +113,7 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => {
<Flex w="full" flexDir="column" gap={4}>
<Flex gap={8} w="full" alignItems="center">
<Flex flexDir="column" gap={4} h={controlAdapterType === 'ip_adapter' ? 40 : 32} w="full">
{controlAdapterType === 'ip_adapter' && <ParamControlAdapterIPMethod id={id} />}
<ParamControlAdapterIPMethod id={id} />
<ParamControlAdapterWeight id={id} />
<ParamControlAdapterBeginEnd id={id} />
</Flex>

View File

@ -93,15 +93,16 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => {
return;
}
if (activeTabName === 'unifiedCanvas') {
if (activeTabName === 'canvas') {
dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension));
} else {
const options = { updateAspectRatio: true, clamp: true };
const { width, height } = calculateNewSize(
controlImage.width / controlImage.height,
optimalDimension * optimalDimension
);
dispatch(widthChanged({ width, updateAspectRatio: true }));
dispatch(heightChanged({ height, updateAspectRatio: true }));
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
}
}, [controlImage, activeTabName, dispatch, optimalDimension]);

View File

@ -46,9 +46,13 @@ const ParamControlAdapterIPMethod = ({ id }: Props) => {
const value = useMemo(() => options.find((o) => o.value === method), [options, method]);
if (!method) {
return null;
}
return (
<FormControl>
<InformationalPopover feature="ipAdapterMethod">
<InformationalPopover feature="controlNetResizeMode">
<FormLabel>{t('controlnet.ipAdapterMethod')}</FormLabel>
</InformationalPopover>
<Combobox value={value} options={options} isDisabled={!isEnabled} onChange={handleIPMethodChanged} />

View File

@ -102,9 +102,13 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => {
);
return (
<Flex gap={4}>
<Flex sx={{ gap: 2 }}>
<Tooltip label={selectedModel?.description}>
<FormControl isDisabled={!isEnabled} isInvalid={!value || mainModel?.base !== modelConfig?.base} w="full">
<FormControl
isDisabled={!isEnabled}
isInvalid={!value || mainModel?.base !== modelConfig?.base}
sx={{ width: '100%' }}
>
<Combobox
options={options}
placeholder={t('controlnet.selectModel')}
@ -118,8 +122,7 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => {
<FormControl
isDisabled={!isEnabled}
isInvalid={!value || mainModel?.base !== modelConfig?.base}
width="max-content"
minWidth={28}
sx={{ width: 'max-content', minWidth: 28 }}
>
<Combobox
options={clipVisionOptions}

View File

@ -5,15 +5,15 @@ import {
selectControlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useControlAdapterIPMethod = (id: string) => {
const selector = useMemo(
() =>
createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => {
const ca = selectControlAdapterById(controlAdapters, id);
assert(ca?.type === 'ip_adapter');
return ca.method;
const cn = selectControlAdapterById(controlAdapters, id);
if (cn && cn?.type === 'ip_adapter') {
return cn.method;
}
}),
[id]
);

View File

@ -7,7 +7,7 @@ import { buildControlAdapter } from 'features/controlAdapters/util/buildControlA
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { merge, uniq } from 'lodash-es';
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { socketInvocationError } from 'services/events/actions';
import { v4 as uuidv4 } from 'uuid';
@ -134,46 +134,23 @@ export const controlAdaptersSlice = createSlice({
const { id, isEnabled } = action.payload;
caAdapter.updateOne(state, { id, changes: { isEnabled } });
},
controlAdapterImageChanged: (state, action: PayloadAction<{ id: string; controlImage: ImageDTO | null }>) => {
controlAdapterImageChanged: (
state,
action: PayloadAction<{
id: string;
controlImage: string | null;
}>
) => {
const { id, controlImage } = action.payload;
const ca = selectControlAdapterById(state, id);
if (!ca) {
return;
}
if (isControlNetOrT2IAdapter(ca)) {
if (controlImage) {
const { image_name, width, height } = controlImage;
const processorNode = deepClone(ca.processorNode);
const minDim = Math.min(controlImage.width, controlImage.height);
if ('detect_resolution' in processorNode) {
processorNode.detect_resolution = minDim;
}
if ('image_resolution' in processorNode) {
processorNode.image_resolution = minDim;
}
if ('resolution' in processorNode) {
processorNode.resolution = minDim;
}
caAdapter.updateOne(state, {
id,
changes: {
processorNode,
controlImage: image_name,
controlImageDimensions: { width, height },
processedControlImage: null,
},
});
} else {
caAdapter.updateOne(state, {
id,
changes: { controlImage: null, controlImageDimensions: null, processedControlImage: null },
});
}
} else {
// ip adapter
caAdapter.updateOne(state, { id, changes: { controlImage: controlImage?.image_name ?? null } });
}
caAdapter.updateOne(state, {
id,
changes: { controlImage, processedControlImage: null },
});
if (controlImage !== null && isControlNetOrT2IAdapter(ca) && ca.processorType !== 'none') {
state.pendingControlImages.push(id);
@ -183,7 +160,7 @@ export const controlAdaptersSlice = createSlice({
state,
action: PayloadAction<{
id: string;
processedControlImage: ImageDTO | null;
processedControlImage: string | null;
}>
) => {
const { id, processedControlImage } = action.payload;
@ -196,24 +173,12 @@ export const controlAdaptersSlice = createSlice({
return;
}
if (processedControlImage) {
const { image_name, width, height } = processedControlImage;
caAdapter.updateOne(state, {
id,
changes: {
processedControlImage: image_name,
processedControlImageDimensions: { width, height },
},
});
} else {
caAdapter.updateOne(state, {
id,
changes: {
processedControlImage: null,
processedControlImageDimensions: null,
},
});
}
caAdapter.updateOne(state, {
id,
changes: {
processedControlImage,
},
});
state.pendingControlImages = state.pendingControlImages.filter((pendingId) => pendingId !== id);
},
@ -227,7 +192,7 @@ export const controlAdaptersSlice = createSlice({
state,
action: PayloadAction<{
id: string;
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig | null;
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig;
}>
) => {
const { id, modelConfig } = action.payload;
@ -236,11 +201,6 @@ export const controlAdaptersSlice = createSlice({
return;
}
if (modelConfig === null) {
caAdapter.updateOne(state, { id, changes: { model: null } });
return;
}
const model = zModelIdentifierField.parse(modelConfig);
if (!isControlNetOrT2IAdapter(cn)) {
@ -248,36 +208,22 @@ export const controlAdaptersSlice = createSlice({
return;
}
const update: Update<ControlNetConfig | T2IAdapterConfig, string> = {
id,
changes: { model, shouldAutoConfig: true },
};
update.changes.processedControlImage = null;
if (modelConfig.type === 'ip_adapter') {
// should never happen...
return;
}
// We always update the model
const update: Update<ControlNetConfig | T2IAdapterConfig, string> = { id, changes: { model } };
// Build the default processor for this model
const processor = buildControlAdapterProcessor(modelConfig);
if (processor.processorType !== cn.processorNode.type) {
// If the processor type has changed, update the processor node
update.changes.shouldAutoConfig = true;
update.changes.processedControlImage = null;
update.changes.processorType = processor.processorType;
update.changes.processorNode = processor.processorNode;
update.changes.processorType = processor.processorType;
update.changes.processorNode = processor.processorNode;
if (cn.controlImageDimensions) {
const minDim = Math.min(cn.controlImageDimensions.width, cn.controlImageDimensions.height);
if ('detect_resolution' in update.changes.processorNode) {
update.changes.processorNode.detect_resolution = minDim;
}
if ('image_resolution' in update.changes.processorNode) {
update.changes.processorNode.image_resolution = minDim;
}
if ('resolution' in update.changes.processorNode) {
update.changes.processorNode.resolution = minDim;
}
}
}
caAdapter.updateOne(state, update);
},
controlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
@ -394,23 +340,8 @@ export const controlAdaptersSlice = createSlice({
if (update.changes.shouldAutoConfig && modelConfig) {
const processor = buildControlAdapterProcessor(modelConfig);
if (processor.processorType !== cn.processorNode.type) {
update.changes.processorType = processor.processorType;
update.changes.processorNode = processor.processorNode;
// Copy image resolution settings, urgh
if (cn.controlImageDimensions) {
const minDim = Math.min(cn.controlImageDimensions.width, cn.controlImageDimensions.height);
if ('detect_resolution' in update.changes.processorNode) {
update.changes.processorNode.detect_resolution = minDim;
}
if ('image_resolution' in update.changes.processorNode) {
update.changes.processorNode.image_resolution = minDim;
}
if ('resolution' in update.changes.processorNode) {
update.changes.processorNode.resolution = minDim;
}
}
}
update.changes.processorType = processor.processorType;
update.changes.processorNode = processor.processorNode;
}
caAdapter.updateOne(state, update);

View File

@ -225,9 +225,7 @@ export type ControlNetConfig = {
controlMode: ControlMode;
resizeMode: ResizeMode;
controlImage: string | null;
controlImageDimensions: { width: number; height: number } | null;
processedControlImage: string | null;
processedControlImageDimensions: { width: number; height: number } | null;
processorType: ControlAdapterProcessorType;
processorNode: RequiredControlAdapterProcessorNode;
shouldAutoConfig: boolean;
@ -243,9 +241,7 @@ export type T2IAdapterConfig = {
endStepPct: number;
resizeMode: ResizeMode;
controlImage: string | null;
controlImageDimensions: { width: number; height: number } | null;
processedControlImage: string | null;
processedControlImageDimensions: { width: number; height: number } | null;
processorType: ControlAdapterProcessorType;
processorNode: RequiredControlAdapterProcessorNode;
shouldAutoConfig: boolean;

View File

@ -20,9 +20,7 @@ export const initialControlNet: Omit<ControlNetConfig, 'id'> = {
controlMode: 'balanced',
resizeMode: 'just_resize',
controlImage: null,
controlImageDimensions: null,
processedControlImage: null,
processedControlImageDimensions: null,
processorType: 'canny_image_processor',
processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation,
shouldAutoConfig: true,
@ -37,9 +35,7 @@ export const initialT2IAdapter: Omit<T2IAdapterConfig, 'id'> = {
endStepPct: 1,
resizeMode: 'just_resize',
controlImage: null,
controlImageDimensions: null,
processedControlImage: null,
processedControlImageDimensions: null,
processorType: 'canny_image_processor',
processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation,
shouldAutoConfig: true,

View File

@ -1,6 +1,6 @@
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -11,6 +11,7 @@ export const AddLayerButton = memo(() => {
const dispatch = useAppDispatch();
const [addCALayer, isAddCALayerDisabled] = useAddCALayer();
const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer();
const [addIILayer, isAddIILayerDisabled] = useAddIILayer();
const addRGLayer = useCallback(() => {
dispatch(rgLayerAdded());
}, [dispatch]);
@ -30,6 +31,9 @@ export const AddLayerButton = memo(() => {
<MenuItem icon={<PiPlusBold />} onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}>
{t('controlLayers.globalIPAdapterLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIILayer} isDisabled={isAddIILayerDisabled}>
{t('controlLayers.globalInitialImageLayer')}
</MenuItem>
</MenuList>
</Menu>
);

View File

@ -5,6 +5,7 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
@ -17,37 +18,28 @@ type Props = {
export const CALayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected);
const onClickCapture = useCallback(() => {
const onClick = useCallback(() => {
// Must be capture so that the layer is selected before deleting/resetting/etc
dispatch(layerSelected(layerId));
}, [dispatch, layerId]);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
return (
<Flex
gap={2}
onClickCapture={onClickCapture}
bg={isSelected ? 'base.400' : 'base.800'}
px={2}
borderRadius="base"
py="1px"
>
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="control_adapter_layer" />
<Spacer />
<CALayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<CALayerControlAdapterWrapper layerId={layerId} />
</Flex>
)}
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="control_adapter_layer" />
<Spacer />
<CALayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<CALayerControlAdapterWrapper layerId={layerId} />
</Flex>
)}
</LayerWrapper>
);
});

View File

@ -9,7 +9,7 @@ import {
caOrIPALayerWeightChanged,
selectCALayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import type { ControlMode, ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import type { CALayerImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
import type {
@ -40,7 +40,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => {
);
const onChangeControlMode = useCallback(
(controlMode: ControlMode) => {
(controlMode: ControlModeV2) => {
dispatch(
caLayerControlModeChanged({
layerId,

View File

@ -55,7 +55,7 @@ const CALayerOpacity = ({ layerId }: Props) => {
onDoubleClick={stopPropagation}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverContent onDoubleClick={stopPropagation}>
<PopoverArrow />
<PopoverBody>
<Flex direction="column" gap={2}>

View File

@ -1,10 +1,10 @@
import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
import { ControlAdapterModelCombobox } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox';
import type {
ControlMode,
ControlNetConfig,
ControlModeV2,
ControlNetConfigV2,
ProcessorConfig,
T2IAdapterConfig,
T2IAdapterConfigV2,
} from 'features/controlLayers/util/controlAdapters';
import type { TypesafeDroppableData } from 'features/dnd/types';
import { memo } from 'react';
@ -21,9 +21,9 @@ import { ControlAdapterProcessorTypeSelect } from './ControlAdapterProcessorType
import { ControlAdapterWeight } from './ControlAdapterWeight';
type Props = {
controlAdapter: ControlNetConfig | T2IAdapterConfig;
controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2;
onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void;
onChangeControlMode: (controlMode: ControlMode) => void;
onChangeControlMode: (controlMode: ControlModeV2) => void;
onChangeWeight: (weight: number) => void;
onChangeProcessorConfig: (processorConfig: ProcessorConfig | null) => void;
onChangeModel: (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => void;

View File

@ -1,15 +1,15 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { ControlMode } from 'features/controlLayers/util/controlAdapters';
import { isControlMode } from 'features/controlLayers/util/controlAdapters';
import type { ControlModeV2 } from 'features/controlLayers/util/controlAdapters';
import { isControlModeV2 } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
type Props = {
controlMode: ControlMode;
onChange: (controlMode: ControlMode) => void;
controlMode: ControlModeV2;
onChange: (controlMode: ControlModeV2) => void;
};
export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }: Props) => {
@ -26,7 +26,7 @@ export const ControlAdapterControlModeSelect = memo(({ controlMode, onChange }:
const handleControlModeChange = useCallback<ComboboxOnChange>(
(v) => {
assert(isControlMode(v?.value));
assert(isControlModeV2(v?.value));
onChange(v.value);
},
[onChange]

View File

@ -6,7 +6,7 @@ import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { ControlNetConfig, T2IAdapterConfig } from 'features/controlLayers/util/controlAdapters';
import type { ControlNetConfigV2, T2IAdapterConfigV2 } from 'features/controlLayers/util/controlAdapters';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
@ -23,7 +23,7 @@ import {
import type { ImageDTO, PostUploadAction } from 'services/api/types';
type Props = {
controlAdapter: ControlNetConfig | T2IAdapterConfig;
controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2;
onChangeImage: (imageDTO: ImageDTO | null) => void;
droppableData: TypesafeDroppableData;
postUploadAction: PostUploadAction;
@ -80,22 +80,24 @@ export const ControlAdapterImagePreview = memo(
return;
}
if (activeTabName === 'unifiedCanvas') {
if (activeTabName === 'canvas') {
dispatch(
setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)
);
} else {
const options = { updateAspectRatio: true, clamp: true };
if (shift) {
const { width, height } = controlImage;
dispatch(widthChanged({ width, updateAspectRatio: true }));
dispatch(heightChanged({ height, updateAspectRatio: true }));
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(
controlImage.width / controlImage.height,
optimalDimension * optimalDimension
);
dispatch(widthChanged({ width, updateAspectRatio: true }));
dispatch(heightChanged({ height, updateAspectRatio: true }));
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
}
}
}, [controlImage, activeTabName, dispatch, optimalDimension, shift]);

View File

@ -4,7 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { ProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS, isProcessorType } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA, isProcessorTypeV2 } from 'features/controlLayers/util/controlAdapters';
import { configSelector } from 'features/system/store/configSelectors';
import { includes, map } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
@ -26,7 +26,7 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro
const { t } = useTranslation();
const disabledProcessors = useAppSelector(selectDisabledProcessors);
const options = useMemo(() => {
return map(CONTROLNET_PROCESSORS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter(
return map(CA_PROCESSOR_DATA, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter(
(o) => !includes(disabledProcessors, o.value)
);
}, [disabledProcessors, t]);
@ -36,8 +36,8 @@ export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Pro
if (!v) {
onChange(null);
} else {
assert(isProcessorType(v.value));
onChange(CONTROLNET_PROCESSORS[v.value].buildDefaults());
assert(isProcessorTypeV2(v.value));
onChange(CA_PROCESSOR_DATA[v.value].buildDefaults());
}
},
[onChange]

View File

@ -4,18 +4,18 @@ import { ControlAdapterWeight } from 'features/controlLayers/components/ControlA
import { IPAdapterImagePreview } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterImagePreview';
import { IPAdapterMethod } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterMethod';
import { IPAdapterModelSelect } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapterModelSelect';
import type { CLIPVisionModel, IPAdapterConfig, IPMethod } from 'features/controlLayers/util/controlAdapters';
import type { CLIPVisionModelV2, IPAdapterConfigV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import type { TypesafeDroppableData } from 'features/dnd/types';
import { memo } from 'react';
import type { ImageDTO, IPAdapterModelConfig, PostUploadAction } from 'services/api/types';
type Props = {
ipAdapter: IPAdapterConfig;
ipAdapter: IPAdapterConfigV2;
onChangeBeginEndStepPct: (beginEndStepPct: [number, number]) => void;
onChangeWeight: (weight: number) => void;
onChangeIPMethod: (method: IPMethod) => void;
onChangeIPMethod: (method: IPMethodV2) => void;
onChangeModel: (modelConfig: IPAdapterModelConfig) => void;
onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void;
onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void;
onChangeImage: (imageDTO: ImageDTO | null) => void;
droppableData: TypesafeDroppableData;
postUploadAction: PostUploadAction;

View File

@ -46,22 +46,23 @@ export const IPAdapterImagePreview = memo(
return;
}
if (activeTabName === 'unifiedCanvas') {
if (activeTabName === 'canvas') {
dispatch(
setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)
);
} else {
const options = { updateAspectRatio: true, clamp: true };
if (shift) {
const { width, height } = controlImage;
dispatch(widthChanged({ width, updateAspectRatio: true }));
dispatch(heightChanged({ height, updateAspectRatio: true }));
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(
controlImage.width / controlImage.height,
optimalDimension * optimalDimension
);
dispatch(widthChanged({ width, updateAspectRatio: true }));
dispatch(heightChanged({ height, updateAspectRatio: true }));
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
}
}
}, [controlImage, activeTabName, dispatch, optimalDimension, shift]);

View File

@ -1,20 +1,20 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type { IPMethod } from 'features/controlLayers/util/controlAdapters';
import { isIPMethod } from 'features/controlLayers/util/controlAdapters';
import type { IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import { isIPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
type Props = {
method: IPMethod;
onChange: (method: IPMethod) => void;
method: IPMethodV2;
onChange: (method: IPMethodV2) => void;
};
export const IPAdapterMethod = memo(({ method, onChange }: Props) => {
const { t } = useTranslation();
const options: { label: string; value: IPMethod }[] = useMemo(
const options: { label: string; value: IPMethodV2 }[] = useMemo(
() => [
{ label: t('controlnet.full'), value: 'full' },
{ label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' },
@ -24,7 +24,7 @@ export const IPAdapterMethod = memo(({ method, onChange }: Props) => {
);
const _onChange = useCallback<ComboboxOnChange>(
(v) => {
assert(isIPMethod(v?.value));
assert(isIPMethodV2(v?.value));
onChange(v.value);
},
[onChange]

View File

@ -2,8 +2,8 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import type { CLIPVisionModel } from 'features/controlLayers/util/controlAdapters';
import { isCLIPVisionModel } from 'features/controlLayers/util/controlAdapters';
import type { CLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters';
import { isCLIPVisionModelV2 } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useIPAdapterModels } from 'services/api/hooks/modelsByType';
@ -18,8 +18,8 @@ const CLIP_VISION_OPTIONS = [
type Props = {
modelKey: string | null;
onChangeModel: (modelConfig: IPAdapterModelConfig) => void;
clipVisionModel: CLIPVisionModel;
onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModel) => void;
clipVisionModel: CLIPVisionModelV2;
onChangeCLIPVisionModel: (clipVisionModel: CLIPVisionModelV2) => void;
};
export const IPAdapterModelSelect = memo(
@ -41,7 +41,7 @@ export const IPAdapterModelSelect = memo(
const _onChangeCLIPVisionModel = useCallback<ComboboxOnChange>(
(v) => {
assert(isCLIPVisionModel(v?.value));
assert(isCLIPVisionModelV2(v?.value));
onChangeCLIPVisionModel(v.value);
},
[onChangeCLIPVisionModel]

View File

@ -1,13 +1,13 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import { type CannyProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA, type CannyProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<CannyProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['canny_image_processor'].buildDefaults();
const DEFAULTS = CA_PROCESSOR_DATA['canny_image_processor'].buildDefaults();
export const CannyProcessor = ({ onChange, config }: Props) => {
const { t } = useTranslation();

View File

@ -1,13 +1,13 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import { type ColorMapProcessorConfig, CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA, type ColorMapProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<ColorMapProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['color_map_image_processor'].buildDefaults();
const DEFAULTS = CA_PROCESSOR_DATA['color_map_image_processor'].buildDefaults();
export const ColorMapProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();

View File

@ -1,14 +1,14 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { ContentShuffleProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<ContentShuffleProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['content_shuffle_image_processor'].buildDefaults();
const DEFAULTS = CA_PROCESSOR_DATA['content_shuffle_image_processor'].buildDefaults();
export const ContentShuffleProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();

View File

@ -1,7 +1,7 @@
import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { DWOpenposeProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<DWOpenposeProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['dw_openpose_image_processor'].buildDefaults();
const DEFAULTS = CA_PROCESSOR_DATA['dw_openpose_image_processor'].buildDefaults();
export const DWOpenposeProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();

View File

@ -2,14 +2,14 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<DepthAnythingProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['depth_anything_image_processor'].buildDefaults();
const DEFAULTS = CA_PROCESSOR_DATA['depth_anything_image_processor'].buildDefaults();
export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();

View File

@ -1,13 +1,13 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import { CONTROLNET_PROCESSORS, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA, type MediapipeFaceProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<MediapipeFaceProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['mediapipe_face_processor'].buildDefaults();
const DEFAULTS = CA_PROCESSOR_DATA['mediapipe_face_processor'].buildDefaults();
export const MediapipeFaceProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();

View File

@ -1,14 +1,14 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { MidasDepthProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<MidasDepthProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['midas_depth_image_processor'].buildDefaults();
const DEFAULTS = CA_PROCESSOR_DATA['midas_depth_image_processor'].buildDefaults();
export const MidasDepthProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();

View File

@ -1,14 +1,14 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types';
import type { MlsdProcessorConfig } from 'features/controlLayers/util/controlAdapters';
import { CONTROLNET_PROCESSORS } from 'features/controlLayers/util/controlAdapters';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<MlsdProcessorConfig>;
const DEFAULTS = CONTROLNET_PROCESSORS['mlsd_image_processor'].buildDefaults();
const DEFAULTS = CA_PROCESSOR_DATA['mlsd_image_processor'].buildDefaults();
export const MlsdImageProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();

View File

@ -2,16 +2,19 @@
import { Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton';
import { CALayer } from 'features/controlLayers/components/CALayer/CALayer';
import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton';
import { IILayer } from 'features/controlLayers/components/IILayer/IILayer';
import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer';
import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer';
import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import type { Layer } from 'features/controlLayers/store/types';
import { partition } from 'lodash-es';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer);
@ -19,20 +22,24 @@ const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice,
});
export const ControlLayersPanelContent = memo(() => {
const { t } = useTranslation();
const layerIdTypePairs = useAppSelector(selectLayerIdTypePairs);
return (
<Flex flexDir="column" gap={4} w="full" h="full">
<Flex flexDir="column" gap={2} w="full" h="full">
<Flex justifyContent="space-around">
<AddLayerButton />
<DeleteAllLayersButton />
</Flex>
<ScrollableContent>
<Flex flexDir="column" gap={4}>
{layerIdTypePairs.map(({ id, type }) => (
<LayerWrapper key={id} id={id} type={type} />
))}
</Flex>
</ScrollableContent>
{layerIdTypePairs.length > 0 && (
<ScrollableContent>
<Flex flexDir="column" gap={2}>
{layerIdTypePairs.map(({ id, type }) => (
<LayerWrapper key={id} id={id} type={type} />
))}
</Flex>
</ScrollableContent>
)}
{layerIdTypePairs.length === 0 && <IAINoContentFallback icon={null} label={t('controlLayers.noLayersAdded')} />}
</Flex>
);
});
@ -54,6 +61,9 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => {
if (type === 'ip_adapter_layer') {
return <IPALayer key={id} layerId={id} />;
}
if (type === 'initial_image_layer') {
return <IILayer key={id} layerId={id} />;
}
});
LayerWrapper.displayName = 'LayerWrapper';

View File

@ -4,15 +4,26 @@ import { BrushSize } from 'features/controlLayers/components/BrushSize';
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
import { memo } from 'react';
export const ControlLayersToolbar = memo(() => {
return (
<Flex gap={4}>
<BrushSize />
<ToolChooser />
<UndoRedoButtonGroup />
<ControlLayersSettingsPopover />
<Flex w="full" gap={2}>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineEnd="auto" />
</Flex>
<Flex flex={1} gap={2} justifyContent="center">
<BrushSize />
<ToolChooser />
<UndoRedoButtonGroup />
<ControlLayersSettingsPopover />
</Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
<ViewerButton />
</Flex>
</Flex>
</Flex>
);
});

View File

@ -0,0 +1,83 @@
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IILayerOpacity from 'features/controlLayers/components/IILayer/IILayerOpacity';
import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import {
iiLayerImageChanged,
layerSelected,
selectIILayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import type { IILayerImageDropData } from 'features/dnd/types';
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
import { memo, useCallback, useMemo } from 'react';
import type { IILayerImagePostUploadAction, ImageDTO } from 'services/api/types';
type Props = {
layerId: string;
};
export const IILayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const layer = useAppSelector((s) => selectIILayerOrThrow(s.controlLayers.present, layerId));
const onClick = useCallback(() => {
dispatch(layerSelected(layerId));
}, [dispatch, layerId]);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(iiLayerImageChanged({ layerId, imageDTO }));
},
[dispatch, layerId]
);
const droppableData = useMemo<IILayerImageDropData>(
() => ({
actionType: 'SET_II_LAYER_IMAGE',
context: {
layerId,
},
id: layerId,
}),
[layerId]
);
const postUploadAction = useMemo<IILayerImagePostUploadAction>(
() => ({
layerId,
type: 'SET_II_LAYER_IMAGE',
}),
[layerId]
);
return (
<LayerWrapper onClick={onClick} borderColor={layer.isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="initial_image_layer" />
<Spacer />
<IILayerOpacity layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<ImageToImageStrength />
<InitialImagePreview
image={layer.image}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>
</Flex>
)}
</LayerWrapper>
);
});
IILayer.displayName = 'IILayer';

View File

@ -0,0 +1,98 @@
import {
CompositeNumberInput,
CompositeSlider,
Flex,
FormControl,
FormLabel,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import {
iiLayerOpacityChanged,
isInitialImageLayer,
selectControlLayersSlice,
} from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi';
import { assert } from 'tsafe';
type Props = {
layerId: string;
};
const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`;
const IILayerOpacity = ({ layerId }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectOpacity = useMemo(
() =>
createSelector(selectControlLayersSlice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isInitialImageLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
return Math.round(layer.opacity * 100);
}),
[layerId]
);
const opacity = useAppSelector(selectOpacity);
const onChangeOpacity = useCallback(
(v: number) => {
dispatch(iiLayerOpacityChanged({ layerId, opacity: v / 100 }));
},
[dispatch, layerId]
);
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton
aria-label={t('controlLayers.opacity')}
size="sm"
icon={<PiDropHalfFill size={16} />}
variant="ghost"
onDoubleClick={stopPropagation}
/>
</PopoverTrigger>
<PopoverContent onDoubleClick={stopPropagation}>
<PopoverArrow />
<PopoverBody>
<Flex direction="column" gap={2}>
<FormControl orientation="horizontal">
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
<CompositeSlider
min={0}
max={100}
step={1}
value={opacity}
defaultValue={100}
onChange={onChangeOpacity}
marks={marks}
w={48}
/>
<CompositeNumberInput
min={0}
max={100}
step={1}
value={opacity}
defaultValue={100}
onChange={onChangeOpacity}
w={24}
format={formatPct}
/>
</FormControl>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default memo(IILayerOpacity);

View File

@ -0,0 +1,109 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { ImageDTO, PostUploadAction } from 'services/api/types';
type Props = {
image: ImageWithDims | null;
onChangeImage: (imageDTO: ImageDTO | null) => void;
droppableData: TypesafeDroppableData;
postUploadAction: PostUploadAction;
};
export const InitialImagePreview = memo(({ image, onChangeImage, droppableData, postUploadAction }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isConnected = useAppSelector((s) => s.system.isConnected);
const activeTabName = useAppSelector(activeTabNameSelector);
const optimalDimension = useAppSelector(selectOptimalDimension);
const shift = useShiftModifier();
const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.imageName ?? skipToken);
const onReset = useCallback(() => {
onChangeImage(null);
}, [onChangeImage]);
const onUseSize = useCallback(() => {
if (!imageDTO) {
return;
}
if (activeTabName === 'canvas') {
dispatch(setBoundingBoxDimensions({ width: imageDTO.width, height: imageDTO.height }, optimalDimension));
} else {
const options = { updateAspectRatio: true, clamp: true };
if (shift) {
const { width, height } = imageDTO;
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(
imageDTO.width / imageDTO.height,
optimalDimension * optimalDimension
);
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
}
}
}, [imageDTO, activeTabName, dispatch, optimalDimension, shift]);
const draggableData = useMemo<ImageDraggableData | undefined>(() => {
if (imageDTO) {
return {
id: 'initial_image_layer',
payloadType: 'IMAGE_DTO',
payload: { imageDTO: imageDTO },
};
}
}, [imageDTO]);
useEffect(() => {
if (isConnected && isErrorControlImage) {
onReset();
}
}, [onReset, isConnected, isErrorControlImage]);
return (
<Flex position="relative" w="full" h={36} alignItems="center" justifyContent="center">
<IAIDndImage
draggableData={draggableData}
droppableData={droppableData}
imageDTO={imageDTO}
postUploadAction={postUploadAction}
/>
<>
<IAIDndImageIcon
onClick={onReset}
icon={imageDTO ? <PiArrowCounterClockwiseBold size={16} /> : undefined}
tooltip={t('controlnet.resetControlImage')}
/>
<IAIDndImageIcon
onClick={onUseSize}
icon={imageDTO ? <PiRulerBold size={16} /> : undefined}
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
styleOverrides={useSizeStyleOverrides}
/>
</>
</Flex>
);
});
InitialImagePreview.displayName = 'InitialImagePreview';
const useSizeStyleOverrides: SystemStyleObject = { mt: 6 };

View File

@ -3,6 +3,7 @@ import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPAL
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { memo } from 'react';
type Props = {
@ -12,21 +13,19 @@ type Props = {
export const IPALayer = memo(({ layerId }: Props) => {
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
return (
<Flex gap={2} bg="base.800" borderRadius="base" p="1px" px={2}>
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="ip_adapter_layer" />
<Spacer />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<IPALayerIPAdapterWrapper layerId={layerId} />
</Flex>
)}
<LayerWrapper borderColor="base.800">
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="ip_adapter_layer" />
<Spacer />
<LayerDeleteButton layerId={layerId} />
</Flex>
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<IPALayerIPAdapterWrapper layerId={layerId} />
</Flex>
)}
</LayerWrapper>
);
});

View File

@ -9,7 +9,7 @@ import {
ipaLayerModelChanged,
selectIPALayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import type { IPALayerImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types';
@ -42,7 +42,7 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => {
);
const onChangeIPMethod = useCallback(
(method: IPMethod) => {
(method: IPMethodV2) => {
dispatch(ipaLayerMethodChanged({ layerId, method }));
},
[dispatch, layerId]
@ -56,7 +56,7 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => {
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModel) => {
(clipVisionModel: CLIPVisionModelV2) => {
dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel }));
},
[dispatch, layerId]

View File

@ -37,7 +37,9 @@ export const LayerMenu = memo(({ layerId }: Props) => {
<MenuDivider />
</>
)}
{(layerType === 'regional_guidance_layer' || layerType === 'control_adapter_layer') && (
{(layerType === 'regional_guidance_layer' ||
layerType === 'control_adapter_layer' ||
layerType === 'initial_image_layer') && (
<>
<LayerMenuArrangeActions layerId={layerId} />
<MenuDivider />

View File

@ -16,6 +16,8 @@ export const LayerTitle = memo(({ type }: Props) => {
return t('controlLayers.globalControlAdapter');
} else if (type === 'ip_adapter_layer') {
return t('controlLayers.globalIPAdapter');
} else if (type === 'initial_image_layer') {
return t('controlLayers.globalInitialImage');
}
}, [t, type]);

View File

@ -0,0 +1,21 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = PropsWithChildren<{
onClick?: () => void;
borderColor: ChakraProps['bg'];
}>;
export const LayerWrapper = memo(({ onClick, borderColor, children }: Props) => {
return (
<Flex gap={2} onClick={onClick} bg={borderColor} px={2} borderRadius="base" py="1px">
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
{children}
</Flex>
</Flex>
);
});
LayerWrapper.displayName = 'LayerWrapper';

View File

@ -7,6 +7,7 @@ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import {
isRegionalGuidanceLayer,
layerSelected,
@ -52,32 +53,30 @@ export const RGLayer = memo(({ layerId }: Props) => {
dispatch(layerSelected(layerId));
}, [dispatch, layerId]);
return (
<Flex gap={2} onClick={onClick} bg={isSelected ? color : 'base.800'} px={2} borderRadius="base" py="1px">
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="regional_guidance_layer" />
<Spacer />
{autoNegative === 'invert' && (
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
{t('controlLayers.autoNegative')}
</Badge>
)}
<RGLayerColorPicker layerId={layerId} />
<RGLayerSettingsPopover layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons layerId={layerId} />}
{hasPositivePrompt && <RGLayerPositivePrompt layerId={layerId} />}
{hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />}
{hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />}
</Flex>
<LayerWrapper onClick={onClick} borderColor={isSelected ? color : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="regional_guidance_layer" />
<Spacer />
{autoNegative === 'invert' && (
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
{t('controlLayers.autoNegative')}
</Badge>
)}
<RGLayerColorPicker layerId={layerId} />
<RGLayerSettingsPopover layerId={layerId} />
<LayerMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons layerId={layerId} />}
{hasPositivePrompt && <RGLayerPositivePrompt layerId={layerId} />}
{hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />}
{hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />}
</Flex>
)}
</LayerWrapper>
);
});

View File

@ -11,7 +11,7 @@ import {
rgLayerIPAdapterWeightChanged,
selectRGLayerIPAdapterOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import type { CLIPVisionModel, IPMethod } from 'features/controlLayers/util/controlAdapters';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import type { RGLayerIPAdapterImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
import { PiTrashSimpleBold } from 'react-icons/pi';
@ -51,7 +51,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
);
const onChangeIPMethod = useCallback(
(method: IPMethod) => {
(method: IPMethodV2) => {
dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method }));
},
[dispatch, ipAdapterId, layerId]
@ -65,7 +65,7 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModel) => {
(clipVisionModel: CLIPVisionModelV2) => {
dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel }));
},
[dispatch, ipAdapterId, layerId]

View File

@ -6,8 +6,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useMouseEvents } from 'features/controlLayers/hooks/mouseEventHooks';
import {
$cursorPosition,
$isMouseOver,
$lastCursorPos,
$lastMouseDownPos,
$tool,
isRegionalGuidanceLayer,
@ -48,10 +47,9 @@ const useStageRenderer = (
const dispatch = useAppDispatch();
const state = useAppSelector((s) => s.controlLayers.present);
const tool = useStore($tool);
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel } = useMouseEvents();
const cursorPosition = useStore($cursorPosition);
const mouseEventHandlers = useMouseEvents();
const lastCursorPos = useStore($lastCursorPos);
const lastMouseDownPos = useStore($lastMouseDownPos);
const isMouseOver = useStore($isMouseOver);
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
const selectedLayerType = useAppSelector(selectSelectedLayerType);
const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]);
@ -90,23 +88,21 @@ const useStageRenderer = (
if (asPreview) {
return;
}
stage.on('mousedown', onMouseDown);
stage.on('mouseup', onMouseUp);
stage.on('mousemove', onMouseMove);
stage.on('mouseenter', onMouseEnter);
stage.on('mouseleave', onMouseLeave);
stage.on('wheel', onMouseWheel);
stage.on('mousedown', mouseEventHandlers.onMouseDown);
stage.on('mouseup', mouseEventHandlers.onMouseUp);
stage.on('mousemove', mouseEventHandlers.onMouseMove);
stage.on('mouseleave', mouseEventHandlers.onMouseLeave);
stage.on('wheel', mouseEventHandlers.onMouseWheel);
return () => {
log.trace('Cleaning up stage listeners');
stage.off('mousedown', onMouseDown);
stage.off('mouseup', onMouseUp);
stage.off('mousemove', onMouseMove);
stage.off('mouseenter', onMouseEnter);
stage.off('mouseleave', onMouseLeave);
stage.off('wheel', onMouseWheel);
stage.off('mousedown', mouseEventHandlers.onMouseDown);
stage.off('mouseup', mouseEventHandlers.onMouseUp);
stage.off('mousemove', mouseEventHandlers.onMouseMove);
stage.off('mouseleave', mouseEventHandlers.onMouseLeave);
stage.off('wheel', mouseEventHandlers.onMouseWheel);
};
}, [stage, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel]);
}, [stage, asPreview, mouseEventHandlers]);
useLayoutEffect(() => {
log.trace('Updating stage dimensions');
@ -145,9 +141,8 @@ const useStageRenderer = (
selectedLayerIdColor,
selectedLayerType,
state.globalMaskLayerOpacity,
cursorPosition,
lastCursorPos,
lastMouseDownPos,
isMouseOver,
state.brushSize
);
}, [
@ -157,9 +152,8 @@ const useStageRenderer = (
selectedLayerIdColor,
selectedLayerType,
state.globalMaskLayerOpacity,
cursorPosition,
lastCursorPos,
lastMouseDownPos,
isMouseOver,
state.brushSize,
renderers,
]);

View File

@ -4,9 +4,9 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
$tool,
layerReset,
selectControlLayersSlice,
selectedLayerDeleted,
selectedLayerReset,
} from 'features/controlLayers/store/controlLayersSlice';
import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
@ -22,6 +22,7 @@ export const ToolChooser: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isDisabled = useAppSelector(selectIsDisabled);
const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId);
const tool = useStore($tool);
const setToolToBrush = useCallback(() => {
@ -42,8 +43,11 @@ export const ToolChooser: React.FC = () => {
useHotkeys('v', setToolToMove, { enabled: !isDisabled }, [isDisabled]);
const resetSelectedLayer = useCallback(() => {
dispatch(selectedLayerReset());
}, [dispatch]);
if (selectedLayerId === null) {
return;
}
dispatch(layerReset(selectedLayerId));
}, [dispatch, selectedLayerId]);
useHotkeys('shift+c', resetSelectedLayer);
const deleteSelectedLayer = useCallback(() => {

View File

@ -1,11 +1,17 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { caLayerAdded, ipaLayerAdded, rgLayerIPAdapterAdded } from 'features/controlLayers/store/controlLayersSlice';
import {
caLayerAdded,
iiLayerAdded,
ipaLayerAdded,
isInitialImageLayer,
rgLayerIPAdapterAdded,
} from 'features/controlLayers/store/controlLayersSlice';
import {
buildControlNet,
buildIPAdapter,
buildT2IAdapter,
CONTROLNET_PROCESSORS,
isProcessorType,
CA_PROCESSOR_DATA,
isProcessorTypeV2,
} from 'features/controlLayers/util/controlAdapters';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { useCallback, useMemo } from 'react';
@ -30,8 +36,8 @@ export const useAddCALayer = () => {
const id = uuidv4();
const defaultPreprocessor = model.default_settings?.preprocessor;
const processorConfig = isProcessorType(defaultPreprocessor)
? CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(baseModel)
const processorConfig = isProcessorTypeV2(defaultPreprocessor)
? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(baseModel)
: null;
const builder = model.type === 'controlnet' ? buildControlNet : buildT2IAdapter;
@ -93,3 +99,13 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => {
return [addIPAdapter, isDisabled] as const;
};
export const useAddIILayer = () => {
const dispatch = useAppDispatch();
const isDisabled = useAppSelector((s) => Boolean(s.controlLayers.present.layers.find(isInitialImageLayer)));
const addIILayer = useCallback(() => {
dispatch(iiLayerAdded(null));
}, [dispatch]);
return [addIILayer, isDisabled] as const;
};

View File

@ -1,4 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import {
isControlAdapterLayer,
@ -69,7 +70,7 @@ export const useLayerType = (layerId: string) => {
export const useLayerOpacity = (layerId: string) => {
const selectLayer = useMemo(
() =>
createSelector(selectControlLayersSlice, (controlLayers) => {
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled };

View File

@ -3,9 +3,8 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom';
import {
$cursorPosition,
$isMouseDown,
$isMouseOver,
$isDrawing,
$lastCursorPos,
$lastMouseDownPos,
$tool,
brushSizeChanged,
@ -16,16 +15,41 @@ import {
import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node';
import type { Vector2d } from 'konva/lib/types';
import { useCallback, useRef } from 'react';
import { clamp } from 'lodash-es';
import { useCallback, useMemo, useRef } from 'react';
const getIsFocused = (stage: Konva.Stage) => {
return stage.container().contains(document.activeElement);
};
const getIsMouseDown = (e: KonvaEventObject<MouseEvent>) => e.evt.buttons === 1;
const SNAP_PX = 10;
export const snapPosToStage = (pos: Vector2d, stage: Konva.Stage) => {
const snappedPos = { ...pos };
// Get the normalized threshold for snapping to the edge of the stage
const thresholdX = SNAP_PX / stage.scaleX();
const thresholdY = SNAP_PX / stage.scaleY();
const stageWidth = stage.width() / stage.scaleX();
const stageHeight = stage.height() / stage.scaleY();
// Snap to the edge of the stage if within threshold
if (pos.x - thresholdX < 0) {
snappedPos.x = 0;
} else if (pos.x + thresholdX > stageWidth) {
snappedPos.x = Math.floor(stageWidth);
}
if (pos.y - thresholdY < 0) {
snappedPos.y = 0;
} else if (pos.y + thresholdY > stageHeight) {
snappedPos.y = Math.floor(stageHeight);
}
return snappedPos;
};
export const getScaledFlooredCursorPosition = (stage: Konva.Stage) => {
const pointerPosition = stage.getPointerPosition();
const stageTransform = stage.getAbsoluteTransform().copy();
if (!pointerPosition || !stageTransform) {
if (!pointerPosition) {
return;
}
const scaledCursorPosition = stageTransform.invert().point(pointerPosition);
@ -40,33 +64,41 @@ const syncCursorPos = (stage: Konva.Stage): Vector2d | null => {
if (!pos) {
return null;
}
$cursorPosition.set(pos);
$lastCursorPos.set(pos);
return pos;
};
const BRUSH_SPACING = 20;
const BRUSH_SPACING_PCT = 10;
const MIN_BRUSH_SPACING_PX = 5;
const MAX_BRUSH_SPACING_PX = 15;
export const useMouseEvents = () => {
const dispatch = useAppDispatch();
const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId);
const selectedLayerType = useAppSelector((s) => {
const selectedLayer = s.controlLayers.present.layers.find((l) => l.id === s.controlLayers.present.selectedLayerId);
if (!selectedLayer) {
return null;
}
return selectedLayer.type;
});
const tool = useStore($tool);
const lastCursorPosRef = useRef<[number, number] | null>(null);
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection);
const brushSize = useAppSelector((s) => s.controlLayers.present.brushSize);
const brushSpacingPx = useMemo(
() => clamp(brushSize / BRUSH_SPACING_PCT, MIN_BRUSH_SPACING_PX, MAX_BRUSH_SPACING_PX),
[brushSize]
);
const onMouseDown = useCallback(
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
(e: KonvaEventObject<MouseEvent>) => {
const stage = e.target.getStage();
if (!stage) {
return;
}
const pos = syncCursorPos(stage);
if (!pos) {
return;
}
$isMouseDown.set(true);
$lastMouseDownPos.set(pos);
if (!selectedLayerId) {
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
return;
}
if (tool === 'brush' || tool === 'eraser') {
@ -77,126 +109,105 @@ export const useMouseEvents = () => {
tool,
})
);
$isDrawing.set(true);
$lastMouseDownPos.set(pos);
} else if (tool === 'rect') {
$lastMouseDownPos.set(snapPosToStage(pos, stage));
}
},
[dispatch, selectedLayerId, tool]
[dispatch, selectedLayerId, selectedLayerType, tool]
);
const onMouseUp = useCallback(
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
const stage = e.target.getStage();
if (!stage) {
return;
}
$isMouseDown.set(false);
const pos = $cursorPosition.get();
const lastPos = $lastMouseDownPos.get();
const tool = $tool.get();
if (pos && lastPos && selectedLayerId && tool === 'rect') {
dispatch(
rgLayerRectAdded({
layerId: selectedLayerId,
rect: {
x: Math.min(pos.x, lastPos.x),
y: Math.min(pos.y, lastPos.y),
width: Math.abs(pos.x - lastPos.x),
height: Math.abs(pos.y - lastPos.y),
},
})
);
}
$lastMouseDownPos.set(null);
},
[dispatch, selectedLayerId]
);
const onMouseMove = useCallback(
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
const stage = e.target.getStage();
if (!stage) {
return;
}
const pos = syncCursorPos(stage);
if (!pos || !selectedLayerId) {
return;
}
if (getIsFocused(stage) && $isMouseOver.get() && $isMouseDown.get() && (tool === 'brush' || tool === 'eraser')) {
if (lastCursorPosRef.current) {
// Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number
if (Math.hypot(lastCursorPosRef.current[0] - pos.x, lastCursorPosRef.current[1] - pos.y) < BRUSH_SPACING) {
return;
}
}
lastCursorPosRef.current = [pos.x, pos.y];
dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: lastCursorPosRef.current }));
}
},
[dispatch, selectedLayerId, tool]
);
const onMouseLeave = useCallback(
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
const stage = e.target.getStage();
if (!stage) {
return;
}
const pos = syncCursorPos(stage);
if (
pos &&
selectedLayerId &&
getIsFocused(stage) &&
$isMouseOver.get() &&
$isMouseDown.get() &&
(tool === 'brush' || tool === 'eraser')
) {
dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: [pos.x, pos.y] }));
}
$isMouseOver.set(false);
$isMouseDown.set(false);
$cursorPosition.set(null);
},
[selectedLayerId, tool, dispatch]
);
const onMouseEnter = useCallback(
(e: KonvaEventObject<MouseEvent>) => {
const stage = e.target.getStage();
if (!stage) {
return;
}
$isMouseOver.set(true);
const pos = $lastCursorPos.get();
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
return;
}
const lastPos = $lastMouseDownPos.get();
const tool = $tool.get();
if (lastPos && selectedLayerId && tool === 'rect') {
const snappedPos = snapPosToStage(pos, stage);
dispatch(
rgLayerRectAdded({
layerId: selectedLayerId,
rect: {
x: Math.min(snappedPos.x, lastPos.x),
y: Math.min(snappedPos.y, lastPos.y),
width: Math.abs(snappedPos.x - lastPos.x),
height: Math.abs(snappedPos.y - lastPos.y),
},
})
);
}
$isDrawing.set(false);
$lastMouseDownPos.set(null);
},
[dispatch, selectedLayerId, selectedLayerType]
);
const onMouseMove = useCallback(
(e: KonvaEventObject<MouseEvent>) => {
const stage = e.target.getStage();
if (!stage) {
return;
}
const pos = syncCursorPos(stage);
if (!pos) {
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
return;
}
if (!getIsFocused(stage)) {
return;
}
if (e.evt.buttons !== 1) {
$isMouseDown.set(false);
} else {
$isMouseDown.set(true);
if (!selectedLayerId) {
return;
}
if (tool === 'brush' || tool === 'eraser') {
dispatch(
rgLayerLineAdded({
layerId: selectedLayerId,
points: [pos.x, pos.y, pos.x, pos.y],
tool,
})
);
if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) {
if ($isDrawing.get()) {
// Continue the last line
if (lastCursorPosRef.current) {
// Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number
if (Math.hypot(lastCursorPosRef.current[0] - pos.x, lastCursorPosRef.current[1] - pos.y) < brushSpacingPx) {
return;
}
}
lastCursorPosRef.current = [pos.x, pos.y];
dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: lastCursorPosRef.current }));
} else {
// Start a new line
dispatch(rgLayerLineAdded({ layerId: selectedLayerId, points: [pos.x, pos.y, pos.x, pos.y], tool }));
}
$isDrawing.set(true);
}
},
[dispatch, selectedLayerId, tool]
[brushSpacingPx, dispatch, selectedLayerId, selectedLayerType, tool]
);
const onMouseLeave = useCallback(
(e: KonvaEventObject<MouseEvent>) => {
const stage = e.target.getStage();
if (!stage) {
return;
}
const pos = syncCursorPos(stage);
if (!pos || !selectedLayerId || selectedLayerType !== 'regional_guidance_layer') {
return;
}
if (getIsFocused(stage) && getIsMouseDown(e) && (tool === 'brush' || tool === 'eraser')) {
dispatch(rgLayerPointsAdded({ layerId: selectedLayerId, point: [pos.x, pos.y] }));
}
$isDrawing.set(false);
$lastCursorPos.set(null);
$lastMouseDownPos.set(null);
},
[selectedLayerId, selectedLayerType, tool, dispatch]
);
const onMouseWheel = useCallback(
(e: KonvaEventObject<WheelEvent>) => {
e.evt.preventDefault();
if (selectedLayerType !== 'regional_guidance_layer' || (tool !== 'brush' && tool !== 'eraser')) {
return;
}
// checking for ctrl key is pressed or not,
// so that brush size can be controlled using ctrl + scroll up/down
@ -210,8 +221,8 @@ export const useMouseEvents = () => {
dispatch(brushSizeChanged(calculateNewBrushSize(brushSize, delta)));
}
},
[shouldInvertBrushSizeScrollDirection, brushSize, dispatch]
[selectedLayerType, tool, shouldInvertBrushSizeScrollDirection, dispatch, brushSize]
);
return { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel };
return { onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel };
};

View File

@ -1,20 +1,43 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import {
isControlAdapterLayer,
isInitialImageLayer,
isIPAdapterLayer,
isRegionalGuidanceLayer,
selectControlLayersSlice,
} from 'features/controlLayers/store/controlLayersSlice';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => {
const validLayers = controlLayers.present.layers
.filter(isRegionalGuidanceLayer)
.filter((l) => l.isEnabled)
.filter((l) => {
let count = 0;
controlLayers.present.layers.forEach((l) => {
if (isRegionalGuidanceLayer(l)) {
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
const hasAtLeastOneImagePrompt = l.ipAdapters.length > 0;
return hasTextPrompt || hasAtLeastOneImagePrompt;
});
const hasAtLeastOneImagePrompt = l.ipAdapters.filter((ipa) => Boolean(ipa.image)).length > 0;
if (hasTextPrompt || hasAtLeastOneImagePrompt) {
count += 1;
}
}
if (isControlAdapterLayer(l)) {
if (l.controlAdapter.image || l.controlAdapter.processedImage) {
count += 1;
}
}
if (isIPAdapterLayer(l)) {
if (l.ipAdapter.image) {
count += 1;
}
}
if (isInitialImageLayer(l)) {
if (l.image) {
count += 1;
}
}
});
return validLayers.length;
return count;
});
export const useControlLayersTitle = () => {

View File

@ -3,17 +3,18 @@ import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
import type {
CLIPVisionModel,
ControlMode,
ControlNetConfig,
IPAdapterConfig,
IPMethod,
CLIPVisionModelV2,
ControlModeV2,
ControlNetConfigV2,
IPAdapterConfigV2,
IPMethodV2,
ProcessorConfig,
T2IAdapterConfig,
T2IAdapterConfigV2,
} from 'features/controlLayers/util/controlAdapters';
import {
buildControlAdapterProcessor,
buildControlAdapterProcessorV2,
controlNetToT2IAdapter,
imageDTOToImageWithDims,
t2iAdapterToControlNet,
@ -38,6 +39,7 @@ import type {
ControlAdapterLayer,
ControlLayersState,
DrawingTool,
InitialImageLayer,
IPAdapterLayer,
Layer,
RegionalGuidanceLayer,
@ -70,18 +72,13 @@ export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanc
export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer =>
layer?.type === 'control_adapter_layer';
export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => layer?.type === 'ip_adapter_layer';
export const isRenderableLayer = (layer?: Layer): layer is RegionalGuidanceLayer | ControlAdapterLayer =>
layer?.type === 'regional_guidance_layer' || layer?.type === 'control_adapter_layer';
const resetLayer = (layer: Layer) => {
if (layer.type === 'regional_guidance_layer') {
layer.maskObjects = [];
layer.bbox = null;
layer.isEnabled = true;
layer.needsPixelBbox = false;
layer.bboxNeedsUpdate = false;
return;
}
};
export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => layer?.type === 'initial_image_layer';
export const isRenderableLayer = (
layer?: Layer
): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer =>
layer?.type === 'regional_guidance_layer' ||
layer?.type === 'control_adapter_layer' ||
layer?.type === 'initial_image_layer';
export const selectCALayerOrThrow = (state: ControlLayersState, layerId: string): ControlAdapterLayer => {
const layer = state.layers.find((l) => l.id === layerId);
@ -93,6 +90,11 @@ export const selectIPALayerOrThrow = (state: ControlLayersState, layerId: string
assert(isIPAdapterLayer(layer));
return layer;
};
export const selectIILayerOrThrow = (state: ControlLayersState, layerId: string): InitialImageLayer => {
const layer = state.layers.find((l) => l.id === layerId);
assert(isInitialImageLayer(layer));
return layer;
};
const selectCAOrIPALayerOrThrow = (
state: ControlLayersState,
layerId: string
@ -110,7 +112,7 @@ export const selectRGLayerIPAdapterOrThrow = (
state: ControlLayersState,
layerId: string,
ipAdapterId: string
): IPAdapterConfig => {
): IPAdapterConfigV2 => {
const layer = state.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer));
const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId);
@ -151,6 +153,9 @@ export const controlLayersSlice = createSlice({
layer.x = x;
layer.y = y;
}
if (isRegionalGuidanceLayer(layer)) {
layer.uploadedMaskImage = null;
}
},
layerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => {
const { layerId, bbox } = action.payload;
@ -161,14 +166,21 @@ export const controlLayersSlice = createSlice({
if (bbox === null && layer.type === 'regional_guidance_layer') {
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
layer.maskObjects = [];
layer.uploadedMaskImage = null;
layer.needsPixelBbox = false;
}
}
},
layerReset: (state, action: PayloadAction<string>) => {
const layer = state.layers.find((l) => l.id === action.payload);
if (layer) {
resetLayer(layer);
// TODO(psyche): Should other layer types also have reset functionality?
if (isRegionalGuidanceLayer(layer)) {
layer.maskObjects = [];
layer.bbox = null;
layer.isEnabled = true;
layer.needsPixelBbox = false;
layer.bboxNeedsUpdate = false;
layer.uploadedMaskImage = null;
}
},
layerDeleted: (state, action: PayloadAction<string>) => {
@ -201,12 +213,6 @@ export const controlLayersSlice = createSlice({
moveToFront(renderableLayers, cb);
state.layers = [...ipAdapterLayers, ...renderableLayers];
},
selectedLayerReset: (state) => {
const layer = state.layers.find((l) => l.id === state.selectedLayerId);
if (layer) {
resetLayer(layer);
}
},
selectedLayerDeleted: (state) => {
state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId);
state.selectedLayerId = state.layers[0]?.id ?? null;
@ -221,7 +227,7 @@ export const controlLayersSlice = createSlice({
caLayerAdded: {
reducer: (
state,
action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }>
action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2 }>
) => {
const { layerId, controlAdapter } = action.payload;
const layer: ControlAdapterLayer = {
@ -245,7 +251,7 @@ export const controlLayersSlice = createSlice({
}
}
},
prepare: (controlAdapter: ControlNetConfig | T2IAdapterConfig) => ({
prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({
payload: { layerId: uuidv4(), controlAdapter },
}),
},
@ -297,7 +303,7 @@ export const controlLayersSlice = createSlice({
layer.controlAdapter = controlNetToT2IAdapter(layer.controlAdapter);
}
const candidateProcessorConfig = buildControlAdapterProcessor(modelConfig);
const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig);
if (candidateProcessorConfig?.type !== layer.controlAdapter.processorConfig?.type) {
// The processor has changed. For example, the previous model was a Canny model and the new model is a Depth
// model. We need to use the new processor.
@ -305,7 +311,7 @@ export const controlLayersSlice = createSlice({
layer.controlAdapter.processorConfig = candidateProcessorConfig;
}
},
caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlMode }>) => {
caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlModeV2 }>) => {
const { layerId, controlMode } = action.payload;
const layer = selectCALayerOrThrow(state, layerId);
assert(layer.controlAdapter.type === 'controlnet');
@ -340,11 +346,17 @@ export const controlLayersSlice = createSlice({
const layer = selectCALayerOrThrow(state, layerId);
layer.controlAdapter.isProcessingImage = isProcessingImage;
},
caLayerControlNetsDeleted: (state) => {
state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 'controlnet');
},
caLayerT2IAdaptersDeleted: (state) => {
state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 't2i_adapter');
},
//#endregion
//#region IP Adapter Layers
ipaLayerAdded: {
reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => {
reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => {
const { layerId, ipAdapter } = action.payload;
const layer: IPAdapterLayer = {
id: getIPALayerId(layerId),
@ -354,14 +366,14 @@ export const controlLayersSlice = createSlice({
};
state.layers.push(layer);
},
prepare: (ipAdapter: IPAdapterConfig) => ({ payload: { layerId: uuidv4(), ipAdapter } }),
prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }),
},
ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
const { layerId, imageDTO } = action.payload;
const layer = selectIPALayerOrThrow(state, layerId);
layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethod }>) => {
ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethodV2 }>) => {
const { layerId, method } = action.payload;
const layer = selectIPALayerOrThrow(state, layerId);
layer.ipAdapter.method = method;
@ -383,12 +395,15 @@ export const controlLayersSlice = createSlice({
},
ipaLayerCLIPVisionModelChanged: (
state,
action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModel }>
action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModelV2 }>
) => {
const { layerId, clipVisionModel } = action.payload;
const layer = selectIPALayerOrThrow(state, layerId);
layer.ipAdapter.clipVisionModel = clipVisionModel;
},
ipaLayersDeleted: (state) => {
state.layers = state.layers.filter((l) => !isIPAdapterLayer(l));
},
//#endregion
//#region CA or IPA Layers
@ -435,6 +450,7 @@ export const controlLayersSlice = createSlice({
negativePrompt: null,
ipAdapters: [],
isSelected: true,
uploadedMaskImage: null,
};
state.layers.push(layer);
state.selectedLayerId = layer.id;
@ -484,6 +500,7 @@ export const controlLayersSlice = createSlice({
strokeWidth: state.brushSize,
});
layer.bboxNeedsUpdate = true;
layer.uploadedMaskImage = null;
if (!layer.needsPixelBbox && tool === 'eraser') {
layer.needsPixelBbox = true;
}
@ -503,6 +520,7 @@ export const controlLayersSlice = createSlice({
// TODO: Handle this in the event listener
lastLine.points.push(point[0] - layer.x, point[1] - layer.y);
layer.bboxNeedsUpdate = true;
layer.uploadedMaskImage = null;
},
rgLayerRectAdded: {
reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect; rectUuid: string }>) => {
@ -522,9 +540,15 @@ export const controlLayersSlice = createSlice({
height: rect.height,
});
layer.bboxNeedsUpdate = true;
layer.uploadedMaskImage = null;
},
prepare: (payload: { layerId: string; rect: IRect }) => ({ payload: { ...payload, rectUuid: uuidv4() } }),
},
rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => {
const { layerId, imageDTO } = action.payload;
const layer = selectRGLayerOrThrow(state, layerId);
layer.uploadedMaskImage = imageDTOToImageWithDims(imageDTO);
},
rgLayerAutoNegativeChanged: (
state,
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
@ -533,7 +557,7 @@ export const controlLayersSlice = createSlice({
const layer = selectRGLayerOrThrow(state, layerId);
layer.autoNegative = autoNegative;
},
rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfig }>) => {
rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => {
const { layerId, ipAdapter } = action.payload;
const layer = selectRGLayerOrThrow(state, layerId);
layer.ipAdapters.push(ipAdapter);
@ -569,7 +593,7 @@ export const controlLayersSlice = createSlice({
},
rgLayerIPAdapterMethodChanged: (
state,
action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethod }>
action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethodV2 }>
) => {
const { layerId, ipAdapterId, method } = action.payload;
const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId);
@ -593,7 +617,7 @@ export const controlLayersSlice = createSlice({
},
rgLayerIPAdapterCLIPVisionModelChanged: (
state,
action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModel }>
action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }>
) => {
const { layerId, ipAdapterId, clipVisionModel } = action.payload;
const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId);
@ -601,6 +625,49 @@ export const controlLayersSlice = createSlice({
},
//#endregion
//#region Initial Image Layer
iiLayerAdded: {
reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
const { layerId, imageDTO } = action.payload;
// Highlander! There can be only one!
state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true));
const layer: InitialImageLayer = {
id: layerId,
type: 'initial_image_layer',
opacity: 1,
x: 0,
y: 0,
bbox: null,
bboxNeedsUpdate: false,
isEnabled: true,
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
isSelected: true,
};
state.layers.push(layer);
state.selectedLayerId = layer.id;
for (const layer of state.layers.filter(isRenderableLayer)) {
if (layer.id !== layerId) {
layer.isSelected = false;
}
}
},
prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: 'initial_image_layer', imageDTO } }),
},
iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
const { layerId, imageDTO } = action.payload;
const layer = selectIILayerOrThrow(state, layerId);
layer.bbox = null;
layer.bboxNeedsUpdate = true;
layer.isEnabled = true;
layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
iiLayerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => {
const { layerId, opacity } = action.payload;
const layer = selectIILayerOrThrow(state, layerId);
layer.opacity = opacity;
},
//#endregion
//#region Globals
positivePromptChanged: (state, action: PayloadAction<string>) => {
state.positivePrompt = action.payload;
@ -617,20 +684,20 @@ export const controlLayersSlice = createSlice({
shouldConcatPromptsChanged: (state, action: PayloadAction<boolean>) => {
state.shouldConcatPrompts = action.payload;
},
widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean }>) => {
const { width, updateAspectRatio } = action.payload;
state.size.width = width;
widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => {
const { width, updateAspectRatio, clamp } = action.payload;
state.size.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width;
if (updateAspectRatio) {
state.size.aspectRatio.value = width / state.size.height;
state.size.aspectRatio.value = state.size.width / state.size.height;
state.size.aspectRatio.id = 'Free';
state.size.aspectRatio.isLocked = false;
}
},
heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean }>) => {
const { height, updateAspectRatio } = action.payload;
state.size.height = height;
heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>) => {
const { height, updateAspectRatio, clamp } = action.payload;
state.size.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height;
if (updateAspectRatio) {
state.size.aspectRatio.value = state.size.width / height;
state.size.aspectRatio.value = state.size.width / state.size.height;
state.size.aspectRatio.id = 'Free';
state.size.aspectRatio.isLocked = false;
}
@ -728,7 +795,6 @@ export const {
layerMovedToFront,
layerMovedBackward,
layerMovedToBack,
selectedLayerReset,
selectedLayerDeleted,
allLayersDeleted,
// CA Layers
@ -741,12 +807,15 @@ export const {
caLayerIsFilterEnabledChanged,
caLayerOpacityChanged,
caLayerIsProcessingImageChanged,
caLayerControlNetsDeleted,
caLayerT2IAdaptersDeleted,
// IPA Layers
ipaLayerAdded,
ipaLayerImageChanged,
ipaLayerMethodChanged,
ipaLayerModelChanged,
ipaLayerCLIPVisionModelChanged,
ipaLayersDeleted,
// CA or IPA Layers
caOrIPALayerWeightChanged,
caOrIPALayerBeginEndStepPctChanged,
@ -758,6 +827,7 @@ export const {
rgLayerLineAdded,
rgLayerPointsAdded,
rgLayerRectAdded,
rgLayerMaskImageUploaded,
rgLayerAutoNegativeChanged,
rgLayerIPAdapterAdded,
rgLayerIPAdapterDeleted,
@ -767,6 +837,10 @@ export const {
rgLayerIPAdapterMethodChanged,
rgLayerIPAdapterModelChanged,
rgLayerIPAdapterCLIPVisionModelChanged,
// II Layer
iiLayerAdded,
iiLayerImageChanged,
iiLayerOpacityChanged,
// Globals
positivePromptChanged,
negativePromptChanged,
@ -789,11 +863,10 @@ const migrateControlLayersState = (state: any): any => {
return state;
};
export const $isMouseDown = atom(false);
export const $isMouseOver = atom(false);
export const $isDrawing = atom(false);
export const $lastMouseDownPos = atom<Vector2d | null>(null);
export const $tool = atom<Tool>('brush');
export const $cursorPosition = atom<Vector2d | null>(null);
export const $lastCursorPos = atom<Vector2d | null>(null);
// IDs for singleton Konva layers and objects
export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer';
@ -813,7 +886,10 @@ export const RG_LAYER_NAME = 'regional_guidance_layer';
export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line';
export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group';
export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect';
export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer';
export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image';
export const LAYER_BBOX_NAME = 'layer.bbox';
export const COMPOSITING_RECT_NAME = 'compositing-rect';
// Getters for non-singleton layer and object IDs
const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;
@ -823,6 +899,7 @@ export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`;
export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`;
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {

View File

@ -1,4 +1,9 @@
import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlLayers/util/controlAdapters';
import type {
ControlNetConfigV2,
ImageWithDims,
IPAdapterConfigV2,
T2IAdapterConfigV2,
} from 'features/controlLayers/util/controlAdapters';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type {
ParameterAutoNegative,
@ -50,12 +55,12 @@ export type ControlAdapterLayer = RenderableLayerBase & {
type: 'control_adapter_layer'; // technically, also t2i adapter layer
opacity: number;
isFilterEnabled: boolean;
controlAdapter: ControlNetConfig | T2IAdapterConfig;
controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2;
};
export type IPAdapterLayer = LayerBase & {
type: 'ip_adapter_layer';
ipAdapter: IPAdapterConfig;
ipAdapter: IPAdapterConfigV2;
};
export type RegionalGuidanceLayer = RenderableLayerBase & {
@ -63,13 +68,20 @@ export type RegionalGuidanceLayer = RenderableLayerBase & {
maskObjects: (VectorMaskLine | VectorMaskRect)[];
positivePrompt: ParameterPositivePrompt | null;
negativePrompt: ParameterNegativePrompt | null; // Up to one text prompt per mask
ipAdapters: IPAdapterConfig[]; // Any number of image prompts
ipAdapters: IPAdapterConfigV2[]; // Any number of image prompts
previewColor: RgbColor;
autoNegative: ParameterAutoNegative;
needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object
uploadedMaskImage: ImageWithDims | null;
};
export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer;
export type InitialImageLayer = RenderableLayerBase & {
type: 'initial_image_layer';
opacity: number;
image: ImageWithDims | null;
};
export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer | InitialImageLayer;
export type ControlLayersState = {
_version: 1;

View File

@ -123,7 +123,7 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal
return correctedLayerBbox;
};
export const getLayerBboxFast = (layer: KonvaLayerType): IRect | null => {
export const getLayerBboxFast = (layer: KonvaLayerType): IRect => {
const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG);
return {
x: Math.floor(bbox.x),

View File

@ -4,20 +4,20 @@ import { assert } from 'tsafe';
import { describe, test } from 'vitest';
import type {
CLIPVisionModel,
ControlMode,
CLIPVisionModelV2,
ControlModeV2,
DepthAnythingModelSize,
IPMethod,
IPMethodV2,
ProcessorConfig,
ProcessorType,
ProcessorTypeV2,
} from './controlAdapters';
describe('Control Adapter Types', () => {
test('ProcessorType', () => assert<Equals<ProcessorConfig['type'], ProcessorType>>());
test('IP Adapter Method', () => assert<Equals<NonNullable<S['IPAdapterInvocation']['method']>, IPMethod>>());
test('ProcessorType', () => assert<Equals<ProcessorConfig['type'], ProcessorTypeV2>>());
test('IP Adapter Method', () => assert<Equals<NonNullable<S['IPAdapterInvocation']['method']>, IPMethodV2>>());
test('CLIP Vision Model', () =>
assert<Equals<NonNullable<S['IPAdapterInvocation']['clip_vision_model']>, CLIPVisionModel>>());
test('Control Mode', () => assert<Equals<NonNullable<S['ControlNetInvocation']['control_mode']>, ControlMode>>());
assert<Equals<NonNullable<S['IPAdapterInvocation']['clip_vision_model']>, CLIPVisionModelV2>>());
test('Control Mode', () => assert<Equals<NonNullable<S['ControlNetInvocation']['control_mode']>, ControlModeV2>>());
test('DepthAnything Model Size', () =>
assert<Equals<NonNullable<S['DepthAnythingImageProcessorInvocation']['model_size']>, DepthAnythingModelSize>>());
});

View File

@ -94,45 +94,45 @@ type ControlAdapterBase = {
beginEndStepPct: [number, number];
};
const zControlMode = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
export type ControlMode = z.infer<typeof zControlMode>;
export const isControlMode = (v: unknown): v is ControlMode => zControlMode.safeParse(v).success;
const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
export type ControlModeV2 = z.infer<typeof zControlModeV2>;
export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success;
export type ControlNetConfig = ControlAdapterBase & {
export type ControlNetConfigV2 = ControlAdapterBase & {
type: 'controlnet';
model: ParameterControlNetModel | null;
controlMode: ControlMode;
controlMode: ControlModeV2;
};
export const isControlNetConfig = (ca: ControlNetConfig | T2IAdapterConfig): ca is ControlNetConfig =>
export const isControlNetConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is ControlNetConfigV2 =>
ca.type === 'controlnet';
export type T2IAdapterConfig = ControlAdapterBase & {
export type T2IAdapterConfigV2 = ControlAdapterBase & {
type: 't2i_adapter';
model: ParameterT2IAdapterModel | null;
};
export const isT2IAdapterConfig = (ca: ControlNetConfig | T2IAdapterConfig): ca is T2IAdapterConfig =>
export const isT2IAdapterConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is T2IAdapterConfigV2 =>
ca.type === 't2i_adapter';
const zCLIPVisionModel = z.enum(['ViT-H', 'ViT-G']);
export type CLIPVisionModel = z.infer<typeof zCLIPVisionModel>;
export const isCLIPVisionModel = (v: unknown): v is CLIPVisionModel => zCLIPVisionModel.safeParse(v).success;
const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']);
export type CLIPVisionModelV2 = z.infer<typeof zCLIPVisionModelV2>;
export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success;
const zIPMethod = z.enum(['full', 'style', 'composition']);
export type IPMethod = z.infer<typeof zIPMethod>;
export const isIPMethod = (v: unknown): v is IPMethod => zIPMethod.safeParse(v).success;
const zIPMethodV2 = z.enum(['full', 'style', 'composition']);
export type IPMethodV2 = z.infer<typeof zIPMethodV2>;
export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success;
export type IPAdapterConfig = {
export type IPAdapterConfigV2 = {
id: string;
type: 'ip_adapter';
weight: number;
method: IPMethod;
method: IPMethodV2;
image: ImageWithDims | null;
model: ParameterIPAdapterModel | null;
clipVisionModel: CLIPVisionModel;
clipVisionModel: CLIPVisionModelV2;
beginEndStepPct: [number, number];
};
const zProcessorType = z.enum([
const zProcessorTypeV2 = z.enum([
'canny_image_processor',
'color_map_image_processor',
'content_shuffle_image_processor',
@ -148,10 +148,10 @@ const zProcessorType = z.enum([
'pidi_image_processor',
'zoe_depth_image_processor',
]);
export type ProcessorType = z.infer<typeof zProcessorType>;
export const isProcessorType = (v: unknown): v is ProcessorType => zProcessorType.safeParse(v).success;
export type ProcessorTypeV2 = z.infer<typeof zProcessorTypeV2>;
export const isProcessorTypeV2 = (v: unknown): v is ProcessorTypeV2 => zProcessorTypeV2.safeParse(v).success;
type ProcessorData<T extends ProcessorType> = {
type ProcessorData<T extends ProcessorTypeV2> = {
type: T;
labelTKey: string;
descriptionTKey: string;
@ -165,7 +165,7 @@ type ProcessorData<T extends ProcessorType> = {
const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height);
type CAProcessorsData = {
[key in ProcessorType]: ProcessorData<key>;
[key in ProcessorTypeV2]: ProcessorData<key>;
};
/**
* A dict of ControlNet processors, including:
@ -176,7 +176,7 @@ type CAProcessorsData = {
*
* TODO: Generate from the OpenAPI schema
*/
export const CONTROLNET_PROCESSORS: CAProcessorsData = {
export const CA_PROCESSOR_DATA: CAProcessorsData = {
canny_image_processor: {
type: 'canny_image_processor',
labelTKey: 'controlnet.canny',
@ -405,7 +405,7 @@ export const CONTROLNET_PROCESSORS: CAProcessorsData = {
},
};
const initialControlNet: Omit<ControlNetConfig, 'id'> = {
export const initialControlNetV2: Omit<ControlNetConfigV2, 'id'> = {
type: 'controlnet',
model: null,
weight: 1,
@ -414,10 +414,10 @@ const initialControlNet: Omit<ControlNetConfig, 'id'> = {
image: null,
processedImage: null,
isProcessingImage: false,
processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(),
processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
};
const initialT2IAdapter: Omit<T2IAdapterConfig, 'id'> = {
export const initialT2IAdapterV2: Omit<T2IAdapterConfigV2, 'id'> = {
type: 't2i_adapter',
model: null,
weight: 1,
@ -425,10 +425,10 @@ const initialT2IAdapter: Omit<T2IAdapterConfig, 'id'> = {
image: null,
processedImage: null,
isProcessingImage: false,
processorConfig: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults(),
processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(),
};
const initialIPAdapter: Omit<IPAdapterConfig, 'id'> = {
export const initialIPAdapterV2: Omit<IPAdapterConfigV2, 'id'> = {
type: 'ip_adapter',
image: null,
model: null,
@ -438,26 +438,26 @@ const initialIPAdapter: Omit<IPAdapterConfig, 'id'> = {
weight: 1,
};
export const buildControlNet = (id: string, overrides?: Partial<ControlNetConfig>): ControlNetConfig => {
return merge(deepClone(initialControlNet), { id, ...overrides });
export const buildControlNet = (id: string, overrides?: Partial<ControlNetConfigV2>): ControlNetConfigV2 => {
return merge(deepClone(initialControlNetV2), { id, ...overrides });
};
export const buildT2IAdapter = (id: string, overrides?: Partial<T2IAdapterConfig>): T2IAdapterConfig => {
return merge(deepClone(initialT2IAdapter), { id, ...overrides });
export const buildT2IAdapter = (id: string, overrides?: Partial<T2IAdapterConfigV2>): T2IAdapterConfigV2 => {
return merge(deepClone(initialT2IAdapterV2), { id, ...overrides });
};
export const buildIPAdapter = (id: string, overrides?: Partial<IPAdapterConfig>): IPAdapterConfig => {
return merge(deepClone(initialIPAdapter), { id, ...overrides });
export const buildIPAdapter = (id: string, overrides?: Partial<IPAdapterConfigV2>): IPAdapterConfigV2 => {
return merge(deepClone(initialIPAdapterV2), { id, ...overrides });
};
export const buildControlAdapterProcessor = (
export const buildControlAdapterProcessorV2 = (
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig
): ProcessorConfig | null => {
const defaultPreprocessor = modelConfig.default_settings?.preprocessor;
if (!isProcessorType(defaultPreprocessor)) {
if (!isProcessorTypeV2(defaultPreprocessor)) {
return null;
}
const processorConfig = CONTROLNET_PROCESSORS[defaultPreprocessor].buildDefaults(modelConfig.base);
const processorConfig = CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults(modelConfig.base);
return processorConfig;
};
@ -467,15 +467,15 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO)
height,
});
export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfig): ControlNetConfig => {
export const t2iAdapterToControlNet = (t2iAdapter: T2IAdapterConfigV2): ControlNetConfigV2 => {
return {
...deepClone(t2iAdapter),
type: 'controlnet',
controlMode: initialControlNet.controlMode,
controlMode: initialControlNetV2.controlMode,
};
};
export const controlNetToT2IAdapter = (controlNet: ControlNetConfig): T2IAdapterConfig => {
export const controlNetToT2IAdapter = (controlNet: ControlNetConfigV2): T2IAdapterConfigV2 => {
return {
...omit(deepClone(controlNet), 'controlMode'),
type: 't2i_adapter',

View File

@ -1,16 +1,21 @@
import { getStore } from 'app/store/nanostores/store';
import { rgbaColorToString, rgbColorToString } from 'features/canvas/util/colorToString';
import { getScaledFlooredCursorPosition } from 'features/controlLayers/hooks/mouseEventHooks';
import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/hooks/mouseEventHooks';
import {
$tool,
BACKGROUND_LAYER_ID,
BACKGROUND_RECT_ID,
CA_LAYER_IMAGE_NAME,
CA_LAYER_NAME,
COMPOSITING_RECT_NAME,
getCALayerImageId,
getIILayerImageId,
getLayerBboxId,
getRGLayerObjectGroupId,
INITIAL_IMAGE_LAYER_IMAGE_NAME,
INITIAL_IMAGE_LAYER_NAME,
isControlAdapterLayer,
isInitialImageLayer,
isRegionalGuidanceLayer,
isRenderableLayer,
LAYER_BBOX_NAME,
@ -28,6 +33,7 @@ import {
} from 'features/controlLayers/store/controlLayersSlice';
import type {
ControlAdapterLayer,
InitialImageLayer,
Layer,
RegionalGuidanceLayer,
Tool,
@ -35,6 +41,7 @@ import type {
VectorMaskRect,
} from 'features/controlLayers/store/types';
import { getLayerBboxFast, getLayerBboxPixels } from 'features/controlLayers/util/bbox';
import { t } from 'i18next';
import Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types';
import { debounce } from 'lodash-es';
@ -52,7 +59,8 @@ const STAGE_BG_DATAURL =
const mapId = (object: { id: string }) => object.id;
const selectRenderableLayers = (n: Konva.Node) => n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME;
const selectRenderableLayers = (n: Konva.Node) =>
n.name() === RG_LAYER_NAME || n.name() === CA_LAYER_NAME || n.name() === INITIAL_IMAGE_LAYER_NAME;
const selectVectorMaskObjects = (node: Konva.Node) => {
return node.name() === RG_LAYER_LINE_NAME || node.name() === RG_LAYER_RECT_NAME;
@ -137,10 +145,9 @@ const renderToolPreview = (
globalMaskLayerOpacity: number,
cursorPos: Vector2d | null,
lastMouseDownPos: Vector2d | null,
isMouseOver: boolean,
brushSize: number
) => {
const layerCount = stage.find(`.${RG_LAYER_NAME}`).length;
const layerCount = stage.find(selectRenderableLayers).length;
// Update the stage's pointer style
if (layerCount === 0) {
// We have no layers, so we should not render any tool
@ -161,7 +168,7 @@ const renderToolPreview = (
const toolPreviewLayer = stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`) ?? createToolPreviewLayer(stage);
if (!isMouseOver || layerCount === 0) {
if (!cursorPos || layerCount === 0) {
// We can bail early if the mouse isn't over the stage or there are no layers
toolPreviewLayer.visible(false);
return;
@ -205,12 +212,13 @@ const renderToolPreview = (
}
if (cursorPos && lastMouseDownPos && tool === 'rect') {
const snappedPos = snapPosToStage(cursorPos, stage);
const rectPreview = toolPreviewLayer.findOne<Konva.Rect>(`#${TOOL_PREVIEW_RECT_ID}`);
rectPreview?.setAttrs({
x: Math.min(cursorPos.x, lastMouseDownPos.x),
y: Math.min(cursorPos.y, lastMouseDownPos.y),
width: Math.abs(cursorPos.x - lastMouseDownPos.x),
height: Math.abs(cursorPos.y - lastMouseDownPos.y),
x: Math.min(snappedPos.x, lastMouseDownPos.x),
y: Math.min(snappedPos.y, lastMouseDownPos.y),
width: Math.abs(snappedPos.x - lastMouseDownPos.x),
height: Math.abs(snappedPos.y - lastMouseDownPos.y),
});
rectPreview?.visible(true);
} else {
@ -317,6 +325,12 @@ const createVectorMaskRect = (reduxObject: VectorMaskRect, konvaGroup: Konva.Gro
return vectorMaskRect;
};
const createCompositingRect = (konvaLayer: Konva.Layer): Konva.Rect => {
const compositingRect = new Konva.Rect({ name: COMPOSITING_RECT_NAME, listening: false });
konvaLayer.add(compositingRect);
return compositingRect;
};
/**
* Renders a vector mask layer.
* @param stage The konva stage to render on.
@ -394,19 +408,157 @@ const renderRegionalGuidanceLayer = (
groupNeedsCache = true;
}
if (konvaObjectGroup.children.length === 0) {
if (konvaObjectGroup.getChildren().length === 0) {
// No objects - clear the cache to reset the previous pixel data
konvaObjectGroup.clearCache();
} else if (groupNeedsCache) {
konvaObjectGroup.cache();
return;
}
// Updating group opacity does not require re-caching
if (konvaObjectGroup.opacity() !== globalMaskLayerOpacity) {
const compositingRect =
konvaLayer.findOne<Konva.Rect>(`.${COMPOSITING_RECT_NAME}`) ?? createCompositingRect(konvaLayer);
/**
* When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows
* shapes to render as a "raster" layer with all pixels drawn at the same color and opacity.
*
* Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The
* effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity.
* Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes.
*
* Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to
* a single raster image, and _then_ applied the 50% opacity.
*/
if (reduxLayer.isSelected && tool !== 'move') {
// We must clear the cache first so Konva will re-draw the group with the new compositing rect
if (konvaObjectGroup.isCached()) {
konvaObjectGroup.clearCache();
}
// The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work
konvaObjectGroup.opacity(1);
compositingRect.setAttrs({
// The rect should be the size of the layer - use the fast method bc it's OK if the rect is larger
...getLayerBboxFast(konvaLayer),
fill: rgbColor,
opacity: globalMaskLayerOpacity,
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
globalCompositeOperation: 'source-in',
visible: true,
// This rect must always be on top of all other shapes
zIndex: konvaObjectGroup.getChildren().length,
});
} else {
// The compositing rect should only be shown when the layer is selected.
compositingRect.visible(false);
// Cache only if needed - or if we are on this code path and _don't_ have a cache
if (groupNeedsCache || !konvaObjectGroup.isCached()) {
konvaObjectGroup.cache();
}
// Updating group opacity does not require re-caching
konvaObjectGroup.opacity(globalMaskLayerOpacity);
}
};
const createInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLayer): Konva.Layer => {
const konvaLayer = new Konva.Layer({
id: reduxLayer.id,
name: INITIAL_IMAGE_LAYER_NAME,
imageSmoothingEnabled: true,
});
stage.add(konvaLayer);
return konvaLayer;
};
const createInitialImageLayerImage = (konvaLayer: Konva.Layer, image: HTMLImageElement): Konva.Image => {
const konvaImage = new Konva.Image({
name: INITIAL_IMAGE_LAYER_IMAGE_NAME,
image,
});
konvaLayer.add(konvaImage);
return konvaImage;
};
const updateInitialImageLayerImageAttrs = (
stage: Konva.Stage,
konvaImage: Konva.Image,
reduxLayer: InitialImageLayer
) => {
const newWidth = stage.width() / stage.scaleX();
const newHeight = stage.height() / stage.scaleY();
if (
konvaImage.width() !== newWidth ||
konvaImage.height() !== newHeight ||
konvaImage.visible() !== reduxLayer.isEnabled
) {
konvaImage.setAttrs({
opacity: reduxLayer.opacity,
scaleX: 1,
scaleY: 1,
width: stage.width() / stage.scaleX(),
height: stage.height() / stage.scaleY(),
visible: reduxLayer.isEnabled,
});
}
if (konvaImage.opacity() !== reduxLayer.opacity) {
konvaImage.opacity(reduxLayer.opacity);
}
};
const updateInitialImageLayerImageSource = async (
stage: Konva.Stage,
konvaLayer: Konva.Layer,
reduxLayer: InitialImageLayer
) => {
if (reduxLayer.image) {
const { imageName } = reduxLayer.image;
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
const imageDTO = await req.unwrap();
req.unsubscribe();
const imageEl = new Image();
const imageId = getIILayerImageId(reduxLayer.id, imageName);
imageEl.onload = () => {
// Find the existing image or create a new one - must find using the name, bc the id may have just changed
const konvaImage =
konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`) ??
createInitialImageLayerImage(konvaLayer, imageEl);
// Update the image's attributes
konvaImage.setAttrs({
id: imageId,
image: imageEl,
});
updateInitialImageLayerImageAttrs(stage, konvaImage, reduxLayer);
imageEl.id = imageId;
};
imageEl.src = imageDTO.image_url;
} else {
konvaLayer.findOne(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`)?.destroy();
}
};
const renderInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLayer) => {
const konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`) ?? createInitialImageLayer(stage, reduxLayer);
const konvaImage = konvaLayer.findOne<Konva.Image>(`.${INITIAL_IMAGE_LAYER_IMAGE_NAME}`);
const canvasImageSource = konvaImage?.image();
let imageSourceNeedsUpdate = false;
if (canvasImageSource instanceof HTMLImageElement) {
const image = reduxLayer.image;
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) {
imageSourceNeedsUpdate = true;
} else if (!image) {
imageSourceNeedsUpdate = true;
}
} else if (!canvasImageSource) {
imageSourceNeedsUpdate = true;
}
if (imageSourceNeedsUpdate) {
updateInitialImageLayerImageSource(stage, konvaLayer, reduxLayer);
} else if (konvaImage) {
updateInitialImageLayerImageAttrs(stage, konvaImage, reduxLayer);
}
};
const createControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLayer): Konva.Layer => {
const konvaLayer = new Konva.Layer({
id: reduxLayer.id,
@ -547,6 +699,9 @@ const renderLayers = (
if (isControlAdapterLayer(reduxLayer)) {
renderControlNetLayer(stage, reduxLayer);
}
if (isInitialImageLayer(reduxLayer)) {
renderInitialImageLayer(stage, reduxLayer);
}
}
};
@ -711,7 +866,7 @@ const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => {
y: 0,
align: 'center',
verticalAlign: 'middle',
text: 'No Layers Added',
text: t('controlLayers.noLayersAdded'),
fontFamily: '"Inter Variable", sans-serif',
fontStyle: '600',
fill: 'white',

View File

@ -3,6 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors';
import {
@ -12,7 +13,6 @@ import {
} from 'features/deleteImageModal/store/slice';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
import { some } from 'lodash-es';
import type { ChangeEvent } from 'react';
@ -24,24 +24,24 @@ import ImageUsageMessage from './ImageUsageMessage';
const selectImageUsages = createMemoizedSelector(
[
selectDeleteImageModalSlice,
selectGenerationSlice,
selectCanvasSlice,
selectNodesSlice,
selectControlAdaptersSlice,
selectControlLayersSlice,
selectImageUsage,
],
(deleteImageModal, generation, canvas, nodes, controlAdapters, imagesUsage) => {
(deleteImageModal, canvas, nodes, controlAdapters, controlLayers, imagesUsage) => {
const { imagesToDelete } = deleteImageModal;
const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) =>
getImageUsage(generation, canvas, nodes, controlAdapters, image_name)
getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name)
);
const imageUsageSummary: ImageUsage = {
isInitialImage: some(allImageUsage, (i) => i.isInitialImage),
isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage),
isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
isControlImage: some(allImageUsage, (i) => i.isControlImage),
isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
};
return {

View File

@ -29,10 +29,10 @@ const ImageUsageMessage = (props: Props) => {
<>
<Text>{topMessage}</Text>
<UnorderedList paddingInlineStart={6}>
{imageUsage.isInitialImage && <ListItem>{t('common.img2img')}</ListItem>}
{imageUsage.isCanvasImage && <ListItem>{t('common.unifiedCanvas')}</ListItem>}
{imageUsage.isCanvasImage && <ListItem>{t('ui.tabs.canvasTab')}</ListItem>}
{imageUsage.isControlImage && <ListItem>{t('common.controlNet')}</ListItem>}
{imageUsage.isNodesImage && <ListItem>{t('common.nodeEditor')}</ListItem>}
{imageUsage.isNodesImage && <ListItem>{t('ui.tabs.workflowsTab')}</ListItem>}
{imageUsage.isControlLayerImage && <ListItem>{t('ui.tabs.generationTab')}</ListItem>}
</UnorderedList>
<Text>{bottomMessage}</Text>
</>

View File

@ -7,26 +7,30 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice';
import type { ControlAdaptersState } from 'features/controlAdapters/store/types';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import {
isControlAdapterLayer,
isInitialImageLayer,
isIPAdapterLayer,
isRegionalGuidanceLayer,
selectControlLayersSlice,
} from 'features/controlLayers/store/controlLayersSlice';
import type { ControlLayersState } from 'features/controlLayers/store/types';
import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import type { NodesState } from 'features/nodes/store/types';
import { isImageFieldInputInstance } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import type { GenerationState } from 'features/parameters/store/types';
import { some } from 'lodash-es';
import type { ImageUsage } from './types';
export const getImageUsage = (
generation: GenerationState,
canvas: CanvasState,
nodes: NodesState,
controlAdapters: ControlAdaptersState,
controlLayers: ControlLayersState,
image_name: string
) => {
const isInitialImage = generation.initialImage?.imageName === image_name;
const isCanvasImage = canvas.layerState.objects.some((obj) => obj.kind === 'image' && obj.imageName === image_name);
const isNodesImage = nodes.nodes.filter(isInvocationNode).some((node) => {
@ -40,11 +44,29 @@ export const getImageUsage = (
(ca) => ca.controlImage === image_name || (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === image_name)
);
const isControlLayerImage = controlLayers.layers.some((l) => {
if (isRegionalGuidanceLayer(l)) {
return l.ipAdapters.some((ipa) => ipa.image?.imageName === image_name);
}
if (isControlAdapterLayer(l)) {
return (
l.controlAdapter.image?.imageName === image_name || l.controlAdapter.processedImage?.imageName === image_name
);
}
if (isIPAdapterLayer(l)) {
return l.ipAdapter.image?.imageName === image_name;
}
if (isInitialImageLayer(l)) {
return l.image?.imageName === image_name;
}
return false;
});
const imageUsage: ImageUsage = {
isInitialImage,
isCanvasImage,
isNodesImage,
isControlImage,
isControlLayerImage,
};
return imageUsage;
@ -52,11 +74,11 @@ export const getImageUsage = (
export const selectImageUsage = createMemoizedSelector(
selectDeleteImageModalSlice,
selectGenerationSlice,
selectCanvasSlice,
selectNodesSlice,
selectControlAdaptersSlice,
(deleteImageModal, generation, canvas, nodes, controlAdapters) => {
selectControlLayersSlice,
(deleteImageModal, canvas, nodes, controlAdapters, controlLayers) => {
const { imagesToDelete } = deleteImageModal;
if (!imagesToDelete.length) {
@ -64,7 +86,7 @@ export const selectImageUsage = createMemoizedSelector(
}
const imagesUsage = imagesToDelete.map((i) =>
getImageUsage(generation, canvas, nodes, controlAdapters, i.image_name)
getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, i.image_name)
);
return imagesUsage;

View File

@ -6,8 +6,8 @@ export type DeleteImageState = {
};
export type ImageUsage = {
isInitialImage: boolean;
isCanvasImage: boolean;
isNodesImage: boolean;
isControlImage: boolean;
isControlLayerImage: boolean;
};

View File

@ -7,7 +7,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback } from 'react';
const selectZoom = createSelector([selectNodesSlice, activeTabNameSelector], (nodes, activeTabName) =>
activeTabName === 'nodes' ? nodes.viewport.zoom : 1
activeTabName === 'workflows' ? nodes.viewport.zoom : 1
);
/**

View File

@ -22,10 +22,6 @@ type CurrentImageDropData = BaseDropData & {
actionType: 'SET_CURRENT_IMAGE';
};
type InitialImageDropData = BaseDropData & {
actionType: 'SET_INITIAL_IMAGE';
};
type ControlAdapterDropData = BaseDropData & {
actionType: 'SET_CONTROL_ADAPTER_IMAGE';
context: {
@ -55,6 +51,13 @@ export type RGLayerIPAdapterImageDropData = BaseDropData & {
};
};
export type IILayerImageDropData = BaseDropData & {
actionType: 'SET_II_LAYER_IMAGE';
context: {
layerId: string;
};
};
export type CanvasInitialImageDropData = BaseDropData & {
actionType: 'SET_CANVAS_INITIAL_IMAGE';
};
@ -78,7 +81,6 @@ export type RemoveFromBoardDropData = BaseDropData & {
export type TypesafeDroppableData =
| CurrentImageDropData
| InitialImageDropData
| ControlAdapterDropData
| CanvasInitialImageDropData
| NodesImageDropData
@ -86,7 +88,8 @@ export type TypesafeDroppableData =
| RemoveFromBoardDropData
| CALayerImageDropData
| IPALayerImageDropData
| RGLayerIPAdapterImageDropData;
| RGLayerIPAdapterImageDropData
| IILayerImageDropData;
type BaseDragData = {
id: string;

View File

@ -15,8 +15,6 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active:
switch (actionType) {
case 'SET_CURRENT_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_INITIAL_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_CONTROL_ADAPTER_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_CA_LAYER_IMAGE':
@ -25,6 +23,8 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active:
return payloadType === 'IMAGE_DTO';
case 'SET_RG_LAYER_IP_ADAPTER_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_II_LAYER_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_CANVAS_INITIAL_IMAGE':
return payloadType === 'IMAGE_DTO';
case 'SET_NODES_IMAGE':

View File

@ -15,11 +15,11 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { some } from 'lodash-es';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@ -43,17 +43,17 @@ const DeleteBoardModal = (props: Props) => {
const selectImageUsageSummary = useMemo(
() =>
createMemoizedSelector(
[selectGenerationSlice, selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice],
(generation, canvas, nodes, controlAdapters) => {
[selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectControlLayersSlice],
(canvas, nodes, controlAdapters, controlLayers) => {
const allImageUsage = (boardImageNames ?? []).map((imageName) =>
getImageUsage(generation, canvas, nodes, controlAdapters, imageName)
getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName)
);
const imageUsageSummary: ImageUsage = {
isInitialImage: some(allImageUsage, (i) => i.isInitialImage),
isCanvasImage: some(allImageUsage, (i) => i.isCanvasImage),
isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
isControlImage: some(allImageUsage, (i) => i.isControlImage),
isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
};
return imageUsageSummary;

View File

@ -1,254 +0,0 @@
import { ButtonGroup, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppToaster } from 'app/components/Toaster';
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { sentImageToImg2Img } from 'features/gallery/store/actions';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers';
import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings';
import { initialImageSelected } from 'features/parameters/store/actions';
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { setShouldShowImageDetails, setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import {
PiArrowsCounterClockwiseBold,
PiAsteriskBold,
PiDotsThreeOutlineFill,
PiFlowArrowBold,
PiHourglassHighBold,
PiInfoBold,
PiPlantBold,
PiQuotesBold,
PiRulerBold,
} from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
const selectShouldDisableToolbarButtons = createSelector(
selectSystemSlice,
selectGallerySlice,
selectLastSelectedImage,
(system, gallery, lastSelectedImage) => {
const hasProgressImage = Boolean(system.denoiseProgress?.progress_image);
return hasProgressImage || !lastSelectedImage;
}
);
const CurrentImageButtons = () => {
const dispatch = useAppDispatch();
const isConnected = useAppSelector((s) => s.system.isConnected);
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
const selection = useAppSelector((s) => s.gallery.selection);
const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons);
const isUpscalingEnabled = useFeatureStatus('upscaling');
const isQueueMutationInProgress = useIsQueueMutationInProgress();
const toaster = useAppToaster();
const { t } = useTranslation();
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } =
useImageActions(lastSelectedImage?.image_name);
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
const handleLoadWorkflow = useCallback(() => {
if (!lastSelectedImage || !lastSelectedImage.has_workflow) {
return;
}
getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name);
}, [getAndLoadEmbeddedWorkflow, lastSelectedImage]);
useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]);
useHotkeys('a', recallAll, [recallAll]);
useHotkeys('s', recallSeed, [recallSeed]);
useHotkeys('p', recallPrompts, [recallPrompts]);
useHotkeys('r', remix, [remix]);
const handleUseSize = useCallback(() => {
parseAndRecallImageDimensions(lastSelectedImage);
}, [lastSelectedImage]);
useHotkeys('d', handleUseSize, [handleUseSize]);
const handleSendToImageToImage = useCallback(() => {
dispatch(sentImageToImg2Img());
dispatch(initialImageSelected(imageDTO));
}, [dispatch, imageDTO]);
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
const handleClickUpscale = useCallback(() => {
if (!imageDTO) {
return;
}
dispatch(upscaleRequested({ imageDTO }));
}, [dispatch, imageDTO]);
const handleDelete = useCallback(() => {
if (!imageDTO) {
return;
}
dispatch(imagesToDeleteSelected(selection));
}, [dispatch, imageDTO, selection]);
useHotkeys(
'Shift+U',
() => {
handleClickUpscale();
},
{
enabled: () => Boolean(isUpscalingEnabled && !shouldDisableToolbarButtons && isConnected),
},
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected]
);
const handleClickShowImageDetails = useCallback(
() => dispatch(setShouldShowImageDetails(!shouldShowImageDetails)),
[dispatch, shouldShowImageDetails]
);
useHotkeys(
'i',
() => {
if (imageDTO) {
handleClickShowImageDetails();
} else {
toaster({
title: t('toast.metadataLoadFailed'),
status: 'error',
duration: 2500,
isClosable: true,
});
}
},
[imageDTO, shouldShowImageDetails, toaster]
);
useHotkeys(
'delete',
() => {
handleDelete();
},
[dispatch, imageDTO]
);
const handleClickProgressImagesToggle = useCallback(() => {
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
}, [dispatch, shouldShowProgressInViewer]);
return (
<>
<Flex flexWrap="wrap" justifyContent="center" alignItems="center" gap={2}>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<Menu isLazy>
<MenuButton
as={IconButton}
aria-label={t('parameters.imageActions')}
tooltip={t('parameters.imageActions')}
isDisabled={!imageDTO}
icon={<PiDotsThreeOutlineFill />}
/>
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
</Menu>
</ButtonGroup>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<IconButton
icon={<PiFlowArrowBold />}
tooltip={`${t('nodes.loadWorkflow')} (W)`}
aria-label={`${t('nodes.loadWorkflow')} (W)`}
isDisabled={!imageDTO?.has_workflow}
onClick={handleLoadWorkflow}
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiArrowsCounterClockwiseBold />}
tooltip={`${t('parameters.remixImage')} (R)`}
aria-label={`${t('parameters.remixImage')} (R)`}
isDisabled={!hasMetadata}
onClick={remix}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiQuotesBold />}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!hasPrompts}
onClick={recallPrompts}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiPlantBold />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!hasSeed}
onClick={recallSeed}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiRulerBold />}
tooltip={`${t('parameters.useSize')} (D)`}
aria-label={`${t('parameters.useSize')} (D)`}
onClick={handleUseSize}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiAsteriskBold />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={!hasMetadata}
onClick={recallAll}
/>
</ButtonGroup>
{isUpscalingEnabled && (
<ButtonGroup isDisabled={isQueueMutationInProgress}>
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
</ButtonGroup>
)}
<ButtonGroup>
<IconButton
icon={<PiInfoBold />}
tooltip={`${t('parameters.info')} (I)`}
aria-label={`${t('parameters.info')} (I)`}
isChecked={shouldShowImageDetails}
onClick={handleClickShowImageDetails}
/>
</ButtonGroup>
<ButtonGroup>
<IconButton
aria-label={t('settings.displayInProgress')}
tooltip={t('settings.displayInProgress')}
icon={<PiHourglassHighBold />}
isChecked={shouldShowProgressInViewer}
onClick={handleClickProgressImagesToggle}
/>
</ButtonGroup>
<ButtonGroup>
<DeleteImageButton onClick={handleDelete} />
</ButtonGroup>
</Flex>
</>
);
};
export default memo(CurrentImageButtons);

View File

@ -1,24 +0,0 @@
import { Flex } from '@invoke-ai/ui-library';
import { memo } from 'react';
import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview';
const CurrentImageDisplay = () => {
return (
<Flex
position="relative"
flexDirection="column"
height="100%"
width="100%"
rowGap={4}
alignItems="center"
justifyContent="center"
>
<CurrentImageButtons />
<CurrentImagePreview />
</Flex>
);
};
export default memo(CurrentImageDisplay);

View File

@ -7,10 +7,10 @@ import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
import { useDownloadImage } from 'common/hooks/useDownloadImage';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions';
import { initialImageSelected } from 'features/parameters/store/actions';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { setActiveTab } from 'features/ui/store/uiSlice';
@ -45,7 +45,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const toaster = useAppToaster();
const isCanvasEnabled = useFeatureStatus('unifiedCanvas');
const isCanvasEnabled = useFeatureStatus('canvas');
const customStarUi = useStore($customStarUI);
const { downloadImage } = useDownloadImage();
@ -72,13 +72,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const handleSendToImageToImage = useCallback(() => {
dispatch(sentImageToImg2Img());
dispatch(initialImageSelected(imageDTO));
dispatch(iiLayerAdded(imageDTO));
}, [dispatch, imageDTO]);
const handleSendToCanvas = useCallback(() => {
dispatch(sentImageToCanvas());
flushSync(() => {
dispatch(setActiveTab('unifiedCanvas'));
dispatch(setActiveTab('canvas'));
});
dispatch(setInitialCanvasImage(imageDTO, optimalDimension));

View File

@ -1,9 +1,14 @@
import { useAppSelector } from 'app/store/storeHooks';
import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets';
import { MetadataControlNetsV2 } from 'features/metadata/components/MetadataControlNetsV2';
import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters';
import { MetadataIPAdaptersV2 } from 'features/metadata/components/MetadataIPAdaptersV2';
import { MetadataItem } from 'features/metadata/components/MetadataItem';
import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs';
import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters';
import { MetadataT2IAdaptersV2 } from 'features/metadata/components/MetadataT2IAdaptersV2';
import { handlers } from 'features/metadata/util/handlers';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
type Props = {
@ -11,6 +16,7 @@ type Props = {
};
const ImageMetadataActions = (props: Props) => {
const activeTabName = useAppSelector(activeTabNameSelector);
const { metadata } = props;
if (!metadata || Object.keys(metadata).length === 0) {
@ -46,9 +52,12 @@ const ImageMetadataActions = (props: Props) => {
<MetadataItem metadata={metadata} handlers={handlers.refinerStart} />
<MetadataItem metadata={metadata} handlers={handlers.refinerSteps} />
<MetadataLoRAs metadata={metadata} />
<MetadataControlNets metadata={metadata} />
<MetadataT2IAdapters metadata={metadata} />
<MetadataIPAdapters metadata={metadata} />
{activeTabName !== 'generation' && <MetadataControlNets metadata={metadata} />}
{activeTabName !== 'generation' && <MetadataT2IAdapters metadata={metadata} />}
{activeTabName !== 'generation' && <MetadataIPAdapters metadata={metadata} />}
{activeTabName === 'generation' && <MetadataControlNetsV2 metadata={metadata} />}
{activeTabName === 'generation' && <MetadataT2IAdaptersV2 metadata={metadata} />}
{activeTabName === 'generation' && <MetadataIPAdaptersV2 metadata={metadata} />}
</>
);
};

View File

@ -0,0 +1,202 @@
import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { skipToken } from '@reduxjs/toolkit/query';
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { sentImageToImg2Img } from 'features/gallery/store/actions';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers';
import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings';
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import {
PiArrowsCounterClockwiseBold,
PiAsteriskBold,
PiDotsThreeOutlineFill,
PiFlowArrowBold,
PiPlantBold,
PiQuotesBold,
PiRulerBold,
} from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
const selectShouldDisableToolbarButtons = createSelector(
selectSystemSlice,
selectGallerySlice,
selectLastSelectedImage,
(system, gallery, lastSelectedImage) => {
const hasProgressImage = Boolean(system.denoiseProgress?.progress_image);
return hasProgressImage || !lastSelectedImage;
}
);
const CurrentImageButtons = () => {
const dispatch = useAppDispatch();
const isConnected = useAppSelector((s) => s.system.isConnected);
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
const selection = useAppSelector((s) => s.gallery.selection);
const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons);
const isUpscalingEnabled = useFeatureStatus('upscaling');
const isQueueMutationInProgress = useIsQueueMutationInProgress();
const { t } = useTranslation();
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } =
useImageActions(lastSelectedImage?.image_name);
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
const handleLoadWorkflow = useCallback(() => {
if (!lastSelectedImage || !lastSelectedImage.has_workflow) {
return;
}
getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name);
}, [getAndLoadEmbeddedWorkflow, lastSelectedImage]);
useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]);
useHotkeys('a', recallAll, [recallAll]);
useHotkeys('s', recallSeed, [recallSeed]);
useHotkeys('p', recallPrompts, [recallPrompts]);
useHotkeys('r', remix, [remix]);
const handleUseSize = useCallback(() => {
parseAndRecallImageDimensions(lastSelectedImage);
}, [lastSelectedImage]);
useHotkeys('d', handleUseSize, [handleUseSize]);
const handleSendToImageToImage = useCallback(() => {
if (!imageDTO) {
return;
}
dispatch(sentImageToImg2Img());
dispatch(iiLayerAdded(imageDTO));
}, [dispatch, imageDTO]);
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
const handleClickUpscale = useCallback(() => {
if (!imageDTO) {
return;
}
dispatch(upscaleRequested({ imageDTO }));
}, [dispatch, imageDTO]);
const handleDelete = useCallback(() => {
if (!imageDTO) {
return;
}
dispatch(imagesToDeleteSelected(selection));
}, [dispatch, imageDTO, selection]);
useHotkeys(
'Shift+U',
() => {
handleClickUpscale();
},
{
enabled: () => Boolean(isUpscalingEnabled && !shouldDisableToolbarButtons && isConnected),
},
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected]
);
useHotkeys(
'delete',
() => {
handleDelete();
},
[dispatch, imageDTO]
);
return (
<>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<Menu isLazy>
<MenuButton
as={IconButton}
aria-label={t('parameters.imageActions')}
tooltip={t('parameters.imageActions')}
isDisabled={!imageDTO}
icon={<PiDotsThreeOutlineFill />}
/>
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
</Menu>
</ButtonGroup>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<IconButton
icon={<PiFlowArrowBold />}
tooltip={`${t('nodes.loadWorkflow')} (W)`}
aria-label={`${t('nodes.loadWorkflow')} (W)`}
isDisabled={!imageDTO?.has_workflow}
onClick={handleLoadWorkflow}
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiArrowsCounterClockwiseBold />}
tooltip={`${t('parameters.remixImage')} (R)`}
aria-label={`${t('parameters.remixImage')} (R)`}
isDisabled={!hasMetadata}
onClick={remix}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiQuotesBold />}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!hasPrompts}
onClick={recallPrompts}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiPlantBold />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!hasSeed}
onClick={recallSeed}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiRulerBold />}
tooltip={`${t('parameters.useSize')} (D)`}
aria-label={`${t('parameters.useSize')} (D)`}
onClick={handleUseSize}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiAsteriskBold />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={!hasMetadata}
onClick={recallAll}
/>
</ButtonGroup>
{isUpscalingEnabled && (
<ButtonGroup isDisabled={isQueueMutationInProgress}>
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
</ButtonGroup>
)}
<ButtonGroup>
<DeleteImageButton onClick={handleDelete} />
</ButtonGroup>
</>
);
};
export default memo(CurrentImageButtons);

View File

@ -5,24 +5,25 @@ import { useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import ProgressImage from 'features/gallery/components/CurrentImage/ProgressImage';
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImageBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import ProgressImage from './ProgressImage';
const selectLastSelectedImageName = createSelector(
selectLastSelectedImage,
(lastSelectedImage) => lastSelectedImage?.image_name
);
const CurrentImagePreview = () => {
const { t } = useTranslation();
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
const imageName = useAppSelector(selectLastSelectedImageName);
const hasDenoiseProgress = useAppSelector((s) => Boolean(s.system.denoiseProgress));
@ -50,17 +51,12 @@ const CurrentImagePreview = () => {
// Show and hide the next/prev buttons on mouse move
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
const timeoutId = useRef(0);
const { t } = useTranslation();
const handleMouseOver = useCallback(() => {
const onMouseOver = useCallback(() => {
setShouldShowNextPrevButtons(true);
window.clearTimeout(timeoutId.current);
}, []);
const handleMouseOut = useCallback(() => {
const onMouseOut = useCallback(() => {
timeoutId.current = window.setTimeout(() => {
setShouldShowNextPrevButtons(false);
}, 500);
@ -68,8 +64,8 @@ const CurrentImagePreview = () => {
return (
<Flex
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
width="full"
height="full"
alignItems="center"
@ -91,16 +87,40 @@ const CurrentImagePreview = () => {
dataTestId="image-preview"
/>
)}
{shouldShowImageDetails && imageDTO && (
<Box position="absolute" top="0" width="full" height="full" borderRadius="base">
<ImageMetadataViewer image={imageDTO} />
</Box>
)}
<AnimatePresence>
{!shouldShowImageDetails && imageDTO && shouldShowNextPrevButtons && (
<motion.div key="nextPrevButtons" initial={initial} animate={animate} exit={exit} style={motionStyles}>
{shouldShowImageDetails && imageDTO && (
<Box
as={motion.div}
key="metadataViewer"
initial={initial}
animate={animateMetadata}
exit={exit}
position="absolute"
top={0}
width="full"
height="full"
borderRadius="base"
>
<ImageMetadataViewer image={imageDTO} />
</Box>
)}
</AnimatePresence>
<AnimatePresence>
{shouldShowNextPrevButtons && imageDTO && (
<Box
as={motion.div}
key="nextPrevButtons"
initial={initial}
animate={animateArrows}
exit={exit}
position="absolute"
top={0}
width="full"
height="full"
pointerEvents="none"
>
<NextPrevImageButtons />
</motion.div>
</Box>
)}
</AnimatePresence>
</Flex>
@ -112,18 +132,15 @@ export default memo(CurrentImagePreview);
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
const animateArrows: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
transition: { duration: 0.07 },
};
const animateMetadata: AnimationProps['animate'] = {
opacity: 0.8,
transition: { duration: 0.07 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.1 },
};
const motionStyles: CSSProperties = {
position: 'absolute',
top: '0',
width: '100%',
height: '100%',
pointerEvents: 'none',
transition: { duration: 0.07 },
};

View File

@ -0,0 +1,39 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsDownUpBold } from 'react-icons/pi';
import { useImageViewer } from './useImageViewer';
const TAB_NAME_TO_TKEY_SHORT: Record<InvokeTabName, string> = {
generation: 'controlLayers.controlLayers',
canvas: 'ui.tabs.canvas',
workflows: 'ui.tabs.workflows',
models: 'ui.tabs.models',
queue: 'ui.tabs.queue',
};
export const EditorButton = () => {
const { t } = useTranslation();
const { onClose } = useImageViewer();
const activeTabName = useAppSelector(activeTabNameSelector);
const tooltip = useMemo(
() => t('gallery.switchTo', { tab: t(TAB_NAME_TO_TKEY_SHORT[activeTabName]) }),
[t, activeTabName]
);
return (
<Button
aria-label={tooltip}
tooltip={tooltip}
onClick={onClose}
variant="outline"
leftIcon={<PiArrowsDownUpBold />}
>
{t(TAB_NAME_TO_TKEY_SHORT[activeTabName])}
</Button>
);
};

View File

@ -0,0 +1,92 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview';
import { EditorButton } from './EditorButton';
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.07 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.07 },
};
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
export const ImageViewer = memo(() => {
const { isOpen, onToggle, onClose } = useImageViewer();
const activeTabName = useAppSelector(activeTabNameSelector);
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
const shouldShowViewer = useMemo(() => {
if (!isViewerEnabled) {
return false;
}
return isOpen;
}, [isOpen, isViewerEnabled]);
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
// The AnimatePresence mode must be wait - else framer can get confused if you spam the toggle button
return (
<AnimatePresence mode="wait">
{shouldShowViewer && (
<Flex
key="imageViewer"
as={motion.div}
initial={initial}
animate={animate}
exit={exit}
layerStyle="first"
borderRadius="base"
position="absolute"
flexDirection="column"
top={0}
right={0}
bottom={0}
left={0}
p={2}
rowGap={4}
alignItems="center"
justifyContent="center"
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
>
<Flex w="full" gap={2}>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineEnd="auto">
<ToggleProgressButton />
<ToggleMetadataViewerButton />
</Flex>
</Flex>
<Flex flex={1} gap={2} justifyContent="center">
<CurrentImageButtons />
</Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
<EditorButton />
</Flex>
</Flex>
</Flex>
<CurrentImagePreview />
</Flex>
)}
</AnimatePresence>
);
});
ImageViewer.displayName = 'ImageViewer';

View File

@ -0,0 +1,42 @@
import { IconButton } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppToaster } from 'app/components/Toaster';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { setShouldShowImageDetails } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiInfoBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
export const ToggleMetadataViewerButton = memo(() => {
const dispatch = useAppDispatch();
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
const toaster = useAppToaster();
const { t } = useTranslation();
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
const toggleMetadataViewer = useCallback(
() => dispatch(setShouldShowImageDetails(!shouldShowImageDetails)),
[dispatch, shouldShowImageDetails]
);
useHotkeys('i', toggleMetadataViewer, { enabled: Boolean(imageDTO) }, [imageDTO, shouldShowImageDetails, toaster]);
return (
<IconButton
icon={<PiInfoBold />}
tooltip={`${t('parameters.info')} (I)`}
aria-label={`${t('parameters.info')} (I)`}
onClick={toggleMetadataViewer}
isDisabled={!imageDTO}
variant="outline"
colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'}
/>
);
});
ToggleMetadataViewerButton.displayName = 'ToggleMetadataViewerButton';

View File

@ -0,0 +1,29 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiHourglassHighBold } from 'react-icons/pi';
export const ToggleProgressButton = memo(() => {
const dispatch = useAppDispatch();
const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
const { t } = useTranslation();
const onClick = useCallback(() => {
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
}, [dispatch, shouldShowProgressInViewer]);
return (
<IconButton
aria-label={t('settings.displayInProgress')}
tooltip={t('settings.displayInProgress')}
icon={<PiHourglassHighBold />}
onClick={onClick}
variant="outline"
colorScheme={shouldShowProgressInViewer ? 'invokeBlue' : 'base'}
/>
);
});
ToggleProgressButton.displayName = 'ToggleProgressButton';

View File

@ -0,0 +1,25 @@
import { Button } from '@invoke-ai/ui-library';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsDownUpBold } from 'react-icons/pi';
import { useImageViewer } from './useImageViewer';
export const ViewerButton = () => {
const { t } = useTranslation();
const { onOpen } = useImageViewer();
const tooltip = useMemo(() => t('gallery.switchTo', { tab: t('common.viewer') }), [t]);
return (
<Button
aria-label={tooltip}
tooltip={tooltip}
onClick={onOpen}
variant="outline"
pointerEvents="auto"
leftIcon={<PiArrowsDownUpBold />}
>
{t('common.viewer')}
</Button>
);
};

View File

@ -0,0 +1,22 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { useCallback } from 'react';
export const useImageViewer = () => {
const dispatch = useAppDispatch();
const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
const onClose = useCallback(() => {
dispatch(isImageViewerOpenChanged(false));
}, [dispatch]);
const onOpen = useCallback(() => {
dispatch(isImageViewerOpenChanged(true));
}, [dispatch]);
const onToggle = useCallback(() => {
dispatch(isImageViewerOpenChanged(!isOpen));
}, [dispatch, isOpen]);
return { isOpen, onOpen, onClose, onToggle };
};

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