mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-20 05:18:17 -05:00
Compare commits
580 Commits
v4.2.9.dev
...
v4.2.9.dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c5abd44a7 | ||
|
|
765d99ac2f | ||
|
|
ac9a66a628 | ||
|
|
0ea88dc170 | ||
|
|
8369826d22 | ||
|
|
0e354f5164 | ||
|
|
41f2ee2633 | ||
|
|
4e74006c5f | ||
|
|
48edb6e023 | ||
|
|
aeae6af0a1 | ||
|
|
ab11d9af8e | ||
|
|
2e84327ca4 | ||
|
|
fa6842121c | ||
|
|
c402aa397d | ||
|
|
a58c8adc38 | ||
|
|
d43e2d690e | ||
|
|
284f768810 | ||
|
|
e933d1ae2b | ||
|
|
1e134de771 | ||
|
|
29c47c8be5 | ||
|
|
e1122c541d | ||
|
|
2f81d1ac83 | ||
|
|
56fbe751db | ||
|
|
93f1d67fbf | ||
|
|
9467b937ff | ||
|
|
4242e6e6c2 | ||
|
|
9b39452b3e | ||
|
|
85b23784cf | ||
|
|
085cc82926 | ||
|
|
0098c33f81 | ||
|
|
292e00ab68 | ||
|
|
6c1fb2d06e | ||
|
|
d60605fcd8 | ||
|
|
38ed720ff2 | ||
|
|
22203b8eb0 | ||
|
|
cf5fa792a1 | ||
|
|
c636633a8e | ||
|
|
55fe1ebc53 | ||
|
|
3c2fa6b475 | ||
|
|
9b927de2e0 | ||
|
|
6a62854e7d | ||
|
|
312093cbb0 | ||
|
|
06fe14e1fc | ||
|
|
1b54e58726 | ||
|
|
219d7c9611 | ||
|
|
9f742a669e | ||
|
|
41e324fd51 | ||
|
|
ce55a96125 | ||
|
|
64e60a7fde | ||
|
|
972f03960a | ||
|
|
5a403f087d | ||
|
|
fe59d7f3b0 | ||
|
|
b2b2b73aed | ||
|
|
20b563c4cb | ||
|
|
263a0ef5b4 | ||
|
|
e8723b7cd3 | ||
|
|
03e05b2068 | ||
|
|
6c0482a71d | ||
|
|
e6153e6fa4 | ||
|
|
6d209c6cc3 | ||
|
|
beb4e823dc | ||
|
|
61ba4c606b | ||
|
|
af840cedf3 | ||
|
|
0bf0bca03f | ||
|
|
e470eaf8f3 | ||
|
|
377db3f726 | ||
|
|
77f020a997 | ||
|
|
34e2eda625 | ||
|
|
e1d559db69 | ||
|
|
23a98e2ed6 | ||
|
|
fe3b2ed357 | ||
|
|
eedf81dcc5 | ||
|
|
dbef1a9e06 | ||
|
|
a41406ca9a | ||
|
|
f126a61f66 | ||
|
|
89c79276f3 | ||
|
|
423e463b95 | ||
|
|
52202e45de | ||
|
|
100832c66d | ||
|
|
a58b91b221 | ||
|
|
3af6d79852 | ||
|
|
1303e18e93 | ||
|
|
301da97670 | ||
|
|
17e76981bb | ||
|
|
9c1732e2bb | ||
|
|
a3179e7a3f | ||
|
|
f86b50d18a | ||
|
|
307885f505 | ||
|
|
4b49c1dd6b | ||
|
|
f917cefa84 | ||
|
|
bea98438fc | ||
|
|
17d3275086 | ||
|
|
059b7a0fcf | ||
|
|
05d3a989f6 | ||
|
|
590ae70c12 | ||
|
|
5240ec6e6f | ||
|
|
04772b642c | ||
|
|
65f6cb416f | ||
|
|
24c2028739 | ||
|
|
b0db9a3f56 | ||
|
|
3ea83574c0 | ||
|
|
05252a9bfc | ||
|
|
ce854f086e | ||
|
|
ff0c16978c | ||
|
|
41cc650031 | ||
|
|
c3f7554053 | ||
|
|
3f597a1c60 | ||
|
|
ccffdf1878 | ||
|
|
474089e892 | ||
|
|
778e8ad161 | ||
|
|
9f29892c24 | ||
|
|
56fd46a069 | ||
|
|
579e594861 | ||
|
|
af3440fbe3 | ||
|
|
cc101f55c4 | ||
|
|
ef1adf07f5 | ||
|
|
625c05d9be | ||
|
|
8ad3d8f738 | ||
|
|
4759875733 | ||
|
|
768e6a3c55 | ||
|
|
45bd85c039 | ||
|
|
9f94c5a8bd | ||
|
|
23fdd65961 | ||
|
|
8034195c30 | ||
|
|
08761127c9 | ||
|
|
4a10010b6c | ||
|
|
14cc5e2453 | ||
|
|
3d87adea60 | ||
|
|
36e8232ab6 | ||
|
|
72722a73be | ||
|
|
a09aa232a9 | ||
|
|
7ae8b64699 | ||
|
|
60e0d17f34 | ||
|
|
bf8bef2f00 | ||
|
|
b586d67bac | ||
|
|
31e5e5af13 | ||
|
|
94871e88cd | ||
|
|
00e56d1968 | ||
|
|
43672a53ab | ||
|
|
45097ed2a6 | ||
|
|
871f6b9f95 | ||
|
|
e6476e3c75 | ||
|
|
ac9b5f246d | ||
|
|
8bc72a2744 | ||
|
|
f76f1d89d7 | ||
|
|
7b54762b5e | ||
|
|
bc6faf6a6d | ||
|
|
e7ae1ac9b2 | ||
|
|
dcb436adb1 | ||
|
|
80f0441905 | ||
|
|
8cde803654 | ||
|
|
62445680ad | ||
|
|
7685e36886 | ||
|
|
4c196844bd | ||
|
|
b36159bda4 | ||
|
|
b02948d49a | ||
|
|
f442d206be | ||
|
|
21ed6bccd8 | ||
|
|
143ce7f00b | ||
|
|
28e716139b | ||
|
|
80a7c0c521 | ||
|
|
255ad3d2ad | ||
|
|
089bc9c7d8 | ||
|
|
ee7dafaf57 | ||
|
|
516ecdb0ee | ||
|
|
b77675f74d | ||
|
|
eea5c8efad | ||
|
|
09f1aac3a3 | ||
|
|
dd1dcb5eba | ||
|
|
757bd62ebe | ||
|
|
5a3127949b | ||
|
|
ced934c0a3 | ||
|
|
c32445084f | ||
|
|
9f1af0cdaa | ||
|
|
0d26cab400 | ||
|
|
c8de2da3fc | ||
|
|
ca089a105e | ||
|
|
22000918d6 | ||
|
|
6affc28da4 | ||
|
|
f659995e1c | ||
|
|
56fb3e738f | ||
|
|
56d450a907 | ||
|
|
d3cdcef36b | ||
|
|
19434e73b4 | ||
|
|
f7b3df9583 | ||
|
|
4da4b3bd50 | ||
|
|
e83513882a | ||
|
|
5adc784b6b | ||
|
|
f177513523 | ||
|
|
8ebcf79b1a | ||
|
|
c7e5f24704 | ||
|
|
ab3eb32ec8 | ||
|
|
d76509e5cb | ||
|
|
04f56aab82 | ||
|
|
c7913cbbbb | ||
|
|
0556468518 | ||
|
|
1c7ef827b6 | ||
|
|
5720ed4d64 | ||
|
|
7f05af4a68 | ||
|
|
6db615ed5a | ||
|
|
465f020c86 | ||
|
|
f05b77088f | ||
|
|
80a5abf1ad | ||
|
|
7a6e8de60f | ||
|
|
8364fa74cf | ||
|
|
14f4566dd0 | ||
|
|
6145378923 | ||
|
|
68e2606427 | ||
|
|
0f3eb04d1a | ||
|
|
4a355323b2 | ||
|
|
8601fbb4ea | ||
|
|
db885aa180 | ||
|
|
c18fb980a2 | ||
|
|
b630dbdf20 | ||
|
|
29ac1b5e01 | ||
|
|
506d3b079e | ||
|
|
0670e6b53a | ||
|
|
76124ea35b | ||
|
|
6eae3470cd | ||
|
|
c7ba7ac876 | ||
|
|
edc733abd9 | ||
|
|
a56ded664e | ||
|
|
31ace5fb0c | ||
|
|
11010236b3 | ||
|
|
5f061ac1e2 | ||
|
|
72919fa34e | ||
|
|
d5ca99fc3c | ||
|
|
e49b72ee4e | ||
|
|
abe8db8154 | ||
|
|
e0e5941384 | ||
|
|
86e1f4e8b0 | ||
|
|
447d873ef0 | ||
|
|
b21d613ce4 | ||
|
|
fc91adb32f | ||
|
|
71885db5fd | ||
|
|
b88d14b3df | ||
|
|
d98d35a8a8 | ||
|
|
87bc0ebd73 | ||
|
|
7b6ba3f690 | ||
|
|
b0d8948428 | ||
|
|
b32d681cee | ||
|
|
11a66d1d09 | ||
|
|
e41987f08c | ||
|
|
34b57ec188 | ||
|
|
d74843be31 | ||
|
|
1216c6f9c9 | ||
|
|
865b6017d3 | ||
|
|
922a021821 | ||
|
|
0b5f4cac57 | ||
|
|
c988c58c63 | ||
|
|
ceb8cbf59e | ||
|
|
52e9f43c46 | ||
|
|
4e5e7761fc | ||
|
|
9879999a65 | ||
|
|
bedaca70a3 | ||
|
|
2dd2225d2e | ||
|
|
d82031eec1 | ||
|
|
e5f2860b74 | ||
|
|
fa3560bb61 | ||
|
|
9b23f6ce30 | ||
|
|
5d6aa6cfd5 | ||
|
|
7d1819335f | ||
|
|
539e7a3f2d | ||
|
|
1686924ac8 | ||
|
|
556c1dc67b | ||
|
|
00f7093e65 | ||
|
|
79eb11dce9 | ||
|
|
0bf48c0d41 | ||
|
|
3f33e5f770 | ||
|
|
da3888ba9e | ||
|
|
a2f91b1055 | ||
|
|
d26095dfa1 | ||
|
|
83e786bd1e | ||
|
|
4cae12a507 | ||
|
|
d8e3708e0f | ||
|
|
f4de2fd3b1 | ||
|
|
e1cb30bbb4 | ||
|
|
97e0edc549 | ||
|
|
f4e66bf14f | ||
|
|
a6a7fe8aba | ||
|
|
a273f72560 | ||
|
|
b5126f45d6 | ||
|
|
ba3bb7cbf3 | ||
|
|
608279487b | ||
|
|
72b5374916 | ||
|
|
08b03212ca | ||
|
|
7e341a05a1 | ||
|
|
e665d08ee1 | ||
|
|
ba6362dc9d | ||
|
|
48f0797c43 | ||
|
|
640b0c4939 | ||
|
|
287c61e277 | ||
|
|
f7b2516109 | ||
|
|
b530eb49d4 | ||
|
|
fa94979ab6 | ||
|
|
54f2acf5b9 | ||
|
|
b6d845a4d0 | ||
|
|
1095b7c37f | ||
|
|
136ffd97ca | ||
|
|
80163d0af2 | ||
|
|
e1c6e926e7 | ||
|
|
2bb74abf31 | ||
|
|
0d4b91afe0 | ||
|
|
6c688d6878 | ||
|
|
243feecef9 | ||
|
|
abd22ba087 | ||
|
|
ab25546e97 | ||
|
|
925f0fca2a | ||
|
|
066366d885 | ||
|
|
61d52e96b7 | ||
|
|
051e88ca90 | ||
|
|
e873b69850 | ||
|
|
661fd55556 | ||
|
|
402f5a4717 | ||
|
|
81bf52ef37 | ||
|
|
8ff92796df | ||
|
|
68af60e12e | ||
|
|
cce6bf9428 | ||
|
|
078908fbea | ||
|
|
7275caaf5b | ||
|
|
d9487c1df4 | ||
|
|
3a9f955388 | ||
|
|
e46c7acd2e | ||
|
|
b771664851 | ||
|
|
7c21819d20 | ||
|
|
a57e618d47 | ||
|
|
c9849a79ea | ||
|
|
f1643fec08 | ||
|
|
951e63ca87 | ||
|
|
8e539c8a8c | ||
|
|
1e689a4902 | ||
|
|
7bbd25b5ec | ||
|
|
b1c7236117 | ||
|
|
ae3e473024 | ||
|
|
fd616f247c | ||
|
|
45dca2c821 | ||
|
|
40dc108c84 | ||
|
|
a421c25952 | ||
|
|
562d0afdbb | ||
|
|
2ce4698eef | ||
|
|
cb53108041 | ||
|
|
5fa65e5cc6 | ||
|
|
e8b0b6cef5 | ||
|
|
eca2712828 | ||
|
|
2804c0aede | ||
|
|
0429f0480d | ||
|
|
024759a0fc | ||
|
|
9a94aef2b0 | ||
|
|
e329cb45cd | ||
|
|
0dc38bd684 | ||
|
|
98ebca5f8c | ||
|
|
05cb3e03cf | ||
|
|
181132c149 | ||
|
|
a69aa00155 | ||
|
|
47d415e31c | ||
|
|
667a156817 | ||
|
|
00f39b977e | ||
|
|
e5776e2bd6 | ||
|
|
2b21f54897 | ||
|
|
678d12fcd5 | ||
|
|
03f06f611e | ||
|
|
6571e0f814 | ||
|
|
44f91026e1 | ||
|
|
56237328f1 | ||
|
|
ff68901e89 | ||
|
|
e0e7adb2b2 | ||
|
|
0923a5b128 | ||
|
|
75f8a84c79 | ||
|
|
af815cf7eb | ||
|
|
ef4d6c26f6 | ||
|
|
5087b306c0 | ||
|
|
a5708eaefe | ||
|
|
389bfc9e31 | ||
|
|
fd269e91e0 | ||
|
|
80136b0dfc | ||
|
|
9595eff1f9 | ||
|
|
c3c95754f7 | ||
|
|
22ab63fe8d | ||
|
|
5fefcab475 | ||
|
|
771a05b894 | ||
|
|
e2d8aaa923 | ||
|
|
0951aecb13 | ||
|
|
b1fe6f9853 | ||
|
|
551dd393aa | ||
|
|
78b4562184 | ||
|
|
c49b90e621 | ||
|
|
89e6233fbf | ||
|
|
3f9496c237 | ||
|
|
36e94af598 | ||
|
|
a181a684f5 | ||
|
|
bb712b3b3f | ||
|
|
e795de5647 | ||
|
|
bdc428cdd8 | ||
|
|
e4376e21dd | ||
|
|
77acc7baed | ||
|
|
9db1556c4d | ||
|
|
65de8b329b | ||
|
|
08dae5b047 | ||
|
|
8d2f056407 | ||
|
|
e66ef2e25e | ||
|
|
4d3ee7e082 | ||
|
|
fe48fda2f3 | ||
|
|
0f66753aa1 | ||
|
|
a18878474b | ||
|
|
0aa4568fd4 | ||
|
|
1de7e5760a | ||
|
|
135d6f2763 | ||
|
|
061767ede3 | ||
|
|
7204844bcb | ||
|
|
f2279ecadd | ||
|
|
75694869d2 | ||
|
|
d029680ac1 | ||
|
|
41c195d936 | ||
|
|
03ea005e9c | ||
|
|
6d936a7c44 | ||
|
|
fba17b93a6 | ||
|
|
73a7a27ea1 | ||
|
|
79287c2d16 | ||
|
|
662c5f4b77 | ||
|
|
7728ca6843 | ||
|
|
9607372f89 | ||
|
|
d27f948b78 | ||
|
|
b7aab81717 | ||
|
|
2998287f61 | ||
|
|
55d7f0ff5b | ||
|
|
4564f36d4a | ||
|
|
319de5c4e9 | ||
|
|
eee499faa3 | ||
|
|
63c5e42f2a | ||
|
|
bd16dc4479 | ||
|
|
49371ddec9 | ||
|
|
6a10d31b19 | ||
|
|
c951e733d3 | ||
|
|
7ed24cf847 | ||
|
|
821b7a0435 | ||
|
|
1b0344c412 | ||
|
|
03ca3c4b3d | ||
|
|
b939192b16 | ||
|
|
7ccf559a06 | ||
|
|
9eb091f873 | ||
|
|
3bd5521641 | ||
|
|
ced748e419 | ||
|
|
fbd137da9f | ||
|
|
03baebced6 | ||
|
|
cb19c1c370 | ||
|
|
788bad61d0 | ||
|
|
8f5f9bd44e | ||
|
|
2873e3e084 | ||
|
|
b004f17ae3 | ||
|
|
bea1e8c99b | ||
|
|
111493223f | ||
|
|
0a5ac2baec | ||
|
|
eec3c3b884 | ||
|
|
07b72c3d70 | ||
|
|
766e8c4eb0 | ||
|
|
57c257d10d | ||
|
|
d497da0e61 | ||
|
|
62310e7929 | ||
|
|
d79aa173a6 | ||
|
|
fbfdd3e003 | ||
|
|
a62b4a26ef | ||
|
|
817d4168c6 | ||
|
|
7e0a6d1538 | ||
|
|
ebc498ad19 | ||
|
|
b97b8c6ce6 | ||
|
|
b8abff65a1 | ||
|
|
a953dc1dbd | ||
|
|
a7c9848e99 | ||
|
|
73a1449eaf | ||
|
|
59f57ff542 | ||
|
|
e9204b87e3 | ||
|
|
7dd11bd60a | ||
|
|
275fc2ccf9 | ||
|
|
a2ef8d9d47 | ||
|
|
196779ff19 | ||
|
|
aee3147365 | ||
|
|
eaca940956 | ||
|
|
06006733e2 | ||
|
|
14d0bfbef6 | ||
|
|
0c9cf73702 | ||
|
|
3b864921ac | ||
|
|
f41539532f | ||
|
|
657009c254 | ||
|
|
c47e02c309 | ||
|
|
ce8a7bc178 | ||
|
|
488ca87787 | ||
|
|
d965df8ca9 | ||
|
|
995c26751e | ||
|
|
dd09723a2a | ||
|
|
5ff5af3ba2 | ||
|
|
4cb85404c0 | ||
|
|
50bc2f100d | ||
|
|
f65ce6a019 | ||
|
|
c28b635f2d | ||
|
|
e55896240d | ||
|
|
2b478ee7e1 | ||
|
|
69912a35ea | ||
|
|
9f1bd98c7e | ||
|
|
b531d6b7f0 | ||
|
|
8aa963fb81 | ||
|
|
b76e0ab4e4 | ||
|
|
aea03b4e92 | ||
|
|
b39e95966c | ||
|
|
d53e5e0158 | ||
|
|
0368dd651b | ||
|
|
84a4a1024e | ||
|
|
af4f258489 | ||
|
|
ddfc8785b4 | ||
|
|
d8515b6efc | ||
|
|
6a07f007a4 | ||
|
|
7a5a0c8075 | ||
|
|
5ed2e9b0fc | ||
|
|
aeb0a45eb6 | ||
|
|
21e814d766 | ||
|
|
cafc1839e2 | ||
|
|
e937aa831f | ||
|
|
890e6a95ed | ||
|
|
a5b7274359 | ||
|
|
172acf2cf5 | ||
|
|
b49fdf6407 | ||
|
|
5184d05bc2 | ||
|
|
7ef4553fc9 | ||
|
|
d6bd1e4a49 | ||
|
|
29413f20a7 | ||
|
|
04a44c8ea7 | ||
|
|
426f1b6f9a | ||
|
|
9c7f5ed321 | ||
|
|
4c37c7f280 | ||
|
|
a2d13cacbf | ||
|
|
aa127b83a3 | ||
|
|
e55192ae2a | ||
|
|
5159fcbc33 | ||
|
|
02ad7a0f93 | ||
|
|
bfa496e37f | ||
|
|
fdf347af26 | ||
|
|
0833dbb19d | ||
|
|
1b6bf58e58 | ||
|
|
5ead7bc7b4 | ||
|
|
f326d17856 | ||
|
|
908aa9beea | ||
|
|
4071e96245 | ||
|
|
b4daf29bd8 | ||
|
|
bf185339c2 | ||
|
|
df3abc75c2 | ||
|
|
28fc9a387c | ||
|
|
8533f207dc | ||
|
|
d135c48319 | ||
|
|
ca9090d070 | ||
|
|
93b185dc3b | ||
|
|
98e5efa895 | ||
|
|
c6774b829d | ||
|
|
22925f92bd | ||
|
|
302efcf6e8 | ||
|
|
76f9f90f0a | ||
|
|
5ba338e471 | ||
|
|
01f101c6f2 | ||
|
|
5606aec78d | ||
|
|
db90e1fe8b | ||
|
|
ae96c479f2 | ||
|
|
344ed2c83e | ||
|
|
1985944659 | ||
|
|
915357a6c1 | ||
|
|
63c34e78d7 | ||
|
|
366c460c1f | ||
|
|
40cab08133 | ||
|
|
51de25122a | ||
|
|
90313091db | ||
|
|
9982219d18 | ||
|
|
b3fe03b8f9 | ||
|
|
6edd15d68a | ||
|
|
0e2b328c88 | ||
|
|
25d7f9c316 | ||
|
|
3870ebdf29 | ||
|
|
7595d05191 | ||
|
|
21af727d79 | ||
|
|
5691829de6 | ||
|
|
20e6a57cf1 | ||
|
|
d0c40a8b5b | ||
|
|
f663215f25 | ||
|
|
7c5dea6d12 |
37
.github/workflows/build-container.yml
vendored
37
.github/workflows/build-container.yml
vendored
@@ -13,12 +13,6 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- 'v*.*.*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
push-to-registry:
|
|
||||||
description: Push the built image to the container registry
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -56,15 +50,16 @@ jobs:
|
|||||||
df -h
|
df -h
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
images: |
|
images: |
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
|
${{ env.DOCKERHUB_REPOSITORY }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
@@ -77,33 +72,49 @@ jobs:
|
|||||||
suffix=-${{ matrix.gpu-driver }},onlatest=false
|
suffix=-${{ matrix.gpu-driver }},onlatest=false
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
with:
|
with:
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# - name: Login to Docker Hub
|
||||||
|
# if: github.event_name != 'pull_request' && vars.DOCKERHUB_REPOSITORY != ''
|
||||||
|
# uses: docker/login-action@v2
|
||||||
|
# with:
|
||||||
|
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build container
|
- name: Build container
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: ${{ github.ref == 'refs/heads/main' || github.ref_type == 'tag' || github.event.inputs.push-to-registry }}
|
push: ${{ github.ref == 'refs/heads/main' || github.ref_type == 'tag' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: |
|
cache-from: |
|
||||||
type=gha,scope=${{ github.ref_name }}-${{ matrix.gpu-driver }}
|
type=gha,scope=${{ github.ref_name }}-${{ matrix.gpu-driver }}
|
||||||
type=gha,scope=main-${{ matrix.gpu-driver }}
|
type=gha,scope=main-${{ matrix.gpu-driver }}
|
||||||
cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-${{ matrix.gpu-driver }}
|
cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-${{ matrix.gpu-driver }}
|
||||||
|
|
||||||
|
# - name: Docker Hub Description
|
||||||
|
# if: github.ref == 'refs/heads/main' || github.ref == 'refs/tags/*' && vars.DOCKERHUB_REPOSITORY != ''
|
||||||
|
# uses: peter-evans/dockerhub-description@v3
|
||||||
|
# with:
|
||||||
|
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
# repository: ${{ vars.DOCKERHUB_REPOSITORY }}
|
||||||
|
# short-description: ${{ github.event.repository.description }}
|
||||||
|
|||||||
@@ -196,22 +196,6 @@ tips to reduce the problem:
|
|||||||
=== "12GB VRAM GPU"
|
=== "12GB VRAM GPU"
|
||||||
|
|
||||||
This should be sufficient to generate larger images up to about 1280x1280.
|
This should be sufficient to generate larger images up to about 1280x1280.
|
||||||
|
|
||||||
## Checkpoint Models Load Slowly or Use Too Much RAM
|
|
||||||
|
|
||||||
The difference between diffusers models (a folder containing multiple
|
|
||||||
subfolders) and checkpoint models (a file ending with .safetensors or
|
|
||||||
.ckpt) is that InvokeAI is able to load diffusers models into memory
|
|
||||||
incrementally, while checkpoint models must be loaded all at
|
|
||||||
once. With very large models, or systems with limited RAM, you may
|
|
||||||
experience slowdowns and other memory-related issues when loading
|
|
||||||
checkpoint models.
|
|
||||||
|
|
||||||
To solve this, go to the Model Manager tab (the cube), select the
|
|
||||||
checkpoint model that's giving you trouble, and press the "Convert"
|
|
||||||
button in the upper right of your browser window. This will conver the
|
|
||||||
checkpoint into a diffusers model, after which loading should be
|
|
||||||
faster and less memory-intensive.
|
|
||||||
|
|
||||||
## Memory Leak (Linux)
|
## Memory Leak (Linux)
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,8 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
|
||||||
import traceback
|
import traceback
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from enum import Enum
|
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import List, Optional, Type
|
from typing import List, Optional, Type
|
||||||
|
|
||||||
@@ -19,7 +17,6 @@ from starlette.exceptions import HTTPException
|
|||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
from invokeai.app.api.dependencies import ApiDependencies
|
from invokeai.app.api.dependencies import ApiDependencies
|
||||||
from invokeai.app.services.config import get_config
|
|
||||||
from invokeai.app.services.model_images.model_images_common import ModelImageFileNotFoundException
|
from invokeai.app.services.model_images.model_images_common import ModelImageFileNotFoundException
|
||||||
from invokeai.app.services.model_install.model_install_common import ModelInstallJob
|
from invokeai.app.services.model_install.model_install_common import ModelInstallJob
|
||||||
from invokeai.app.services.model_records import (
|
from invokeai.app.services.model_records import (
|
||||||
@@ -34,7 +31,6 @@ from invokeai.backend.model_manager.config import (
|
|||||||
ModelFormat,
|
ModelFormat,
|
||||||
ModelType,
|
ModelType,
|
||||||
)
|
)
|
||||||
from invokeai.backend.model_manager.load.model_cache.model_cache_base import CacheStats
|
|
||||||
from invokeai.backend.model_manager.metadata.fetch.huggingface import HuggingFaceMetadataFetch
|
from invokeai.backend.model_manager.metadata.fetch.huggingface import HuggingFaceMetadataFetch
|
||||||
from invokeai.backend.model_manager.metadata.metadata_base import ModelMetadataWithFiles, UnknownMetadataException
|
from invokeai.backend.model_manager.metadata.metadata_base import ModelMetadataWithFiles, UnknownMetadataException
|
||||||
from invokeai.backend.model_manager.search import ModelSearch
|
from invokeai.backend.model_manager.search import ModelSearch
|
||||||
@@ -54,13 +50,6 @@ class ModelsList(BaseModel):
|
|||||||
model_config = ConfigDict(use_enum_values=True)
|
model_config = ConfigDict(use_enum_values=True)
|
||||||
|
|
||||||
|
|
||||||
class CacheType(str, Enum):
|
|
||||||
"""Cache type - one of vram or ram."""
|
|
||||||
|
|
||||||
RAM = "RAM"
|
|
||||||
VRAM = "VRAM"
|
|
||||||
|
|
||||||
|
|
||||||
def add_cover_image_to_model_config(config: AnyModelConfig, dependencies: Type[ApiDependencies]) -> AnyModelConfig:
|
def add_cover_image_to_model_config(config: AnyModelConfig, dependencies: Type[ApiDependencies]) -> AnyModelConfig:
|
||||||
"""Add a cover image URL to a model configuration."""
|
"""Add a cover image URL to a model configuration."""
|
||||||
cover_image = dependencies.invoker.services.model_images.get_url(config.key)
|
cover_image = dependencies.invoker.services.model_images.get_url(config.key)
|
||||||
@@ -808,83 +797,3 @@ async def get_starter_models() -> list[StarterModel]:
|
|||||||
model.dependencies = missing_deps
|
model.dependencies = missing_deps
|
||||||
|
|
||||||
return starter_models
|
return starter_models
|
||||||
|
|
||||||
|
|
||||||
@model_manager_router.get(
|
|
||||||
"/model_cache",
|
|
||||||
operation_id="get_cache_size",
|
|
||||||
response_model=float,
|
|
||||||
summary="Get maximum size of model manager RAM or VRAM cache.",
|
|
||||||
)
|
|
||||||
async def get_cache_size(cache_type: CacheType = Query(description="The cache type", default=CacheType.RAM)) -> float:
|
|
||||||
"""Return the current RAM or VRAM cache size setting (in GB)."""
|
|
||||||
cache = ApiDependencies.invoker.services.model_manager.load.ram_cache
|
|
||||||
value = 0.0
|
|
||||||
if cache_type == CacheType.RAM:
|
|
||||||
value = cache.max_cache_size
|
|
||||||
elif cache_type == CacheType.VRAM:
|
|
||||||
value = cache.max_vram_cache_size
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@model_manager_router.put(
|
|
||||||
"/model_cache",
|
|
||||||
operation_id="set_cache_size",
|
|
||||||
response_model=float,
|
|
||||||
summary="Set maximum size of model manager RAM or VRAM cache, optionally writing new value out to invokeai.yaml config file.",
|
|
||||||
)
|
|
||||||
async def set_cache_size(
|
|
||||||
value: float = Query(description="The new value for the maximum cache size"),
|
|
||||||
cache_type: CacheType = Query(description="The cache type", default=CacheType.RAM),
|
|
||||||
persist: bool = Query(description="Write new value out to invokeai.yaml", default=False),
|
|
||||||
) -> float:
|
|
||||||
"""Set the current RAM or VRAM cache size setting (in GB). ."""
|
|
||||||
cache = ApiDependencies.invoker.services.model_manager.load.ram_cache
|
|
||||||
app_config = get_config()
|
|
||||||
# Record initial state.
|
|
||||||
vram_old = app_config.vram
|
|
||||||
ram_old = app_config.ram
|
|
||||||
|
|
||||||
# Prepare target state.
|
|
||||||
vram_new = vram_old
|
|
||||||
ram_new = ram_old
|
|
||||||
if cache_type == CacheType.RAM:
|
|
||||||
ram_new = value
|
|
||||||
elif cache_type == CacheType.VRAM:
|
|
||||||
vram_new = value
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unexpected {cache_type=}.")
|
|
||||||
|
|
||||||
config_path = app_config.config_file_path
|
|
||||||
new_config_path = config_path.with_suffix(".yaml.new")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Try to apply the target state.
|
|
||||||
cache.max_vram_cache_size = vram_new
|
|
||||||
cache.max_cache_size = ram_new
|
|
||||||
app_config.ram = ram_new
|
|
||||||
app_config.vram = vram_new
|
|
||||||
if persist:
|
|
||||||
app_config.write_file(new_config_path)
|
|
||||||
shutil.move(new_config_path, config_path)
|
|
||||||
except Exception as e:
|
|
||||||
# If there was a failure, restore the initial state.
|
|
||||||
cache.max_cache_size = ram_old
|
|
||||||
cache.max_vram_cache_size = vram_old
|
|
||||||
app_config.ram = ram_old
|
|
||||||
app_config.vram = vram_old
|
|
||||||
|
|
||||||
raise RuntimeError("Failed to update cache size") from e
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@model_manager_router.get(
|
|
||||||
"/stats",
|
|
||||||
operation_id="get_stats",
|
|
||||||
response_model=Optional[CacheStats],
|
|
||||||
summary="Get model manager RAM cache performance statistics.",
|
|
||||||
)
|
|
||||||
async def get_stats() -> Optional[CacheStats]:
|
|
||||||
"""Return performance statistics on the model manager's RAM cache. Will return null if no models have been loaded."""
|
|
||||||
|
|
||||||
return ApiDependencies.invoker.services.model_manager.load.ram_cache.stats
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
|
|||||||
)
|
)
|
||||||
denoise_mask: Optional[DenoiseMaskField] = InputField(
|
denoise_mask: Optional[DenoiseMaskField] = InputField(
|
||||||
default=None,
|
default=None,
|
||||||
description=FieldDescriptions.denoise_mask,
|
description=FieldDescriptions.mask,
|
||||||
input=Input.Connection,
|
input=Input.Connection,
|
||||||
ui_order=8,
|
ui_order=8,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class FieldDescriptions:
|
|||||||
)
|
)
|
||||||
num_1 = "The first number"
|
num_1 = "The first number"
|
||||||
num_2 = "The second number"
|
num_2 = "The second number"
|
||||||
denoise_mask = "A mask of the region to apply the denoising process to."
|
mask = "The mask to use for the operation"
|
||||||
board = "The board to save the image to"
|
board = "The board to save the image to"
|
||||||
image = "The image to process"
|
image = "The image to process"
|
||||||
tile_size = "Tile size"
|
tile_size = "Tile size"
|
||||||
|
|||||||
@@ -1,249 +0,0 @@
|
|||||||
from typing import Callable, Optional
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torchvision.transforms as tv_transforms
|
|
||||||
from torchvision.transforms.functional import resize as tv_resize
|
|
||||||
|
|
||||||
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
|
|
||||||
from invokeai.app.invocations.fields import (
|
|
||||||
DenoiseMaskField,
|
|
||||||
FieldDescriptions,
|
|
||||||
FluxConditioningField,
|
|
||||||
Input,
|
|
||||||
InputField,
|
|
||||||
LatentsField,
|
|
||||||
WithBoard,
|
|
||||||
WithMetadata,
|
|
||||||
)
|
|
||||||
from invokeai.app.invocations.model import TransformerField
|
|
||||||
from invokeai.app.invocations.primitives import LatentsOutput
|
|
||||||
from invokeai.app.services.shared.invocation_context import InvocationContext
|
|
||||||
from invokeai.backend.flux.denoise import denoise
|
|
||||||
from invokeai.backend.flux.inpaint_extension import InpaintExtension
|
|
||||||
from invokeai.backend.flux.model import Flux
|
|
||||||
from invokeai.backend.flux.sampling_utils import (
|
|
||||||
clip_timestep_schedule,
|
|
||||||
generate_img_ids,
|
|
||||||
get_noise,
|
|
||||||
get_schedule,
|
|
||||||
pack,
|
|
||||||
unpack,
|
|
||||||
)
|
|
||||||
from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
|
|
||||||
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import FLUXConditioningInfo
|
|
||||||
from invokeai.backend.util.devices import TorchDevice
|
|
||||||
|
|
||||||
|
|
||||||
@invocation(
|
|
||||||
"flux_denoise",
|
|
||||||
title="FLUX Denoise",
|
|
||||||
tags=["image", "flux"],
|
|
||||||
category="image",
|
|
||||||
version="1.0.0",
|
|
||||||
classification=Classification.Prototype,
|
|
||||||
)
|
|
||||||
class FluxDenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
|
|
||||||
"""Run denoising process with a FLUX transformer model."""
|
|
||||||
|
|
||||||
# If latents is provided, this means we are doing image-to-image.
|
|
||||||
latents: Optional[LatentsField] = InputField(
|
|
||||||
default=None,
|
|
||||||
description=FieldDescriptions.latents,
|
|
||||||
input=Input.Connection,
|
|
||||||
)
|
|
||||||
# denoise_mask is used for image-to-image inpainting. Only the masked region is modified.
|
|
||||||
denoise_mask: Optional[DenoiseMaskField] = InputField(
|
|
||||||
default=None,
|
|
||||||
description=FieldDescriptions.denoise_mask,
|
|
||||||
input=Input.Connection,
|
|
||||||
)
|
|
||||||
denoising_start: float = InputField(
|
|
||||||
default=0.0,
|
|
||||||
ge=0,
|
|
||||||
le=1,
|
|
||||||
description=FieldDescriptions.denoising_start,
|
|
||||||
)
|
|
||||||
denoising_end: float = InputField(default=1.0, ge=0, le=1, description=FieldDescriptions.denoising_end)
|
|
||||||
transformer: TransformerField = InputField(
|
|
||||||
description=FieldDescriptions.flux_model,
|
|
||||||
input=Input.Connection,
|
|
||||||
title="Transformer",
|
|
||||||
)
|
|
||||||
positive_text_conditioning: FluxConditioningField = InputField(
|
|
||||||
description=FieldDescriptions.positive_cond, input=Input.Connection
|
|
||||||
)
|
|
||||||
width: int = InputField(default=1024, multiple_of=16, description="Width of the generated image.")
|
|
||||||
height: int = InputField(default=1024, multiple_of=16, description="Height of the generated image.")
|
|
||||||
num_steps: int = InputField(
|
|
||||||
default=4, description="Number of diffusion steps. Recommended values are schnell: 4, dev: 50."
|
|
||||||
)
|
|
||||||
guidance: float = InputField(
|
|
||||||
default=4.0,
|
|
||||||
description="The guidance strength. Higher values adhere more strictly to the prompt, and will produce less diverse images. FLUX dev only, ignored for schnell.",
|
|
||||||
)
|
|
||||||
seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
|
|
||||||
|
|
||||||
@torch.no_grad()
|
|
||||||
def invoke(self, context: InvocationContext) -> LatentsOutput:
|
|
||||||
latents = self._run_diffusion(context)
|
|
||||||
latents = latents.detach().to("cpu")
|
|
||||||
|
|
||||||
name = context.tensors.save(tensor=latents)
|
|
||||||
return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
|
|
||||||
|
|
||||||
def _run_diffusion(
|
|
||||||
self,
|
|
||||||
context: InvocationContext,
|
|
||||||
):
|
|
||||||
inference_dtype = torch.bfloat16
|
|
||||||
|
|
||||||
# Load the conditioning data.
|
|
||||||
cond_data = context.conditioning.load(self.positive_text_conditioning.conditioning_name)
|
|
||||||
assert len(cond_data.conditionings) == 1
|
|
||||||
flux_conditioning = cond_data.conditionings[0]
|
|
||||||
assert isinstance(flux_conditioning, FLUXConditioningInfo)
|
|
||||||
flux_conditioning = flux_conditioning.to(dtype=inference_dtype)
|
|
||||||
t5_embeddings = flux_conditioning.t5_embeds
|
|
||||||
clip_embeddings = flux_conditioning.clip_embeds
|
|
||||||
|
|
||||||
# Load the input latents, if provided.
|
|
||||||
init_latents = context.tensors.load(self.latents.latents_name) if self.latents else None
|
|
||||||
if init_latents is not None:
|
|
||||||
init_latents = init_latents.to(device=TorchDevice.choose_torch_device(), dtype=inference_dtype)
|
|
||||||
|
|
||||||
# Prepare input noise.
|
|
||||||
noise = get_noise(
|
|
||||||
num_samples=1,
|
|
||||||
height=self.height,
|
|
||||||
width=self.width,
|
|
||||||
device=TorchDevice.choose_torch_device(),
|
|
||||||
dtype=inference_dtype,
|
|
||||||
seed=self.seed,
|
|
||||||
)
|
|
||||||
|
|
||||||
transformer_info = context.models.load(self.transformer.transformer)
|
|
||||||
is_schnell = "schnell" in transformer_info.config.config_path
|
|
||||||
|
|
||||||
# Calculate the timestep schedule.
|
|
||||||
image_seq_len = noise.shape[-1] * noise.shape[-2] // 4
|
|
||||||
timesteps = get_schedule(
|
|
||||||
num_steps=self.num_steps,
|
|
||||||
image_seq_len=image_seq_len,
|
|
||||||
shift=not is_schnell,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Clip the timesteps schedule based on denoising_start and denoising_end.
|
|
||||||
timesteps = clip_timestep_schedule(timesteps, self.denoising_start, self.denoising_end)
|
|
||||||
|
|
||||||
# Prepare input latent image.
|
|
||||||
if init_latents is not None:
|
|
||||||
# If init_latents is provided, we are doing image-to-image.
|
|
||||||
|
|
||||||
if is_schnell:
|
|
||||||
context.logger.warning(
|
|
||||||
"Running image-to-image with a FLUX schnell model. This is not recommended. The results are likely "
|
|
||||||
"to be poor. Consider using a FLUX dev model instead."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Noise the orig_latents by the appropriate amount for the first timestep.
|
|
||||||
t_0 = timesteps[0]
|
|
||||||
x = t_0 * noise + (1.0 - t_0) * init_latents
|
|
||||||
else:
|
|
||||||
# init_latents are not provided, so we are not doing image-to-image (i.e. we are starting from pure noise).
|
|
||||||
if self.denoising_start > 1e-5:
|
|
||||||
raise ValueError("denoising_start should be 0 when initial latents are not provided.")
|
|
||||||
|
|
||||||
x = noise
|
|
||||||
|
|
||||||
# If len(timesteps) == 1, then short-circuit. We are just noising the input latents, but not taking any
|
|
||||||
# denoising steps.
|
|
||||||
if len(timesteps) <= 1:
|
|
||||||
return x
|
|
||||||
|
|
||||||
inpaint_mask = self._prep_inpaint_mask(context, x)
|
|
||||||
|
|
||||||
b, _c, h, w = x.shape
|
|
||||||
img_ids = generate_img_ids(h=h, w=w, batch_size=b, device=x.device, dtype=x.dtype)
|
|
||||||
|
|
||||||
bs, t5_seq_len, _ = t5_embeddings.shape
|
|
||||||
txt_ids = torch.zeros(bs, t5_seq_len, 3, dtype=inference_dtype, device=TorchDevice.choose_torch_device())
|
|
||||||
|
|
||||||
# Pack all latent tensors.
|
|
||||||
init_latents = pack(init_latents) if init_latents is not None else None
|
|
||||||
inpaint_mask = pack(inpaint_mask) if inpaint_mask is not None else None
|
|
||||||
noise = pack(noise)
|
|
||||||
x = pack(x)
|
|
||||||
|
|
||||||
# Now that we have 'packed' the latent tensors, verify that we calculated the image_seq_len correctly.
|
|
||||||
assert image_seq_len == x.shape[1]
|
|
||||||
|
|
||||||
# Prepare inpaint extension.
|
|
||||||
inpaint_extension: InpaintExtension | None = None
|
|
||||||
if inpaint_mask is not None:
|
|
||||||
assert init_latents is not None
|
|
||||||
inpaint_extension = InpaintExtension(
|
|
||||||
init_latents=init_latents,
|
|
||||||
inpaint_mask=inpaint_mask,
|
|
||||||
noise=noise,
|
|
||||||
)
|
|
||||||
|
|
||||||
with transformer_info as transformer:
|
|
||||||
assert isinstance(transformer, Flux)
|
|
||||||
|
|
||||||
x = denoise(
|
|
||||||
model=transformer,
|
|
||||||
img=x,
|
|
||||||
img_ids=img_ids,
|
|
||||||
txt=t5_embeddings,
|
|
||||||
txt_ids=txt_ids,
|
|
||||||
vec=clip_embeddings,
|
|
||||||
timesteps=timesteps,
|
|
||||||
step_callback=self._build_step_callback(context),
|
|
||||||
guidance=self.guidance,
|
|
||||||
inpaint_extension=inpaint_extension,
|
|
||||||
)
|
|
||||||
|
|
||||||
x = unpack(x.float(), self.height, self.width)
|
|
||||||
return x
|
|
||||||
|
|
||||||
def _prep_inpaint_mask(self, context: InvocationContext, latents: torch.Tensor) -> torch.Tensor | None:
|
|
||||||
"""Prepare the inpaint mask.
|
|
||||||
|
|
||||||
- Loads the mask
|
|
||||||
- Resizes if necessary
|
|
||||||
- Casts to same device/dtype as latents
|
|
||||||
- Expands mask to the same shape as latents so that they line up after 'packing'
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context (InvocationContext): The invocation context, for loading the inpaint mask.
|
|
||||||
latents (torch.Tensor): A latent image tensor. In 'unpacked' format. Used to determine the target shape,
|
|
||||||
device, and dtype for the inpaint mask.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
torch.Tensor | None: Inpaint mask.
|
|
||||||
"""
|
|
||||||
if self.denoise_mask is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
mask = context.tensors.load(self.denoise_mask.mask_name)
|
|
||||||
|
|
||||||
_, _, latent_height, latent_width = latents.shape
|
|
||||||
mask = tv_resize(
|
|
||||||
img=mask,
|
|
||||||
size=[latent_height, latent_width],
|
|
||||||
interpolation=tv_transforms.InterpolationMode.BILINEAR,
|
|
||||||
antialias=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
mask = mask.to(device=latents.device, dtype=latents.dtype)
|
|
||||||
|
|
||||||
# Expand the inpaint mask to the same shape as `latents` so that when we 'pack' `mask` it lines up with
|
|
||||||
# `latents`.
|
|
||||||
return mask.expand_as(latents)
|
|
||||||
|
|
||||||
def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
|
|
||||||
def step_callback(state: PipelineIntermediateState) -> None:
|
|
||||||
state.latents = unpack(state.latents.float(), self.height, self.width).squeeze()
|
|
||||||
context.util.flux_step_callback(state)
|
|
||||||
|
|
||||||
return step_callback
|
|
||||||
169
invokeai/app/invocations/flux_text_to_image.py
Normal file
169
invokeai/app/invocations/flux_text_to_image.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import torch
|
||||||
|
from einops import rearrange
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
|
||||||
|
from invokeai.app.invocations.fields import (
|
||||||
|
FieldDescriptions,
|
||||||
|
FluxConditioningField,
|
||||||
|
Input,
|
||||||
|
InputField,
|
||||||
|
WithBoard,
|
||||||
|
WithMetadata,
|
||||||
|
)
|
||||||
|
from invokeai.app.invocations.model import TransformerField, VAEField
|
||||||
|
from invokeai.app.invocations.primitives import ImageOutput
|
||||||
|
from invokeai.app.services.session_processor.session_processor_common import CanceledException
|
||||||
|
from invokeai.app.services.shared.invocation_context import InvocationContext
|
||||||
|
from invokeai.backend.flux.model import Flux
|
||||||
|
from invokeai.backend.flux.modules.autoencoder import AutoEncoder
|
||||||
|
from invokeai.backend.flux.sampling import denoise, get_noise, get_schedule, prepare_latent_img_patches, unpack
|
||||||
|
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import FLUXConditioningInfo
|
||||||
|
from invokeai.backend.util.devices import TorchDevice
|
||||||
|
|
||||||
|
|
||||||
|
@invocation(
|
||||||
|
"flux_text_to_image",
|
||||||
|
title="FLUX Text to Image",
|
||||||
|
tags=["image", "flux"],
|
||||||
|
category="image",
|
||||||
|
version="1.0.0",
|
||||||
|
classification=Classification.Prototype,
|
||||||
|
)
|
||||||
|
class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||||
|
"""Text-to-image generation using a FLUX model."""
|
||||||
|
|
||||||
|
transformer: TransformerField = InputField(
|
||||||
|
description=FieldDescriptions.flux_model,
|
||||||
|
input=Input.Connection,
|
||||||
|
title="Transformer",
|
||||||
|
)
|
||||||
|
vae: VAEField = InputField(
|
||||||
|
description=FieldDescriptions.vae,
|
||||||
|
input=Input.Connection,
|
||||||
|
)
|
||||||
|
positive_text_conditioning: FluxConditioningField = InputField(
|
||||||
|
description=FieldDescriptions.positive_cond, input=Input.Connection
|
||||||
|
)
|
||||||
|
width: int = InputField(default=1024, multiple_of=16, description="Width of the generated image.")
|
||||||
|
height: int = InputField(default=1024, multiple_of=16, description="Height of the generated image.")
|
||||||
|
num_steps: int = InputField(
|
||||||
|
default=4, description="Number of diffusion steps. Recommend values are schnell: 4, dev: 50."
|
||||||
|
)
|
||||||
|
guidance: float = InputField(
|
||||||
|
default=4.0,
|
||||||
|
description="The guidance strength. Higher values adhere more strictly to the prompt, and will produce less diverse images. FLUX dev only, ignored for schnell.",
|
||||||
|
)
|
||||||
|
seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||||
|
latents = self._run_diffusion(context)
|
||||||
|
image = self._run_vae_decoding(context, latents)
|
||||||
|
image_dto = context.images.save(image=image)
|
||||||
|
return ImageOutput.build(image_dto)
|
||||||
|
|
||||||
|
def _run_diffusion(
|
||||||
|
self,
|
||||||
|
context: InvocationContext,
|
||||||
|
):
|
||||||
|
inference_dtype = torch.bfloat16
|
||||||
|
|
||||||
|
# Load the conditioning data.
|
||||||
|
cond_data = context.conditioning.load(self.positive_text_conditioning.conditioning_name)
|
||||||
|
assert len(cond_data.conditionings) == 1
|
||||||
|
flux_conditioning = cond_data.conditionings[0]
|
||||||
|
assert isinstance(flux_conditioning, FLUXConditioningInfo)
|
||||||
|
flux_conditioning = flux_conditioning.to(dtype=inference_dtype)
|
||||||
|
t5_embeddings = flux_conditioning.t5_embeds
|
||||||
|
clip_embeddings = flux_conditioning.clip_embeds
|
||||||
|
|
||||||
|
transformer_info = context.models.load(self.transformer.transformer)
|
||||||
|
|
||||||
|
# Prepare input noise.
|
||||||
|
x = get_noise(
|
||||||
|
num_samples=1,
|
||||||
|
height=self.height,
|
||||||
|
width=self.width,
|
||||||
|
device=TorchDevice.choose_torch_device(),
|
||||||
|
dtype=inference_dtype,
|
||||||
|
seed=self.seed,
|
||||||
|
)
|
||||||
|
|
||||||
|
x, img_ids = prepare_latent_img_patches(x)
|
||||||
|
|
||||||
|
is_schnell = "schnell" in transformer_info.config.config_path
|
||||||
|
|
||||||
|
timesteps = get_schedule(
|
||||||
|
num_steps=self.num_steps,
|
||||||
|
image_seq_len=x.shape[1],
|
||||||
|
shift=not is_schnell,
|
||||||
|
)
|
||||||
|
|
||||||
|
bs, t5_seq_len, _ = t5_embeddings.shape
|
||||||
|
txt_ids = torch.zeros(bs, t5_seq_len, 3, dtype=inference_dtype, device=TorchDevice.choose_torch_device())
|
||||||
|
|
||||||
|
with transformer_info as transformer:
|
||||||
|
assert isinstance(transformer, Flux)
|
||||||
|
|
||||||
|
def step_callback() -> None:
|
||||||
|
if context.util.is_canceled():
|
||||||
|
raise CanceledException
|
||||||
|
|
||||||
|
# TODO: Make this look like the image before re-enabling
|
||||||
|
# latent_image = unpack(img.float(), self.height, self.width)
|
||||||
|
# latent_image = latent_image.squeeze() # Remove unnecessary dimensions
|
||||||
|
# flattened_tensor = latent_image.reshape(-1) # Flatten to shape [48*128*128]
|
||||||
|
|
||||||
|
# # Create a new tensor of the required shape [255, 255, 3]
|
||||||
|
# latent_image = flattened_tensor[: 255 * 255 * 3].reshape(255, 255, 3) # Reshape to RGB format
|
||||||
|
|
||||||
|
# # Convert to a NumPy array and then to a PIL Image
|
||||||
|
# image = Image.fromarray(latent_image.cpu().numpy().astype(np.uint8))
|
||||||
|
|
||||||
|
# (width, height) = image.size
|
||||||
|
# width *= 8
|
||||||
|
# height *= 8
|
||||||
|
|
||||||
|
# dataURL = image_to_dataURL(image, image_format="JPEG")
|
||||||
|
|
||||||
|
# # TODO: move this whole function to invocation context to properly reference these variables
|
||||||
|
# context._services.events.emit_invocation_denoise_progress(
|
||||||
|
# context._data.queue_item,
|
||||||
|
# context._data.invocation,
|
||||||
|
# state,
|
||||||
|
# ProgressImage(dataURL=dataURL, width=width, height=height),
|
||||||
|
# )
|
||||||
|
|
||||||
|
x = denoise(
|
||||||
|
model=transformer,
|
||||||
|
img=x,
|
||||||
|
img_ids=img_ids,
|
||||||
|
txt=t5_embeddings,
|
||||||
|
txt_ids=txt_ids,
|
||||||
|
vec=clip_embeddings,
|
||||||
|
timesteps=timesteps,
|
||||||
|
step_callback=step_callback,
|
||||||
|
guidance=self.guidance,
|
||||||
|
)
|
||||||
|
|
||||||
|
x = unpack(x.float(), self.height, self.width)
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
def _run_vae_decoding(
|
||||||
|
self,
|
||||||
|
context: InvocationContext,
|
||||||
|
latents: torch.Tensor,
|
||||||
|
) -> Image.Image:
|
||||||
|
vae_info = context.models.load(self.vae.vae)
|
||||||
|
with vae_info as vae:
|
||||||
|
assert isinstance(vae, AutoEncoder)
|
||||||
|
latents = latents.to(dtype=TorchDevice.choose_torch_dtype())
|
||||||
|
img = vae.decode(latents)
|
||||||
|
|
||||||
|
img = img.clamp(-1, 1)
|
||||||
|
img = rearrange(img[0], "c h w -> h w c")
|
||||||
|
img_pil = Image.fromarray((127.5 * (img + 1.0)).byte().cpu().numpy())
|
||||||
|
|
||||||
|
return img_pil
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import torch
|
|
||||||
from einops import rearrange
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
|
|
||||||
from invokeai.app.invocations.fields import (
|
|
||||||
FieldDescriptions,
|
|
||||||
Input,
|
|
||||||
InputField,
|
|
||||||
LatentsField,
|
|
||||||
WithBoard,
|
|
||||||
WithMetadata,
|
|
||||||
)
|
|
||||||
from invokeai.app.invocations.model import VAEField
|
|
||||||
from invokeai.app.invocations.primitives import ImageOutput
|
|
||||||
from invokeai.app.services.shared.invocation_context import InvocationContext
|
|
||||||
from invokeai.backend.flux.modules.autoencoder import AutoEncoder
|
|
||||||
from invokeai.backend.model_manager.load.load_base import LoadedModel
|
|
||||||
from invokeai.backend.util.devices import TorchDevice
|
|
||||||
|
|
||||||
|
|
||||||
@invocation(
|
|
||||||
"flux_vae_decode",
|
|
||||||
title="FLUX Latents to Image",
|
|
||||||
tags=["latents", "image", "vae", "l2i", "flux"],
|
|
||||||
category="latents",
|
|
||||||
version="1.0.0",
|
|
||||||
)
|
|
||||||
class FluxVaeDecodeInvocation(BaseInvocation, WithMetadata, WithBoard):
|
|
||||||
"""Generates an image from latents."""
|
|
||||||
|
|
||||||
latents: LatentsField = InputField(
|
|
||||||
description=FieldDescriptions.latents,
|
|
||||||
input=Input.Connection,
|
|
||||||
)
|
|
||||||
vae: VAEField = InputField(
|
|
||||||
description=FieldDescriptions.vae,
|
|
||||||
input=Input.Connection,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _vae_decode(self, vae_info: LoadedModel, latents: torch.Tensor) -> Image.Image:
|
|
||||||
with vae_info as vae:
|
|
||||||
assert isinstance(vae, AutoEncoder)
|
|
||||||
latents = latents.to(device=TorchDevice.choose_torch_device(), dtype=TorchDevice.choose_torch_dtype())
|
|
||||||
img = vae.decode(latents)
|
|
||||||
|
|
||||||
img = img.clamp(-1, 1)
|
|
||||||
img = rearrange(img[0], "c h w -> h w c") # noqa: F821
|
|
||||||
img_pil = Image.fromarray((127.5 * (img + 1.0)).byte().cpu().numpy())
|
|
||||||
return img_pil
|
|
||||||
|
|
||||||
@torch.no_grad()
|
|
||||||
def invoke(self, context: InvocationContext) -> ImageOutput:
|
|
||||||
latents = context.tensors.load(self.latents.latents_name)
|
|
||||||
vae_info = context.models.load(self.vae.vae)
|
|
||||||
image = self._vae_decode(vae_info=vae_info, latents=latents)
|
|
||||||
|
|
||||||
TorchDevice.empty_cache()
|
|
||||||
image_dto = context.images.save(image=image)
|
|
||||||
return ImageOutput.build(image_dto)
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import einops
|
|
||||||
import torch
|
|
||||||
|
|
||||||
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
|
|
||||||
from invokeai.app.invocations.fields import (
|
|
||||||
FieldDescriptions,
|
|
||||||
ImageField,
|
|
||||||
Input,
|
|
||||||
InputField,
|
|
||||||
)
|
|
||||||
from invokeai.app.invocations.model import VAEField
|
|
||||||
from invokeai.app.invocations.primitives import LatentsOutput
|
|
||||||
from invokeai.app.services.shared.invocation_context import InvocationContext
|
|
||||||
from invokeai.backend.flux.modules.autoencoder import AutoEncoder
|
|
||||||
from invokeai.backend.model_manager import LoadedModel
|
|
||||||
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
|
|
||||||
from invokeai.backend.util.devices import TorchDevice
|
|
||||||
|
|
||||||
|
|
||||||
@invocation(
|
|
||||||
"flux_vae_encode",
|
|
||||||
title="FLUX Image to Latents",
|
|
||||||
tags=["latents", "image", "vae", "i2l", "flux"],
|
|
||||||
category="latents",
|
|
||||||
version="1.0.0",
|
|
||||||
)
|
|
||||||
class FluxVaeEncodeInvocation(BaseInvocation):
|
|
||||||
"""Encodes an image into latents."""
|
|
||||||
|
|
||||||
image: ImageField = InputField(
|
|
||||||
description="The image to encode.",
|
|
||||||
)
|
|
||||||
vae: VAEField = InputField(
|
|
||||||
description=FieldDescriptions.vae,
|
|
||||||
input=Input.Connection,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
|
|
||||||
# TODO(ryand): Expose seed parameter at the invocation level.
|
|
||||||
# TODO(ryand): Write a util function for generating random tensors that is consistent across devices / dtypes.
|
|
||||||
# There's a starting point in get_noise(...), but it needs to be extracted and generalized. This function
|
|
||||||
# should be used for VAE encode sampling.
|
|
||||||
generator = torch.Generator(device=TorchDevice.choose_torch_device()).manual_seed(0)
|
|
||||||
with vae_info as vae:
|
|
||||||
assert isinstance(vae, AutoEncoder)
|
|
||||||
image_tensor = image_tensor.to(
|
|
||||||
device=TorchDevice.choose_torch_device(), dtype=TorchDevice.choose_torch_dtype()
|
|
||||||
)
|
|
||||||
latents = vae.encode(image_tensor, sample=True, generator=generator)
|
|
||||||
return latents
|
|
||||||
|
|
||||||
@torch.no_grad()
|
|
||||||
def invoke(self, context: InvocationContext) -> LatentsOutput:
|
|
||||||
image = context.images.get_pil(self.image.image_name)
|
|
||||||
|
|
||||||
vae_info = context.models.load(self.vae.vae)
|
|
||||||
|
|
||||||
image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
|
|
||||||
if image_tensor.dim() == 3:
|
|
||||||
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
|
|
||||||
|
|
||||||
latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
|
|
||||||
|
|
||||||
latents = latents.to("cpu")
|
|
||||||
name = context.tensors.save(tensor=latents)
|
|
||||||
return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
|
|
||||||
@@ -126,7 +126,7 @@ class ImageMaskToTensorInvocation(BaseInvocation, WithMetadata):
|
|||||||
title="Tensor Mask to Image",
|
title="Tensor Mask to Image",
|
||||||
tags=["mask"],
|
tags=["mask"],
|
||||||
category="mask",
|
category="mask",
|
||||||
version="1.1.0",
|
version="1.0.0",
|
||||||
)
|
)
|
||||||
class MaskTensorToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
class MaskTensorToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||||
"""Convert a mask tensor to an image."""
|
"""Convert a mask tensor to an image."""
|
||||||
@@ -135,11 +135,6 @@ class MaskTensorToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
|||||||
|
|
||||||
def invoke(self, context: InvocationContext) -> ImageOutput:
|
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||||
mask = context.tensors.load(self.mask.tensor_name)
|
mask = context.tensors.load(self.mask.tensor_name)
|
||||||
|
|
||||||
# Squeeze the channel dimension if it exists.
|
|
||||||
if mask.dim() == 3:
|
|
||||||
mask = mask.squeeze(0)
|
|
||||||
|
|
||||||
# Ensure that the mask is binary.
|
# Ensure that the mask is binary.
|
||||||
if mask.dtype != torch.bool:
|
if mask.dtype != torch.bool:
|
||||||
mask = mask > 0.5
|
mask = mask > 0.5
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class HFModelSource(StringLikeSource):
|
|||||||
if self.variant:
|
if self.variant:
|
||||||
base += f":{self.variant or ''}"
|
base += f":{self.variant or ''}"
|
||||||
if self.subfolder:
|
if self.subfolder:
|
||||||
base += f"::{self.subfolder.as_posix()}"
|
base += f":{self.subfolder}"
|
||||||
return base
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from invokeai.app.services.image_records.image_records_common import ImageCatego
|
|||||||
from invokeai.app.services.images.images_common import ImageDTO
|
from invokeai.app.services.images.images_common import ImageDTO
|
||||||
from invokeai.app.services.invocation_services import InvocationServices
|
from invokeai.app.services.invocation_services import InvocationServices
|
||||||
from invokeai.app.services.model_records.model_records_base import UnknownModelException
|
from invokeai.app.services.model_records.model_records_base import UnknownModelException
|
||||||
from invokeai.app.util.step_callback import flux_step_callback, stable_diffusion_step_callback
|
from invokeai.app.util.step_callback import stable_diffusion_step_callback
|
||||||
from invokeai.backend.model_manager.config import (
|
from invokeai.backend.model_manager.config import (
|
||||||
AnyModel,
|
AnyModel,
|
||||||
AnyModelConfig,
|
AnyModelConfig,
|
||||||
@@ -557,24 +557,6 @@ class UtilInterface(InvocationContextInterface):
|
|||||||
is_canceled=self.is_canceled,
|
is_canceled=self.is_canceled,
|
||||||
)
|
)
|
||||||
|
|
||||||
def flux_step_callback(self, intermediate_state: PipelineIntermediateState) -> None:
|
|
||||||
"""
|
|
||||||
The step callback emits a progress event with the current step, the total number of
|
|
||||||
steps, a preview image, and some other internal metadata.
|
|
||||||
|
|
||||||
This should be called after each denoising step.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
intermediate_state: The intermediate state of the diffusion pipeline.
|
|
||||||
"""
|
|
||||||
|
|
||||||
flux_step_callback(
|
|
||||||
context_data=self._data,
|
|
||||||
intermediate_state=intermediate_state,
|
|
||||||
events=self._services.events,
|
|
||||||
is_canceled=self.is_canceled,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InvocationContext:
|
class InvocationContext:
|
||||||
"""Provides access to various services and data for the current invocation.
|
"""Provides access to various services and data for the current invocation.
|
||||||
|
|||||||
@@ -1,407 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "FLUX Image to Image",
|
|
||||||
"author": "InvokeAI",
|
|
||||||
"description": "A simple image-to-image workflow using a FLUX dev model. ",
|
|
||||||
"version": "1.0.4",
|
|
||||||
"contact": "",
|
|
||||||
"tags": "image2image, flux, image-to-image",
|
|
||||||
"notes": "Prerequisite model downloads: T5 Encoder, CLIP-L Encoder, and FLUX VAE. Quantized and un-quantized versions can be found in the starter models tab within your Model Manager. We recommend using FLUX dev models for image-to-image workflows. The image-to-image performance with FLUX schnell models is poor.",
|
|
||||||
"exposedFields": [
|
|
||||||
{
|
|
||||||
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"fieldName": "model"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"fieldName": "t5_encoder_model"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"fieldName": "clip_embed_model"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"fieldName": "vae_model"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nodeId": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"fieldName": "denoising_start"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nodeId": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
|
||||||
"fieldName": "prompt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nodeId": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"fieldName": "num_steps"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"meta": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"category": "default"
|
|
||||||
},
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "2981a67c-480f-4237-9384-26b68dbf912b",
|
|
||||||
"type": "invocation",
|
|
||||||
"data": {
|
|
||||||
"id": "2981a67c-480f-4237-9384-26b68dbf912b",
|
|
||||||
"type": "flux_vae_encode",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"label": "",
|
|
||||||
"notes": "",
|
|
||||||
"isOpen": true,
|
|
||||||
"isIntermediate": true,
|
|
||||||
"useCache": true,
|
|
||||||
"inputs": {
|
|
||||||
"image": {
|
|
||||||
"name": "image",
|
|
||||||
"label": "",
|
|
||||||
"value": {
|
|
||||||
"image_name": "8a5c62aa-9335-45d2-9c71-89af9fc1f8d4.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vae": {
|
|
||||||
"name": "vae",
|
|
||||||
"label": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"x": 732.7680166609682,
|
|
||||||
"y": -24.37398171806909
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"type": "invocation",
|
|
||||||
"data": {
|
|
||||||
"id": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"type": "flux_denoise",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"label": "",
|
|
||||||
"notes": "",
|
|
||||||
"isOpen": true,
|
|
||||||
"isIntermediate": true,
|
|
||||||
"useCache": true,
|
|
||||||
"inputs": {
|
|
||||||
"board": {
|
|
||||||
"name": "board",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"name": "metadata",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"latents": {
|
|
||||||
"name": "latents",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"denoise_mask": {
|
|
||||||
"name": "denoise_mask",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"denoising_start": {
|
|
||||||
"name": "denoising_start",
|
|
||||||
"label": "",
|
|
||||||
"value": 0.04
|
|
||||||
},
|
|
||||||
"denoising_end": {
|
|
||||||
"name": "denoising_end",
|
|
||||||
"label": "",
|
|
||||||
"value": 1
|
|
||||||
},
|
|
||||||
"transformer": {
|
|
||||||
"name": "transformer",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"positive_text_conditioning": {
|
|
||||||
"name": "positive_text_conditioning",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"width": {
|
|
||||||
"name": "width",
|
|
||||||
"label": "",
|
|
||||||
"value": 1024
|
|
||||||
},
|
|
||||||
"height": {
|
|
||||||
"name": "height",
|
|
||||||
"label": "",
|
|
||||||
"value": 1024
|
|
||||||
},
|
|
||||||
"num_steps": {
|
|
||||||
"name": "num_steps",
|
|
||||||
"label": "Steps (Recommend 30 for Dev, 4 for Schnell)",
|
|
||||||
"value": 30
|
|
||||||
},
|
|
||||||
"guidance": {
|
|
||||||
"name": "guidance",
|
|
||||||
"label": "",
|
|
||||||
"value": 4
|
|
||||||
},
|
|
||||||
"seed": {
|
|
||||||
"name": "seed",
|
|
||||||
"label": "",
|
|
||||||
"value": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"x": 1182.8836633018684,
|
|
||||||
"y": -251.38882958913183
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
|
|
||||||
"type": "invocation",
|
|
||||||
"data": {
|
|
||||||
"id": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
|
|
||||||
"type": "flux_vae_decode",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"label": "",
|
|
||||||
"notes": "",
|
|
||||||
"isOpen": true,
|
|
||||||
"isIntermediate": false,
|
|
||||||
"useCache": true,
|
|
||||||
"inputs": {
|
|
||||||
"board": {
|
|
||||||
"name": "board",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"name": "metadata",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"latents": {
|
|
||||||
"name": "latents",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"vae": {
|
|
||||||
"name": "vae",
|
|
||||||
"label": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"x": 1575.5797431839133,
|
|
||||||
"y": -209.00150975507415
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"type": "invocation",
|
|
||||||
"data": {
|
|
||||||
"id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"type": "flux_model_loader",
|
|
||||||
"version": "1.0.4",
|
|
||||||
"label": "",
|
|
||||||
"notes": "",
|
|
||||||
"isOpen": true,
|
|
||||||
"isIntermediate": true,
|
|
||||||
"useCache": false,
|
|
||||||
"inputs": {
|
|
||||||
"model": {
|
|
||||||
"name": "model",
|
|
||||||
"label": "Model (dev variant recommended for Image-to-Image)"
|
|
||||||
},
|
|
||||||
"t5_encoder_model": {
|
|
||||||
"name": "t5_encoder_model",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"clip_embed_model": {
|
|
||||||
"name": "clip_embed_model",
|
|
||||||
"label": "",
|
|
||||||
"value": {
|
|
||||||
"key": "fa23a584-b623-415d-832a-21b5098ff1a1",
|
|
||||||
"hash": "blake3:17c19f0ef941c3b7609a9c94a659ca5364de0be364a91d4179f0e39ba17c3b70",
|
|
||||||
"name": "clip-vit-large-patch14",
|
|
||||||
"base": "any",
|
|
||||||
"type": "clip_embed"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vae_model": {
|
|
||||||
"name": "vae_model",
|
|
||||||
"label": "",
|
|
||||||
"value": {
|
|
||||||
"key": "74fc82ba-c0a8-479d-a890-2126f82da758",
|
|
||||||
"hash": "blake3:ce21cb76364aa6e2421311cf4a4b5eb052a76c4f1cd207b50703d8978198a068",
|
|
||||||
"name": "FLUX.1-schnell_ae",
|
|
||||||
"base": "flux",
|
|
||||||
"type": "vae"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"x": 328.1809894659957,
|
|
||||||
"y": -90.2241133566946
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
|
||||||
"type": "invocation",
|
|
||||||
"data": {
|
|
||||||
"id": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
|
||||||
"type": "flux_text_encoder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"label": "",
|
|
||||||
"notes": "",
|
|
||||||
"isOpen": true,
|
|
||||||
"isIntermediate": true,
|
|
||||||
"useCache": true,
|
|
||||||
"inputs": {
|
|
||||||
"clip": {
|
|
||||||
"name": "clip",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"t5_encoder": {
|
|
||||||
"name": "t5_encoder",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"t5_max_seq_len": {
|
|
||||||
"name": "t5_max_seq_len",
|
|
||||||
"label": "T5 Max Seq Len",
|
|
||||||
"value": 256
|
|
||||||
},
|
|
||||||
"prompt": {
|
|
||||||
"name": "prompt",
|
|
||||||
"label": "",
|
|
||||||
"value": "a cat wearing a birthday hat"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"x": 745.8823365057267,
|
|
||||||
"y": -299.60249175851914
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4754c534-a5f3-4ad0-9382-7887985e668c",
|
|
||||||
"type": "invocation",
|
|
||||||
"data": {
|
|
||||||
"id": "4754c534-a5f3-4ad0-9382-7887985e668c",
|
|
||||||
"type": "rand_int",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"label": "",
|
|
||||||
"notes": "",
|
|
||||||
"isOpen": true,
|
|
||||||
"isIntermediate": true,
|
|
||||||
"useCache": false,
|
|
||||||
"inputs": {
|
|
||||||
"low": {
|
|
||||||
"name": "low",
|
|
||||||
"label": "",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
"high": {
|
|
||||||
"name": "high",
|
|
||||||
"label": "",
|
|
||||||
"value": 2147483647
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"x": 725.834098928012,
|
|
||||||
"y": 496.2710031089931
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"edges": [
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-2981a67c-480f-4237-9384-26b68dbf912bheight-ace0258f-67d7-4eee-a218-6fff27065214height",
|
|
||||||
"type": "default",
|
|
||||||
"source": "2981a67c-480f-4237-9384-26b68dbf912b",
|
|
||||||
"target": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"sourceHandle": "height",
|
|
||||||
"targetHandle": "height"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-2981a67c-480f-4237-9384-26b68dbf912bwidth-ace0258f-67d7-4eee-a218-6fff27065214width",
|
|
||||||
"type": "default",
|
|
||||||
"source": "2981a67c-480f-4237-9384-26b68dbf912b",
|
|
||||||
"target": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"sourceHandle": "width",
|
|
||||||
"targetHandle": "width"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-2981a67c-480f-4237-9384-26b68dbf912blatents-ace0258f-67d7-4eee-a218-6fff27065214latents",
|
|
||||||
"type": "default",
|
|
||||||
"source": "2981a67c-480f-4237-9384-26b68dbf912b",
|
|
||||||
"target": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"sourceHandle": "latents",
|
|
||||||
"targetHandle": "latents"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90vae-2981a67c-480f-4237-9384-26b68dbf912bvae",
|
|
||||||
"type": "default",
|
|
||||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"target": "2981a67c-480f-4237-9384-26b68dbf912b",
|
|
||||||
"sourceHandle": "vae",
|
|
||||||
"targetHandle": "vae"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-ace0258f-67d7-4eee-a218-6fff27065214latents-7e5172eb-48c1-44db-a770-8fd83e1435d1latents",
|
|
||||||
"type": "default",
|
|
||||||
"source": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"target": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
|
|
||||||
"sourceHandle": "latents",
|
|
||||||
"targetHandle": "latents"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-4754c534-a5f3-4ad0-9382-7887985e668cvalue-ace0258f-67d7-4eee-a218-6fff27065214seed",
|
|
||||||
"type": "default",
|
|
||||||
"source": "4754c534-a5f3-4ad0-9382-7887985e668c",
|
|
||||||
"target": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"sourceHandle": "value",
|
|
||||||
"targetHandle": "seed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90transformer-ace0258f-67d7-4eee-a218-6fff27065214transformer",
|
|
||||||
"type": "default",
|
|
||||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"target": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"sourceHandle": "transformer",
|
|
||||||
"targetHandle": "transformer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cconditioning-ace0258f-67d7-4eee-a218-6fff27065214positive_text_conditioning",
|
|
||||||
"type": "default",
|
|
||||||
"source": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
|
||||||
"target": "ace0258f-67d7-4eee-a218-6fff27065214",
|
|
||||||
"sourceHandle": "conditioning",
|
|
||||||
"targetHandle": "positive_text_conditioning"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90vae-7e5172eb-48c1-44db-a770-8fd83e1435d1vae",
|
|
||||||
"type": "default",
|
|
||||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"target": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
|
|
||||||
"sourceHandle": "vae",
|
|
||||||
"targetHandle": "vae"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90max_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
|
|
||||||
"type": "default",
|
|
||||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
|
||||||
"sourceHandle": "max_seq_len",
|
|
||||||
"targetHandle": "t5_max_seq_len"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90t5_encoder-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_encoder",
|
|
||||||
"type": "default",
|
|
||||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
|
||||||
"sourceHandle": "t5_encoder",
|
|
||||||
"targetHandle": "t5_encoder"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90clip-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cclip",
|
|
||||||
"type": "default",
|
|
||||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
|
||||||
"sourceHandle": "clip",
|
|
||||||
"targetHandle": "clip"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "FLUX Text to Image",
|
"name": "FLUX Text to Image",
|
||||||
"author": "InvokeAI",
|
"author": "InvokeAI",
|
||||||
"description": "A simple text-to-image workflow using FLUX dev or schnell models.",
|
"description": "A simple text-to-image workflow using FLUX dev or schnell models. Prerequisite model downloads: T5 Encoder, CLIP-L Encoder, and FLUX VAE. Quantized and un-quantized versions can be found in the starter models tab within your Model Manager. We recommend 4 steps for FLUX schnell models and 30 steps for FLUX dev models.",
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"contact": "",
|
"contact": "",
|
||||||
"tags": "text2image, flux",
|
"tags": "text2image, flux",
|
||||||
@@ -11,25 +11,17 @@
|
|||||||
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||||
"fieldName": "model"
|
"fieldName": "model"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"fieldName": "t5_encoder_model"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"fieldName": "clip_embed_model"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"fieldName": "vae_model"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"nodeId": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
"nodeId": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
||||||
"fieldName": "prompt"
|
"fieldName": "prompt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"nodeId": "4fe24f07-f906-4f55-ab2c-9beee56ef5bd",
|
"nodeId": "159bdf1b-79e7-4174-b86e-d40e646964c8",
|
||||||
"fieldName": "num_steps"
|
"fieldName": "num_steps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||||
|
"fieldName": "t5_encoder_model"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"meta": {
|
"meta": {
|
||||||
@@ -37,121 +29,6 @@
|
|||||||
"category": "default"
|
"category": "default"
|
||||||
},
|
},
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
|
||||||
"id": "4fe24f07-f906-4f55-ab2c-9beee56ef5bd",
|
|
||||||
"type": "invocation",
|
|
||||||
"data": {
|
|
||||||
"id": "4fe24f07-f906-4f55-ab2c-9beee56ef5bd",
|
|
||||||
"type": "flux_denoise",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"label": "",
|
|
||||||
"notes": "",
|
|
||||||
"isOpen": true,
|
|
||||||
"isIntermediate": true,
|
|
||||||
"useCache": true,
|
|
||||||
"inputs": {
|
|
||||||
"board": {
|
|
||||||
"name": "board",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"name": "metadata",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"latents": {
|
|
||||||
"name": "latents",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"denoise_mask": {
|
|
||||||
"name": "denoise_mask",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"denoising_start": {
|
|
||||||
"name": "denoising_start",
|
|
||||||
"label": "",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
"denoising_end": {
|
|
||||||
"name": "denoising_end",
|
|
||||||
"label": "",
|
|
||||||
"value": 1
|
|
||||||
},
|
|
||||||
"transformer": {
|
|
||||||
"name": "transformer",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"positive_text_conditioning": {
|
|
||||||
"name": "positive_text_conditioning",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"width": {
|
|
||||||
"name": "width",
|
|
||||||
"label": "",
|
|
||||||
"value": 1024
|
|
||||||
},
|
|
||||||
"height": {
|
|
||||||
"name": "height",
|
|
||||||
"label": "",
|
|
||||||
"value": 1024
|
|
||||||
},
|
|
||||||
"num_steps": {
|
|
||||||
"name": "num_steps",
|
|
||||||
"label": "Steps (Recommend 30 for Dev, 4 for Schnell)",
|
|
||||||
"value": 30
|
|
||||||
},
|
|
||||||
"guidance": {
|
|
||||||
"name": "guidance",
|
|
||||||
"label": "",
|
|
||||||
"value": 4
|
|
||||||
},
|
|
||||||
"seed": {
|
|
||||||
"name": "seed",
|
|
||||||
"label": "",
|
|
||||||
"value": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"x": 1186.1868226120378,
|
|
||||||
"y": -214.9459927686657
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
|
|
||||||
"type": "invocation",
|
|
||||||
"data": {
|
|
||||||
"id": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
|
|
||||||
"type": "flux_vae_decode",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"label": "",
|
|
||||||
"notes": "",
|
|
||||||
"isOpen": true,
|
|
||||||
"isIntermediate": false,
|
|
||||||
"useCache": true,
|
|
||||||
"inputs": {
|
|
||||||
"board": {
|
|
||||||
"name": "board",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"name": "metadata",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"latents": {
|
|
||||||
"name": "latents",
|
|
||||||
"label": ""
|
|
||||||
},
|
|
||||||
"vae": {
|
|
||||||
"name": "vae",
|
|
||||||
"label": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"x": 1575.5797431839133,
|
|
||||||
"y": -209.00150975507415
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
"id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||||
"type": "invocation",
|
"type": "invocation",
|
||||||
@@ -222,8 +99,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"position": {
|
"position": {
|
||||||
"x": 778.4899149328337,
|
"x": 824.1970602278849,
|
||||||
"y": -100.36469216659502
|
"y": 146.98251001061735
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -252,52 +129,77 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"position": {
|
"position": {
|
||||||
"x": 800.9667463219505,
|
"x": 822.9899179655476,
|
||||||
"y": 285.8297267547506
|
"y": 360.9657214885052
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "159bdf1b-79e7-4174-b86e-d40e646964c8",
|
||||||
|
"type": "invocation",
|
||||||
|
"data": {
|
||||||
|
"id": "159bdf1b-79e7-4174-b86e-d40e646964c8",
|
||||||
|
"type": "flux_text_to_image",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"label": "",
|
||||||
|
"notes": "",
|
||||||
|
"isOpen": true,
|
||||||
|
"isIntermediate": false,
|
||||||
|
"useCache": true,
|
||||||
|
"inputs": {
|
||||||
|
"board": {
|
||||||
|
"name": "board",
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"name": "metadata",
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"transformer": {
|
||||||
|
"name": "transformer",
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"vae": {
|
||||||
|
"name": "vae",
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"positive_text_conditioning": {
|
||||||
|
"name": "positive_text_conditioning",
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"name": "width",
|
||||||
|
"label": "",
|
||||||
|
"value": 1024
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"name": "height",
|
||||||
|
"label": "",
|
||||||
|
"value": 1024
|
||||||
|
},
|
||||||
|
"num_steps": {
|
||||||
|
"name": "num_steps",
|
||||||
|
"label": "Steps (Recommend 30 for Dev, 4 for Schnell)",
|
||||||
|
"value": 30
|
||||||
|
},
|
||||||
|
"guidance": {
|
||||||
|
"name": "guidance",
|
||||||
|
"label": "",
|
||||||
|
"value": 4
|
||||||
|
},
|
||||||
|
"seed": {
|
||||||
|
"name": "seed",
|
||||||
|
"label": "",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"x": 1216.3900791301849,
|
||||||
|
"y": 5.500841807102248
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"edges": [
|
"edges": [
|
||||||
{
|
|
||||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90transformer-4fe24f07-f906-4f55-ab2c-9beee56ef5bdtransformer",
|
|
||||||
"type": "default",
|
|
||||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"target": "4fe24f07-f906-4f55-ab2c-9beee56ef5bd",
|
|
||||||
"sourceHandle": "transformer",
|
|
||||||
"targetHandle": "transformer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cconditioning-4fe24f07-f906-4f55-ab2c-9beee56ef5bdpositive_text_conditioning",
|
|
||||||
"type": "default",
|
|
||||||
"source": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
|
||||||
"target": "4fe24f07-f906-4f55-ab2c-9beee56ef5bd",
|
|
||||||
"sourceHandle": "conditioning",
|
|
||||||
"targetHandle": "positive_text_conditioning"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-4754c534-a5f3-4ad0-9382-7887985e668cvalue-4fe24f07-f906-4f55-ab2c-9beee56ef5bdseed",
|
|
||||||
"type": "default",
|
|
||||||
"source": "4754c534-a5f3-4ad0-9382-7887985e668c",
|
|
||||||
"target": "4fe24f07-f906-4f55-ab2c-9beee56ef5bd",
|
|
||||||
"sourceHandle": "value",
|
|
||||||
"targetHandle": "seed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-4fe24f07-f906-4f55-ab2c-9beee56ef5bdlatents-7e5172eb-48c1-44db-a770-8fd83e1435d1latents",
|
|
||||||
"type": "default",
|
|
||||||
"source": "4fe24f07-f906-4f55-ab2c-9beee56ef5bd",
|
|
||||||
"target": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
|
|
||||||
"sourceHandle": "latents",
|
|
||||||
"targetHandle": "latents"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90vae-7e5172eb-48c1-44db-a770-8fd83e1435d1vae",
|
|
||||||
"type": "default",
|
|
||||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
|
||||||
"target": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
|
|
||||||
"sourceHandle": "vae",
|
|
||||||
"targetHandle": "vae"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90max_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
|
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90max_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
@@ -306,6 +208,14 @@
|
|||||||
"sourceHandle": "max_seq_len",
|
"sourceHandle": "max_seq_len",
|
||||||
"targetHandle": "t5_max_seq_len"
|
"targetHandle": "t5_max_seq_len"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90vae-159bdf1b-79e7-4174-b86e-d40e646964c8vae",
|
||||||
|
"type": "default",
|
||||||
|
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||||
|
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
|
||||||
|
"sourceHandle": "vae",
|
||||||
|
"targetHandle": "vae"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90t5_encoder-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_encoder",
|
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90t5_encoder-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_encoder",
|
||||||
"type": "default",
|
"type": "default",
|
||||||
@@ -321,6 +231,30 @@
|
|||||||
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
||||||
"sourceHandle": "clip",
|
"sourceHandle": "clip",
|
||||||
"targetHandle": "clip"
|
"targetHandle": "clip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90transformer-159bdf1b-79e7-4174-b86e-d40e646964c8transformer",
|
||||||
|
"type": "default",
|
||||||
|
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||||
|
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
|
||||||
|
"sourceHandle": "transformer",
|
||||||
|
"targetHandle": "transformer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "reactflow__edge-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cconditioning-159bdf1b-79e7-4174-b86e-d40e646964c8positive_text_conditioning",
|
||||||
|
"type": "default",
|
||||||
|
"source": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
||||||
|
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
|
||||||
|
"sourceHandle": "conditioning",
|
||||||
|
"targetHandle": "positive_text_conditioning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "reactflow__edge-4754c534-a5f3-4ad0-9382-7887985e668cvalue-159bdf1b-79e7-4174-b86e-d40e646964c8seed",
|
||||||
|
"type": "default",
|
||||||
|
"source": "4754c534-a5f3-4ad0-9382-7887985e668c",
|
||||||
|
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
|
||||||
|
"sourceHandle": "value",
|
||||||
|
"targetHandle": "seed"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,25 +38,6 @@ SD1_5_LATENT_RGB_FACTORS = [
|
|||||||
[-0.1307, -0.1874, -0.7445], # L4
|
[-0.1307, -0.1874, -0.7445], # L4
|
||||||
]
|
]
|
||||||
|
|
||||||
FLUX_LATENT_RGB_FACTORS = [
|
|
||||||
[-0.0412, 0.0149, 0.0521],
|
|
||||||
[0.0056, 0.0291, 0.0768],
|
|
||||||
[0.0342, -0.0681, -0.0427],
|
|
||||||
[-0.0258, 0.0092, 0.0463],
|
|
||||||
[0.0863, 0.0784, 0.0547],
|
|
||||||
[-0.0017, 0.0402, 0.0158],
|
|
||||||
[0.0501, 0.1058, 0.1152],
|
|
||||||
[-0.0209, -0.0218, -0.0329],
|
|
||||||
[-0.0314, 0.0083, 0.0896],
|
|
||||||
[0.0851, 0.0665, -0.0472],
|
|
||||||
[-0.0534, 0.0238, -0.0024],
|
|
||||||
[0.0452, -0.0026, 0.0048],
|
|
||||||
[0.0892, 0.0831, 0.0881],
|
|
||||||
[-0.1117, -0.0304, -0.0789],
|
|
||||||
[0.0027, -0.0479, -0.0043],
|
|
||||||
[-0.1146, -0.0827, -0.0598],
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def sample_to_lowres_estimated_image(
|
def sample_to_lowres_estimated_image(
|
||||||
samples: torch.Tensor, latent_rgb_factors: torch.Tensor, smooth_matrix: Optional[torch.Tensor] = None
|
samples: torch.Tensor, latent_rgb_factors: torch.Tensor, smooth_matrix: Optional[torch.Tensor] = None
|
||||||
@@ -113,32 +94,3 @@ def stable_diffusion_step_callback(
|
|||||||
intermediate_state,
|
intermediate_state,
|
||||||
ProgressImage(dataURL=dataURL, width=width, height=height),
|
ProgressImage(dataURL=dataURL, width=width, height=height),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def flux_step_callback(
|
|
||||||
context_data: "InvocationContextData",
|
|
||||||
intermediate_state: PipelineIntermediateState,
|
|
||||||
events: "EventServiceBase",
|
|
||||||
is_canceled: Callable[[], bool],
|
|
||||||
) -> None:
|
|
||||||
if is_canceled():
|
|
||||||
raise CanceledException
|
|
||||||
sample = intermediate_state.latents
|
|
||||||
latent_rgb_factors = torch.tensor(FLUX_LATENT_RGB_FACTORS, dtype=sample.dtype, device=sample.device)
|
|
||||||
latent_image_perm = sample.permute(1, 2, 0).to(dtype=sample.dtype, device=sample.device)
|
|
||||||
latent_image = latent_image_perm @ latent_rgb_factors
|
|
||||||
latents_ubyte = (
|
|
||||||
((latent_image + 1) / 2).clamp(0, 1).mul(0xFF) # change scale from -1..1 to 0..1 # to 0..255
|
|
||||||
).to(device="cpu", dtype=torch.uint8)
|
|
||||||
image = Image.fromarray(latents_ubyte.cpu().numpy())
|
|
||||||
(width, height) = image.size
|
|
||||||
width *= 8
|
|
||||||
height *= 8
|
|
||||||
dataURL = image_to_dataURL(image, image_format="JPEG")
|
|
||||||
|
|
||||||
events.emit_invocation_denoise_progress(
|
|
||||||
context_data.queue_item,
|
|
||||||
context_data.invocation,
|
|
||||||
intermediate_state,
|
|
||||||
ProgressImage(dataURL=dataURL, width=width, height=height),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
from typing import Callable
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
from invokeai.backend.flux.inpaint_extension import InpaintExtension
|
|
||||||
from invokeai.backend.flux.model import Flux
|
|
||||||
from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
|
|
||||||
|
|
||||||
|
|
||||||
def denoise(
|
|
||||||
model: Flux,
|
|
||||||
# model input
|
|
||||||
img: torch.Tensor,
|
|
||||||
img_ids: torch.Tensor,
|
|
||||||
txt: torch.Tensor,
|
|
||||||
txt_ids: torch.Tensor,
|
|
||||||
vec: torch.Tensor,
|
|
||||||
# sampling parameters
|
|
||||||
timesteps: list[float],
|
|
||||||
step_callback: Callable[[PipelineIntermediateState], None],
|
|
||||||
guidance: float,
|
|
||||||
inpaint_extension: InpaintExtension | None,
|
|
||||||
):
|
|
||||||
step = 0
|
|
||||||
# guidance_vec is ignored for schnell.
|
|
||||||
guidance_vec = torch.full((img.shape[0],), guidance, device=img.device, dtype=img.dtype)
|
|
||||||
for t_curr, t_prev in tqdm(list(zip(timesteps[:-1], timesteps[1:], strict=True))):
|
|
||||||
t_vec = torch.full((img.shape[0],), t_curr, dtype=img.dtype, device=img.device)
|
|
||||||
pred = model(
|
|
||||||
img=img,
|
|
||||||
img_ids=img_ids,
|
|
||||||
txt=txt,
|
|
||||||
txt_ids=txt_ids,
|
|
||||||
y=vec,
|
|
||||||
timesteps=t_vec,
|
|
||||||
guidance=guidance_vec,
|
|
||||||
)
|
|
||||||
preview_img = img - t_curr * pred
|
|
||||||
img = img + (t_prev - t_curr) * pred
|
|
||||||
|
|
||||||
if inpaint_extension is not None:
|
|
||||||
img = inpaint_extension.merge_intermediate_latents_with_init_latents(img, t_prev)
|
|
||||||
|
|
||||||
step_callback(
|
|
||||||
PipelineIntermediateState(
|
|
||||||
step=step,
|
|
||||||
order=1,
|
|
||||||
total_steps=len(timesteps),
|
|
||||||
timestep=int(t_curr),
|
|
||||||
latents=preview_img,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
step += 1
|
|
||||||
|
|
||||||
return img
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import torch
|
|
||||||
|
|
||||||
|
|
||||||
class InpaintExtension:
|
|
||||||
"""A class for managing inpainting with FLUX."""
|
|
||||||
|
|
||||||
def __init__(self, init_latents: torch.Tensor, inpaint_mask: torch.Tensor, noise: torch.Tensor):
|
|
||||||
"""Initialize InpaintExtension.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
init_latents (torch.Tensor): The initial latents (i.e. un-noised at timestep 0). In 'packed' format.
|
|
||||||
inpaint_mask (torch.Tensor): A mask specifying which elements to inpaint. Range [0, 1]. Values of 1 will be
|
|
||||||
re-generated. Values of 0 will remain unchanged. Values between 0 and 1 can be used to blend the
|
|
||||||
inpainted region with the background. In 'packed' format.
|
|
||||||
noise (torch.Tensor): The noise tensor used to noise the init_latents. In 'packed' format.
|
|
||||||
"""
|
|
||||||
assert init_latents.shape == inpaint_mask.shape == noise.shape
|
|
||||||
self._init_latents = init_latents
|
|
||||||
self._inpaint_mask = inpaint_mask
|
|
||||||
self._noise = noise
|
|
||||||
|
|
||||||
def merge_intermediate_latents_with_init_latents(
|
|
||||||
self, intermediate_latents: torch.Tensor, timestep: float
|
|
||||||
) -> torch.Tensor:
|
|
||||||
"""Merge the intermediate latents with the initial latents for the current timestep using the inpaint mask. I.e.
|
|
||||||
update the intermediate latents to keep the regions that are not being inpainted on the correct noise
|
|
||||||
trajectory.
|
|
||||||
|
|
||||||
This function should be called after each denoising step.
|
|
||||||
"""
|
|
||||||
# Noise the init latents for the current timestep.
|
|
||||||
noised_init_latents = self._noise * timestep + (1.0 - timestep) * self._init_latents
|
|
||||||
|
|
||||||
# Merge the intermediate latents with the noised_init_latents using the inpaint_mask.
|
|
||||||
return intermediate_latents * self._inpaint_mask + noised_init_latents * (1.0 - self._inpaint_mask)
|
|
||||||
@@ -258,17 +258,16 @@ class Decoder(nn.Module):
|
|||||||
|
|
||||||
|
|
||||||
class DiagonalGaussian(nn.Module):
|
class DiagonalGaussian(nn.Module):
|
||||||
def __init__(self, chunk_dim: int = 1):
|
def __init__(self, sample: bool = True, chunk_dim: int = 1):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.sample = sample
|
||||||
self.chunk_dim = chunk_dim
|
self.chunk_dim = chunk_dim
|
||||||
|
|
||||||
def forward(self, z: Tensor, sample: bool = True, generator: torch.Generator | None = None) -> Tensor:
|
def forward(self, z: Tensor) -> Tensor:
|
||||||
mean, logvar = torch.chunk(z, 2, dim=self.chunk_dim)
|
mean, logvar = torch.chunk(z, 2, dim=self.chunk_dim)
|
||||||
if sample:
|
if self.sample:
|
||||||
std = torch.exp(0.5 * logvar)
|
std = torch.exp(0.5 * logvar)
|
||||||
# Unfortunately, torch.randn_like(...) does not accept a generator argument at the time of writing, so we
|
return mean + std * torch.randn_like(mean)
|
||||||
# have to use torch.randn(...) instead.
|
|
||||||
return mean + std * torch.randn(size=mean.size(), generator=generator, dtype=mean.dtype, device=mean.device)
|
|
||||||
else:
|
else:
|
||||||
return mean
|
return mean
|
||||||
|
|
||||||
@@ -298,21 +297,8 @@ class AutoEncoder(nn.Module):
|
|||||||
self.scale_factor = params.scale_factor
|
self.scale_factor = params.scale_factor
|
||||||
self.shift_factor = params.shift_factor
|
self.shift_factor = params.shift_factor
|
||||||
|
|
||||||
def encode(self, x: Tensor, sample: bool = True, generator: torch.Generator | None = None) -> Tensor:
|
def encode(self, x: Tensor) -> Tensor:
|
||||||
"""Run VAE encoding on input tensor x.
|
z = self.reg(self.encoder(x))
|
||||||
|
|
||||||
Args:
|
|
||||||
x (Tensor): Input image tensor. Shape: (batch_size, in_channels, height, width).
|
|
||||||
sample (bool, optional): If True, sample from the encoded distribution, else, return the distribution mean.
|
|
||||||
Defaults to True.
|
|
||||||
generator (torch.Generator | None, optional): Optional random number generator for reproducibility.
|
|
||||||
Defaults to None.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tensor: Encoded latent tensor. Shape: (batch_size, z_channels, latent_height, latent_width).
|
|
||||||
"""
|
|
||||||
|
|
||||||
z = self.reg(self.encoder(x), sample=sample, generator=generator)
|
|
||||||
z = self.scale_factor * (z - self.shift_factor)
|
z = self.scale_factor * (z - self.shift_factor)
|
||||||
return z
|
return z
|
||||||
|
|
||||||
|
|||||||
167
invokeai/backend/flux/sampling.py
Normal file
167
invokeai/backend/flux/sampling.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Initially pulled from https://github.com/black-forest-labs/flux
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from einops import rearrange, repeat
|
||||||
|
from torch import Tensor
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from invokeai.backend.flux.model import Flux
|
||||||
|
from invokeai.backend.flux.modules.conditioner import HFEncoder
|
||||||
|
|
||||||
|
|
||||||
|
def get_noise(
|
||||||
|
num_samples: int,
|
||||||
|
height: int,
|
||||||
|
width: int,
|
||||||
|
device: torch.device,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
seed: int,
|
||||||
|
):
|
||||||
|
# We always generate noise on the same device and dtype then cast to ensure consistency across devices/dtypes.
|
||||||
|
rand_device = "cpu"
|
||||||
|
rand_dtype = torch.float16
|
||||||
|
return torch.randn(
|
||||||
|
num_samples,
|
||||||
|
16,
|
||||||
|
# allow for packing
|
||||||
|
2 * math.ceil(height / 16),
|
||||||
|
2 * math.ceil(width / 16),
|
||||||
|
device=rand_device,
|
||||||
|
dtype=rand_dtype,
|
||||||
|
generator=torch.Generator(device=rand_device).manual_seed(seed),
|
||||||
|
).to(device=device, dtype=dtype)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare(t5: HFEncoder, clip: HFEncoder, img: Tensor, prompt: str | list[str]) -> dict[str, Tensor]:
|
||||||
|
bs, c, h, w = img.shape
|
||||||
|
if bs == 1 and not isinstance(prompt, str):
|
||||||
|
bs = len(prompt)
|
||||||
|
|
||||||
|
img = rearrange(img, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=2, pw=2)
|
||||||
|
if img.shape[0] == 1 and bs > 1:
|
||||||
|
img = repeat(img, "1 ... -> bs ...", bs=bs)
|
||||||
|
|
||||||
|
img_ids = torch.zeros(h // 2, w // 2, 3)
|
||||||
|
img_ids[..., 1] = img_ids[..., 1] + torch.arange(h // 2)[:, None]
|
||||||
|
img_ids[..., 2] = img_ids[..., 2] + torch.arange(w // 2)[None, :]
|
||||||
|
img_ids = repeat(img_ids, "h w c -> b (h w) c", b=bs)
|
||||||
|
|
||||||
|
if isinstance(prompt, str):
|
||||||
|
prompt = [prompt]
|
||||||
|
txt = t5(prompt)
|
||||||
|
if txt.shape[0] == 1 and bs > 1:
|
||||||
|
txt = repeat(txt, "1 ... -> bs ...", bs=bs)
|
||||||
|
txt_ids = torch.zeros(bs, txt.shape[1], 3)
|
||||||
|
|
||||||
|
vec = clip(prompt)
|
||||||
|
if vec.shape[0] == 1 and bs > 1:
|
||||||
|
vec = repeat(vec, "1 ... -> bs ...", bs=bs)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"img": img,
|
||||||
|
"img_ids": img_ids.to(img.device),
|
||||||
|
"txt": txt.to(img.device),
|
||||||
|
"txt_ids": txt_ids.to(img.device),
|
||||||
|
"vec": vec.to(img.device),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def time_shift(mu: float, sigma: float, t: Tensor):
|
||||||
|
return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma)
|
||||||
|
|
||||||
|
|
||||||
|
def get_lin_function(x1: float = 256, y1: float = 0.5, x2: float = 4096, y2: float = 1.15) -> Callable[[float], float]:
|
||||||
|
m = (y2 - y1) / (x2 - x1)
|
||||||
|
b = y1 - m * x1
|
||||||
|
return lambda x: m * x + b
|
||||||
|
|
||||||
|
|
||||||
|
def get_schedule(
|
||||||
|
num_steps: int,
|
||||||
|
image_seq_len: int,
|
||||||
|
base_shift: float = 0.5,
|
||||||
|
max_shift: float = 1.15,
|
||||||
|
shift: bool = True,
|
||||||
|
) -> list[float]:
|
||||||
|
# extra step for zero
|
||||||
|
timesteps = torch.linspace(1, 0, num_steps + 1)
|
||||||
|
|
||||||
|
# shifting the schedule to favor high timesteps for higher signal images
|
||||||
|
if shift:
|
||||||
|
# eastimate mu based on linear estimation between two points
|
||||||
|
mu = get_lin_function(y1=base_shift, y2=max_shift)(image_seq_len)
|
||||||
|
timesteps = time_shift(mu, 1.0, timesteps)
|
||||||
|
|
||||||
|
return timesteps.tolist()
|
||||||
|
|
||||||
|
|
||||||
|
def denoise(
|
||||||
|
model: Flux,
|
||||||
|
# model input
|
||||||
|
img: Tensor,
|
||||||
|
img_ids: Tensor,
|
||||||
|
txt: Tensor,
|
||||||
|
txt_ids: Tensor,
|
||||||
|
vec: Tensor,
|
||||||
|
# sampling parameters
|
||||||
|
timesteps: list[float],
|
||||||
|
step_callback: Callable[[], None],
|
||||||
|
guidance: float = 4.0,
|
||||||
|
):
|
||||||
|
# guidance_vec is ignored for schnell.
|
||||||
|
guidance_vec = torch.full((img.shape[0],), guidance, device=img.device, dtype=img.dtype)
|
||||||
|
for t_curr, t_prev in tqdm(list(zip(timesteps[:-1], timesteps[1:], strict=True))):
|
||||||
|
t_vec = torch.full((img.shape[0],), t_curr, dtype=img.dtype, device=img.device)
|
||||||
|
pred = model(
|
||||||
|
img=img,
|
||||||
|
img_ids=img_ids,
|
||||||
|
txt=txt,
|
||||||
|
txt_ids=txt_ids,
|
||||||
|
y=vec,
|
||||||
|
timesteps=t_vec,
|
||||||
|
guidance=guidance_vec,
|
||||||
|
)
|
||||||
|
|
||||||
|
img = img + (t_prev - t_curr) * pred
|
||||||
|
step_callback()
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def unpack(x: Tensor, height: int, width: int) -> Tensor:
|
||||||
|
return rearrange(
|
||||||
|
x,
|
||||||
|
"b (h w) (c ph pw) -> b c (h ph) (w pw)",
|
||||||
|
h=math.ceil(height / 16),
|
||||||
|
w=math.ceil(width / 16),
|
||||||
|
ph=2,
|
||||||
|
pw=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_latent_img_patches(latent_img: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
"""Convert an input image in latent space to patches for diffusion.
|
||||||
|
|
||||||
|
This implementation was extracted from:
|
||||||
|
https://github.com/black-forest-labs/flux/blob/c00d7c60b085fce8058b9df845e036090873f2ce/src/flux/sampling.py#L32
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[Tensor, Tensor]: (img, img_ids), as defined in the original flux repo.
|
||||||
|
"""
|
||||||
|
bs, c, h, w = latent_img.shape
|
||||||
|
|
||||||
|
# Pixel unshuffle with a scale of 2, and flatten the height/width dimensions to get an array of patches.
|
||||||
|
img = rearrange(latent_img, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=2, pw=2)
|
||||||
|
if img.shape[0] == 1 and bs > 1:
|
||||||
|
img = repeat(img, "1 ... -> bs ...", bs=bs)
|
||||||
|
|
||||||
|
# Generate patch position ids.
|
||||||
|
img_ids = torch.zeros(h // 2, w // 2, 3, device=img.device, dtype=img.dtype)
|
||||||
|
img_ids[..., 1] = img_ids[..., 1] + torch.arange(h // 2, device=img.device, dtype=img.dtype)[:, None]
|
||||||
|
img_ids[..., 2] = img_ids[..., 2] + torch.arange(w // 2, device=img.device, dtype=img.dtype)[None, :]
|
||||||
|
img_ids = repeat(img_ids, "h w c -> b (h w) c", b=bs)
|
||||||
|
|
||||||
|
return img, img_ids
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
# Initially pulled from https://github.com/black-forest-labs/flux
|
|
||||||
|
|
||||||
import math
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from einops import rearrange, repeat
|
|
||||||
|
|
||||||
|
|
||||||
def get_noise(
|
|
||||||
num_samples: int,
|
|
||||||
height: int,
|
|
||||||
width: int,
|
|
||||||
device: torch.device,
|
|
||||||
dtype: torch.dtype,
|
|
||||||
seed: int,
|
|
||||||
):
|
|
||||||
# We always generate noise on the same device and dtype then cast to ensure consistency across devices/dtypes.
|
|
||||||
rand_device = "cpu"
|
|
||||||
rand_dtype = torch.float16
|
|
||||||
return torch.randn(
|
|
||||||
num_samples,
|
|
||||||
16,
|
|
||||||
# allow for packing
|
|
||||||
2 * math.ceil(height / 16),
|
|
||||||
2 * math.ceil(width / 16),
|
|
||||||
device=rand_device,
|
|
||||||
dtype=rand_dtype,
|
|
||||||
generator=torch.Generator(device=rand_device).manual_seed(seed),
|
|
||||||
).to(device=device, dtype=dtype)
|
|
||||||
|
|
||||||
|
|
||||||
def time_shift(mu: float, sigma: float, t: torch.Tensor) -> torch.Tensor:
|
|
||||||
return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma)
|
|
||||||
|
|
||||||
|
|
||||||
def get_lin_function(x1: float = 256, y1: float = 0.5, x2: float = 4096, y2: float = 1.15) -> Callable[[float], float]:
|
|
||||||
m = (y2 - y1) / (x2 - x1)
|
|
||||||
b = y1 - m * x1
|
|
||||||
return lambda x: m * x + b
|
|
||||||
|
|
||||||
|
|
||||||
def get_schedule(
|
|
||||||
num_steps: int,
|
|
||||||
image_seq_len: int,
|
|
||||||
base_shift: float = 0.5,
|
|
||||||
max_shift: float = 1.15,
|
|
||||||
shift: bool = True,
|
|
||||||
) -> list[float]:
|
|
||||||
# extra step for zero
|
|
||||||
timesteps = torch.linspace(1, 0, num_steps + 1)
|
|
||||||
|
|
||||||
# shifting the schedule to favor high timesteps for higher signal images
|
|
||||||
if shift:
|
|
||||||
# estimate mu based on linear estimation between two points
|
|
||||||
mu = get_lin_function(y1=base_shift, y2=max_shift)(image_seq_len)
|
|
||||||
timesteps = time_shift(mu, 1.0, timesteps)
|
|
||||||
|
|
||||||
return timesteps.tolist()
|
|
||||||
|
|
||||||
|
|
||||||
def _find_last_index_ge_val(timesteps: list[float], val: float, eps: float = 1e-6) -> int:
|
|
||||||
"""Find the last index in timesteps that is >= val.
|
|
||||||
|
|
||||||
We use epsilon-close equality to avoid potential floating point errors.
|
|
||||||
"""
|
|
||||||
idx = len(list(filter(lambda t: t >= (val - eps), timesteps))) - 1
|
|
||||||
assert idx >= 0
|
|
||||||
return idx
|
|
||||||
|
|
||||||
|
|
||||||
def clip_timestep_schedule(timesteps: list[float], denoising_start: float, denoising_end: float) -> list[float]:
|
|
||||||
"""Clip the timestep schedule to the denoising range.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
timesteps (list[float]): The original timestep schedule: [1.0, ..., 0.0].
|
|
||||||
denoising_start (float): A value in [0, 1] specifying the start of the denoising process. E.g. a value of 0.2
|
|
||||||
would mean that the denoising process start at the last timestep in the schedule >= 0.8.
|
|
||||||
denoising_end (float): A value in [0, 1] specifying the end of the denoising process. E.g. a value of 0.8 would
|
|
||||||
mean that the denoising process end at the last timestep in the schedule >= 0.2.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[float]: The clipped timestep schedule.
|
|
||||||
"""
|
|
||||||
assert 0.0 <= denoising_start <= 1.0
|
|
||||||
assert 0.0 <= denoising_end <= 1.0
|
|
||||||
assert denoising_start <= denoising_end
|
|
||||||
|
|
||||||
t_start_val = 1.0 - denoising_start
|
|
||||||
t_end_val = 1.0 - denoising_end
|
|
||||||
|
|
||||||
t_start_idx = _find_last_index_ge_val(timesteps, t_start_val)
|
|
||||||
t_end_idx = _find_last_index_ge_val(timesteps, t_end_val)
|
|
||||||
|
|
||||||
clipped_timesteps = timesteps[t_start_idx : t_end_idx + 1]
|
|
||||||
|
|
||||||
return clipped_timesteps
|
|
||||||
|
|
||||||
|
|
||||||
def unpack(x: torch.Tensor, height: int, width: int) -> torch.Tensor:
|
|
||||||
"""Unpack flat array of patch embeddings to latent image."""
|
|
||||||
return rearrange(
|
|
||||||
x,
|
|
||||||
"b (h w) (c ph pw) -> b c (h ph) (w pw)",
|
|
||||||
h=math.ceil(height / 16),
|
|
||||||
w=math.ceil(width / 16),
|
|
||||||
ph=2,
|
|
||||||
pw=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def pack(x: torch.Tensor) -> torch.Tensor:
|
|
||||||
"""Pack latent image to flattented array of patch embeddings."""
|
|
||||||
# Pixel unshuffle with a scale of 2, and flatten the height/width dimensions to get an array of patches.
|
|
||||||
return rearrange(x, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=2, pw=2)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_img_ids(h: int, w: int, batch_size: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
|
|
||||||
"""Generate tensor of image position ids.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
h (int): Height of image in latent space.
|
|
||||||
w (int): Width of image in latent space.
|
|
||||||
batch_size (int): Batch size.
|
|
||||||
device (torch.device): Device.
|
|
||||||
dtype (torch.dtype): dtype.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
torch.Tensor: Image position ids.
|
|
||||||
"""
|
|
||||||
img_ids = torch.zeros(h // 2, w // 2, 3, device=device, dtype=dtype)
|
|
||||||
img_ids[..., 1] = img_ids[..., 1] + torch.arange(h // 2, device=device, dtype=dtype)[:, None]
|
|
||||||
img_ids[..., 2] = img_ids[..., 2] + torch.arange(w // 2, device=device, dtype=dtype)[None, :]
|
|
||||||
img_ids = repeat(img_ids, "h w c -> b (h w) c", b=batch_size)
|
|
||||||
return img_ids
|
|
||||||
@@ -66,9 +66,8 @@ class ModelLoader(ModelLoaderBase):
|
|||||||
return (model_base / config.path).resolve()
|
return (model_base / config.path).resolve()
|
||||||
|
|
||||||
def _load_and_cache(self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> ModelLockerBase:
|
def _load_and_cache(self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> ModelLockerBase:
|
||||||
stats_name = ":".join([config.base, config.type, config.name, (submodel_type or "")])
|
|
||||||
try:
|
try:
|
||||||
return self._ram_cache.get(config.key, submodel_type, stats_name=stats_name)
|
return self._ram_cache.get(config.key, submodel_type)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ class ModelLoader(ModelLoaderBase):
|
|||||||
return self._ram_cache.get(
|
return self._ram_cache.get(
|
||||||
key=config.key,
|
key=config.key,
|
||||||
submodel_type=submodel_type,
|
submodel_type=submodel_type,
|
||||||
stats_name=stats_name,
|
stats_name=":".join([config.base, config.type, config.name, (submodel_type or "")]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_size_fs(
|
def get_size_fs(
|
||||||
|
|||||||
@@ -128,24 +128,7 @@ class ModelCacheBase(ABC, Generic[T]):
|
|||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def max_cache_size(self) -> float:
|
def max_cache_size(self) -> float:
|
||||||
"""Return the maximum size the RAM cache can grow to."""
|
"""Return true if the cache is configured to lazily offload models in VRAM."""
|
||||||
pass
|
|
||||||
|
|
||||||
@max_cache_size.setter
|
|
||||||
@abstractmethod
|
|
||||||
def max_cache_size(self, value: float) -> None:
|
|
||||||
"""Set the cap on vram cache size."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def max_vram_cache_size(self) -> float:
|
|
||||||
"""Return the maximum size the VRAM cache can grow to."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@max_vram_cache_size.setter
|
|
||||||
@abstractmethod
|
|
||||||
def max_vram_cache_size(self, value: float) -> float:
|
|
||||||
"""Set the maximum size the VRAM cache can grow to."""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
|||||||
max_vram_cache_size: float,
|
max_vram_cache_size: float,
|
||||||
execution_device: torch.device = torch.device("cuda"),
|
execution_device: torch.device = torch.device("cuda"),
|
||||||
storage_device: torch.device = torch.device("cpu"),
|
storage_device: torch.device = torch.device("cpu"),
|
||||||
precision: torch.dtype = torch.float16,
|
|
||||||
lazy_offloading: bool = True,
|
lazy_offloading: bool = True,
|
||||||
log_memory_usage: bool = False,
|
log_memory_usage: bool = False,
|
||||||
logger: Optional[Logger] = None,
|
logger: Optional[Logger] = None,
|
||||||
@@ -82,13 +81,11 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
|||||||
:param max_vram_cache_size: Maximum size of the execution_device cache in GBs.
|
:param max_vram_cache_size: Maximum size of the execution_device cache in GBs.
|
||||||
:param execution_device: Torch device to load active model into [torch.device('cuda')]
|
:param execution_device: Torch device to load active model into [torch.device('cuda')]
|
||||||
:param storage_device: Torch device to save inactive model in [torch.device('cpu')]
|
:param storage_device: Torch device to save inactive model in [torch.device('cpu')]
|
||||||
:param precision: Precision for loaded models [torch.float16]
|
:param lazy_offloading: Keep model in VRAM until another model needs to be loaded.
|
||||||
:param lazy_offloading: Keep model in VRAM until another model needs to be loaded
|
|
||||||
:param log_memory_usage: If True, a memory snapshot will be captured before and after every model cache
|
:param log_memory_usage: If True, a memory snapshot will be captured before and after every model cache
|
||||||
operation, and the result will be logged (at debug level). There is a time cost to capturing the memory
|
operation, and the result will be logged (at debug level). There is a time cost to capturing the memory
|
||||||
snapshots, so it is recommended to disable this feature unless you are actively inspecting the model cache's
|
snapshots, so it is recommended to disable this feature unless you are actively inspecting the model cache's
|
||||||
behaviour.
|
behaviour.
|
||||||
:param logger: InvokeAILogger to use (otherwise creates one)
|
|
||||||
"""
|
"""
|
||||||
# allow lazy offloading only when vram cache enabled
|
# allow lazy offloading only when vram cache enabled
|
||||||
self._lazy_offloading = lazy_offloading and max_vram_cache_size > 0
|
self._lazy_offloading = lazy_offloading and max_vram_cache_size > 0
|
||||||
@@ -133,16 +130,6 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
|||||||
"""Set the cap on cache size."""
|
"""Set the cap on cache size."""
|
||||||
self._max_cache_size = value
|
self._max_cache_size = value
|
||||||
|
|
||||||
@property
|
|
||||||
def max_vram_cache_size(self) -> float:
|
|
||||||
"""Return the cap on vram cache size."""
|
|
||||||
return self._max_vram_cache_size
|
|
||||||
|
|
||||||
@max_vram_cache_size.setter
|
|
||||||
def max_vram_cache_size(self, value: float) -> None:
|
|
||||||
"""Set the cap on vram cache size."""
|
|
||||||
self._max_vram_cache_size = value
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stats(self) -> Optional[CacheStats]:
|
def stats(self) -> Optional[CacheStats]:
|
||||||
"""Return collected CacheStats object."""
|
"""Return collected CacheStats object."""
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ from invokeai.backend.model_manager.config import (
|
|||||||
)
|
)
|
||||||
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
from invokeai.backend.model_manager.load.load_default import ModelLoader
|
||||||
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
|
||||||
from invokeai.backend.model_manager.util.model_util import convert_bundle_to_flux_transformer_checkpoint
|
|
||||||
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -191,8 +190,6 @@ class FluxCheckpointModel(ModelLoader):
|
|||||||
with SilenceWarnings():
|
with SilenceWarnings():
|
||||||
model = Flux(params[config.config_path])
|
model = Flux(params[config.config_path])
|
||||||
sd = load_file(model_path)
|
sd = load_file(model_path)
|
||||||
if "model.diffusion_model.double_blocks.0.img_attn.norm.key_norm.scale" in sd:
|
|
||||||
sd = convert_bundle_to_flux_transformer_checkpoint(sd)
|
|
||||||
model.load_state_dict(sd, assign=True)
|
model.load_state_dict(sd, assign=True)
|
||||||
return model
|
return model
|
||||||
|
|
||||||
@@ -233,7 +230,5 @@ class FluxBnbQuantizednf4bCheckpointModel(ModelLoader):
|
|||||||
model = Flux(params[config.config_path])
|
model = Flux(params[config.config_path])
|
||||||
model = quantize_model_nf4(model, modules_to_not_convert=set(), compute_dtype=torch.bfloat16)
|
model = quantize_model_nf4(model, modules_to_not_convert=set(), compute_dtype=torch.bfloat16)
|
||||||
sd = load_file(model_path)
|
sd = load_file(model_path)
|
||||||
if "model.diffusion_model.double_blocks.0.img_attn.norm.key_norm.scale" in sd:
|
|
||||||
sd = convert_bundle_to_flux_transformer_checkpoint(sd)
|
|
||||||
model.load_state_dict(sd, assign=True)
|
model.load_state_dict(sd, assign=True)
|
||||||
return model
|
return model
|
||||||
|
|||||||
@@ -108,8 +108,6 @@ class ModelProbe(object):
|
|||||||
"CLIPVisionModelWithProjection": ModelType.CLIPVision,
|
"CLIPVisionModelWithProjection": ModelType.CLIPVision,
|
||||||
"T2IAdapter": ModelType.T2IAdapter,
|
"T2IAdapter": ModelType.T2IAdapter,
|
||||||
"CLIPModel": ModelType.CLIPEmbed,
|
"CLIPModel": ModelType.CLIPEmbed,
|
||||||
"CLIPTextModel": ModelType.CLIPEmbed,
|
|
||||||
"T5EncoderModel": ModelType.T5Encoder,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -226,18 +224,7 @@ class ModelProbe(object):
|
|||||||
ckpt = ckpt.get("state_dict", ckpt)
|
ckpt = ckpt.get("state_dict", ckpt)
|
||||||
|
|
||||||
for key in [str(k) for k in ckpt.keys()]:
|
for key in [str(k) for k in ckpt.keys()]:
|
||||||
if key.startswith(
|
if key.startswith(("cond_stage_model.", "first_stage_model.", "model.diffusion_model.", "double_blocks.")):
|
||||||
(
|
|
||||||
"cond_stage_model.",
|
|
||||||
"first_stage_model.",
|
|
||||||
"model.diffusion_model.",
|
|
||||||
# FLUX models in the official BFL format contain keys with the "double_blocks." prefix.
|
|
||||||
"double_blocks.",
|
|
||||||
# Some FLUX checkpoint files contain transformer keys prefixed with "model.diffusion_model".
|
|
||||||
# This prefix is typically used to distinguish between multiple models bundled in a single file.
|
|
||||||
"model.diffusion_model.double_blocks.",
|
|
||||||
)
|
|
||||||
):
|
|
||||||
# Keys starting with double_blocks are associated with Flux models
|
# Keys starting with double_blocks are associated with Flux models
|
||||||
return ModelType.Main
|
return ModelType.Main
|
||||||
elif key.startswith(("encoder.conv_in", "decoder.conv_in")):
|
elif key.startswith(("encoder.conv_in", "decoder.conv_in")):
|
||||||
@@ -296,16 +283,9 @@ class ModelProbe(object):
|
|||||||
if (folder_path / "image_encoder.txt").exists():
|
if (folder_path / "image_encoder.txt").exists():
|
||||||
return ModelType.IPAdapter
|
return ModelType.IPAdapter
|
||||||
|
|
||||||
config_path = None
|
i = folder_path / "model_index.json"
|
||||||
for p in [
|
c = folder_path / "config.json"
|
||||||
folder_path / "model_index.json", # pipeline
|
config_path = i if i.exists() else c if c.exists() else None
|
||||||
folder_path / "config.json", # most diffusers
|
|
||||||
folder_path / "text_encoder_2" / "config.json", # T5 text encoder
|
|
||||||
folder_path / "text_encoder" / "config.json", # T5 CLIP
|
|
||||||
]:
|
|
||||||
if p.exists():
|
|
||||||
config_path = p
|
|
||||||
break
|
|
||||||
|
|
||||||
if config_path:
|
if config_path:
|
||||||
with open(config_path, "r") as file:
|
with open(config_path, "r") as file:
|
||||||
@@ -348,10 +328,7 @@ class ModelProbe(object):
|
|||||||
# TODO: Decide between dev/schnell
|
# TODO: Decide between dev/schnell
|
||||||
checkpoint = ModelProbe._scan_and_load_checkpoint(model_path)
|
checkpoint = ModelProbe._scan_and_load_checkpoint(model_path)
|
||||||
state_dict = checkpoint.get("state_dict") or checkpoint
|
state_dict = checkpoint.get("state_dict") or checkpoint
|
||||||
if (
|
if "guidance_in.out_layer.weight" in state_dict:
|
||||||
"guidance_in.out_layer.weight" in state_dict
|
|
||||||
or "model.diffusion_model.guidance_in.out_layer.weight" in state_dict
|
|
||||||
):
|
|
||||||
# For flux, this is a key in invokeai.backend.flux.util.params
|
# For flux, this is a key in invokeai.backend.flux.util.params
|
||||||
# Due to model type and format being the descriminator for model configs this
|
# Due to model type and format being the descriminator for model configs this
|
||||||
# is used rather than attempting to support flux with separate model types and format
|
# is used rather than attempting to support flux with separate model types and format
|
||||||
@@ -359,7 +336,7 @@ class ModelProbe(object):
|
|||||||
config_file = "flux-dev"
|
config_file = "flux-dev"
|
||||||
else:
|
else:
|
||||||
# For flux, this is a key in invokeai.backend.flux.util.params
|
# For flux, this is a key in invokeai.backend.flux.util.params
|
||||||
# Due to model type and format being the discriminator for model configs this
|
# Due to model type and format being the descriminator for model configs this
|
||||||
# is used rather than attempting to support flux with separate model types and format
|
# is used rather than attempting to support flux with separate model types and format
|
||||||
# If changed in the future, please fix me
|
# If changed in the future, please fix me
|
||||||
config_file = "flux-schnell"
|
config_file = "flux-schnell"
|
||||||
@@ -466,10 +443,7 @@ class CheckpointProbeBase(ProbeBase):
|
|||||||
|
|
||||||
def get_format(self) -> ModelFormat:
|
def get_format(self) -> ModelFormat:
|
||||||
state_dict = self.checkpoint.get("state_dict") or self.checkpoint
|
state_dict = self.checkpoint.get("state_dict") or self.checkpoint
|
||||||
if (
|
if "double_blocks.0.img_attn.proj.weight.quant_state.bitsandbytes__nf4" in state_dict:
|
||||||
"double_blocks.0.img_attn.proj.weight.quant_state.bitsandbytes__nf4" in state_dict
|
|
||||||
or "model.diffusion_model.double_blocks.0.img_attn.proj.weight.quant_state.bitsandbytes__nf4" in state_dict
|
|
||||||
):
|
|
||||||
return ModelFormat.BnbQuantizednf4b
|
return ModelFormat.BnbQuantizednf4b
|
||||||
return ModelFormat("checkpoint")
|
return ModelFormat("checkpoint")
|
||||||
|
|
||||||
@@ -496,10 +470,7 @@ class PipelineCheckpointProbe(CheckpointProbeBase):
|
|||||||
def get_base_type(self) -> BaseModelType:
|
def get_base_type(self) -> BaseModelType:
|
||||||
checkpoint = self.checkpoint
|
checkpoint = self.checkpoint
|
||||||
state_dict = self.checkpoint.get("state_dict") or checkpoint
|
state_dict = self.checkpoint.get("state_dict") or checkpoint
|
||||||
if (
|
if "double_blocks.0.img_attn.norm.key_norm.scale" in state_dict:
|
||||||
"double_blocks.0.img_attn.norm.key_norm.scale" in state_dict
|
|
||||||
or "model.diffusion_model.double_blocks.0.img_attn.norm.key_norm.scale" in state_dict
|
|
||||||
):
|
|
||||||
return BaseModelType.Flux
|
return BaseModelType.Flux
|
||||||
key_name = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight"
|
key_name = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight"
|
||||||
if key_name in state_dict and state_dict[key_name].shape[-1] == 768:
|
if key_name in state_dict and state_dict[key_name].shape[-1] == 768:
|
||||||
@@ -776,27 +747,8 @@ class TextualInversionFolderProbe(FolderProbeBase):
|
|||||||
|
|
||||||
|
|
||||||
class T5EncoderFolderProbe(FolderProbeBase):
|
class T5EncoderFolderProbe(FolderProbeBase):
|
||||||
def get_base_type(self) -> BaseModelType:
|
|
||||||
return BaseModelType.Any
|
|
||||||
|
|
||||||
def get_format(self) -> ModelFormat:
|
def get_format(self) -> ModelFormat:
|
||||||
path = self.model_path / "text_encoder_2"
|
return ModelFormat.T5Encoder
|
||||||
if (path / "model.safetensors.index.json").exists():
|
|
||||||
return ModelFormat.T5Encoder
|
|
||||||
files = list(path.glob("*.safetensors"))
|
|
||||||
if len(files) == 0:
|
|
||||||
raise InvalidModelConfigException(f"{self.model_path.as_posix()}: no .safetensors files found")
|
|
||||||
|
|
||||||
# shortcut: look for the quantization in the name
|
|
||||||
if any(x for x in files if "llm_int8" in x.as_posix()):
|
|
||||||
return ModelFormat.BnbQuantizedLlmInt8b
|
|
||||||
|
|
||||||
# more reliable path: probe contents for a 'SCB' key
|
|
||||||
ckpt = read_checkpoint_meta(files[0], scan=True)
|
|
||||||
if any("SCB" in x for x in ckpt.keys()):
|
|
||||||
return ModelFormat.BnbQuantizedLlmInt8b
|
|
||||||
|
|
||||||
raise InvalidModelConfigException(f"{self.model_path.as_posix()}: unknown model format")
|
|
||||||
|
|
||||||
|
|
||||||
class ONNXFolderProbe(PipelineFolderProbe):
|
class ONNXFolderProbe(PipelineFolderProbe):
|
||||||
|
|||||||
@@ -133,29 +133,3 @@ def lora_token_vector_length(checkpoint: Dict[str, torch.Tensor]) -> Optional[in
|
|||||||
break
|
break
|
||||||
|
|
||||||
return lora_token_vector_length
|
return lora_token_vector_length
|
||||||
|
|
||||||
|
|
||||||
def convert_bundle_to_flux_transformer_checkpoint(
|
|
||||||
transformer_state_dict: dict[str, torch.Tensor],
|
|
||||||
) -> dict[str, torch.Tensor]:
|
|
||||||
original_state_dict: dict[str, torch.Tensor] = {}
|
|
||||||
keys_to_remove: list[str] = []
|
|
||||||
|
|
||||||
for k, v in transformer_state_dict.items():
|
|
||||||
if not k.startswith("model.diffusion_model"):
|
|
||||||
keys_to_remove.append(k) # This can be removed in the future if we only want to delete transformer keys
|
|
||||||
continue
|
|
||||||
if k.endswith("scale"):
|
|
||||||
# Scale math must be done at bfloat16 due to our current flux model
|
|
||||||
# support limitations at inference time
|
|
||||||
v = v.to(dtype=torch.bfloat16)
|
|
||||||
new_key = k.replace("model.diffusion_model.", "")
|
|
||||||
original_state_dict[new_key] = v
|
|
||||||
keys_to_remove.append(k)
|
|
||||||
|
|
||||||
# Remove processed keys from the original dictionary, leaving others in case
|
|
||||||
# other model state dicts need to be pulled
|
|
||||||
for k in keys_to_remove:
|
|
||||||
del transformer_state_dict[k]
|
|
||||||
|
|
||||||
return original_state_dict
|
|
||||||
|
|||||||
@@ -136,7 +136,6 @@
|
|||||||
"@vitest/coverage-v8": "^1.5.0",
|
"@vitest/coverage-v8": "^1.5.0",
|
||||||
"@vitest/ui": "^1.5.0",
|
"@vitest/ui": "^1.5.0",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"csstype": "^3.1.3",
|
|
||||||
"dpdm": "^3.14.0",
|
"dpdm": "^3.14.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-i18next": "^6.0.9",
|
"eslint-plugin-i18next": "^6.0.9",
|
||||||
|
|||||||
3
invokeai/frontend/web/pnpm-lock.yaml
generated
3
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -238,9 +238,6 @@ devDependencies:
|
|||||||
concurrently:
|
concurrently:
|
||||||
specifier: ^8.2.2
|
specifier: ^8.2.2
|
||||||
version: 8.2.2
|
version: 8.2.2
|
||||||
csstype:
|
|
||||||
specifier: ^3.1.3
|
|
||||||
version: 3.1.3
|
|
||||||
dpdm:
|
dpdm:
|
||||||
specifier: ^3.14.0
|
specifier: ^3.14.0
|
||||||
version: 3.14.0
|
version: 3.14.0
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1654,8 +1654,6 @@
|
|||||||
"storeNotInitialized": "Store is not initialized"
|
"storeNotInitialized": "Store is not initialized"
|
||||||
},
|
},
|
||||||
"controlLayers": {
|
"controlLayers": {
|
||||||
"bookmark": "Bookmark for Quick Switch",
|
|
||||||
"removeBookmark": "Remove Bookmark",
|
|
||||||
"saveCanvasToGallery": "Save Canvas To Gallery",
|
"saveCanvasToGallery": "Save Canvas To Gallery",
|
||||||
"saveBboxToGallery": "Save Bbox To Gallery",
|
"saveBboxToGallery": "Save Bbox To Gallery",
|
||||||
"savedToGalleryOk": "Saved to Gallery",
|
"savedToGalleryOk": "Saved to Gallery",
|
||||||
@@ -1674,7 +1672,6 @@
|
|||||||
"clearCaches": "Clear Caches",
|
"clearCaches": "Clear Caches",
|
||||||
"recalculateRects": "Recalculate Rects",
|
"recalculateRects": "Recalculate Rects",
|
||||||
"clipToBbox": "Clip Strokes to Bbox",
|
"clipToBbox": "Clip Strokes to Bbox",
|
||||||
"compositeMaskedRegions": "Composite Masked Regions",
|
|
||||||
"addLayer": "Add Layer",
|
"addLayer": "Add Layer",
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
"moveToFront": "Move to Front",
|
"moveToFront": "Move to Front",
|
||||||
@@ -1728,12 +1725,12 @@
|
|||||||
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
|
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
|
||||||
"controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
|
"controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
|
||||||
"rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
|
"rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
|
||||||
"globalIPAdapters_withCount_hidden": "Global IP Adapters ({{count}} hidden)",
|
"ipAdapters_withCount_hidden": "IP Adapters ({{count}} hidden)",
|
||||||
"inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
|
"inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
|
||||||
"regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
|
"regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
|
||||||
"controlLayers_withCount_visible": "Control Layers ({{count}})",
|
"controlLayers_withCount_visible": "Control Layers ({{count}})",
|
||||||
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
|
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
|
||||||
"globalIPAdapters_withCount_visible": "Global IP Adapters ({{count}})",
|
"ipAdapters_withCount_visible": "IP Adapters ({{count}})",
|
||||||
"inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})",
|
"inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})",
|
||||||
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
|
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
|
||||||
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
|
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
|
||||||
@@ -1746,8 +1743,8 @@
|
|||||||
"clearProcessor": "Clear Processor",
|
"clearProcessor": "Clear Processor",
|
||||||
"resetProcessor": "Reset Processor to Defaults",
|
"resetProcessor": "Reset Processor to Defaults",
|
||||||
"noLayersAdded": "No Layers Added",
|
"noLayersAdded": "No Layers Added",
|
||||||
"layer_one": "Layer",
|
"layers_one": "Layer",
|
||||||
"layer_other": "Layers",
|
"layers_other": "Layers",
|
||||||
"objects_zero": "empty",
|
"objects_zero": "empty",
|
||||||
"objects_one": "{{count}} object",
|
"objects_one": "{{count}} object",
|
||||||
"objects_other": "{{count}} objects",
|
"objects_other": "{{count}} objects",
|
||||||
@@ -1783,6 +1780,7 @@
|
|||||||
"bbox": "Bbox",
|
"bbox": "Bbox",
|
||||||
"move": "Move",
|
"move": "Move",
|
||||||
"view": "View",
|
"view": "View",
|
||||||
|
"transform": "Transform",
|
||||||
"colorPicker": "Color Picker"
|
"colorPicker": "Color Picker"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
@@ -1792,13 +1790,6 @@
|
|||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"cancel": "Cancel"
|
"cancel": "Cancel"
|
||||||
},
|
|
||||||
"transform": {
|
|
||||||
"transform": "Transform",
|
|
||||||
"fitToBbox": "Fit to Bbox",
|
|
||||||
"reset": "Reset",
|
|
||||||
"apply": "Apply",
|
|
||||||
"cancel": "Cancel"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"upscaling": {
|
"upscaling": {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
|
|
||||||
let didStartStaging = false;
|
let didStartStaging = false;
|
||||||
|
|
||||||
if (!state.canvasSession.isStaging && state.canvasSettings.sendToCanvas) {
|
if (!state.canvasSession.isStaging && state.canvasSession.sendToCanvas) {
|
||||||
dispatch(sessionStartedStaging());
|
dispatch(sessionStartedStaging());
|
||||||
didStartStaging = true;
|
didStartStaging = true;
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
|
|
||||||
const { g, noise, posCond } = buildGraphResult.value;
|
const { g, noise, posCond } = buildGraphResult.value;
|
||||||
|
|
||||||
const destination = state.canvasSettings.sendToCanvas ? 'canvas' : 'gallery';
|
const destination = state.canvasSession.sendToCanvas ? 'canvas' : 'gallery';
|
||||||
|
|
||||||
const prepareBatchResult = withResult(() =>
|
const prepareBatchResult = withResult(() =>
|
||||||
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination)
|
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/contr
|
|||||||
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
|
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
|
||||||
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
|
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
|
||||||
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
|
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||||
|
import { toolPersistConfig, toolSlice } from 'features/controlLayers/store/toolSlice';
|
||||||
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
||||||
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
|
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
|
||||||
@@ -62,6 +63,7 @@ const allReducers = {
|
|||||||
[upscaleSlice.name]: upscaleSlice.reducer,
|
[upscaleSlice.name]: upscaleSlice.reducer,
|
||||||
[stylePresetSlice.name]: stylePresetSlice.reducer,
|
[stylePresetSlice.name]: stylePresetSlice.reducer,
|
||||||
[paramsSlice.name]: paramsSlice.reducer,
|
[paramsSlice.name]: paramsSlice.reducer,
|
||||||
|
[toolSlice.name]: toolSlice.reducer,
|
||||||
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
|
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
|
||||||
[canvasSessionSlice.name]: canvasSessionSlice.reducer,
|
[canvasSessionSlice.name]: canvasSessionSlice.reducer,
|
||||||
[lorasSlice.name]: lorasSlice.reducer,
|
[lorasSlice.name]: lorasSlice.reducer,
|
||||||
@@ -107,6 +109,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
|||||||
[upscalePersistConfig.name]: upscalePersistConfig,
|
[upscalePersistConfig.name]: upscalePersistConfig,
|
||||||
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
|
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
|
||||||
[paramsPersistConfig.name]: paramsPersistConfig,
|
[paramsPersistConfig.name]: paramsPersistConfig,
|
||||||
|
[toolPersistConfig.name]: toolPersistConfig,
|
||||||
[canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig,
|
[canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig,
|
||||||
[canvasSessionPersistConfig.name]: canvasSessionPersistConfig,
|
[canvasSessionPersistConfig.name]: canvasSessionPersistConfig,
|
||||||
[lorasPersistConfig.name]: lorasPersistConfig,
|
[lorasPersistConfig.name]: lorasPersistConfig,
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const createSelector = (templates: Templates, isConnected: boolean) =>
|
|||||||
canvas.controlLayers.entities
|
canvas.controlLayers.entities
|
||||||
.filter((controlLayer) => controlLayer.isEnabled)
|
.filter((controlLayer) => controlLayer.isEnabled)
|
||||||
.forEach((controlLayer, i) => {
|
.forEach((controlLayer, i) => {
|
||||||
const layerLiteral = i18n.t('controlLayers.layer_one');
|
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||||
const layerNumber = i + 1;
|
const layerNumber = i + 1;
|
||||||
const layerType = i18n.t(LAYER_TYPE_TO_TKEY['control_layer']);
|
const layerType = i18n.t(LAYER_TYPE_TO_TKEY['control_layer']);
|
||||||
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||||
@@ -158,7 +158,7 @@ const createSelector = (templates: Templates, isConnected: boolean) =>
|
|||||||
canvas.ipAdapters.entities
|
canvas.ipAdapters.entities
|
||||||
.filter((entity) => entity.isEnabled)
|
.filter((entity) => entity.isEnabled)
|
||||||
.forEach((entity, i) => {
|
.forEach((entity, i) => {
|
||||||
const layerLiteral = i18n.t('controlLayers.layer_one');
|
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||||
const layerNumber = i + 1;
|
const layerNumber = i + 1;
|
||||||
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
||||||
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||||
@@ -186,7 +186,7 @@ const createSelector = (templates: Templates, isConnected: boolean) =>
|
|||||||
canvas.regions.entities
|
canvas.regions.entities
|
||||||
.filter((entity) => entity.isEnabled)
|
.filter((entity) => entity.isEnabled)
|
||||||
.forEach((entity, i) => {
|
.forEach((entity, i) => {
|
||||||
const layerLiteral = i18n.t('controlLayers.layer_one');
|
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||||
const layerNumber = i + 1;
|
const layerNumber = i + 1;
|
||||||
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
||||||
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||||
@@ -223,7 +223,7 @@ const createSelector = (templates: Templates, isConnected: boolean) =>
|
|||||||
canvas.rasterLayers.entities
|
canvas.rasterLayers.entities
|
||||||
.filter((entity) => entity.isEnabled)
|
.filter((entity) => entity.isEnabled)
|
||||||
.forEach((entity, i) => {
|
.forEach((entity, i) => {
|
||||||
const layerLiteral = i18n.t('controlLayers.layer_one');
|
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||||
const layerNumber = i + 1;
|
const layerNumber = i + 1;
|
||||||
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
||||||
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const CanvasAddEntityButtons = memo(() => {
|
|||||||
{t('controlLayers.controlLayer')}
|
{t('controlLayers.controlLayer')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
|
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||||
{t('controlLayers.globalIPAdapter')}
|
{t('controlLayers.ipAdapter')}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
|
|
||||||
import {
|
import {
|
||||||
controlLayerAdded,
|
controlLayerAdded,
|
||||||
inpaintMaskAdded,
|
inpaintMaskAdded,
|
||||||
@@ -15,7 +14,6 @@ import { PiPlusBold } from 'react-icons/pi';
|
|||||||
export const CanvasEntityListMenuItems = memo(() => {
|
export const CanvasEntityListMenuItems = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const defaultIPAdapter = useDefaultIPAdapter();
|
|
||||||
const addInpaintMask = useCallback(() => {
|
const addInpaintMask = useCallback(() => {
|
||||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@@ -29,9 +27,8 @@ export const CanvasEntityListMenuItems = memo(() => {
|
|||||||
dispatch(controlLayerAdded({ isSelected: true }));
|
dispatch(controlLayerAdded({ isSelected: true }));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
const addIPAdapter = useCallback(() => {
|
const addIPAdapter = useCallback(() => {
|
||||||
const overrides = { ipAdapter: defaultIPAdapter };
|
dispatch(ipaAdded({ isSelected: true }));
|
||||||
dispatch(ipaAdded({ isSelected: true, overrides }));
|
}, [dispatch]);
|
||||||
}, [defaultIPAdapter, dispatch]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -48,7 +45,7 @@ export const CanvasEntityListMenuItems = memo(() => {
|
|||||||
{t('controlLayers.controlLayer')}
|
{t('controlLayers.controlLayer')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||||
{t('controlLayers.globalIPAdapter')}
|
{t('controlLayers.ipAdapter')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
selectEntity,
|
selectEntity,
|
||||||
selectSelectedEntityIdentifier,
|
selectSelectedEntityIdentifier,
|
||||||
} from 'features/controlLayers/store/selectors';
|
} from 'features/controlLayers/store/selectors';
|
||||||
import { isRenderableEntity } from 'features/controlLayers/store/types';
|
import { isDrawableEntity } from 'features/controlLayers/store/types';
|
||||||
import { clamp, round } from 'lodash-es';
|
import { clamp, round } from 'lodash-es';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
@@ -37,11 +37,11 @@ function formatPct(v: number | string) {
|
|||||||
return `${round(Number(v), 2).toLocaleString()}%`;
|
return `${round(Number(v), 2).toLocaleString()}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapSliderValueToRawValue(value: number) {
|
function mapSliderValueToOpacity(value: number) {
|
||||||
return value / 100;
|
return value / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapRawValueToSliderValue(opacity: number) {
|
function mapOpacityToSliderValue(opacity: number) {
|
||||||
return opacity * 100;
|
return opacity * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,14 +50,14 @@ function formatSliderValue(value: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const marks = [
|
const marks = [
|
||||||
mapRawValueToSliderValue(0),
|
mapOpacityToSliderValue(0),
|
||||||
mapRawValueToSliderValue(0.25),
|
mapOpacityToSliderValue(0.25),
|
||||||
mapRawValueToSliderValue(0.5),
|
mapOpacityToSliderValue(0.5),
|
||||||
mapRawValueToSliderValue(0.75),
|
mapOpacityToSliderValue(0.75),
|
||||||
mapRawValueToSliderValue(1),
|
mapOpacityToSliderValue(1),
|
||||||
];
|
];
|
||||||
|
|
||||||
const sliderDefaultValue = mapRawValueToSliderValue(1);
|
const sliderDefaultValue = mapOpacityToSliderValue(1);
|
||||||
|
|
||||||
const snapCandidates = marks.slice(1, marks.length - 1);
|
const snapCandidates = marks.slice(1, marks.length - 1);
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => {
|
|||||||
if (!selectedEntity) {
|
if (!selectedEntity) {
|
||||||
return 1; // fallback to 100% opacity
|
return 1; // fallback to 100% opacity
|
||||||
}
|
}
|
||||||
if (!isRenderableEntity(selectedEntity)) {
|
if (!isDrawableEntity(selectedEntity)) {
|
||||||
return 1; // fallback to 100% opacity
|
return 1; // fallback to 100% opacity
|
||||||
}
|
}
|
||||||
// Opacity is a float from 0-1, but we want to display it as a percentage
|
// Opacity is a float from 0-1, but we want to display it as a percentage
|
||||||
@@ -95,7 +95,7 @@ export const SelectedEntityOpacity = memo(() => {
|
|||||||
if (!$shift.get()) {
|
if (!$shift.get()) {
|
||||||
snappedOpacity = snapToNearest(opacity, snapCandidates, 2);
|
snappedOpacity = snapToNearest(opacity, snapCandidates, 2);
|
||||||
}
|
}
|
||||||
const mappedOpacity = mapSliderValueToRawValue(snappedOpacity);
|
const mappedOpacity = mapSliderValueToOpacity(snappedOpacity);
|
||||||
|
|
||||||
dispatch(entityOpacityChanged({ entityIdentifier: selectedEntityIdentifier, opacity: mappedOpacity }));
|
dispatch(entityOpacityChanged({ entityIdentifier: selectedEntityIdentifier, opacity: mappedOpacity }));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { $alt, IconButton } from '@invoke-ai/ui-library';
|
import { $shift, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
@@ -7,7 +7,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const CanvasToolbarResetViewButton = memo(() => {
|
export const CanvasResetViewButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const canvasManager = useStore($canvasManager);
|
const canvasManager = useStore($canvasManager);
|
||||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||||
@@ -27,7 +27,7 @@ export const CanvasToolbarResetViewButton = memo(() => {
|
|||||||
}, [canvasManager]);
|
}, [canvasManager]);
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
if ($alt.get()) {
|
if ($shift.get()) {
|
||||||
resetView();
|
resetView();
|
||||||
} else {
|
} else {
|
||||||
resetZoom();
|
resetZoom();
|
||||||
@@ -35,7 +35,7 @@ export const CanvasToolbarResetViewButton = memo(() => {
|
|||||||
}, [resetView, resetZoom]);
|
}, [resetView, resetZoom]);
|
||||||
|
|
||||||
useHotkeys('r', resetView, { enabled: isCanvasActive }, [isCanvasActive]);
|
useHotkeys('r', resetView, { enabled: isCanvasActive }, [isCanvasActive]);
|
||||||
useHotkeys('alt+r', resetZoom, { enabled: isCanvasActive }, [isCanvasActive]);
|
useHotkeys('shift+r', resetZoom, { enabled: isCanvasActive }, [isCanvasActive]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -48,4 +48,4 @@ export const CanvasToolbarResetViewButton = memo(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CanvasToolbarResetViewButton.displayName = 'CanvasToolbarResetViewButton';
|
CanvasResetViewButton.displayName = 'CanvasResetViewButton';
|
||||||
@@ -15,8 +15,9 @@ import {
|
|||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/controlLayers/konva/constants';
|
||||||
import { snapToNearest } from 'features/controlLayers/konva/util';
|
import { snapToNearest } from 'features/controlLayers/konva/util';
|
||||||
import { round } from 'lodash-es';
|
import { clamp, round } from 'lodash-es';
|
||||||
import { computed } from 'nanostores';
|
import { computed } from 'nanostores';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
@@ -31,7 +32,7 @@ function formatPct(v: number | string) {
|
|||||||
return `${round(Number(v), 2).toLocaleString()}%`;
|
return `${round(Number(v), 2).toLocaleString()}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapSliderValueToRawValue(value: number) {
|
function mapSliderValueToScale(value: number) {
|
||||||
if (value <= 40) {
|
if (value <= 40) {
|
||||||
// 0 to 40 -> 10% to 100%
|
// 0 to 40 -> 10% to 100%
|
||||||
return 10 + (90 * value) / 40;
|
return 10 + (90 * value) / 40;
|
||||||
@@ -44,58 +45,64 @@ function mapSliderValueToRawValue(value: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapRawValueToSliderValue(value: number) {
|
function mapScaleToSliderValue(scale: number) {
|
||||||
if (value <= 100) {
|
if (scale <= 100) {
|
||||||
return ((value - 10) * 40) / 90;
|
return ((scale - 10) * 40) / 90;
|
||||||
} else if (value <= 500) {
|
} else if (scale <= 500) {
|
||||||
return 40 + ((value - 100) * 30) / 400;
|
return 40 + ((scale - 100) * 30) / 400;
|
||||||
} else {
|
} else {
|
||||||
return 70 + ((value - 500) * 30) / 1500;
|
return 70 + ((scale - 500) * 30) / 1500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSliderValue(value: number) {
|
function formatSliderValue(value: number) {
|
||||||
return String(mapSliderValueToRawValue(value));
|
return String(mapSliderValueToScale(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
const marks = [
|
const marks = [
|
||||||
mapRawValueToSliderValue(10),
|
mapScaleToSliderValue(10),
|
||||||
mapRawValueToSliderValue(50),
|
mapScaleToSliderValue(50),
|
||||||
mapRawValueToSliderValue(100),
|
mapScaleToSliderValue(100),
|
||||||
mapRawValueToSliderValue(500),
|
mapScaleToSliderValue(500),
|
||||||
mapRawValueToSliderValue(2000),
|
mapScaleToSliderValue(2000),
|
||||||
];
|
];
|
||||||
|
|
||||||
const sliderDefaultValue = mapRawValueToSliderValue(100);
|
const sliderDefaultValue = mapScaleToSliderValue(100);
|
||||||
|
|
||||||
const snapCandidates = marks.slice(1, marks.length - 1);
|
const snapCandidates = marks.slice(1, marks.length - 1);
|
||||||
|
|
||||||
export const CanvasToolbarScale = memo(() => {
|
export const CanvasScale = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const scale = useStore(computed(canvasManager.stage.$stageAttrs, (attrs) => attrs.scale));
|
const scale = useStore(computed(canvasManager.stateApi.$stageAttrs, (attrs) => attrs.scale));
|
||||||
const [localScale, setLocalScale] = useState(scale * 100);
|
const [localScale, setLocalScale] = useState(scale * 100);
|
||||||
|
|
||||||
const onChangeSlider = useCallback(
|
const onChangeSlider = useCallback(
|
||||||
(scale: number) => {
|
(scale: number) => {
|
||||||
|
if (!canvasManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let snappedScale = scale;
|
let snappedScale = scale;
|
||||||
// Do not snap if shift key is held
|
// Do not snap if shift key is held
|
||||||
if (!$shift.get()) {
|
if (!$shift.get()) {
|
||||||
snappedScale = snapToNearest(scale, snapCandidates, 2);
|
snappedScale = snapToNearest(scale, snapCandidates, 2);
|
||||||
}
|
}
|
||||||
const mappedScale = mapSliderValueToRawValue(snappedScale);
|
const mappedScale = mapSliderValueToScale(snappedScale);
|
||||||
canvasManager.stage.setScale(mappedScale / 100);
|
canvasManager.stage.setScale(mappedScale / 100);
|
||||||
},
|
},
|
||||||
[canvasManager]
|
[canvasManager]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onBlur = useCallback(() => {
|
const onBlur = useCallback(() => {
|
||||||
|
if (!canvasManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isNaN(Number(localScale))) {
|
if (isNaN(Number(localScale))) {
|
||||||
canvasManager.stage.setScale(1);
|
canvasManager.stage.setScale(1);
|
||||||
setLocalScale(100);
|
setLocalScale(100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
canvasManager.stage.setScale(localScale / 100);
|
canvasManager.stage.setScale(clamp(localScale / 100, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE));
|
||||||
}, [canvasManager, localScale]);
|
}, [canvasManager, localScale]);
|
||||||
|
|
||||||
const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => {
|
const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => {
|
||||||
@@ -123,8 +130,8 @@ export const CanvasToolbarScale = memo(() => {
|
|||||||
<NumberInput
|
<NumberInput
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
min={canvasManager.stage.config.MIN_SCALE * 100}
|
min={MIN_CANVAS_SCALE * 100}
|
||||||
max={canvasManager.stage.config.MAX_SCALE * 100}
|
max={MAX_CANVAS_SCALE * 100}
|
||||||
value={localScale}
|
value={localScale}
|
||||||
onChange={onChangeNumberInput}
|
onChange={onChangeNumberInput}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
@@ -155,7 +162,7 @@ export const CanvasToolbarScale = memo(() => {
|
|||||||
<CompositeSlider
|
<CompositeSlider
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
value={mapRawValueToSliderValue(localScale)}
|
value={mapScaleToSliderValue(localScale)}
|
||||||
onChange={onChangeSlider}
|
onChange={onChangeSlider}
|
||||||
defaultValue={sliderDefaultValue}
|
defaultValue={sliderDefaultValue}
|
||||||
marks={marks}
|
marks={marks}
|
||||||
@@ -168,4 +175,4 @@ export const CanvasToolbarScale = memo(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CanvasToolbarScale.displayName = 'CanvasToolbarScale';
|
CanvasScale.displayName = 'CanvasScale';
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { IconSwitch } from 'common/components/IconSwitch';
|
import { IconSwitch } from 'common/components/IconSwitch';
|
||||||
import {
|
import { selectIsComposing, sessionSendToCanvasChanged } from 'features/controlLayers/store/canvasSessionSlice';
|
||||||
selectCanvasSettingsSlice,
|
|
||||||
settingsSendToCanvasChanged,
|
|
||||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
|
import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
|
||||||
@@ -36,22 +32,20 @@ const TooltipSendToCanvas = memo(() => {
|
|||||||
|
|
||||||
TooltipSendToCanvas.displayName = 'TooltipSendToCanvas';
|
TooltipSendToCanvas.displayName = 'TooltipSendToCanvas';
|
||||||
|
|
||||||
const selectSendToCanvas = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.sendToCanvas);
|
|
||||||
|
|
||||||
export const CanvasSendToToggle = memo(() => {
|
export const CanvasSendToToggle = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const sendToCanvas = useAppSelector(selectSendToCanvas);
|
const isComposing = useAppSelector(selectIsComposing);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(isChecked: boolean) => {
|
(isChecked: boolean) => {
|
||||||
dispatch(settingsSendToCanvasChanged(isChecked));
|
dispatch(sessionSendToCanvasChanged(isChecked));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconSwitch
|
<IconSwitch
|
||||||
isChecked={sendToCanvas}
|
isChecked={isComposing}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
iconUnchecked={<PiImageBold />}
|
iconUnchecked={<PiImageBold />}
|
||||||
tooltipUnchecked={<TooltipSendToGallery />}
|
tooltipUnchecked={<TooltipSendToGallery />}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/c
|
|||||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||||
import { ControlLayerBadges } from 'features/controlLayers/components/ControlLayer/ControlLayerBadges';
|
import { ControlLayerBadges } from 'features/controlLayers/components/ControlLayer/ControlLayerBadges';
|
||||||
import { ControlLayerControlAdapter } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapter';
|
import { ControlLayerControlAdapter } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapter';
|
||||||
import { ControlLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
@@ -21,7 +21,7 @@ export const ControlLayer = memo(({ id }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||||
<ControlLayerAdapterGate>
|
<EntityLayerAdapterGate>
|
||||||
<CanvasEntityContainer>
|
<CanvasEntityContainer>
|
||||||
<CanvasEntityHeader>
|
<CanvasEntityHeader>
|
||||||
<CanvasEntityPreviewImage />
|
<CanvasEntityPreviewImage />
|
||||||
@@ -34,7 +34,7 @@ export const ControlLayer = memo(({ id }: Props) => {
|
|||||||
<ControlLayerControlAdapter />
|
<ControlLayerControlAdapter />
|
||||||
</CanvasEntitySettingsWrapper>
|
</CanvasEntitySettingsWrapper>
|
||||||
</CanvasEntityContainer>
|
</CanvasEntityContainer>
|
||||||
</ControlLayerAdapterGate>
|
</EntityLayerAdapterGate>
|
||||||
</EntityIdentifierContext.Provider>
|
</EntityIdentifierContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
|
|||||||
} else {
|
} else {
|
||||||
canvasManager.filter.$config.set(IMAGE_FILTERS.canny_image_processor.buildDefaults(modelConfig.base));
|
canvasManager.filter.$config.set(IMAGE_FILTERS.canny_image_processor.buildDefaults(modelConfig.base));
|
||||||
}
|
}
|
||||||
canvasManager.filter.startFilter(entityIdentifier);
|
canvasManager.filter.initialize(entityIdentifier);
|
||||||
canvasManager.filter.previewFilter();
|
canvasManager.filter.previewFilter();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
|
||||||
import { controlLayerConvertedToRasterLayer } from 'features/controlLayers/store/canvasSlice';
|
import { controlLayerConvertedToRasterLayer } from 'features/controlLayers/store/canvasSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -10,7 +9,6 @@ import { PiLightningBold } from 'react-icons/pi';
|
|||||||
export const ControlLayerMenuItemsControlToRaster = memo(() => {
|
export const ControlLayerMenuItemsControlToRaster = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isBusy = useCanvasIsBusy();
|
|
||||||
const entityIdentifier = useEntityIdentifierContext('control_layer');
|
const entityIdentifier = useEntityIdentifierContext('control_layer');
|
||||||
|
|
||||||
const convertControlLayerToRasterLayer = useCallback(() => {
|
const convertControlLayerToRasterLayer = useCallback(() => {
|
||||||
@@ -18,7 +16,7 @@ export const ControlLayerMenuItemsControlToRaster = memo(() => {
|
|||||||
}, [dispatch, entityIdentifier]);
|
}, [dispatch, entityIdentifier]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={convertControlLayerToRasterLayer} icon={<PiLightningBold />} isDisabled={isBusy}>
|
<MenuItem onClick={convertControlLayerToRasterLayer} icon={<PiLightningBold />}>
|
||||||
{t('controlLayers.convertToRasterLayer')}
|
{t('controlLayers.convertToRasterLayer')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import { CanvasEditor } from 'features/controlLayers/components/CanvasEditor';
|
import { CanvasEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||||
|
|
||||||
const meta: Meta<typeof CanvasEditor> = {
|
const meta: Meta<typeof CanvasEditor> = {
|
||||||
title: 'Feature/ControlLayers',
|
title: 'Feature/ControlLayers',
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||||
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
|
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
|
||||||
|
import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar';
|
||||||
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
||||||
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
||||||
import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
|
import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate';
|
||||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||||
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
|
import { Transform } from 'features/controlLayers/components/Transform';
|
||||||
import { Transform } from 'features/controlLayers/components/Transform/Transform';
|
|
||||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import { memo, useRef } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
|
|
||||||
@@ -28,16 +28,16 @@ export const CanvasEditor = memo(() => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
>
|
>
|
||||||
<CanvasToolbar />
|
<ControlLayersToolbar />
|
||||||
<StageComponent />
|
<StageComponent />
|
||||||
<Flex position="absolute" bottom={4} gap={2} align="center" justify="center">
|
<Flex position="absolute" bottom={8} gap={2} align="center" justify="center">
|
||||||
<CanvasManagerProviderGate>
|
<CanvasManagerProviderGate>
|
||||||
<StagingAreaIsStagingGate>
|
<StagingAreaIsStagingGate>
|
||||||
<StagingAreaToolbar />
|
<StagingAreaToolbar />
|
||||||
</StagingAreaIsStagingGate>
|
</StagingAreaIsStagingGate>
|
||||||
</CanvasManagerProviderGate>
|
</CanvasManagerProviderGate>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex position="absolute" bottom={4}>
|
<Flex position="absolute" bottom={8}>
|
||||||
<CanvasManagerProviderGate>
|
<CanvasManagerProviderGate>
|
||||||
<Filter />
|
<Filter />
|
||||||
<Transform />
|
<Transform />
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/* eslint-disable i18next/no-literal-string */
|
||||||
|
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
|
import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton';
|
||||||
|
import { CanvasScale } from 'features/controlLayers/components/CanvasScale';
|
||||||
|
import { SaveToGalleryButton } from 'features/controlLayers/components/SaveToGalleryButton';
|
||||||
|
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
|
||||||
|
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
||||||
|
import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
|
||||||
|
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
|
||||||
|
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { useCanvasUndoRedo } from 'features/controlLayers/hooks/useCanvasUndoRedo';
|
||||||
|
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||||
|
import { ViewerToggle } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const ControlLayersToolbar = memo(() => {
|
||||||
|
useCanvasUndoRedo();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CanvasManagerProviderGate>
|
||||||
|
<Flex w="full" gap={2} alignItems="center">
|
||||||
|
<ToggleProgressButton />
|
||||||
|
<ToolChooser />
|
||||||
|
<Spacer />
|
||||||
|
<ToolSettings />
|
||||||
|
<Spacer />
|
||||||
|
<CanvasScale />
|
||||||
|
<CanvasResetViewButton />
|
||||||
|
<Spacer />
|
||||||
|
<ToolFillColorPicker />
|
||||||
|
<SaveToGalleryButton />
|
||||||
|
<CanvasSettingsPopover />
|
||||||
|
<ViewerToggle />
|
||||||
|
</Flex>
|
||||||
|
</CanvasManagerProviderGate>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ControlLayersToolbar.displayName = 'ControlLayersToolbar';
|
||||||
@@ -4,7 +4,7 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can
|
|||||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||||
import { InpaintMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
@@ -18,7 +18,7 @@ export const InpaintMask = memo(({ id }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||||
<InpaintMaskAdapterGate>
|
<EntityMaskAdapterGate>
|
||||||
<CanvasEntityContainer>
|
<CanvasEntityContainer>
|
||||||
<CanvasEntityHeader>
|
<CanvasEntityHeader>
|
||||||
<CanvasEntityPreviewImage />
|
<CanvasEntityPreviewImage />
|
||||||
@@ -27,7 +27,7 @@ export const InpaintMask = memo(({ id }: Props) => {
|
|||||||
<CanvasEntityHeaderCommonActions />
|
<CanvasEntityHeaderCommonActions />
|
||||||
</CanvasEntityHeader>
|
</CanvasEntityHeader>
|
||||||
</CanvasEntityContainer>
|
</CanvasEntityContainer>
|
||||||
</InpaintMaskAdapterGate>
|
</EntityMaskAdapterGate>
|
||||||
</EntityIdentifierContext.Provider>
|
</EntityIdentifierContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can
|
|||||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||||
import { RasterLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
@@ -18,7 +18,7 @@ export const RasterLayer = memo(({ id }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||||
<RasterLayerAdapterGate>
|
<EntityLayerAdapterGate>
|
||||||
<CanvasEntityContainer>
|
<CanvasEntityContainer>
|
||||||
<CanvasEntityHeader>
|
<CanvasEntityHeader>
|
||||||
<CanvasEntityPreviewImage />
|
<CanvasEntityPreviewImage />
|
||||||
@@ -27,7 +27,7 @@ export const RasterLayer = memo(({ id }: Props) => {
|
|||||||
<CanvasEntityHeaderCommonActions />
|
<CanvasEntityHeaderCommonActions />
|
||||||
</CanvasEntityHeader>
|
</CanvasEntityHeader>
|
||||||
</CanvasEntityContainer>
|
</CanvasEntityContainer>
|
||||||
</RasterLayerAdapterGate>
|
</EntityLayerAdapterGate>
|
||||||
</EntityIdentifierContext.Provider>
|
</EntityIdentifierContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
|
||||||
import { rasterLayerConvertedToControlLayer } from 'features/controlLayers/store/canvasSlice';
|
import { rasterLayerConvertedToControlLayer } from 'features/controlLayers/store/canvasSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -11,14 +10,13 @@ export const RasterLayerMenuItemsRasterToControl = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const entityIdentifier = useEntityIdentifierContext('raster_layer');
|
const entityIdentifier = useEntityIdentifierContext('raster_layer');
|
||||||
const isBusy = useCanvasIsBusy();
|
|
||||||
|
|
||||||
const convertRasterLayerToControlLayer = useCallback(() => {
|
const convertRasterLayerToControlLayer = useCallback(() => {
|
||||||
dispatch(rasterLayerConvertedToControlLayer({ entityIdentifier }));
|
dispatch(rasterLayerConvertedToControlLayer({ entityIdentifier }));
|
||||||
}, [dispatch, entityIdentifier]);
|
}, [dispatch, entityIdentifier]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={convertRasterLayerToControlLayer} icon={<PiLightningBold />} isDisabled={isBusy}>
|
<MenuItem onClick={convertRasterLayerToControlLayer} icon={<PiLightningBold />}>
|
||||||
{t('controlLayers.convertToControlLayer')}
|
{t('controlLayers.convertToControlLayer')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { CanvasEntityPreviewImage } from 'features/controlLayers/components/comm
|
|||||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||||
import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges';
|
import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges';
|
||||||
import { RegionalGuidanceSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings';
|
import { RegionalGuidanceSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings';
|
||||||
import { RegionalGuidanceAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
@@ -20,7 +20,7 @@ export const RegionalGuidance = memo(({ id }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||||
<RegionalGuidanceAdapterGate>
|
<EntityMaskAdapterGate>
|
||||||
<CanvasEntityContainer>
|
<CanvasEntityContainer>
|
||||||
<CanvasEntityHeader>
|
<CanvasEntityHeader>
|
||||||
<CanvasEntityPreviewImage />
|
<CanvasEntityPreviewImage />
|
||||||
@@ -31,7 +31,7 @@ export const RegionalGuidance = memo(({ id }: Props) => {
|
|||||||
</CanvasEntityHeader>
|
</CanvasEntityHeader>
|
||||||
<RegionalGuidanceSettings />
|
<RegionalGuidanceSettings />
|
||||||
</CanvasEntityContainer>
|
</CanvasEntityContainer>
|
||||||
</RegionalGuidanceAdapterGate>
|
</EntityMaskAdapterGate>
|
||||||
</EntityIdentifierContext.Provider>
|
</EntityIdentifierContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { MenuItem } from '@invoke-ai/ui-library';
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
|
||||||
import {
|
import {
|
||||||
rgIPAdapterAdded,
|
rgIPAdapterAdded,
|
||||||
rgNegativePromptChanged,
|
rgNegativePromptChanged,
|
||||||
@@ -16,7 +15,6 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
|
|||||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isBusy = useCanvasIsBusy();
|
|
||||||
const selectValidActions = useMemo(
|
const selectValidActions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||||
@@ -41,15 +39,13 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={addPositivePrompt} isDisabled={!validActions.canAddPositivePrompt || isBusy}>
|
<MenuItem onClick={addPositivePrompt} isDisabled={!validActions.canAddPositivePrompt}>
|
||||||
{t('controlLayers.addPositivePrompt')}
|
{t('controlLayers.addPositivePrompt')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={addNegativePrompt} isDisabled={!validActions.canAddNegativePrompt || isBusy}>
|
<MenuItem onClick={addNegativePrompt} isDisabled={!validActions.canAddNegativePrompt}>
|
||||||
{t('controlLayers.addNegativePrompt')}
|
{t('controlLayers.addNegativePrompt')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={addIPAdapter} isDisabled={isBusy}>
|
<MenuItem onClick={addIPAdapter}>{t('controlLayers.addIPAdapter')}</MenuItem>
|
||||||
{t('controlLayers.addIPAdapter')}
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const log = logger('canvas');
|
|||||||
|
|
||||||
const [useIsSaving] = buildUseBoolean(false);
|
const [useIsSaving] = buildUseBoolean(false);
|
||||||
|
|
||||||
export const CanvasToolbarSaveToGalleryButton = memo(() => {
|
export const SaveToGalleryButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const shift = useShiftModifier();
|
const shift = useShiftModifier();
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
@@ -50,4 +50,4 @@ export const CanvasToolbarSaveToGalleryButton = memo(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CanvasToolbarSaveToGalleryButton.displayName = 'CanvasToolbarSaveToGalleryButton';
|
SaveToGalleryButton.displayName = 'SaveToGalleryButton';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectCanvasSettingsSlice, settingsClipToBboxChanged } from 'features/controlLayers/store/canvasSettingsSlice';
|
import { clipToBboxChanged, selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -13,7 +13,7 @@ export const CanvasSettingsClipToBboxCheckbox = memo(() => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const clipToBbox = useAppSelector(selectClipToBbox);
|
const clipToBbox = useAppSelector(selectClipToBbox);
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(settingsClipToBboxChanged(e.target.checked)),
|
(e: ChangeEvent<HTMLInputElement>) => dispatch(clipToBboxChanged(e.target.checked)),
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import {
|
|
||||||
selectCanvasSettingsSlice,
|
|
||||||
settingsCompositeMaskedRegionsChanged,
|
|
||||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
|
||||||
import type { ChangeEvent } from 'react';
|
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const selectCompositeMaskedRegions = createSelector(
|
|
||||||
selectCanvasSettingsSlice,
|
|
||||||
(canvasSettings) => canvasSettings.compositeMaskedRegions
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CanvasSettingsCompositeMaskedRegionsCheckbox = memo(() => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const compositeMaskedRegions = useAppSelector(selectCompositeMaskedRegions);
|
|
||||||
const onChange = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(settingsCompositeMaskedRegionsChanged(e.target.checked)),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<FormControl w="full">
|
|
||||||
<FormLabel flexGrow={1}>{t('controlLayers.compositeMaskedRegions')}</FormLabel>
|
|
||||||
<Checkbox isChecked={compositeMaskedRegions} onChange={onChange} />
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CanvasSettingsCompositeMaskedRegionsCheckbox.displayName = 'CanvasSettingsCompositeMaskedRegionsCheckbox';
|
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
|
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectDynamicGrid, settingsDynamicGridToggled } from 'features/controlLayers/store/canvasSettingsSlice';
|
import {
|
||||||
|
selectCanvasSettingsSlice,
|
||||||
|
settingsDynamicGridToggled,
|
||||||
|
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
|
||||||
|
|
||||||
export const CanvasSettingsDynamicGridSwitch = memo(() => {
|
export const CanvasSettingsDynamicGridSwitch = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|||||||
@@ -1,33 +1,25 @@
|
|||||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import { invertScrollChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
|
||||||
selectCanvasSettingsSlice,
|
|
||||||
settingsInvertScrollForToolWidthChanged,
|
|
||||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const selectInvertScrollForToolWidth = createSelector(
|
const selectInvertScroll = createSelector(selectToolSlice, (tool) => tool.invertScroll);
|
||||||
selectCanvasSettingsSlice,
|
|
||||||
(settings) => settings.invertScrollForToolWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CanvasSettingsInvertScrollCheckbox = memo(() => {
|
export const CanvasSettingsInvertScrollCheckbox = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const invertScrollForToolWidth = useAppSelector(selectInvertScrollForToolWidth);
|
const invertScroll = useAppSelector(selectInvertScroll);
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
(e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)),
|
||||||
dispatch(settingsInvertScrollForToolWidthChanged(e.target.checked));
|
|
||||||
},
|
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<FormControl w="full">
|
<FormControl w="full">
|
||||||
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
|
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
|
||||||
<Checkbox isChecked={invertScrollForToolWidth} onChange={onChange} />
|
<Checkbox isChecked={invertScroll} onChange={onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { CanvasSettingsAutoSaveCheckbox } from 'features/controlLayers/component
|
|||||||
import { CanvasSettingsClearCachesButton } from 'features/controlLayers/components/Settings/CanvasSettingsClearCachesButton';
|
import { CanvasSettingsClearCachesButton } from 'features/controlLayers/components/Settings/CanvasSettingsClearCachesButton';
|
||||||
import { CanvasSettingsClearHistoryButton } from 'features/controlLayers/components/Settings/CanvasSettingsClearHistoryButton';
|
import { CanvasSettingsClearHistoryButton } from 'features/controlLayers/components/Settings/CanvasSettingsClearHistoryButton';
|
||||||
import { CanvasSettingsClipToBboxCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox';
|
import { CanvasSettingsClipToBboxCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox';
|
||||||
import { CanvasSettingsCompositeMaskedRegionsCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsCompositeMaskedRegionsCheckbox';
|
|
||||||
import { CanvasSettingsDynamicGridSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch';
|
import { CanvasSettingsDynamicGridSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch';
|
||||||
import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox';
|
import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox';
|
||||||
import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo';
|
import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo';
|
||||||
@@ -38,7 +37,6 @@ export const CanvasSettingsPopover = memo(() => {
|
|||||||
<CanvasSettingsAutoSaveCheckbox />
|
<CanvasSettingsAutoSaveCheckbox />
|
||||||
<CanvasSettingsInvertScrollCheckbox />
|
<CanvasSettingsInvertScrollCheckbox />
|
||||||
<CanvasSettingsClipToBboxCheckbox />
|
<CanvasSettingsClipToBboxCheckbox />
|
||||||
<CanvasSettingsCompositeMaskedRegionsCheckbox />
|
|
||||||
<CanvasSettingsDynamicGridSwitch />
|
<CanvasSettingsDynamicGridSwitch />
|
||||||
<CanvasSettingsShowHUDSwitch />
|
<CanvasSettingsShowHUDSwitch />
|
||||||
<CanvasSettingsResetButton />
|
<CanvasSettingsResetButton />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const CanvasSettingsRecalculateRectsButton = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
for (const adapter of canvasManager.getAllAdapters()) {
|
for (const adapter of canvasManager.adapters.getAll()) {
|
||||||
adapter.transformer.requestRectCalculation();
|
adapter.transformer.requestRectCalculation();
|
||||||
}
|
}
|
||||||
}, [canvasManager]);
|
}, [canvasManager]);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { $socket } from 'app/hooks/useSocketIO';
|
import { $socket } from 'app/hooks/useSocketIO';
|
||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { useAppStore } from 'app/store/nanostores/store';
|
import { useAppStore } from 'app/store/nanostores/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
||||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
|
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||||
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
|
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||||
@@ -46,6 +47,9 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null)
|
|||||||
}, [dpr]);
|
}, [dpr]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
|
||||||
|
const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD);
|
||||||
|
|
||||||
export const StageComponent = memo(() => {
|
export const StageComponent = memo(() => {
|
||||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||||
const showHUD = useAppSelector(selectShowHUD);
|
const showHUD = useAppSelector(selectShowHUD);
|
||||||
@@ -78,7 +82,7 @@ export const StageComponent = memo(() => {
|
|||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
bgImage={TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL}
|
bgImage={TRANSPARENCY_CHECKER_PATTERN}
|
||||||
top={0}
|
top={0}
|
||||||
right={0}
|
right={0}
|
||||||
bottom={0}
|
bottom={0}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const StagingAreaToolbar = memo(() => {
|
|||||||
const index = useAppSelector(selectStagedImageIndex);
|
const index = useAppSelector(selectStagedImageIndex);
|
||||||
const selectedImage = useAppSelector(selectSelectedImage);
|
const selectedImage = useAppSelector(selectSelectedImage);
|
||||||
const imageCount = useAppSelector(selectImageCount);
|
const imageCount = useAppSelector(selectImageCount);
|
||||||
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
|
const shouldShowStagedImage = useStore(canvasManager.stateApi.$shouldShowStagedImage);
|
||||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||||
const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation();
|
const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation();
|
||||||
useScopeOnMount('stagingArea');
|
useScopeOnMount('stagingArea');
|
||||||
@@ -83,8 +83,8 @@ export const StagingAreaToolbar = memo(() => {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const onToggleShouldShowStagedImage = useCallback(() => {
|
const onToggleShouldShowStagedImage = useCallback(() => {
|
||||||
canvasManager.stagingArea.$shouldShowStagedImage.set(!shouldShowStagedImage);
|
canvasManager.stateApi.$shouldShowStagedImage.set(!shouldShowStagedImage);
|
||||||
}, [canvasManager.stagingArea.$shouldShowStagedImage, shouldShowStagedImage]);
|
}, [canvasManager.stateApi.$shouldShowStagedImage, shouldShowStagedImage]);
|
||||||
|
|
||||||
const onSaveStagingImage = useCallback(() => {
|
const onSaveStagingImage = useCallback(() => {
|
||||||
if (!selectedImage) {
|
if (!selectedImage) {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||||
import { memo } from 'react';
|
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
|
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiBoundingBoxBold } from 'react-icons/pi';
|
import { PiBoundingBoxBold } from 'react-icons/pi';
|
||||||
@@ -9,18 +13,24 @@ export const ToolBboxButton = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const selectBbox = useSelectTool('bbox');
|
const selectBbox = useSelectTool('bbox');
|
||||||
const isSelected = useToolIsSelected('bbox');
|
const isSelected = useToolIsSelected('bbox');
|
||||||
|
const isFiltering = useIsFiltering();
|
||||||
|
const isTransforming = useIsTransforming();
|
||||||
|
const isStaging = useAppSelector(selectIsStaging);
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isFiltering || isStaging;
|
||||||
|
}, [isFiltering, isStaging, isTransforming]);
|
||||||
|
|
||||||
useHotkeys('c', selectBbox, { enabled: !isSelected }, [selectBbox, isSelected]);
|
useHotkeys('q', selectBbox, { enabled: !isDisabled || isSelected }, [selectBbox, isSelected, isDisabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('controlLayers.tool.bbox')} (C)`}
|
aria-label={`${t('controlLayers.tool.bbox')} (Q)`}
|
||||||
tooltip={`${t('controlLayers.tool.bbox')} (C)`}
|
tooltip={`${t('controlLayers.tool.bbox')} (Q)`}
|
||||||
icon={<PiBoundingBoxBold />}
|
icon={<PiBoundingBoxBold />}
|
||||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||||
variant="solid"
|
variant="outline"
|
||||||
onClick={selectBbox}
|
onClick={selectBbox}
|
||||||
isDisabled={isSelected}
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||||
import { memo } from 'react';
|
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
|
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||||
|
import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPaintBrushBold } from 'react-icons/pi';
|
import { PiPaintBrushBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ToolBrushButton = memo(() => {
|
export const ToolBrushButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isSelected = useToolIsSelected('brush');
|
const isFiltering = useIsFiltering();
|
||||||
|
const isTransforming = useIsTransforming();
|
||||||
|
const isStaging = useAppSelector(selectIsStaging);
|
||||||
const selectBrush = useSelectTool('brush');
|
const selectBrush = useSelectTool('brush');
|
||||||
|
const isSelected = useToolIsSelected('brush');
|
||||||
|
const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable);
|
||||||
|
|
||||||
useHotkeys('b', selectBrush, { enabled: !isSelected }, [isSelected, selectBrush]);
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed;
|
||||||
|
}, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]);
|
||||||
|
|
||||||
|
useHotkeys('b', selectBrush, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectBrush]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -18,9 +31,9 @@ export const ToolBrushButton = memo(() => {
|
|||||||
tooltip={`${t('controlLayers.tool.brush')} (B)`}
|
tooltip={`${t('controlLayers.tool.brush')} (B)`}
|
||||||
icon={<PiPaintBrushBold />}
|
icon={<PiPaintBrushBold />}
|
||||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||||
variant="solid"
|
variant="outline"
|
||||||
onClick={selectBrush}
|
onClick={selectBrush}
|
||||||
isDisabled={isSelected}
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
|
CompositeNumberInput,
|
||||||
CompositeSlider,
|
CompositeSlider,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
IconButton,
|
|
||||||
NumberInput,
|
|
||||||
NumberInputField,
|
|
||||||
Popover,
|
Popover,
|
||||||
PopoverAnchor,
|
|
||||||
PopoverArrow,
|
PopoverArrow,
|
||||||
PopoverBody,
|
PopoverBody,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -14,172 +11,47 @@ import {
|
|||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
import { brushWidthChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
|
||||||
import { selectCanvasSettingsSlice, settingsBrushWidthChanged } from 'features/controlLayers/store/canvasSettingsSlice';
|
import { memo, useCallback } from 'react';
|
||||||
import { clamp } from 'lodash-es';
|
|
||||||
import type { KeyboardEvent } from 'react';
|
|
||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCaretDownBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
const selectBrushWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.brushWidth);
|
const marks = [0, 100, 200, 300];
|
||||||
const formatPx = (v: number | string) => `${v} px`;
|
const formatPx = (v: number | string) => `${v} px`;
|
||||||
|
const selectBrushWidth = createSelector(selectToolSlice, (tool) => tool.brush.width);
|
||||||
function mapSliderValueToRawValue(value: number) {
|
|
||||||
if (value <= 40) {
|
|
||||||
// 0 to 40 on the slider -> 1px to 50px
|
|
||||||
return 1 + (49 * value) / 40;
|
|
||||||
} else if (value <= 70) {
|
|
||||||
// 40 to 70 on the slider -> 50px to 200px
|
|
||||||
return 50 + (150 * (value - 40)) / 30;
|
|
||||||
} else {
|
|
||||||
// 70 to 100 on the slider -> 200px to 600px
|
|
||||||
return 200 + (400 * (value - 70)) / 30;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapRawValueToSliderValue(value: number) {
|
|
||||||
if (value <= 50) {
|
|
||||||
// 1px to 50px -> 0 to 40 on the slider
|
|
||||||
return ((value - 1) * 40) / 49;
|
|
||||||
} else if (value <= 200) {
|
|
||||||
// 50px to 200px -> 40 to 70 on the slider
|
|
||||||
return 40 + ((value - 50) * 30) / 150;
|
|
||||||
} else {
|
|
||||||
// 200px to 600px -> 70 to 100 on the slider
|
|
||||||
return 70 + ((value - 200) * 30) / 400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSliderValue(value: number) {
|
|
||||||
return `${String(mapSliderValueToRawValue(value))} px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const marks = [
|
|
||||||
mapRawValueToSliderValue(1),
|
|
||||||
mapRawValueToSliderValue(50),
|
|
||||||
mapRawValueToSliderValue(200),
|
|
||||||
mapRawValueToSliderValue(600),
|
|
||||||
];
|
|
||||||
|
|
||||||
const sliderDefaultValue = mapRawValueToSliderValue(50);
|
|
||||||
|
|
||||||
export const ToolBrushWidth = memo(() => {
|
export const ToolBrushWidth = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isSelected = useToolIsSelected('brush');
|
|
||||||
const width = useAppSelector(selectBrushWidth);
|
const width = useAppSelector(selectBrushWidth);
|
||||||
const [localValue, setLocalValue] = useState(width);
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(settingsBrushWidthChanged(clamp(Math.round(v), 1, 600)));
|
dispatch(brushWidthChanged(Math.round(v)));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const increment = useCallback(() => {
|
|
||||||
let newWidth = Math.round(width * 1.15);
|
|
||||||
if (newWidth === width) {
|
|
||||||
newWidth += 1;
|
|
||||||
}
|
|
||||||
onChange(newWidth);
|
|
||||||
}, [onChange, width]);
|
|
||||||
|
|
||||||
const decrement = useCallback(() => {
|
|
||||||
let newWidth = Math.round(width * 0.85);
|
|
||||||
if (newWidth === width) {
|
|
||||||
newWidth -= 1;
|
|
||||||
}
|
|
||||||
onChange(newWidth);
|
|
||||||
}, [onChange, width]);
|
|
||||||
|
|
||||||
const onChangeSlider = useCallback(
|
|
||||||
(value: number) => {
|
|
||||||
onChange(mapSliderValueToRawValue(value));
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onBlur = useCallback(() => {
|
|
||||||
if (isNaN(Number(localValue))) {
|
|
||||||
onChange(50);
|
|
||||||
setLocalValue(50);
|
|
||||||
} else {
|
|
||||||
onChange(localValue);
|
|
||||||
}
|
|
||||||
}, [localValue, onChange]);
|
|
||||||
|
|
||||||
const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => {
|
|
||||||
setLocalValue(valueAsNumber);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
|
||||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
onBlur();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onBlur]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLocalValue(width);
|
|
||||||
}, [width]);
|
|
||||||
|
|
||||||
useHotkeys('[', decrement, { enabled: isSelected }, [decrement, isSelected]);
|
|
||||||
useHotkeys(']', increment, { enabled: isSelected }, [increment, isSelected]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<FormControl w="min-content" gap={2}>
|
||||||
<FormControl w="min-content" gap={2}>
|
<FormLabel m={0}>{t('controlLayers.width')}</FormLabel>
|
||||||
<FormLabel m={0}>{t('controlLayers.width')}</FormLabel>
|
<Popover isLazy>
|
||||||
<PopoverAnchor>
|
<PopoverTrigger>
|
||||||
<NumberInput
|
<CompositeNumberInput
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
min={1}
|
min={1}
|
||||||
max={600}
|
max={600}
|
||||||
value={localValue}
|
|
||||||
onChange={onChangeNumberInput}
|
|
||||||
onBlur={onBlur}
|
|
||||||
w="76px"
|
|
||||||
format={formatPx}
|
|
||||||
defaultValue={50}
|
defaultValue={50}
|
||||||
onKeyDown={onKeyDown}
|
value={width}
|
||||||
clampValueOnBlur={false}
|
onChange={onChange}
|
||||||
>
|
w={24}
|
||||||
<NumberInputField paddingInlineEnd={7} />
|
format={formatPx}
|
||||||
<PopoverTrigger>
|
|
||||||
<IconButton
|
|
||||||
aria-label="open-slider"
|
|
||||||
icon={<PiCaretDownBold />}
|
|
||||||
size="sm"
|
|
||||||
variant="link"
|
|
||||||
position="absolute"
|
|
||||||
insetInlineEnd={0}
|
|
||||||
h="full"
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
</NumberInput>
|
|
||||||
</PopoverAnchor>
|
|
||||||
</FormControl>
|
|
||||||
<PopoverContent w={200} pt={0} pb={2} px={4}>
|
|
||||||
<PopoverArrow />
|
|
||||||
<PopoverBody>
|
|
||||||
<CompositeSlider
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
value={mapRawValueToSliderValue(localValue)}
|
|
||||||
onChange={onChangeSlider}
|
|
||||||
defaultValue={sliderDefaultValue}
|
|
||||||
marks={marks}
|
|
||||||
formatValue={formatSliderValue}
|
|
||||||
alwaysShowMarks
|
|
||||||
/>
|
/>
|
||||||
</PopoverBody>
|
</PopoverTrigger>
|
||||||
</PopoverContent>
|
<PopoverContent w={200} py={2} px={4}>
|
||||||
</Popover>
|
<PopoverArrow />
|
||||||
|
<PopoverBody>
|
||||||
|
<CompositeSlider min={1} max={300} defaultValue={50} value={width} onChange={onChange} marks={marks} />
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</FormControl>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import { ToolBrushButton } from 'features/controlLayers/components/Tool/ToolBrus
|
|||||||
import { ToolColorPickerButton } from 'features/controlLayers/components/Tool/ToolColorPickerButton';
|
import { ToolColorPickerButton } from 'features/controlLayers/components/Tool/ToolColorPickerButton';
|
||||||
import { ToolMoveButton } from 'features/controlLayers/components/Tool/ToolMoveButton';
|
import { ToolMoveButton } from 'features/controlLayers/components/Tool/ToolMoveButton';
|
||||||
import { ToolRectButton } from 'features/controlLayers/components/Tool/ToolRectButton';
|
import { ToolRectButton } from 'features/controlLayers/components/Tool/ToolRectButton';
|
||||||
|
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
|
||||||
|
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
|
||||||
|
|
||||||
import { ToolEraserButton } from './ToolEraserButton';
|
import { ToolEraserButton } from './ToolEraserButton';
|
||||||
import { ToolViewButton } from './ToolViewButton';
|
import { ToolViewButton } from './ToolViewButton';
|
||||||
|
|
||||||
export const ToolChooser: React.FC = () => {
|
export const ToolChooser: React.FC = () => {
|
||||||
|
useCanvasResetLayerHotkey();
|
||||||
|
useCanvasDeleteLayerHotkey();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonGroup isAttached>
|
<ButtonGroup isAttached>
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||||
import { memo } from 'react';
|
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
|
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiEyedropperBold } from 'react-icons/pi';
|
import { PiEyedropperBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ToolColorPickerButton = memo(() => {
|
export const ToolColorPickerButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isSelected = useToolIsSelected('colorPicker');
|
const isFiltering = useIsFiltering();
|
||||||
|
const isTransforming = useIsTransforming();
|
||||||
const selectColorPicker = useSelectTool('colorPicker');
|
const selectColorPicker = useSelectTool('colorPicker');
|
||||||
|
const isSelected = useToolIsSelected('colorPicker');
|
||||||
|
const isStaging = useAppSelector(selectIsStaging);
|
||||||
|
|
||||||
useHotkeys('i', selectColorPicker, { enabled: !isSelected }, [selectColorPicker, isSelected]);
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isFiltering || isStaging;
|
||||||
|
}, [isFiltering, isStaging, isTransforming]);
|
||||||
|
|
||||||
|
useHotkeys('i', selectColorPicker, { enabled: !isDisabled || isSelected }, [
|
||||||
|
selectColorPicker,
|
||||||
|
isSelected,
|
||||||
|
isDisabled,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -18,9 +33,9 @@ export const ToolColorPickerButton = memo(() => {
|
|||||||
tooltip={`${t('controlLayers.tool.colorPicker')} (I)`}
|
tooltip={`${t('controlLayers.tool.colorPicker')} (I)`}
|
||||||
icon={<PiEyedropperBold />}
|
icon={<PiEyedropperBold />}
|
||||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||||
variant="solid"
|
variant="outline"
|
||||||
onClick={selectColorPicker}
|
onClick={selectColorPicker}
|
||||||
isDisabled={isSelected}
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||||
import { memo } from 'react';
|
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
|
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||||
|
import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiEraserBold } from 'react-icons/pi';
|
import { PiEraserBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ToolEraserButton = memo(() => {
|
export const ToolEraserButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isSelected = useToolIsSelected('eraser');
|
const isFiltering = useIsFiltering();
|
||||||
|
const isTransforming = useIsTransforming();
|
||||||
|
const isStaging = useAppSelector(selectIsStaging);
|
||||||
const selectEraser = useSelectTool('eraser');
|
const selectEraser = useSelectTool('eraser');
|
||||||
|
const isSelected = useToolIsSelected('eraser');
|
||||||
|
const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable);
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed;
|
||||||
|
}, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]);
|
||||||
|
|
||||||
useHotkeys('e', selectEraser, { enabled: !isSelected }, [isSelected, selectEraser]);
|
useHotkeys('e', selectEraser, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectEraser]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -18,9 +30,9 @@ export const ToolEraserButton = memo(() => {
|
|||||||
tooltip={`${t('controlLayers.tool.eraser')} (E)`}
|
tooltip={`${t('controlLayers.tool.eraser')} (E)`}
|
||||||
icon={<PiEraserBold />}
|
icon={<PiEraserBold />}
|
||||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||||
variant="solid"
|
variant="outline"
|
||||||
onClick={selectEraser}
|
onClick={selectEraser}
|
||||||
isDisabled={isSelected}
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
|
CompositeNumberInput,
|
||||||
CompositeSlider,
|
CompositeSlider,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
IconButton,
|
|
||||||
NumberInput,
|
|
||||||
NumberInputField,
|
|
||||||
Popover,
|
Popover,
|
||||||
PopoverAnchor,
|
|
||||||
PopoverArrow,
|
PopoverArrow,
|
||||||
PopoverBody,
|
PopoverBody,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -14,175 +11,47 @@ import {
|
|||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
import { eraserWidthChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
|
||||||
import {
|
import { memo, useCallback } from 'react';
|
||||||
selectCanvasSettingsSlice,
|
|
||||||
settingsEraserWidthChanged,
|
|
||||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
|
||||||
import { clamp } from 'lodash-es';
|
|
||||||
import type { KeyboardEvent } from 'react';
|
|
||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCaretDownBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
const selectEraserWidth = createSelector(selectCanvasSettingsSlice, (settings) => settings.eraserWidth);
|
const marks = [0, 100, 200, 300];
|
||||||
const formatPx = (v: number | string) => `${v} px`;
|
const formatPx = (v: number | string) => `${v} px`;
|
||||||
|
const selectEraserWidth = createSelector(selectToolSlice, (tool) => tool.eraser.width);
|
||||||
function mapSliderValueToRawValue(value: number) {
|
|
||||||
if (value <= 40) {
|
|
||||||
// 0 to 40 on the slider -> 1px to 50px
|
|
||||||
return 1 + (49 * value) / 40;
|
|
||||||
} else if (value <= 70) {
|
|
||||||
// 40 to 70 on the slider -> 50px to 200px
|
|
||||||
return 50 + (150 * (value - 40)) / 30;
|
|
||||||
} else {
|
|
||||||
// 70 to 100 on the slider -> 200px to 600px
|
|
||||||
return 200 + (400 * (value - 70)) / 30;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapRawValueToSliderValue(value: number) {
|
|
||||||
if (value <= 50) {
|
|
||||||
// 1px to 50px -> 0 to 40 on the slider
|
|
||||||
return ((value - 1) * 40) / 49;
|
|
||||||
} else if (value <= 200) {
|
|
||||||
// 50px to 200px -> 40 to 70 on the slider
|
|
||||||
return 40 + ((value - 50) * 30) / 150;
|
|
||||||
} else {
|
|
||||||
// 200px to 600px -> 70 to 100 on the slider
|
|
||||||
return 70 + ((value - 200) * 30) / 400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSliderValue(value: number) {
|
|
||||||
return `${String(mapSliderValueToRawValue(value))} px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const marks = [
|
|
||||||
mapRawValueToSliderValue(1),
|
|
||||||
mapRawValueToSliderValue(50),
|
|
||||||
mapRawValueToSliderValue(200),
|
|
||||||
mapRawValueToSliderValue(600),
|
|
||||||
];
|
|
||||||
|
|
||||||
const sliderDefaultValue = mapRawValueToSliderValue(50);
|
|
||||||
|
|
||||||
export const ToolEraserWidth = memo(() => {
|
export const ToolEraserWidth = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isSelected = useToolIsSelected('eraser');
|
|
||||||
const width = useAppSelector(selectEraserWidth);
|
const width = useAppSelector(selectEraserWidth);
|
||||||
const [localValue, setLocalValue] = useState(width);
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(settingsEraserWidthChanged(clamp(Math.round(v), 1, 600)));
|
dispatch(eraserWidthChanged(Math.round(v)));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const increment = useCallback(() => {
|
|
||||||
let newWidth = Math.round(width * 1.15);
|
|
||||||
if (newWidth === width) {
|
|
||||||
newWidth += 1;
|
|
||||||
}
|
|
||||||
onChange(newWidth);
|
|
||||||
}, [onChange, width]);
|
|
||||||
|
|
||||||
const decrement = useCallback(() => {
|
|
||||||
let newWidth = Math.round(width * 0.85);
|
|
||||||
if (newWidth === width) {
|
|
||||||
newWidth -= 1;
|
|
||||||
}
|
|
||||||
onChange(newWidth);
|
|
||||||
}, [onChange, width]);
|
|
||||||
|
|
||||||
const onChangeSlider = useCallback(
|
|
||||||
(value: number) => {
|
|
||||||
onChange(mapSliderValueToRawValue(value));
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onBlur = useCallback(() => {
|
|
||||||
if (isNaN(Number(localValue))) {
|
|
||||||
onChange(50);
|
|
||||||
setLocalValue(50);
|
|
||||||
} else {
|
|
||||||
onChange(localValue);
|
|
||||||
}
|
|
||||||
}, [localValue, onChange]);
|
|
||||||
|
|
||||||
const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => {
|
|
||||||
setLocalValue(valueAsNumber);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
|
||||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
onBlur();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onBlur]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLocalValue(width);
|
|
||||||
}, [width]);
|
|
||||||
|
|
||||||
useHotkeys('[', decrement, { enabled: isSelected }, [decrement, isSelected]);
|
|
||||||
useHotkeys(']', increment, { enabled: isSelected }, [increment, isSelected]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<FormControl w="min-content" gap={2}>
|
||||||
<FormControl w="min-content" gap={2}>
|
<FormLabel m={0}>{t('controlLayers.width')}</FormLabel>
|
||||||
<FormLabel m={0}>{t('controlLayers.width')}</FormLabel>
|
<Popover isLazy>
|
||||||
<PopoverAnchor>
|
<PopoverTrigger>
|
||||||
<NumberInput
|
<CompositeNumberInput
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
min={1}
|
min={1}
|
||||||
max={600}
|
max={600}
|
||||||
value={localValue}
|
|
||||||
onChange={onChangeNumberInput}
|
|
||||||
onBlur={onBlur}
|
|
||||||
w="76px"
|
|
||||||
format={formatPx}
|
|
||||||
defaultValue={50}
|
defaultValue={50}
|
||||||
onKeyDown={onKeyDown}
|
value={width}
|
||||||
clampValueOnBlur={false}
|
onChange={onChange}
|
||||||
>
|
w={24}
|
||||||
<NumberInputField paddingInlineEnd={7} />
|
format={formatPx}
|
||||||
<PopoverTrigger>
|
|
||||||
<IconButton
|
|
||||||
aria-label="open-slider"
|
|
||||||
icon={<PiCaretDownBold />}
|
|
||||||
size="sm"
|
|
||||||
variant="link"
|
|
||||||
position="absolute"
|
|
||||||
insetInlineEnd={0}
|
|
||||||
h="full"
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
</NumberInput>
|
|
||||||
</PopoverAnchor>
|
|
||||||
</FormControl>
|
|
||||||
<PopoverContent w={200} pt={0} pb={2} px={4}>
|
|
||||||
<PopoverArrow />
|
|
||||||
<PopoverBody>
|
|
||||||
<CompositeSlider
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
value={mapRawValueToSliderValue(localValue)}
|
|
||||||
onChange={onChangeSlider}
|
|
||||||
defaultValue={sliderDefaultValue}
|
|
||||||
marks={marks}
|
|
||||||
formatValue={formatSliderValue}
|
|
||||||
alwaysShowMarks
|
|
||||||
/>
|
/>
|
||||||
</PopoverBody>
|
</PopoverTrigger>
|
||||||
</PopoverContent>
|
<PopoverContent w={200} py={2} px={4}>
|
||||||
</Popover>
|
<PopoverArrow />
|
||||||
|
<PopoverBody>
|
||||||
|
<CompositeSlider min={1} max={300} defaultValue={50} value={width} onChange={onChange} marks={marks} />
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</FormControl>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIColorPicker from 'common/components/IAIColorPicker';
|
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { selectCanvasSettingsSlice, settingsColorChanged } from 'features/controlLayers/store/canvasSettingsSlice';
|
import { fillChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
|
||||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const selectColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.color);
|
const selectFill = createSelector(selectToolSlice, (tool) => tool.fill);
|
||||||
|
|
||||||
export const ToolColorPicker = memo(() => {
|
export const ToolFillColorPicker = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fill = useAppSelector(selectColor);
|
const fill = useAppSelector(selectFill);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(color: RgbaColor) => {
|
(color: RgbaColor) => {
|
||||||
dispatch(settingsColorChanged(color));
|
dispatch(fillChanged(color));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@@ -40,4 +40,4 @@ export const ToolColorPicker = memo(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ToolColorPicker.displayName = 'ToolFillColorPicker';
|
ToolFillColorPicker.displayName = 'ToolFillColorPicker';
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||||
import { memo } from 'react';
|
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
|
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||||
|
import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCursorBold } from 'react-icons/pi';
|
import { PiCursorBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ToolMoveButton = memo(() => {
|
export const ToolMoveButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isSelected = useToolIsSelected('move');
|
const isFiltering = useIsFiltering();
|
||||||
|
const isTransforming = useIsTransforming();
|
||||||
const selectMove = useSelectTool('move');
|
const selectMove = useSelectTool('move');
|
||||||
|
const isSelected = useToolIsSelected('move');
|
||||||
|
const isStaging = useAppSelector(selectIsStaging);
|
||||||
|
const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable);
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed;
|
||||||
|
}, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]);
|
||||||
|
|
||||||
useHotkeys('v', selectMove, { enabled: !isSelected }, [isSelected, selectMove]);
|
useHotkeys('v', selectMove, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectMove]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -18,9 +30,9 @@ export const ToolMoveButton = memo(() => {
|
|||||||
tooltip={`${t('controlLayers.tool.move')} (V)`}
|
tooltip={`${t('controlLayers.tool.move')} (V)`}
|
||||||
icon={<PiCursorBold />}
|
icon={<PiCursorBold />}
|
||||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||||
variant="solid"
|
variant="outline"
|
||||||
onClick={selectMove}
|
onClick={selectMove}
|
||||||
isDisabled={isSelected}
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||||
import { memo } from 'react';
|
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
|
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||||
|
import { selectIsSelectedEntityDrawable } from 'features/controlLayers/store/selectors';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiRectangleBold } from 'react-icons/pi';
|
import { PiRectangleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ToolRectButton = memo(() => {
|
export const ToolRectButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isSelected = useToolIsSelected('rect');
|
|
||||||
const selectRect = useSelectTool('rect');
|
const selectRect = useSelectTool('rect');
|
||||||
|
const isSelected = useToolIsSelected('rect');
|
||||||
|
const isFiltering = useIsFiltering();
|
||||||
|
const isTransforming = useIsTransforming();
|
||||||
|
const isStaging = useAppSelector(selectIsStaging);
|
||||||
|
const isDrawingToolAllowed = useAppSelector(selectIsSelectedEntityDrawable);
|
||||||
|
|
||||||
useHotkeys('u', selectRect, { enabled: !isSelected }, [isSelected, selectRect]);
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isFiltering || isStaging || !isDrawingToolAllowed;
|
||||||
|
}, [isDrawingToolAllowed, isFiltering, isStaging, isTransforming]);
|
||||||
|
|
||||||
|
useHotkeys('u', selectRect, { enabled: !isDisabled || isSelected }, [isDisabled, isSelected, selectRect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -18,9 +31,9 @@ export const ToolRectButton = memo(() => {
|
|||||||
tooltip={`${t('controlLayers.tool.rectangle')} (U)`}
|
tooltip={`${t('controlLayers.tool.rectangle')} (U)`}
|
||||||
icon={<PiRectangleBold />}
|
icon={<PiRectangleBold />}
|
||||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||||
variant="solid"
|
variant="outline"
|
||||||
onClick={selectRect}
|
onClick={selectRect}
|
||||||
isDisabled={isSelected}
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { memo } from 'react';
|
|||||||
|
|
||||||
export const ToolSettings = memo(() => {
|
export const ToolSettings = memo(() => {
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const tool = useStore(canvasManager.tool.$tool);
|
const tool = useStore(canvasManager.stateApi.$tool);
|
||||||
if (tool === 'brush') {
|
if (tool === 'brush') {
|
||||||
return <ToolBrushWidth />;
|
return <ToolBrushWidth />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||||
import { memo } from 'react';
|
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
|
||||||
|
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||||
|
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiHandBold } from 'react-icons/pi';
|
import { PiHandBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ToolViewButton = memo(() => {
|
export const ToolViewButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isSelected = useToolIsSelected('view');
|
const isTransforming = useIsTransforming();
|
||||||
|
const isFiltering = useIsFiltering();
|
||||||
|
const isStaging = useAppSelector(selectIsStaging);
|
||||||
const selectView = useSelectTool('view');
|
const selectView = useSelectTool('view');
|
||||||
|
const isSelected = useToolIsSelected('view');
|
||||||
|
const isDisabled = useMemo(() => {
|
||||||
|
return isTransforming || isFiltering || isStaging;
|
||||||
|
}, [isFiltering, isStaging, isTransforming]);
|
||||||
|
|
||||||
useHotkeys('h', selectView, { enabled: !isSelected }, [selectView, isSelected]);
|
useHotkeys('h', selectView, { enabled: !isDisabled || isSelected }, [selectView, isSelected, isDisabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -18,9 +28,9 @@ export const ToolViewButton = memo(() => {
|
|||||||
tooltip={`${t('controlLayers.tool.view')} (H)`}
|
tooltip={`${t('controlLayers.tool.view')} (H)`}
|
||||||
icon={<PiHandBold />}
|
icon={<PiHandBold />}
|
||||||
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
colorScheme={isSelected ? 'invokeBlue' : 'base'}
|
||||||
variant="solid"
|
variant="outline"
|
||||||
onClick={selectView}
|
onClick={selectView}
|
||||||
isDisabled={isSelected}
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { useCallback } from 'react';
|
|||||||
|
|
||||||
export const useToolIsSelected = (tool: Tool) => {
|
export const useToolIsSelected = (tool: Tool) => {
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const isSelected = useStore(computed(canvasManager.tool.$tool, (t) => t === tool));
|
const isSelected = useStore(computed(canvasManager.stateApi.$tool, (t) => t === tool));
|
||||||
return isSelected;
|
return isSelected;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSelectTool = (tool: Tool) => {
|
export const useSelectTool = (tool: Tool) => {
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const setTool = useCallback(() => {
|
const setTool = useCallback(() => {
|
||||||
canvasManager.tool.$tool.set(tool);
|
canvasManager.stateApi.$tool.set(tool);
|
||||||
}, [canvasManager.tool.$tool, tool]);
|
}, [canvasManager.stateApi.$tool, tool]);
|
||||||
return setTool;
|
return setTool;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
/* eslint-disable i18next/no-literal-string */
|
|
||||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
|
||||||
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
|
|
||||||
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
|
||||||
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
|
|
||||||
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
|
|
||||||
import { CanvasToolbarResetViewButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton';
|
|
||||||
import { CanvasToolbarSaveToGalleryButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarSaveToGalleryButton';
|
|
||||||
import { CanvasToolbarScale } from 'features/controlLayers/components/Toolbar/CanvasToolbarScale';
|
|
||||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
|
||||||
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
|
|
||||||
import { useCanvasEntityQuickSwitchHotkey } from 'features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey';
|
|
||||||
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
|
|
||||||
import { useCanvasUndoRedoHotkeys } from 'features/controlLayers/hooks/useCanvasUndoRedoHotkeys';
|
|
||||||
import { useNextPrevEntityHotkeys } from 'features/controlLayers/hooks/useNextPrevEntity';
|
|
||||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
|
||||||
import { ViewerToggle } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
export const CanvasToolbar = memo(() => {
|
|
||||||
useCanvasResetLayerHotkey();
|
|
||||||
useCanvasDeleteLayerHotkey();
|
|
||||||
useCanvasUndoRedoHotkeys();
|
|
||||||
useCanvasEntityQuickSwitchHotkey();
|
|
||||||
useNextPrevEntityHotkeys();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CanvasManagerProviderGate>
|
|
||||||
<Flex w="full" gap={2} alignItems="center">
|
|
||||||
<ToggleProgressButton />
|
|
||||||
<ToolChooser />
|
|
||||||
<Spacer />
|
|
||||||
<ToolSettings />
|
|
||||||
<Spacer />
|
|
||||||
<CanvasToolbarScale />
|
|
||||||
<CanvasToolbarResetViewButton />
|
|
||||||
<Spacer />
|
|
||||||
<ToolColorPicker />
|
|
||||||
<CanvasToolbarSaveToGalleryButton />
|
|
||||||
<CanvasSettingsPopover />
|
|
||||||
<ViewerToggle />
|
|
||||||
</Flex>
|
|
||||||
</CanvasManagerProviderGate>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CanvasToolbar.displayName = 'CanvasToolbar';
|
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
|
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntityAdapter/types';
|
import {
|
||||||
|
EntityIdentifierContext,
|
||||||
|
useEntityIdentifierContext,
|
||||||
|
} from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
|
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiArrowsCounterClockwiseBold, PiArrowsOutBold, PiCheckBold, PiXBold } from 'react-icons/pi';
|
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiXBold } from 'react-icons/pi';
|
||||||
|
|
||||||
const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
const TransformBox = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
|
const adapter = useEntityAdapter(entityIdentifier);
|
||||||
const isProcessing = useStore(adapter.transformer.$isProcessing);
|
const isProcessing = useStore(adapter.transformer.$isProcessing);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -24,19 +30,9 @@ const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
|||||||
transitionDuration="normal"
|
transitionDuration="normal"
|
||||||
>
|
>
|
||||||
<Heading size="md" color="base.300" userSelect="none">
|
<Heading size="md" color="base.300" userSelect="none">
|
||||||
{t('controlLayers.transform.transform')}
|
{t('controlLayers.tool.transform')}
|
||||||
</Heading>
|
</Heading>
|
||||||
<ButtonGroup isAttached={false} size="sm" w="full">
|
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||||
<Button
|
|
||||||
leftIcon={<PiArrowsOutBold />}
|
|
||||||
onClick={adapter.transformer.fitProxyRectToBbox}
|
|
||||||
isLoading={isProcessing}
|
|
||||||
loadingText={t('controlLayers.transform.reset')}
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
{t('controlLayers.transform.fitToBbox')}
|
|
||||||
</Button>
|
|
||||||
<Spacer />
|
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<PiArrowsCounterClockwiseBold />}
|
leftIcon={<PiArrowsCounterClockwiseBold />}
|
||||||
onClick={adapter.transformer.resetTransform}
|
onClick={adapter.transformer.resetTransform}
|
||||||
@@ -44,8 +40,9 @@ const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
|||||||
loadingText={t('controlLayers.reset')}
|
loadingText={t('controlLayers.reset')}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
{t('controlLayers.transform.reset')}
|
{t('accessibility.reset')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Spacer />
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<PiCheckBold />}
|
leftIcon={<PiCheckBold />}
|
||||||
onClick={adapter.transformer.applyTransform}
|
onClick={adapter.transformer.applyTransform}
|
||||||
@@ -53,7 +50,7 @@ const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
|||||||
loadingText={t('common.apply')}
|
loadingText={t('common.apply')}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
{t('controlLayers.transform.apply')}
|
{t('common.apply')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<PiXBold />}
|
leftIcon={<PiXBold />}
|
||||||
@@ -62,7 +59,7 @@ const TransformBox = memo(({ adapter }: { adapter: CanvasEntityAdapter }) => {
|
|||||||
loadingText={t('common.cancel')}
|
loadingText={t('common.cancel')}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
{t('controlLayers.transform.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -73,11 +70,15 @@ TransformBox.displayName = 'Transform';
|
|||||||
|
|
||||||
export const Transform = () => {
|
export const Transform = () => {
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const adapter = useStore(canvasManager.stateApi.$transformingAdapter);
|
const transformingEntity = useStore(canvasManager.stateApi.$transformingEntity);
|
||||||
|
|
||||||
if (!adapter) {
|
if (!transformingEntity) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TransformBox adapter={adapter} />;
|
return (
|
||||||
|
<EntityIdentifierContext.Provider value={transformingEntity}>
|
||||||
|
<TransformBox />
|
||||||
|
</EntityIdentifierContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
|
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
|
||||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||||
import { CanvasEntityIsBookmarkedForQuickSwitchToggle } from 'features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle';
|
|
||||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@@ -11,7 +10,6 @@ export const CanvasEntityHeaderCommonActions = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex alignSelf="stretch">
|
<Flex alignSelf="stretch">
|
||||||
<CanvasEntityIsBookmarkedForQuickSwitchToggle />
|
|
||||||
{entityIdentifier.type !== 'ip_adapter' && <CanvasEntityIsLockedToggle />}
|
{entityIdentifier.type !== 'ip_adapter' && <CanvasEntityIsLockedToggle />}
|
||||||
<CanvasEntityEnabledToggle />
|
<CanvasEntityEnabledToggle />
|
||||||
<CanvasEntityDeleteButton />
|
<CanvasEntityDeleteButton />
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
|
||||||
import { useEntityIsBookmarkedForQuickSwitch } from 'features/controlLayers/hooks/useEntityIsBookmarkedForQuickSwitch';
|
|
||||||
import { bookmarkedEntityChanged } from 'features/controlLayers/store/canvasSlice';
|
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiBookmarkSimpleBold, PiBookmarkSimpleFill } from 'react-icons/pi';
|
|
||||||
|
|
||||||
export const CanvasEntityIsBookmarkedForQuickSwitchToggle = memo(() => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
|
||||||
const isBookmarked = useEntityIsBookmarkedForQuickSwitch(entityIdentifier);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const onClick = useCallback(() => {
|
|
||||||
if (isBookmarked) {
|
|
||||||
dispatch(bookmarkedEntityChanged({ entityIdentifier: null }));
|
|
||||||
} else {
|
|
||||||
dispatch(bookmarkedEntityChanged({ entityIdentifier }));
|
|
||||||
}
|
|
||||||
}, [dispatch, entityIdentifier, isBookmarked]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
aria-label={t(isBookmarked ? 'controlLayers.removeBookmark' : 'controlLayers.bookmark')}
|
|
||||||
tooltip={t(isBookmarked ? 'controlLayers.removeBookmark' : 'controlLayers.bookmark')}
|
|
||||||
variant="link"
|
|
||||||
alignSelf="stretch"
|
|
||||||
icon={isBookmarked ? <PiBookmarkSimpleFill /> : <PiBookmarkSimpleBold />}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CanvasEntityIsBookmarkedForQuickSwitchToggle.displayName = 'CanvasEntityIsBookmarkedForQuickSwitchToggle';
|
|
||||||
@@ -2,7 +2,6 @@ import { MenuItem } from '@invoke-ai/ui-library';
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
|
||||||
import {
|
import {
|
||||||
entityArrangedBackwardOne,
|
entityArrangedBackwardOne,
|
||||||
entityArrangedForwardOne,
|
entityArrangedForwardOne,
|
||||||
@@ -56,7 +55,6 @@ export const CanvasEntityMenuItemsArrange = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
const isBusy = useCanvasIsBusy();
|
|
||||||
const selectValidActions = useMemo(
|
const selectValidActions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||||
@@ -88,24 +86,16 @@ export const CanvasEntityMenuItemsArrange = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront || isBusy} icon={<PiArrowLineUpBold />}>
|
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
|
||||||
{t('controlLayers.moveToFront')}
|
{t('controlLayers.moveToFront')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem onClick={moveForwardOne} isDisabled={!validActions.canMoveForwardOne} icon={<PiArrowUpBold />}>
|
||||||
onClick={moveForwardOne}
|
|
||||||
isDisabled={!validActions.canMoveForwardOne || isBusy}
|
|
||||||
icon={<PiArrowUpBold />}
|
|
||||||
>
|
|
||||||
{t('controlLayers.moveForward')}
|
{t('controlLayers.moveForward')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem onClick={moveBackwardOne} isDisabled={!validActions.canMoveBackwardOne} icon={<PiArrowDownBold />}>
|
||||||
onClick={moveBackwardOne}
|
|
||||||
isDisabled={!validActions.canMoveBackwardOne || isBusy}
|
|
||||||
icon={<PiArrowDownBold />}
|
|
||||||
>
|
|
||||||
{t('controlLayers.moveBackward')}
|
{t('controlLayers.moveBackward')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack || isBusy} icon={<PiArrowLineDownBold />}>
|
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
|
||||||
{t('controlLayers.moveToBack')}
|
{t('controlLayers.moveToBack')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
|
||||||
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
|
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -11,14 +10,13 @@ export const CanvasEntityMenuItemsDelete = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
const isBusy = useCanvasIsBusy();
|
|
||||||
|
|
||||||
const deleteEntity = useCallback(() => {
|
const deleteEntity = useCallback(() => {
|
||||||
dispatch(entityDeleted({ entityIdentifier }));
|
dispatch(entityDeleted({ entityIdentifier }));
|
||||||
}, [dispatch, entityIdentifier]);
|
}, [dispatch, entityIdentifier]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} isDestructive isDisabled={isBusy}>
|
<MenuItem onClick={deleteEntity} icon={<PiTrashSimpleBold />} color="error.300">
|
||||||
{t('common.delete')}
|
{t('common.delete')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
|
||||||
import { entityDuplicated } from 'features/controlLayers/store/canvasSlice';
|
import { entityDuplicated } from 'features/controlLayers/store/canvasSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -11,14 +10,13 @@ export const CanvasEntityMenuItemsDuplicate = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
const isBusy = useCanvasIsBusy();
|
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(entityDuplicated({ entityIdentifier }));
|
dispatch(entityDuplicated({ entityIdentifier }));
|
||||||
}, [dispatch, entityIdentifier]);
|
}, [dispatch, entityIdentifier]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={onClick} icon={<PiCopyFill />} isDisabled={isBusy}>
|
<MenuItem onClick={onClick} icon={<PiCopyFill />}>
|
||||||
{t('controlLayers.duplicate')}
|
{t('controlLayers.duplicate')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiShootingStarBold } from 'react-icons/pi';
|
import { PiShootingStarBold } from 'react-icons/pi';
|
||||||
@@ -10,14 +9,13 @@ export const CanvasEntityMenuItemsFilter = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
const isBusy = useCanvasIsBusy();
|
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
canvasManager.filter.startFilter(entityIdentifier);
|
canvasManager.filter.initialize(entityIdentifier);
|
||||||
}, [canvasManager.filter, entityIdentifier]);
|
}, [entityIdentifier, canvasManager.filter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={onClick} icon={<PiShootingStarBold />} isDisabled={isBusy}>
|
<MenuItem onClick={onClick} icon={<PiShootingStarBold />}>
|
||||||
{t('controlLayers.filter.filter')}
|
{t('controlLayers.filter.filter')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
|
||||||
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
|
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -9,16 +10,17 @@ import { PiFrameCornersBold } from 'react-icons/pi';
|
|||||||
export const CanvasEntityMenuItemsTransform = memo(() => {
|
export const CanvasEntityMenuItemsTransform = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
|
const canvasManager = useCanvasManager();
|
||||||
const adapter = useEntityAdapter(entityIdentifier);
|
const adapter = useEntityAdapter(entityIdentifier);
|
||||||
const isBusy = useCanvasIsBusy();
|
const transformingEntity = useStore(canvasManager.stateApi.$transformingEntity);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
adapter.transformer.startTransform();
|
adapter.transformer.startTransform();
|
||||||
}, [adapter.transformer]);
|
}, [adapter.transformer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={onClick} icon={<PiFrameCornersBold />} isDisabled={isBusy}>
|
<MenuItem onClick={onClick} icon={<PiFrameCornersBold />} isDisabled={Boolean(transformingEntity)}>
|
||||||
{t('controlLayers.transform.transform')}
|
{t('controlLayers.tool.transform')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { logger } from 'app/logging/logger';
|
|||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { isOk, withResultAsync } from 'common/util/result';
|
import { isOk, withResultAsync } from 'common/util/result';
|
||||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import { useEntityTypeCount } from 'features/controlLayers/hooks/useEntityTypeCount';
|
|
||||||
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||||
@@ -23,7 +22,6 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const entityCount = useEntityTypeCount(type);
|
|
||||||
const onClick = useCallback(async () => {
|
const onClick = useCallback(async () => {
|
||||||
if (type === 'raster_layer') {
|
if (type === 'raster_layer') {
|
||||||
const rect = canvasManager.stage.getVisibleRect('raster_layer');
|
const rect = canvasManager.stage.getVisibleRect('raster_layer');
|
||||||
@@ -83,7 +81,6 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
|
|||||||
icon={<PiStackBold />}
|
icon={<PiStackBold />}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
alignSelf="stretch"
|
alignSelf="stretch"
|
||||||
isDisabled={entityCount <= 1}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
import { rgbColorToString } from 'common/util/colorCodeTransformers';
|
||||||
import { useEntityAdapter } from 'features/controlLayers/contexts/EntityAdapterContext';
|
import { useEntityAdapter } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern';
|
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
||||||
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
|
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
|
||||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@@ -69,7 +69,7 @@ export const CanvasEntityPreviewImage = memo(() => {
|
|||||||
ctx.globalCompositeOperation = 'source-in';
|
ctx.globalCompositeOperation = 'source-in';
|
||||||
ctx.fillRect(0, 0, rect.width, rect.height);
|
ctx.fillRect(0, 0, rect.width, rect.height);
|
||||||
}
|
}
|
||||||
}, [cache, maskColor]);
|
}, [adapter.transformer, adapter.transformer.nodeRect, adapter.transformer.pixelRect, cache, maskColor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -88,7 +88,7 @@ export const CanvasEntityPreviewImage = memo(() => {
|
|||||||
right={0}
|
right={0}
|
||||||
bottom={0}
|
bottom={0}
|
||||||
left={0}
|
left={0}
|
||||||
bgImage={TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL}
|
bgImage={TRANSPARENCY_CHECKER_PATTERN}
|
||||||
bgSize="5px"
|
bgSize="5px"
|
||||||
opacity={0.1}
|
opacity={0.1}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
|
import type { SyncableMap } from 'common/util/SyncableMap/SyncableMap';
|
||||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterControlLayer';
|
import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter';
|
||||||
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterInpaintMask';
|
import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter';
|
||||||
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterRasterLayer';
|
|
||||||
import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterRegionalGuidance';
|
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import { createContext, memo, useContext, useMemo, useSyncExternalStore } from 'react';
|
import { createContext, memo, useContext, useMemo, useSyncExternalStore } from 'react';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
const EntityAdapterContext = createContext<
|
const EntityAdapterContext = createContext<CanvasEntityLayerAdapter | CanvasEntityMaskAdapter | null>(null);
|
||||||
| CanvasEntityAdapterRasterLayer
|
|
||||||
| CanvasEntityAdapterControlLayer
|
|
||||||
| CanvasEntityAdapterInpaintMask
|
|
||||||
| CanvasEntityAdapterRegionalGuidance
|
|
||||||
| null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
export const RasterLayerAdapterGate = memo(({ children }: PropsWithChildren) => {
|
export const EntityLayerAdapterGate = memo(({ children }: PropsWithChildren) => {
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
const adapters = useSyncExternalStore(
|
const store = useMemo<SyncableMap<string, CanvasEntityLayerAdapter>>(() => {
|
||||||
canvasManager.adapters.rasterLayers.subscribe,
|
if (entityIdentifier.type === 'raster_layer') {
|
||||||
canvasManager.adapters.rasterLayers.getSnapshot
|
return canvasManager.adapters.rasterLayers;
|
||||||
);
|
}
|
||||||
|
if (entityIdentifier.type === 'control_layer') {
|
||||||
|
return canvasManager.adapters.controlLayers;
|
||||||
|
}
|
||||||
|
assert(false, 'Unknown entity type');
|
||||||
|
}, [canvasManager.adapters.controlLayers, canvasManager.adapters.rasterLayers, entityIdentifier.type]);
|
||||||
|
const adapters = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
||||||
const adapter = useMemo(() => {
|
const adapter = useMemo(() => {
|
||||||
return adapters.get(entityIdentifier.id) ?? null;
|
return adapters.get(entityIdentifier.id) ?? null;
|
||||||
}, [adapters, entityIdentifier.id]);
|
}, [adapters, entityIdentifier.id]);
|
||||||
@@ -34,15 +33,28 @@ export const RasterLayerAdapterGate = memo(({ children }: PropsWithChildren) =>
|
|||||||
return <EntityAdapterContext.Provider value={adapter}>{children}</EntityAdapterContext.Provider>;
|
return <EntityAdapterContext.Provider value={adapter}>{children}</EntityAdapterContext.Provider>;
|
||||||
});
|
});
|
||||||
|
|
||||||
RasterLayerAdapterGate.displayName = 'RasterLayerAdapterGate';
|
EntityLayerAdapterGate.displayName = 'EntityLayerAdapterGate';
|
||||||
|
|
||||||
export const ControlLayerAdapterGate = memo(({ children }: PropsWithChildren) => {
|
// export const useEntityLayerAdapter = (): CanvasLayerAdapter => {
|
||||||
|
// const adapter = useContext(EntityAdapterContext);
|
||||||
|
// assert(adapter, 'useEntityLayerAdapter must be used within a EntityLayerAdapterGate');
|
||||||
|
// assert(adapter.type === 'layer_adapter', 'useEntityLayerAdapter must be used with a layer adapter');
|
||||||
|
// return adapter;
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const EntityMaskAdapterGate = memo(({ children }: PropsWithChildren) => {
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
const adapters = useSyncExternalStore(
|
const store = useMemo<SyncableMap<string, CanvasEntityMaskAdapter>>(() => {
|
||||||
canvasManager.adapters.controlLayers.subscribe,
|
if (entityIdentifier.type === 'inpaint_mask') {
|
||||||
canvasManager.adapters.controlLayers.getSnapshot
|
return canvasManager.adapters.inpaintMasks;
|
||||||
);
|
}
|
||||||
|
if (entityIdentifier.type === 'regional_guidance') {
|
||||||
|
return canvasManager.adapters.regionMasks;
|
||||||
|
}
|
||||||
|
assert(false, 'Unknown entity type');
|
||||||
|
}, [canvasManager.adapters.inpaintMasks, canvasManager.adapters.regionMasks, entityIdentifier.type]);
|
||||||
|
const adapters = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
||||||
const adapter = useMemo(() => {
|
const adapter = useMemo(() => {
|
||||||
return adapters.get(entityIdentifier.id) ?? null;
|
return adapters.get(entityIdentifier.id) ?? null;
|
||||||
}, [adapters, entityIdentifier.id]);
|
}, [adapters, entityIdentifier.id]);
|
||||||
@@ -54,53 +66,16 @@ export const ControlLayerAdapterGate = memo(({ children }: PropsWithChildren) =>
|
|||||||
return <EntityAdapterContext.Provider value={adapter}>{children}</EntityAdapterContext.Provider>;
|
return <EntityAdapterContext.Provider value={adapter}>{children}</EntityAdapterContext.Provider>;
|
||||||
});
|
});
|
||||||
|
|
||||||
ControlLayerAdapterGate.displayName = 'ControlLayerAdapterGate';
|
EntityMaskAdapterGate.displayName = 'EntityMaskAdapterGate';
|
||||||
|
|
||||||
export const InpaintMaskAdapterGate = memo(({ children }: PropsWithChildren) => {
|
// export const useEntityMaskAdapter = (): CanvasMaskAdapter => {
|
||||||
const canvasManager = useCanvasManager();
|
// const adapter = useContext(EntityAdapterContext);
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
// assert(adapter, 'useEntityMaskAdapter must be used within a CanvasMaskAdapterGate');
|
||||||
const adapters = useSyncExternalStore(
|
// assert(adapter.type === 'mask_adapter', 'useEntityMaskAdapter must be used with a mask adapter');
|
||||||
canvasManager.adapters.inpaintMasks.subscribe,
|
// return adapter;
|
||||||
canvasManager.adapters.inpaintMasks.getSnapshot
|
// };
|
||||||
);
|
|
||||||
const adapter = useMemo(() => {
|
|
||||||
return adapters.get(entityIdentifier.id) ?? null;
|
|
||||||
}, [adapters, entityIdentifier.id]);
|
|
||||||
|
|
||||||
if (!adapter) {
|
export const useEntityAdapter = (): CanvasEntityLayerAdapter | CanvasEntityMaskAdapter => {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <EntityAdapterContext.Provider value={adapter}>{children}</EntityAdapterContext.Provider>;
|
|
||||||
});
|
|
||||||
|
|
||||||
InpaintMaskAdapterGate.displayName = 'InpaintMaskAdapterGate';
|
|
||||||
|
|
||||||
export const RegionalGuidanceAdapterGate = memo(({ children }: PropsWithChildren) => {
|
|
||||||
const canvasManager = useCanvasManager();
|
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
|
||||||
const adapters = useSyncExternalStore(
|
|
||||||
canvasManager.adapters.regionMasks.subscribe,
|
|
||||||
canvasManager.adapters.regionMasks.getSnapshot
|
|
||||||
);
|
|
||||||
const adapter = useMemo(() => {
|
|
||||||
return adapters.get(entityIdentifier.id) ?? null;
|
|
||||||
}, [adapters, entityIdentifier.id]);
|
|
||||||
|
|
||||||
if (!adapter) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <EntityAdapterContext.Provider value={adapter}>{children}</EntityAdapterContext.Provider>;
|
|
||||||
});
|
|
||||||
|
|
||||||
RegionalGuidanceAdapterGate.displayName = 'RegionalGuidanceAdapterGate';
|
|
||||||
|
|
||||||
export const useEntityAdapter = ():
|
|
||||||
| CanvasEntityAdapterRasterLayer
|
|
||||||
| CanvasEntityAdapterControlLayer
|
|
||||||
| CanvasEntityAdapterInpaintMask
|
|
||||||
| CanvasEntityAdapterRegionalGuidance => {
|
|
||||||
const adapter = useContext(EntityAdapterContext);
|
const adapter = useContext(EntityAdapterContext);
|
||||||
assert(adapter, 'useEntityAdapter must be used within a CanvasRasterLayerAdapterGate');
|
assert(adapter, 'useEntityAdapter must be used within a CanvasRasterLayerAdapterGate');
|
||||||
return adapter;
|
return adapter;
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { entitySelected } from 'features/controlLayers/store/canvasSlice';
|
|
||||||
import {
|
|
||||||
selectBookmarkedEntityIdentifier,
|
|
||||||
selectSelectedEntityIdentifier,
|
|
||||||
} from 'features/controlLayers/store/selectors';
|
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
|
|
||||||
export const useCanvasEntityQuickSwitchHotkey = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const [prev, setPrev] = useState<CanvasEntityIdentifier | null>(null);
|
|
||||||
const [current, setCurrent] = useState<CanvasEntityIdentifier | null>(null);
|
|
||||||
const selected = useAppSelector(selectSelectedEntityIdentifier);
|
|
||||||
const bookmarked = useAppSelector(selectBookmarkedEntityIdentifier);
|
|
||||||
|
|
||||||
// Update prev and current when selected entity changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (current?.id !== selected?.id) {
|
|
||||||
setPrev(current);
|
|
||||||
setCurrent(selected);
|
|
||||||
}
|
|
||||||
}, [current, selected]);
|
|
||||||
|
|
||||||
const onQuickSwitch = useCallback(() => {
|
|
||||||
if (bookmarked) {
|
|
||||||
if (current?.id !== bookmarked.id) {
|
|
||||||
// Switch between current (non-bookmarked) and bookmarked
|
|
||||||
setPrev(current);
|
|
||||||
setCurrent(bookmarked);
|
|
||||||
dispatch(entitySelected({ entityIdentifier: bookmarked }));
|
|
||||||
} else if (prev) {
|
|
||||||
// Switch back to the last non-bookmarked entity
|
|
||||||
setCurrent(prev);
|
|
||||||
dispatch(entitySelected({ entityIdentifier: prev }));
|
|
||||||
}
|
|
||||||
} else if (prev !== null && current !== null) {
|
|
||||||
// Switch between prev and current if no bookmarked entity
|
|
||||||
setPrev(current);
|
|
||||||
setCurrent(prev);
|
|
||||||
dispatch(entitySelected({ entityIdentifier: prev }));
|
|
||||||
}
|
|
||||||
}, [bookmarked, current, dispatch, prev]);
|
|
||||||
|
|
||||||
useHotkeys('q', onQuickSwitch);
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||||
import { canvasRedo, canvasUndo } from 'features/controlLayers/store/canvasSlice';
|
import { canvasRedo, canvasUndo } from 'features/controlLayers/store/canvasSlice';
|
||||||
@@ -6,7 +7,7 @@ import { useCallback } from 'react';
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
export const useCanvasUndoRedoHotkeys = () => {
|
export const useCanvasUndoRedo = () => {
|
||||||
useAssertSingleton('useCanvasUndoRedo');
|
useAssertSingleton('useCanvasUndoRedo');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -1,26 +1,20 @@
|
|||||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterControlLayer';
|
import type { CanvasEntityLayerAdapter } from 'features/controlLayers/konva/CanvasEntityLayerAdapter';
|
||||||
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterInpaintMask';
|
import type { CanvasEntityMaskAdapter } from 'features/controlLayers/konva/CanvasEntityMaskAdapter';
|
||||||
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterRasterLayer';
|
|
||||||
import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntityAdapter/CanvasEntityAdapterRegionalGuidance';
|
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
export const useEntityAdapter = (
|
export const useEntityAdapter = (
|
||||||
entityIdentifier: CanvasEntityIdentifier
|
entityIdentifier: CanvasEntityIdentifier
|
||||||
):
|
): CanvasEntityLayerAdapter | CanvasEntityMaskAdapter => {
|
||||||
| CanvasEntityAdapterRasterLayer
|
|
||||||
| CanvasEntityAdapterControlLayer
|
|
||||||
| CanvasEntityAdapterInpaintMask
|
|
||||||
| CanvasEntityAdapterRegionalGuidance => {
|
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
|
|
||||||
const adapter = useMemo(() => {
|
const adapter = useMemo(() => {
|
||||||
const adapter = canvasManager.getAdapter(entityIdentifier);
|
const entity = canvasManager.stateApi.getEntity(entityIdentifier);
|
||||||
assert(adapter, 'Entity adapter not found');
|
assert(entity, 'Entity adapter not found');
|
||||||
return adapter;
|
return entity.adapter;
|
||||||
}, [canvasManager, entityIdentifier]);
|
}, [canvasManager.stateApi, entityIdentifier]);
|
||||||
|
|
||||||
return adapter;
|
return adapter;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
export const useEntityIsBookmarkedForQuickSwitch = (entityIdentifier: CanvasEntityIdentifier) => {
|
|
||||||
const selectIsBookmarkedForQuickSwitch = useMemo(
|
|
||||||
() =>
|
|
||||||
createSelector(selectCanvasSlice, (canvas) => {
|
|
||||||
return canvas.bookmarkedEntityIdentifier?.id === entityIdentifier.id;
|
|
||||||
}),
|
|
||||||
[entityIdentifier]
|
|
||||||
);
|
|
||||||
const isBookmarkedForQuickSwitch = useAppSelector(selectIsBookmarkedForQuickSwitch);
|
|
||||||
|
|
||||||
return isBookmarkedForQuickSwitch;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
|
||||||
|
import { type CanvasEntityIdentifier, isDrawableEntity } from 'features/controlLayers/store/types';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useEntityObjectCount = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||||
|
const selectObjectCount = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectCanvasSlice, (canvas) => {
|
||||||
|
const entity = selectEntity(canvas, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return 0;
|
||||||
|
} else if (isDrawableEntity(entity)) {
|
||||||
|
return entity.objects.length;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[entityIdentifier]
|
||||||
|
);
|
||||||
|
const objectCount = useAppSelector(selectObjectCount);
|
||||||
|
return objectCount;
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useEntityObjectCount } from 'features/controlLayers/hooks/useEntityObjectCount';
|
||||||
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
|
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
|
||||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@@ -19,27 +20,34 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const selectName = useMemo(() => createSelectName(entityIdentifier), [entityIdentifier]);
|
const selectName = useMemo(() => createSelectName(entityIdentifier), [entityIdentifier]);
|
||||||
const name = useAppSelector(selectName);
|
const name = useAppSelector(selectName);
|
||||||
|
const objectCount = useEntityObjectCount(entityIdentifier);
|
||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
if (name) {
|
if (name) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (entityIdentifier.type) {
|
const parts: string[] = [];
|
||||||
case 'inpaint_mask':
|
if (entityIdentifier.type === 'inpaint_mask') {
|
||||||
return t('controlLayers.inpaintMask');
|
parts.push(t('controlLayers.inpaintMask'));
|
||||||
case 'control_layer':
|
} else if (entityIdentifier.type === 'control_layer') {
|
||||||
return t('controlLayers.controlLayer');
|
parts.push(t('controlLayers.controlLayer'));
|
||||||
case 'raster_layer':
|
} else if (entityIdentifier.type === 'raster_layer') {
|
||||||
return t('controlLayers.rasterLayer');
|
parts.push(t('controlLayers.rasterLayer'));
|
||||||
case 'ip_adapter':
|
} else if (entityIdentifier.type === 'ip_adapter') {
|
||||||
return t('controlLayers.globalIPAdapter');
|
parts.push(t('common.ipAdapter'));
|
||||||
case 'regional_guidance':
|
} else if (entityIdentifier.type === 'regional_guidance') {
|
||||||
return t('controlLayers.regionalGuidance');
|
parts.push(t('controlLayers.regionalGuidance'));
|
||||||
default:
|
} else {
|
||||||
assert(false, 'Unexpected entity type');
|
assert(false, 'Unexpected entity type');
|
||||||
}
|
}
|
||||||
}, [entityIdentifier.type, name, t]);
|
|
||||||
|
if (objectCount > 0) {
|
||||||
|
parts.push(`(${objectCount})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
}, [entityIdentifier.type, name, objectCount, t]);
|
||||||
|
|
||||||
return title;
|
return title;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const useEntityTypeString = (type: CanvasEntityIdentifier['type']): strin
|
|||||||
case 'regional_guidance':
|
case 'regional_guidance':
|
||||||
return t('controlLayers.regionalGuidance');
|
return t('controlLayers.regionalGuidance');
|
||||||
case 'ip_adapter':
|
case 'ip_adapter':
|
||||||
return t('controlLayers.globalIPAdapter');
|
return t('controlLayers.ipAdapter');
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const useEntityTypeTitle = (type: CanvasEntityIdentifier['type']): string
|
|||||||
case 'regional_guidance':
|
case 'regional_guidance':
|
||||||
return t('controlLayers.regionalGuidance_withCount', { count, context });
|
return t('controlLayers.regionalGuidance_withCount', { count, context });
|
||||||
case 'ip_adapter':
|
case 'ip_adapter':
|
||||||
return t('controlLayers.globalIPAdapters_withCount', { count, context });
|
return t('controlLayers.ipAdapters_withCount', { count, context });
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
|
||||||
export const useCanvasIsBusy = () => {
|
export const useIsFiltering = () => {
|
||||||
const canvasManager = useCanvasManager();
|
const canvasManager = useCanvasManager();
|
||||||
const isBusy = useStore(canvasManager.$isBusy);
|
const isFiltering = useStore(canvasManager.filter.$isFiltering);
|
||||||
|
return isFiltering;
|
||||||
return isBusy;
|
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useIsTransforming = () => {
|
||||||
|
const canvasManager = useCanvasManager();
|
||||||
|
const transformingEntity = useStore(canvasManager.stateApi.$transformingEntity);
|
||||||
|
const isTransforming = useMemo(() => {
|
||||||
|
return Boolean(transformingEntity);
|
||||||
|
}, [transformingEntity]);
|
||||||
|
return isTransforming;
|
||||||
|
};
|
||||||
@@ -48,6 +48,7 @@ export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig
|
|||||||
return defaultControlAdapter;
|
return defaultControlAdapter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @knipignore */
|
||||||
export const useDefaultIPAdapter = (): IPAdapterConfig => {
|
export const useDefaultIPAdapter = (): IPAdapterConfig => {
|
||||||
const [modelConfigs] = useIPAdapterModels();
|
const [modelConfigs] = useIPAdapterModels();
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
|
||||||
import { entitySelected } from 'features/controlLayers/store/canvasSlice';
|
|
||||||
import { selectAllEntities, selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
|
||||||
import type { CanvasEntityState } from 'features/controlLayers/store/types';
|
|
||||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
|
|
||||||
const selectNextEntityIdentifier = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
|
||||||
const selectedEntityIdentifier = canvas.selectedEntityIdentifier;
|
|
||||||
const allEntities = selectAllEntities(canvas);
|
|
||||||
let nextEntity: CanvasEntityState | null = null;
|
|
||||||
if (!selectedEntityIdentifier) {
|
|
||||||
nextEntity = allEntities[0] ?? null;
|
|
||||||
} else {
|
|
||||||
const selectedEntityIndex = allEntities.findIndex((entity) => entity.id === selectedEntityIdentifier.id);
|
|
||||||
nextEntity = allEntities[(selectedEntityIndex + 1) % allEntities.length] ?? null;
|
|
||||||
}
|
|
||||||
if (!nextEntity) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return getEntityIdentifier(nextEntity);
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectPrevEntityIdentifier = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
|
||||||
const selectedEntityIdentifier = canvas.selectedEntityIdentifier;
|
|
||||||
const allEntities = selectAllEntities(canvas);
|
|
||||||
let prevEntity: CanvasEntityState | null = null;
|
|
||||||
if (!selectedEntityIdentifier) {
|
|
||||||
prevEntity = allEntities[0] ?? null;
|
|
||||||
} else {
|
|
||||||
const selectedEntityIndex = allEntities.findIndex((entity) => entity.id === selectedEntityIdentifier.id);
|
|
||||||
prevEntity = allEntities[(selectedEntityIndex - 1 + allEntities.length) % allEntities.length] ?? null;
|
|
||||||
}
|
|
||||||
if (!prevEntity) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return getEntityIdentifier(prevEntity);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useNextPrevEntityHotkeys = () => {
|
|
||||||
useAssertSingleton('useNextPrevEntityHotkeys');
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const nextEntityIdentifier = useAppSelector(selectNextEntityIdentifier);
|
|
||||||
const prevEntityIdentifier = useAppSelector(selectPrevEntityIdentifier);
|
|
||||||
|
|
||||||
const selectNextEntity = useCallback(() => {
|
|
||||||
if (nextEntityIdentifier) {
|
|
||||||
dispatch(entitySelected({ entityIdentifier: nextEntityIdentifier }));
|
|
||||||
}
|
|
||||||
}, [dispatch, nextEntityIdentifier]);
|
|
||||||
|
|
||||||
const selectPrevEntity = useCallback(() => {
|
|
||||||
if (prevEntityIdentifier) {
|
|
||||||
dispatch(entitySelected({ entityIdentifier: prevEntityIdentifier }));
|
|
||||||
}
|
|
||||||
}, [dispatch, prevEntityIdentifier]);
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
// “ === alt+[
|
|
||||||
['alt+[', '“'],
|
|
||||||
selectPrevEntity,
|
|
||||||
{ preventDefault: true, ignoreModifiers: true },
|
|
||||||
[selectPrevEntity]
|
|
||||||
);
|
|
||||||
useHotkeys(
|
|
||||||
// ‘ === alt+]
|
|
||||||
['alt+]', '‘'],
|
|
||||||
selectNextEntity,
|
|
||||||
{ preventDefault: true, ignoreModifiers: true },
|
|
||||||
[selectNextEntity]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,95 +1,59 @@
|
|||||||
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
|
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
|
||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC';
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||||
import { selectDynamicGrid } from 'features/controlLayers/store/canvasSettingsSlice';
|
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
|
|
||||||
type CanvasBackgroundModuleConfig = {
|
export class CanvasBackgroundModule extends CanvasModuleABC {
|
||||||
GRID_LINE_COLOR_COARSE: string;
|
|
||||||
GRID_LINE_COLOR_FINE: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG: CanvasBackgroundModuleConfig = {
|
|
||||||
GRID_LINE_COLOR_COARSE: getArbitraryBaseColor(27),
|
|
||||||
GRID_LINE_COLOR_FINE: getArbitraryBaseColor(18),
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a background grid on the canvas, where the grid spacing changes based on the stage scale.
|
|
||||||
*
|
|
||||||
* The grid is only visible when the dynamic grid setting is enabled.
|
|
||||||
*/
|
|
||||||
export class CanvasBackgroundModule extends CanvasModuleBase {
|
|
||||||
readonly type = 'background';
|
readonly type = 'background';
|
||||||
readonly id: string;
|
|
||||||
readonly path: string[];
|
|
||||||
readonly parent: CanvasManager;
|
|
||||||
readonly manager: CanvasManager;
|
|
||||||
readonly log: Logger;
|
|
||||||
|
|
||||||
|
static GRID_LINE_COLOR_COARSE = getArbitraryBaseColor(27);
|
||||||
|
static GRID_LINE_COLOR_FINE = getArbitraryBaseColor(18);
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
path: string[];
|
||||||
|
manager: CanvasManager;
|
||||||
subscriptions = new Set<() => void>();
|
subscriptions = new Set<() => void>();
|
||||||
config: CanvasBackgroundModuleConfig = DEFAULT_CONFIG;
|
log: Logger;
|
||||||
|
|
||||||
/**
|
|
||||||
* The Konva objects that make up the background grid:
|
|
||||||
* - A layer to hold the grid lines
|
|
||||||
* - An array of grid lines
|
|
||||||
*/
|
|
||||||
konva: {
|
konva: {
|
||||||
layer: Konva.Layer;
|
layer: Konva.Layer;
|
||||||
lines: Konva.Line[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(manager: CanvasManager) {
|
constructor(manager: CanvasManager) {
|
||||||
super();
|
super();
|
||||||
this.id = getPrefixedId(this.type);
|
this.id = getPrefixedId(this.type);
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.parent = manager;
|
this.path = this.manager.path.concat(this.id);
|
||||||
this.path = this.manager.buildPath(this);
|
this.log = this.manager.buildLogger(this.getLoggingContext);
|
||||||
this.log = this.manager.buildLogger(this);
|
|
||||||
|
|
||||||
this.log.debug('Creating module');
|
this.log.debug('Creating background module');
|
||||||
|
|
||||||
this.konva = { layer: new Konva.Layer({ name: `${this.type}:layer`, listening: false }), lines: [] };
|
this.konva = { layer: new Konva.Layer({ name: `${this.type}:layer`, listening: false }) };
|
||||||
|
|
||||||
/**
|
this.subscriptions.add(
|
||||||
* The background grid should be rendered when the stage attributes change:
|
this.manager.stateApi.$stageAttrs.listen(() => {
|
||||||
* - scale
|
this.render();
|
||||||
* - position
|
})
|
||||||
* - size
|
);
|
||||||
*/
|
|
||||||
this.subscriptions.add(this.manager.stage.$stageAttrs.listen(this.render));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The background grid should be rendered when the dynamic grid setting changes.
|
|
||||||
*/
|
|
||||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectDynamicGrid, this.render));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize = () => {
|
render() {
|
||||||
this.log.debug('Initializing module');
|
const settings = this.manager.stateApi.getSettings();
|
||||||
this.render();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
if (!settings.dynamicGrid) {
|
||||||
* Renders the background grid.
|
|
||||||
*/
|
|
||||||
render = () => {
|
|
||||||
const dynamicGrid = this.manager.stateApi.runSelector(selectDynamicGrid);
|
|
||||||
|
|
||||||
if (!dynamicGrid) {
|
|
||||||
this.konva.layer.visible(false);
|
this.konva.layer.visible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.konva.layer.visible(true);
|
this.konva.layer.visible(true);
|
||||||
|
|
||||||
|
this.konva.layer.zIndex(0);
|
||||||
const scale = this.manager.stage.getScale();
|
const scale = this.manager.stage.getScale();
|
||||||
const { x, y } = this.manager.stage.getPosition();
|
const { x, y } = this.manager.stage.getPosition();
|
||||||
const { width, height } = this.manager.stage.getSize();
|
const { width, height } = this.manager.stage.getSize();
|
||||||
const gridSpacing = CanvasBackgroundModule.getGridSpacing(scale);
|
const gridSpacing = this.getGridSpacing(scale);
|
||||||
const stageRect = {
|
const stageRect = {
|
||||||
x1: 0,
|
x1: 0,
|
||||||
y1: 0,
|
y1: 0,
|
||||||
@@ -128,44 +92,47 @@ export class CanvasBackgroundModule extends CanvasModuleBase {
|
|||||||
let _y = 0;
|
let _y = 0;
|
||||||
|
|
||||||
this.konva.layer.destroyChildren();
|
this.konva.layer.destroyChildren();
|
||||||
this.konva.lines = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < xSteps; i++) {
|
for (let i = 0; i < xSteps; i++) {
|
||||||
_x = gridFullRect.x1 + i * gridSpacing;
|
_x = gridFullRect.x1 + i * gridSpacing;
|
||||||
const line = new Konva.Line({
|
this.konva.layer.add(
|
||||||
x: _x,
|
new Konva.Line({
|
||||||
y: gridFullRect.y1,
|
x: _x,
|
||||||
points: [0, 0, 0, ySize],
|
y: gridFullRect.y1,
|
||||||
stroke: _x % 64 ? this.config.GRID_LINE_COLOR_FINE : this.config.GRID_LINE_COLOR_COARSE,
|
points: [0, 0, 0, ySize],
|
||||||
strokeWidth,
|
stroke: _x % 64 ? CanvasBackgroundModule.GRID_LINE_COLOR_FINE : CanvasBackgroundModule.GRID_LINE_COLOR_COARSE,
|
||||||
listening: false,
|
strokeWidth,
|
||||||
});
|
listening: false,
|
||||||
this.konva.lines.push(line);
|
})
|
||||||
this.konva.layer.add(line);
|
);
|
||||||
}
|
}
|
||||||
for (let i = 0; i < ySteps; i++) {
|
for (let i = 0; i < ySteps; i++) {
|
||||||
_y = gridFullRect.y1 + i * gridSpacing;
|
_y = gridFullRect.y1 + i * gridSpacing;
|
||||||
const line = new Konva.Line({
|
this.konva.layer.add(
|
||||||
x: gridFullRect.x1,
|
new Konva.Line({
|
||||||
y: _y,
|
x: gridFullRect.x1,
|
||||||
points: [0, 0, xSize, 0],
|
y: _y,
|
||||||
stroke: _y % 64 ? this.config.GRID_LINE_COLOR_FINE : this.config.GRID_LINE_COLOR_COARSE,
|
points: [0, 0, xSize, 0],
|
||||||
strokeWidth,
|
stroke: _y % 64 ? CanvasBackgroundModule.GRID_LINE_COLOR_FINE : CanvasBackgroundModule.GRID_LINE_COLOR_COARSE,
|
||||||
listening: false,
|
strokeWidth,
|
||||||
});
|
listening: false,
|
||||||
this.konva.lines.push(line);
|
})
|
||||||
this.konva.layer.add(line);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy = () => {
|
||||||
|
this.log.trace('Destroying background module');
|
||||||
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||||
|
this.konva.layer.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the grid line spacing for the dynamic grid.
|
* Gets the grid spacing. The value depends on the stage scale - at higher scales, the grid spacing is smaller.
|
||||||
*
|
|
||||||
* The value depends on the stage scale - at higher scales, the grid spacing is smaller.
|
|
||||||
*
|
|
||||||
* @param scale The stage scale
|
* @param scale The stage scale
|
||||||
|
* @returns The grid spacing based on the stage scale
|
||||||
*/
|
*/
|
||||||
static getGridSpacing = (scale: number): number => {
|
getGridSpacing = (scale: number): number => {
|
||||||
if (scale >= 2) {
|
if (scale >= 2) {
|
||||||
return 8;
|
return 8;
|
||||||
}
|
}
|
||||||
@@ -184,10 +151,15 @@ export class CanvasBackgroundModule extends CanvasModuleBase {
|
|||||||
return 256;
|
return 256;
|
||||||
};
|
};
|
||||||
|
|
||||||
destroy = () => {
|
repr = () => {
|
||||||
this.log.trace('Destroying module');
|
return {
|
||||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
id: this.id,
|
||||||
this.subscriptions.clear();
|
path: this.path,
|
||||||
this.konva.layer.destroy();
|
type: this.type,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getLoggingContext = () => {
|
||||||
|
return { ...this.manager.getLoggingContext(), path: this.path.join('.') };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import type { SerializableObject } from 'common/types';
|
||||||
import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple';
|
import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple';
|
||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC';
|
||||||
|
import type { CanvasPreviewModule } from 'features/controlLayers/konva/CanvasPreviewModule';
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||||
import { selectBbox } from 'features/controlLayers/store/selectors';
|
import type { Rect } from 'features/controlLayers/store/types';
|
||||||
import type { Coordinate, Rect } from 'features/controlLayers/store/types';
|
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
@@ -22,56 +23,51 @@ const ALL_ANCHORS: string[] = [
|
|||||||
const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
const CORNER_ANCHORS: string[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||||
const NO_ANCHORS: string[] = [];
|
const NO_ANCHORS: string[] = [];
|
||||||
|
|
||||||
/**
|
export class CanvasBboxModule extends CanvasModuleABC {
|
||||||
* Renders the bounding box. The bounding box can be transformed by the user.
|
|
||||||
*/
|
|
||||||
export class CanvasBboxModule extends CanvasModuleBase {
|
|
||||||
readonly type = 'bbox';
|
readonly type = 'bbox';
|
||||||
readonly id: string;
|
|
||||||
readonly path: string[];
|
|
||||||
readonly parent: CanvasManager;
|
|
||||||
readonly manager: CanvasManager;
|
|
||||||
readonly log: Logger;
|
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
path: string[];
|
||||||
|
manager: CanvasManager;
|
||||||
|
log: Logger;
|
||||||
subscriptions: Set<() => void> = new Set();
|
subscriptions: Set<() => void> = new Set();
|
||||||
|
|
||||||
/**
|
parent: CanvasPreviewModule;
|
||||||
* The Konva objects that make up the bbox:
|
|
||||||
* - A group to hold all the objects
|
|
||||||
* - A transformer to allow the bbox to be transformed
|
|
||||||
* - A transparent rect so the transformer has something to transform
|
|
||||||
*/
|
|
||||||
konva: {
|
konva: {
|
||||||
group: Konva.Group;
|
group: Konva.Group;
|
||||||
|
rect: Konva.Rect;
|
||||||
transformer: Konva.Transformer;
|
transformer: Konva.Transformer;
|
||||||
proxyRect: Konva.Rect;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
constructor(parent: CanvasPreviewModule) {
|
||||||
* Buffer to store the last aspect ratio of the bbox. When the users holds shift while transforming the bbox, this is
|
|
||||||
* used to lock the aspect ratio.
|
|
||||||
*/
|
|
||||||
$aspectRatioBuffer = atom(1);
|
|
||||||
|
|
||||||
constructor(manager: CanvasManager) {
|
|
||||||
super();
|
super();
|
||||||
this.id = getPrefixedId(this.type);
|
this.id = getPrefixedId(this.type);
|
||||||
this.parent = manager;
|
this.parent = parent;
|
||||||
this.manager = manager;
|
this.manager = this.parent.manager;
|
||||||
this.path = this.manager.buildPath(this);
|
this.path = this.parent.path.concat(this.id);
|
||||||
this.log = this.manager.buildLogger(this);
|
this.log = this.manager.buildLogger(this.getLoggingContext);
|
||||||
|
|
||||||
this.log.debug('Creating bbox module');
|
this.log.debug('Creating bbox module');
|
||||||
|
|
||||||
|
// Create a stash to hold onto the last aspect ratio of the bbox - this allows for locking the aspect ratio when
|
||||||
|
// transforming the bbox.
|
||||||
|
const bbox = this.manager.stateApi.getBbox();
|
||||||
|
const $aspectRatioBuffer = atom(bbox.rect.width / bbox.rect.height);
|
||||||
|
|
||||||
this.konva = {
|
this.konva = {
|
||||||
group: new Konva.Group({ name: `${this.type}:group`, listening: true }),
|
group: new Konva.Group({ name: `${this.type}:group`, listening: false }),
|
||||||
// We will use a Konva.Transformer for the generation bbox. Transformers need some shape to transform, so we will
|
// Use a transformer for the generation bbox. Transformers need some shape to transform, we will use a fully
|
||||||
// create a transparent rect for this purpose.
|
// transparent rect for this purpose.
|
||||||
proxyRect: new Konva.Rect({
|
rect: new Konva.Rect({
|
||||||
name: `${this.type}:rect`,
|
name: `${this.type}:rect`,
|
||||||
listening: false,
|
listening: false,
|
||||||
strokeEnabled: false,
|
strokeEnabled: false,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
|
x: bbox.rect.x,
|
||||||
|
y: bbox.rect.y,
|
||||||
|
width: bbox.rect.width,
|
||||||
|
height: bbox.rect.height,
|
||||||
}),
|
}),
|
||||||
transformer: new Konva.Transformer({
|
transformer: new Konva.Transformer({
|
||||||
name: `${this.type}:transformer`,
|
name: `${this.type}:transformer`,
|
||||||
@@ -89,56 +85,171 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
|||||||
anchorCornerRadius: 3,
|
anchorCornerRadius: 3,
|
||||||
shiftBehavior: 'none', // we will implement our own shift behavior
|
shiftBehavior: 'none', // we will implement our own shift behavior
|
||||||
centeredScaling: false,
|
centeredScaling: false,
|
||||||
anchorStyleFunc: this.anchorStyleFunc,
|
anchorStyleFunc: (anchor) => {
|
||||||
anchorDragBoundFunc: this.anchorDragBoundFunc,
|
// Make the x/y resize anchors little bars
|
||||||
|
if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) {
|
||||||
|
anchor.height(8);
|
||||||
|
anchor.offsetY(4);
|
||||||
|
anchor.width(30);
|
||||||
|
anchor.offsetX(15);
|
||||||
|
}
|
||||||
|
if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) {
|
||||||
|
anchor.height(30);
|
||||||
|
anchor.offsetY(15);
|
||||||
|
anchor.width(8);
|
||||||
|
anchor.offsetX(4);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
anchorDragBoundFunc: (_oldAbsPos, newAbsPos) => {
|
||||||
|
// This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed
|
||||||
|
// to konva's internal coordinate system.
|
||||||
|
const stage = this.konva.transformer.getStage();
|
||||||
|
assert(stage, 'Stage must exist');
|
||||||
|
|
||||||
|
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
|
||||||
|
const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64;
|
||||||
|
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
|
||||||
|
const scaledGridSize = gridSize * stage.scaleX();
|
||||||
|
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
|
||||||
|
const stageAbsPos = stage.getAbsolutePosition();
|
||||||
|
// The offset is the remainder of the stage's absolute position divided by the scaled grid size.
|
||||||
|
const offsetX = stageAbsPos.x % scaledGridSize;
|
||||||
|
const offsetY = stageAbsPos.y % scaledGridSize;
|
||||||
|
// Finally, calculate the position by rounding to the grid and adding the offset.
|
||||||
|
return {
|
||||||
|
x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX,
|
||||||
|
y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY,
|
||||||
|
};
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
this.konva.rect.on('dragmove', () => {
|
||||||
|
const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64;
|
||||||
|
const bbox = this.manager.stateApi.getBbox();
|
||||||
|
const bboxRect: Rect = {
|
||||||
|
...bbox.rect,
|
||||||
|
x: roundToMultiple(this.konva.rect.x(), gridSize),
|
||||||
|
y: roundToMultiple(this.konva.rect.y(), gridSize),
|
||||||
|
};
|
||||||
|
this.konva.rect.setAttrs(bboxRect);
|
||||||
|
if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) {
|
||||||
|
this.manager.stateApi.setGenerationBbox(bboxRect);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.konva.proxyRect.on('dragmove', this.onDragMove);
|
this.konva.transformer.on('transform', () => {
|
||||||
this.konva.transformer.on('transform', this.onTransform);
|
// In the transform callback, we calculate the bbox's new dims and pos and update the konva object.
|
||||||
this.konva.transformer.on('transformend', this.onTransformEnd);
|
// Some special handling is needed depending on the anchor being dragged.
|
||||||
|
const anchor = this.konva.transformer.getActiveAnchor();
|
||||||
|
if (!anchor) {
|
||||||
|
// Pretty sure we should always have an anchor here?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The transformer will always be transforming the proxy rect
|
const alt = this.manager.stateApi.$altKey.get();
|
||||||
this.konva.transformer.nodes([this.konva.proxyRect]);
|
const ctrl = this.manager.stateApi.$ctrlKey.get();
|
||||||
this.konva.group.add(this.konva.proxyRect);
|
const meta = this.manager.stateApi.$metaKey.get();
|
||||||
|
const shift = this.manager.stateApi.$shiftKey.get();
|
||||||
|
|
||||||
|
// Grid size depends on the modifier keys
|
||||||
|
let gridSize = ctrl || meta ? 8 : 64;
|
||||||
|
|
||||||
|
// Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the
|
||||||
|
// new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if
|
||||||
|
// we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes.
|
||||||
|
// Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid.
|
||||||
|
if (this.manager.stateApi.$altKey.get()) {
|
||||||
|
gridSize = gridSize * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The coords should be correct per the anchorDragBoundFunc.
|
||||||
|
let x = this.konva.rect.x();
|
||||||
|
let y = this.konva.rect.y();
|
||||||
|
|
||||||
|
// Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height
|
||||||
|
// *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap
|
||||||
|
// them to the grid.
|
||||||
|
let width = roundToMultipleMin(this.konva.rect.width() * this.konva.rect.scaleX(), gridSize);
|
||||||
|
let height = roundToMultipleMin(this.konva.rect.height() * this.konva.rect.scaleY(), gridSize);
|
||||||
|
|
||||||
|
// If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this
|
||||||
|
// if alt/opt is held - this requires math too big for my brain.
|
||||||
|
if (shift && CORNER_ANCHORS.includes(anchor) && !alt) {
|
||||||
|
// Fit the bbox to the last aspect ratio
|
||||||
|
let fittedWidth = Math.sqrt(width * height * $aspectRatioBuffer.get());
|
||||||
|
let fittedHeight = fittedWidth / $aspectRatioBuffer.get();
|
||||||
|
fittedWidth = roundToMultipleMin(fittedWidth, gridSize);
|
||||||
|
fittedHeight = roundToMultipleMin(fittedHeight, gridSize);
|
||||||
|
|
||||||
|
// We need to adjust the x and y coords to have the resize occur from the right origin.
|
||||||
|
if (anchor === 'top-left') {
|
||||||
|
// The transform origin is the bottom-right anchor. Both x and y need to be updated.
|
||||||
|
x = x - (fittedWidth - width);
|
||||||
|
y = y - (fittedHeight - height);
|
||||||
|
}
|
||||||
|
if (anchor === 'top-right') {
|
||||||
|
// The transform origin is the bottom-left anchor. Only y needs to be updated.
|
||||||
|
y = y - (fittedHeight - height);
|
||||||
|
}
|
||||||
|
if (anchor === 'bottom-left') {
|
||||||
|
// The transform origin is the top-right anchor. Only x needs to be updated.
|
||||||
|
x = x - (fittedWidth - width);
|
||||||
|
}
|
||||||
|
// Update the width and height to the fitted dims.
|
||||||
|
width = fittedWidth;
|
||||||
|
height = fittedHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bboxRect = {
|
||||||
|
x: Math.round(x),
|
||||||
|
y: Math.round(y),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the bboxRect's attrs directly with the new transform, and reset its scale to 1.
|
||||||
|
// TODO(psyche): In `renderBboxPreview()` we also call setAttrs, need to do it twice to ensure it renders correctly.
|
||||||
|
// Gotta be a way to avoid setting it twice...
|
||||||
|
this.konva.rect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 });
|
||||||
|
|
||||||
|
// Update the bbox in internal state.
|
||||||
|
this.manager.stateApi.setGenerationBbox(bboxRect);
|
||||||
|
|
||||||
|
// Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start
|
||||||
|
// a transform, get the right aspect ratio, then hold shift to lock it in.
|
||||||
|
if (!shift) {
|
||||||
|
$aspectRatioBuffer.set(bboxRect.width / bboxRect.height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.konva.transformer.on('transformend', () => {
|
||||||
|
// Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held,
|
||||||
|
// we have the correct aspect ratio to start from.
|
||||||
|
$aspectRatioBuffer.set(this.konva.rect.width() / this.konva.rect.height());
|
||||||
|
});
|
||||||
|
|
||||||
|
// The transformer will always be transforming the dummy rect
|
||||||
|
this.konva.transformer.nodes([this.konva.rect]);
|
||||||
|
this.konva.group.add(this.konva.rect);
|
||||||
this.konva.group.add(this.konva.transformer);
|
this.konva.group.add(this.konva.transformer);
|
||||||
|
|
||||||
// We will listen to the tool state to determine if the bbox should be visible or not.
|
this.subscriptions.add(this.manager.stateApi.$tool.listen(this.render));
|
||||||
this.subscriptions.add(this.manager.tool.$tool.listen(this.render));
|
|
||||||
|
|
||||||
// Also listen to redux state to update the bbox's position and dimensions.
|
|
||||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectBbox, this.render));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize = () => {
|
|
||||||
this.log.debug('Initializing module');
|
|
||||||
// We need to retain a copy of the bbox state because
|
|
||||||
const { width, height } = this.manager.stateApi.runSelector(selectBbox).rect;
|
|
||||||
// Update the aspect ratio buffer with the initial aspect ratio
|
|
||||||
this.$aspectRatioBuffer.set(width / height);
|
|
||||||
this.render();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the bbox. The bbox is only visible when the tool is set to 'bbox'.
|
|
||||||
*/
|
|
||||||
render = () => {
|
render = () => {
|
||||||
this.log.trace('Rendering');
|
this.log.trace('Rendering bbox module');
|
||||||
|
|
||||||
const { x, y, width, height } = this.manager.stateApi.runSelector(selectBbox).rect;
|
const bbox = this.manager.stateApi.getBbox();
|
||||||
const tool = this.manager.tool.$tool.get();
|
const tool = this.manager.stateApi.$tool.get();
|
||||||
|
|
||||||
this.konva.group.visible(true);
|
this.konva.group.visible(true);
|
||||||
|
this.parent.getLayer().listening(tool === 'bbox');
|
||||||
// We need to reach up to the preview layer to enable/disable listening so that the bbox can be interacted with.
|
this.konva.group.listening(tool === 'bbox');
|
||||||
// If the mangaer is busy, we disable listening so the bbox cannot be interacted with.
|
this.konva.rect.setAttrs({
|
||||||
this.manager.konva.previewLayer.listening(tool === 'bbox' && !this.manager.$isBusy.get());
|
x: bbox.rect.x,
|
||||||
|
y: bbox.rect.y,
|
||||||
this.konva.proxyRect.setAttrs({
|
width: bbox.rect.width,
|
||||||
x,
|
height: bbox.rect.height,
|
||||||
y,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
scaleX: 1,
|
scaleX: 1,
|
||||||
scaleY: 1,
|
scaleY: 1,
|
||||||
listening: tool === 'bbox',
|
listening: tool === 'bbox',
|
||||||
@@ -149,175 +260,21 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
repr = () => {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
type: this.type,
|
||||||
|
path: this.path,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
destroy = () => {
|
destroy = () => {
|
||||||
this.log.trace('Destroying module');
|
this.log.trace('Destroying bbox module');
|
||||||
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||||
this.subscriptions.clear();
|
|
||||||
this.konva.group.destroy();
|
this.konva.group.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
getLoggingContext = (): SerializableObject => {
|
||||||
* Handles the dragmove event on the bbox rect:
|
return { ...this.parent.getLoggingContext(), path: this.path.join('.') };
|
||||||
* - Snaps the bbox position to the grid (determined by ctrl/meta key)
|
|
||||||
* - Pushes the new bbox rect into app state
|
|
||||||
*/
|
|
||||||
onDragMove = () => {
|
|
||||||
const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64;
|
|
||||||
const bbox = this.manager.stateApi.getBbox();
|
|
||||||
const bboxRect: Rect = {
|
|
||||||
...bbox.rect,
|
|
||||||
x: roundToMultiple(this.konva.proxyRect.x(), gridSize),
|
|
||||||
y: roundToMultiple(this.konva.proxyRect.y(), gridSize),
|
|
||||||
};
|
|
||||||
this.konva.proxyRect.setAttrs(bboxRect);
|
|
||||||
if (bbox.rect.x !== bboxRect.x || bbox.rect.y !== bboxRect.y) {
|
|
||||||
this.manager.stateApi.setGenerationBbox(bboxRect);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the transform event on the bbox transformer:
|
|
||||||
* - Snaps the bbox dimensions to the grid (determined by ctrl/meta key)
|
|
||||||
* - Centered scaling when alt is held
|
|
||||||
* - Aspect ratio locking when shift is held
|
|
||||||
* - Pushes the new bbox rect into app state
|
|
||||||
* - Syncs the aspect ratio buffer
|
|
||||||
*/
|
|
||||||
onTransform = () => {
|
|
||||||
// In the transform callback, we calculate the bbox's new dims and pos and update the konva object.
|
|
||||||
// Some special handling is needed depending on the anchor being dragged.
|
|
||||||
const anchor = this.konva.transformer.getActiveAnchor();
|
|
||||||
if (!anchor) {
|
|
||||||
// Pretty sure we should always have an anchor here?
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alt = this.manager.stateApi.$altKey.get();
|
|
||||||
const ctrl = this.manager.stateApi.$ctrlKey.get();
|
|
||||||
const meta = this.manager.stateApi.$metaKey.get();
|
|
||||||
const shift = this.manager.stateApi.$shiftKey.get();
|
|
||||||
|
|
||||||
// Grid size depends on the modifier keys
|
|
||||||
let gridSize = ctrl || meta ? 8 : 64;
|
|
||||||
|
|
||||||
// Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the
|
|
||||||
// new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if
|
|
||||||
// we snapped the width and height to 8px increments, the bbox would be mis-placed by 4px in the x and y axes.
|
|
||||||
// Doubling the grid size ensures the bbox's coords remain aligned to the 8px/64px grid.
|
|
||||||
if (this.manager.stateApi.$altKey.get()) {
|
|
||||||
gridSize = gridSize * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The coords should be correct per the anchorDragBoundFunc.
|
|
||||||
let x = this.konva.proxyRect.x();
|
|
||||||
let y = this.konva.proxyRect.y();
|
|
||||||
|
|
||||||
// Konva transforms by scaling the dims, not directly changing width and height. At this point, the width and height
|
|
||||||
// *have not changed*, only the scale has changed. To get the final height, we need to scale the dims and then snap
|
|
||||||
// them to the grid.
|
|
||||||
let width = roundToMultipleMin(this.konva.proxyRect.width() * this.konva.proxyRect.scaleX(), gridSize);
|
|
||||||
let height = roundToMultipleMin(this.konva.proxyRect.height() * this.konva.proxyRect.scaleY(), gridSize);
|
|
||||||
|
|
||||||
// If shift is held and we are resizing from a corner, retain aspect ratio - needs special handling. We skip this
|
|
||||||
// if alt/opt is held - this requires math too big for my brain.
|
|
||||||
if (shift && CORNER_ANCHORS.includes(anchor) && !alt) {
|
|
||||||
// Fit the bbox to the last aspect ratio
|
|
||||||
let fittedWidth = Math.sqrt(width * height * this.$aspectRatioBuffer.get());
|
|
||||||
let fittedHeight = fittedWidth / this.$aspectRatioBuffer.get();
|
|
||||||
fittedWidth = roundToMultipleMin(fittedWidth, gridSize);
|
|
||||||
fittedHeight = roundToMultipleMin(fittedHeight, gridSize);
|
|
||||||
|
|
||||||
// We need to adjust the x and y coords to have the resize occur from the right origin.
|
|
||||||
if (anchor === 'top-left') {
|
|
||||||
// The transform origin is the bottom-right anchor. Both x and y need to be updated.
|
|
||||||
x = x - (fittedWidth - width);
|
|
||||||
y = y - (fittedHeight - height);
|
|
||||||
}
|
|
||||||
if (anchor === 'top-right') {
|
|
||||||
// The transform origin is the bottom-left anchor. Only y needs to be updated.
|
|
||||||
y = y - (fittedHeight - height);
|
|
||||||
}
|
|
||||||
if (anchor === 'bottom-left') {
|
|
||||||
// The transform origin is the top-right anchor. Only x needs to be updated.
|
|
||||||
x = x - (fittedWidth - width);
|
|
||||||
}
|
|
||||||
// Update the width and height to the fitted dims.
|
|
||||||
width = fittedWidth;
|
|
||||||
height = fittedHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bboxRect = {
|
|
||||||
x: Math.round(x),
|
|
||||||
y: Math.round(y),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the bboxRect's attrs directly with the new transform, and reset its scale to 1.
|
|
||||||
this.konva.proxyRect.setAttrs({ ...bboxRect, scaleX: 1, scaleY: 1 });
|
|
||||||
|
|
||||||
// Update the bbox in internal state.
|
|
||||||
this.manager.stateApi.setGenerationBbox(bboxRect);
|
|
||||||
|
|
||||||
// Update the aspect ratio buffer whenever the shift key is not held - this allows for a nice UX where you can start
|
|
||||||
// a transform, get the right aspect ratio, then hold shift to lock it in.
|
|
||||||
if (!shift) {
|
|
||||||
this.$aspectRatioBuffer.set(bboxRect.width / bboxRect.height);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the transformend event on the bbox transformer:
|
|
||||||
* - Updates the aspect ratio buffer with the new aspect ratio
|
|
||||||
*/
|
|
||||||
onTransformEnd = () => {
|
|
||||||
// Always update the aspect ratio buffer when the transform ends, so if the next transform starts with shift held,
|
|
||||||
// we have the correct aspect ratio to start from.
|
|
||||||
this.$aspectRatioBuffer.set(this.konva.proxyRect.width() / this.konva.proxyRect.height());
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called for each anchor on the transformer. It sets the style of the anchor based on its name.
|
|
||||||
* We make the x/y resize anchors little bars.
|
|
||||||
*/
|
|
||||||
anchorStyleFunc = (anchor: Konva.Rect): void => {
|
|
||||||
if (anchor.hasName('top-center') || anchor.hasName('bottom-center')) {
|
|
||||||
anchor.height(8);
|
|
||||||
anchor.offsetY(4);
|
|
||||||
anchor.width(30);
|
|
||||||
anchor.offsetX(15);
|
|
||||||
}
|
|
||||||
if (anchor.hasName('middle-left') || anchor.hasName('middle-right')) {
|
|
||||||
anchor.height(30);
|
|
||||||
anchor.offsetY(15);
|
|
||||||
anchor.width(8);
|
|
||||||
anchor.offsetX(4);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called for each anchor on the transformer. It sets the drag bounds for the anchor based on the
|
|
||||||
* stage's position and the grid size. Care is taken to ensure the anchor snaps to the grid correctly.
|
|
||||||
*/
|
|
||||||
anchorDragBoundFunc = (oldAbsPos: Coordinate, newAbsPos: Coordinate): Coordinate => {
|
|
||||||
// This function works with absolute position - that is, a position in "physical" pixels on the screen, as opposed
|
|
||||||
// to konva's internal coordinate system.
|
|
||||||
const stage = this.konva.transformer.getStage();
|
|
||||||
assert(stage, 'Stage must exist');
|
|
||||||
|
|
||||||
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
|
|
||||||
const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64;
|
|
||||||
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
|
|
||||||
const scaledGridSize = gridSize * stage.scaleX();
|
|
||||||
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.
|
|
||||||
const stageAbsPos = stage.getAbsolutePosition();
|
|
||||||
// The offset is the remainder of the stage's absolute position divided by the scaled grid size.
|
|
||||||
const offsetX = stageAbsPos.x % scaledGridSize;
|
|
||||||
const offsetY = stageAbsPos.y % scaledGridSize;
|
|
||||||
// Finally, calculate the position by rounding to the grid and adding the offset.
|
|
||||||
return {
|
|
||||||
x: roundToMultiple(newAbsPos.x, scaledGridSize) + offsetX,
|
|
||||||
y: roundToMultiple(newAbsPos.y, scaledGridSize) + offsetY,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
|
||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
|
||||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
|
||||||
import type { CanvasToolModule } from 'features/controlLayers/konva/CanvasToolModule';
|
|
||||||
import { alignCoordForTool, getPrefixedId } from 'features/controlLayers/konva/util';
|
|
||||||
import Konva from 'konva';
|
|
||||||
import type { Logger } from 'roarr';
|
|
||||||
|
|
||||||
type BrushToolPreviewConfig = {
|
|
||||||
/**
|
|
||||||
* The inner border color for the brush tool preview.
|
|
||||||
*/
|
|
||||||
BORDER_INNER_COLOR: string;
|
|
||||||
/**
|
|
||||||
* The outer border color for the brush tool preview.
|
|
||||||
*/
|
|
||||||
BORDER_OUTER_COLOR: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG: BrushToolPreviewConfig = {
|
|
||||||
BORDER_INNER_COLOR: 'rgba(0,0,0,1)',
|
|
||||||
BORDER_OUTER_COLOR: 'rgba(255,255,255,0.8)',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a preview of the brush tool on the canvas.
|
|
||||||
*/
|
|
||||||
export class CanvasBrushToolPreview extends CanvasModuleBase {
|
|
||||||
readonly type = 'brush_tool_preview';
|
|
||||||
readonly id: string;
|
|
||||||
readonly path: string[];
|
|
||||||
readonly parent: CanvasToolModule;
|
|
||||||
readonly manager: CanvasManager;
|
|
||||||
readonly log: Logger;
|
|
||||||
|
|
||||||
config: BrushToolPreviewConfig = DEFAULT_CONFIG;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Konva objects that make up the brush tool preview:
|
|
||||||
* - A group to hold the fill circle and borders
|
|
||||||
* - A circle to fill the brush area
|
|
||||||
* - An inner border ring
|
|
||||||
* - An outer border ring
|
|
||||||
*/
|
|
||||||
konva: {
|
|
||||||
group: Konva.Group;
|
|
||||||
fillCircle: Konva.Circle;
|
|
||||||
innerBorder: Konva.Ring;
|
|
||||||
outerBorder: Konva.Ring;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(parent: CanvasToolModule) {
|
|
||||||
super();
|
|
||||||
this.id = getPrefixedId(this.type);
|
|
||||||
this.parent = parent;
|
|
||||||
this.manager = this.parent.manager;
|
|
||||||
this.path = this.manager.buildPath(this);
|
|
||||||
this.log = this.manager.buildLogger(this);
|
|
||||||
|
|
||||||
this.log.debug('Creating module');
|
|
||||||
|
|
||||||
this.konva = {
|
|
||||||
group: new Konva.Group({ name: `${this.type}:brush_group`, listening: false }),
|
|
||||||
fillCircle: new Konva.Circle({
|
|
||||||
name: `${this.type}:brush_fill_circle`,
|
|
||||||
listening: false,
|
|
||||||
strokeEnabled: false,
|
|
||||||
}),
|
|
||||||
innerBorder: new Konva.Ring({
|
|
||||||
name: `${this.type}:brush_inner_border_ring`,
|
|
||||||
listening: false,
|
|
||||||
innerRadius: 0,
|
|
||||||
outerRadius: 0,
|
|
||||||
fill: this.config.BORDER_INNER_COLOR,
|
|
||||||
strokeEnabled: false,
|
|
||||||
}),
|
|
||||||
outerBorder: new Konva.Ring({
|
|
||||||
name: `${this.type}:brush_outer_border_ring`,
|
|
||||||
listening: false,
|
|
||||||
innerRadius: 0,
|
|
||||||
outerRadius: 0,
|
|
||||||
fill: this.config.BORDER_OUTER_COLOR,
|
|
||||||
strokeEnabled: false,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
this.konva.group.add(this.konva.fillCircle, this.konva.innerBorder, this.konva.outerBorder);
|
|
||||||
}
|
|
||||||
|
|
||||||
render = () => {
|
|
||||||
const cursorPos = this.manager.tool.$cursorPos.get();
|
|
||||||
|
|
||||||
// If the cursor position is not available, do not update the brush preview. The tool module will handle visiblity.
|
|
||||||
if (!cursorPos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = this.manager.stateApi.getSettings();
|
|
||||||
const brushPreviewFill = this.manager.stateApi.getBrushPreviewColor();
|
|
||||||
const alignedCursorPos = alignCoordForTool(cursorPos, settings.brushWidth);
|
|
||||||
const radius = settings.brushWidth / 2;
|
|
||||||
|
|
||||||
// The circle is scaled
|
|
||||||
this.konva.fillCircle.setAttrs({
|
|
||||||
x: alignedCursorPos.x,
|
|
||||||
y: alignedCursorPos.y,
|
|
||||||
radius,
|
|
||||||
fill: rgbaColorToString(brushPreviewFill),
|
|
||||||
});
|
|
||||||
|
|
||||||
// But the borders are in screen-pixels
|
|
||||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
|
||||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
|
||||||
|
|
||||||
this.konva.innerBorder.setAttrs({
|
|
||||||
x: cursorPos.x,
|
|
||||||
y: cursorPos.y,
|
|
||||||
innerRadius: radius,
|
|
||||||
outerRadius: radius + onePixel,
|
|
||||||
});
|
|
||||||
this.konva.outerBorder.setAttrs({
|
|
||||||
x: cursorPos.x,
|
|
||||||
y: cursorPos.y,
|
|
||||||
innerRadius: radius + onePixel,
|
|
||||||
outerRadius: radius + twoPixels,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setVisibility = (visible: boolean) => {
|
|
||||||
this.konva.group.visible(visible);
|
|
||||||
};
|
|
||||||
|
|
||||||
repr = () => {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
type: this.type,
|
|
||||||
path: this.path,
|
|
||||||
config: this.config,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
destroy = () => {
|
|
||||||
this.log.debug('Destroying module');
|
|
||||||
this.konva.group.destroy();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,91 +1,54 @@
|
|||||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC';
|
||||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||||
import type { GenerationMode } from 'features/controlLayers/store/types';
|
import type { GenerationMode } from 'features/controlLayers/store/types';
|
||||||
import { LRUCache } from 'lru-cache';
|
import { LRUCache } from 'lru-cache';
|
||||||
import type { Logger } from 'roarr';
|
import type { Logger } from 'roarr';
|
||||||
|
|
||||||
type CanvasCacheModuleConfig = {
|
export class CanvasCacheModule extends CanvasModuleABC {
|
||||||
/**
|
|
||||||
* The maximum size of the image name cache.
|
|
||||||
*/
|
|
||||||
imageNameCacheSize: number;
|
|
||||||
/**
|
|
||||||
* The maximum size of the canvas element cache.
|
|
||||||
*/
|
|
||||||
canvasElementCacheSize: number;
|
|
||||||
/**
|
|
||||||
* The maximum size of the generation mode cache.
|
|
||||||
*/
|
|
||||||
generationModeCacheSize: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG: CanvasCacheModuleConfig = {
|
|
||||||
imageNameCacheSize: 100,
|
|
||||||
canvasElementCacheSize: 32,
|
|
||||||
generationModeCacheSize: 100,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A cache module for storing the results of expensive calculations. For example, when we rasterize a layer and upload
|
|
||||||
* it to the server, we store the resultant image name in this cache for future use.
|
|
||||||
*/
|
|
||||||
export class CanvasCacheModule extends CanvasModuleBase {
|
|
||||||
readonly type = 'cache';
|
readonly type = 'cache';
|
||||||
readonly id: string;
|
|
||||||
readonly path: string[];
|
|
||||||
readonly log: Logger;
|
|
||||||
readonly parent: CanvasManager;
|
|
||||||
readonly manager: CanvasManager;
|
|
||||||
|
|
||||||
config: CanvasCacheModuleConfig = DEFAULT_CONFIG;
|
id: string;
|
||||||
|
path: string[];
|
||||||
|
log: Logger;
|
||||||
|
manager: CanvasManager;
|
||||||
|
subscriptions = new Set<() => void>();
|
||||||
|
|
||||||
/**
|
imageNameCache = new LRUCache<string, string>({ max: 100 });
|
||||||
* A cache for storing image names. Used as a cache for results of layer/canvas/entity exports. For example, when we
|
canvasElementCache = new LRUCache<string, HTMLCanvasElement>({ max: 32 });
|
||||||
* rasterize a layer and upload it to the server, we store the image name in this cache.
|
generationModeCache = new LRUCache<string, GenerationMode>({ max: 100 });
|
||||||
*
|
|
||||||
* The cache key is a hash of the exported entity's state and the export rect.
|
|
||||||
*/
|
|
||||||
imageNameCache = new LRUCache<string, string>({ max: this.config.imageNameCacheSize });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A cache for storing canvas elements. Similar to the image name cache, but for canvas elements. The primary use is
|
|
||||||
* for caching composite layers. For example, the canvas compositor module uses this to store the canvas elements for
|
|
||||||
* individual raster layers when creating a composite of the layers.
|
|
||||||
*
|
|
||||||
* The cache key is a hash of the exported entity's state and the export rect.
|
|
||||||
*/
|
|
||||||
canvasElementCache = new LRUCache<string, HTMLCanvasElement>({ max: this.config.canvasElementCacheSize });
|
|
||||||
/**
|
|
||||||
* A cache for the generation mode calculation, which is fairly expensive.
|
|
||||||
*
|
|
||||||
* The cache key is a hash of all the objects that contribute to the generation mode calculation (e.g. the composite
|
|
||||||
* raster layer, the composite inpaint mask, and bounding box), and the value is the generation mode.
|
|
||||||
*/
|
|
||||||
generationModeCache = new LRUCache<string, GenerationMode>({ max: this.config.generationModeCacheSize });
|
|
||||||
|
|
||||||
constructor(manager: CanvasManager) {
|
constructor(manager: CanvasManager) {
|
||||||
super();
|
super();
|
||||||
this.id = getPrefixedId('cache');
|
this.id = getPrefixedId('cache');
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.parent = manager;
|
this.path = this.manager.path.concat(this.id);
|
||||||
this.path = this.manager.buildPath(this);
|
this.log = this.manager.buildLogger(this.getLoggingContext);
|
||||||
this.log = this.manager.buildLogger(this);
|
|
||||||
|
|
||||||
this.log.debug('Creating cache module');
|
this.log.debug('Creating cache module');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all caches.
|
|
||||||
*/
|
|
||||||
clearAll = () => {
|
clearAll = () => {
|
||||||
this.canvasElementCache.clear();
|
this.canvasElementCache.clear();
|
||||||
this.imageNameCache.clear();
|
this.imageNameCache.clear();
|
||||||
this.generationModeCache.clear();
|
this.generationModeCache.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
repr = () => {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
path: this.path,
|
||||||
|
type: this.type,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
destroy = () => {
|
destroy = () => {
|
||||||
this.log.debug('Destroying cache module');
|
this.log.debug('Destroying cache module');
|
||||||
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||||
this.clearAll();
|
this.clearAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getLoggingContext = () => {
|
||||||
|
return { ...this.manager.getLoggingContext(), path: this.path.join('.') };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user