mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-18 08:48:03 -05:00
Compare commits
689 Commits
v4.2.9.dev
...
v5.0.0.dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8dadc8c0a | ||
|
|
d421eaeff0 | ||
|
|
d434e80e99 | ||
|
|
918ed5c264 | ||
|
|
2f98c876df | ||
|
|
2b9f9f3d68 | ||
|
|
f326bfe1b0 | ||
|
|
c57966ec38 | ||
|
|
3c5bf48810 | ||
|
|
c0db830398 | ||
|
|
3e3e00b24d | ||
|
|
640af85c93 | ||
|
|
81060eef71 | ||
|
|
ea193903ee | ||
|
|
5a251a4876 | ||
|
|
ce93fdd076 | ||
|
|
788e03fbc3 | ||
|
|
8cb1584379 | ||
|
|
63a2b6cdfe | ||
|
|
b193bc7b14 | ||
|
|
dce488c691 | ||
|
|
615b09d453 | ||
|
|
1dd521a792 | ||
|
|
f6a703c367 | ||
|
|
5a69b62a1a | ||
|
|
c4b9ba8e12 | ||
|
|
c04790158f | ||
|
|
a0ed7baf73 | ||
|
|
f96148ca52 | ||
|
|
96786ed62b | ||
|
|
cc9cc62707 | ||
|
|
07cf5807be | ||
|
|
013414e962 | ||
|
|
10bba49e28 | ||
|
|
7888b4913a | ||
|
|
27b00cedc5 | ||
|
|
0bfdd5acb8 | ||
|
|
6e6629ad74 | ||
|
|
fe257e3e46 | ||
|
|
0b1dc365d0 | ||
|
|
c75a933b17 | ||
|
|
f1525c2a82 | ||
|
|
8266c10c53 | ||
|
|
34554f8555 | ||
|
|
200dbbbf55 | ||
|
|
5a30ff6045 | ||
|
|
73e8837d45 | ||
|
|
6ba78e7632 | ||
|
|
736fabe327 | ||
|
|
ad53169eba | ||
|
|
0a22e6b5a1 | ||
|
|
5eb9e2ba04 | ||
|
|
376a626419 | ||
|
|
5d089ecc77 | ||
|
|
3ed85d821b | ||
|
|
7c29caa245 | ||
|
|
6161c71aa7 | ||
|
|
f7e68ac6d6 | ||
|
|
5074c97683 | ||
|
|
bb6b5c4941 | ||
|
|
ff12f16343 | ||
|
|
397038e6e8 | ||
|
|
fcacc0f70a | ||
|
|
3ed4cf63ae | ||
|
|
4b753941e4 | ||
|
|
dbff929be5 | ||
|
|
44be058ac7 | ||
|
|
437129cf70 | ||
|
|
2e4a48b71c | ||
|
|
618473db02 | ||
|
|
4f4eee76b9 | ||
|
|
1628eb5900 | ||
|
|
92c754df79 | ||
|
|
921ade4b0a | ||
|
|
c38b05c002 | ||
|
|
277708d69e | ||
|
|
02061faaf0 | ||
|
|
a4fb8eb171 | ||
|
|
d3c2432bfb | ||
|
|
0c3e2c45f1 | ||
|
|
d019dbdbd3 | ||
|
|
2538b348c7 | ||
|
|
420178c09f | ||
|
|
75ecc56ca5 | ||
|
|
874b96c67d | ||
|
|
b0eddd19d4 | ||
|
|
cb9d0bce56 | ||
|
|
fd6d080fb0 | ||
|
|
8f6e6960e0 | ||
|
|
58064a1ea5 | ||
|
|
0d94a89d98 | ||
|
|
eb4d447e2f | ||
|
|
487422a53f | ||
|
|
b24f8e2b26 | ||
|
|
fd22ff7c9f | ||
|
|
9cd5107955 | ||
|
|
fd7f0056e3 | ||
|
|
b1673b9222 | ||
|
|
f4e8799561 | ||
|
|
6f85743996 | ||
|
|
243e7c954e | ||
|
|
33d25c7977 | ||
|
|
d11251f380 | ||
|
|
d5e5cae398 | ||
|
|
fa80452abf | ||
|
|
c9e396bee9 | ||
|
|
fd6de78746 | ||
|
|
6096c8ed0a | ||
|
|
5126ec114a | ||
|
|
221c51b498 | ||
|
|
28db39a932 | ||
|
|
5512f80066 | ||
|
|
70e27d6fbb | ||
|
|
e91149111e | ||
|
|
ad7919b3ec | ||
|
|
908f4117fc | ||
|
|
87b294708a | ||
|
|
a0be7c47cd | ||
|
|
87bc3f5f0a | ||
|
|
14dea8bad3 | ||
|
|
9ced9efc64 | ||
|
|
b0414a81ec | ||
|
|
f078e325f8 | ||
|
|
1a5dc202cb | ||
|
|
79d7f023e8 | ||
|
|
913912d96b | ||
|
|
cfa22b345a | ||
|
|
ce4948ec23 | ||
|
|
86d10ca87b | ||
|
|
057b5ec646 | ||
|
|
16eede4288 | ||
|
|
e3c0c638c1 | ||
|
|
e57ee8db35 | ||
|
|
cd2a80cf77 | ||
|
|
300ecd9b16 | ||
|
|
a95fbfe6c6 | ||
|
|
79168b637e | ||
|
|
42c7ddebaa | ||
|
|
ac349573f2 | ||
|
|
28feb013d7 | ||
|
|
4129cddbeb | ||
|
|
4463e6da5d | ||
|
|
db0b97cca1 | ||
|
|
be58b03137 | ||
|
|
2df0056ef1 | ||
|
|
eeca521baf | ||
|
|
93b9792096 | ||
|
|
a8079e5854 | ||
|
|
61f9ba5b46 | ||
|
|
a99ff467f6 | ||
|
|
5abad87ce3 | ||
|
|
931a4ddc88 | ||
|
|
784a615864 | ||
|
|
697c81b74e | ||
|
|
d6b8cb9e7c | ||
|
|
f4a031a412 | ||
|
|
1749abbd97 | ||
|
|
1497afcfda | ||
|
|
ec9fdace22 | ||
|
|
c1a039ef91 | ||
|
|
69d1edc036 | ||
|
|
b69b001755 | ||
|
|
97414f1886 | ||
|
|
478daf8614 | ||
|
|
c74b130303 | ||
|
|
11bc318d8d | ||
|
|
77125bee7e | ||
|
|
4354cd7c38 | ||
|
|
0e31ccaea0 | ||
|
|
e32fa8bd35 | ||
|
|
d209652caa | ||
|
|
fe672ba5e0 | ||
|
|
04d41085a3 | ||
|
|
36604f752e | ||
|
|
9056f446bb | ||
|
|
a33d1b979d | ||
|
|
4670f82e65 | ||
|
|
a07346b364 | ||
|
|
2b1e930cdf | ||
|
|
6b75ea3b01 | ||
|
|
ff8bc93080 | ||
|
|
85eff566dd | ||
|
|
9480691de5 | ||
|
|
463f3dbb35 | ||
|
|
72f304baa5 | ||
|
|
b4d01659e0 | ||
|
|
a733d72089 | ||
|
|
8d0a75cb5f | ||
|
|
8a56702341 | ||
|
|
af638cf5ce | ||
|
|
839248c74c | ||
|
|
4b488de10e | ||
|
|
112d6ead91 | ||
|
|
a5e2e78dee | ||
|
|
b91f79b0bb | ||
|
|
bc75b0259b | ||
|
|
51663c5439 | ||
|
|
86a5e2538c | ||
|
|
ee94b0ce17 | ||
|
|
03b3139705 | ||
|
|
44b8480832 | ||
|
|
e6960320dd | ||
|
|
775353fe82 | ||
|
|
e013aff9fe | ||
|
|
a22bf4c296 | ||
|
|
12a00de2ec | ||
|
|
6cbaf7e0ae | ||
|
|
9b8d814082 | ||
|
|
20a0ed81c5 | ||
|
|
a040d1d2a6 | ||
|
|
7c68b889eb | ||
|
|
5ea3c11883 | ||
|
|
f355d907e6 | ||
|
|
da5b99c840 | ||
|
|
4f9c4f7b40 | ||
|
|
143c47c887 | ||
|
|
2ca5aeda96 | ||
|
|
9b1ee633e6 | ||
|
|
c06998184e | ||
|
|
84aa2b94e5 | ||
|
|
64f7c7bf36 | ||
|
|
18d9263d60 | ||
|
|
e0e8165992 | ||
|
|
e826f8a020 | ||
|
|
9486c50e67 | ||
|
|
66424c3c93 | ||
|
|
cff871e8a6 | ||
|
|
51cd435ad8 | ||
|
|
bc8bf989f3 | ||
|
|
87150b7c6b | ||
|
|
5797797904 | ||
|
|
ab7b9c4523 | ||
|
|
68bf4459c3 | ||
|
|
05052fd21c | ||
|
|
f4e3f87c2e | ||
|
|
f47e1afd57 | ||
|
|
aa4be01bdf | ||
|
|
f8d4d5e546 | ||
|
|
f628b8ad9d | ||
|
|
bb4eb70a4a | ||
|
|
7b1a533bf2 | ||
|
|
d32526f04a | ||
|
|
76bc2cf5db | ||
|
|
90b3ebb27d | ||
|
|
4b65d9145e | ||
|
|
9badbb8dff | ||
|
|
70bcca23a4 | ||
|
|
5ba15d0db5 | ||
|
|
fef70fdac0 | ||
|
|
62eb00aacf | ||
|
|
3f41ea574b | ||
|
|
e865c1a2b5 | ||
|
|
f4a798c64f | ||
|
|
7806604666 | ||
|
|
917f8087b4 | ||
|
|
b7a9a6153a | ||
|
|
fdc5f89060 | ||
|
|
7fc0959c83 | ||
|
|
95ff64d1d6 | ||
|
|
67f94007d0 | ||
|
|
930f27d009 | ||
|
|
6125e0fd13 | ||
|
|
c8e8cf9dfb | ||
|
|
ac8122a154 | ||
|
|
aa7c909f14 | ||
|
|
40d294b29c | ||
|
|
ff1fd5b5b5 | ||
|
|
6aa87d7df2 | ||
|
|
7732eeb815 | ||
|
|
9eb37feb6c | ||
|
|
e3b38339a0 | ||
|
|
f92695e677 | ||
|
|
9473d54fc7 | ||
|
|
33783eae4f | ||
|
|
f6c3570c26 | ||
|
|
96a76a763e | ||
|
|
209bc925c0 | ||
|
|
a77fb427d0 | ||
|
|
6fc358b9ea | ||
|
|
742ac3088d | ||
|
|
8b958e5280 | ||
|
|
71c727e586 | ||
|
|
c590e8bd6f | ||
|
|
71a13c3e91 | ||
|
|
d7e7f049e7 | ||
|
|
b23332e167 | ||
|
|
21c637c365 | ||
|
|
2b7aecb346 | ||
|
|
5c669f7925 | ||
|
|
748f445de5 | ||
|
|
2127ee9a09 | ||
|
|
7ccb1c5938 | ||
|
|
26d1a2ebfe | ||
|
|
e0bd683d88 | ||
|
|
3c5332b236 | ||
|
|
d4e847bd9a | ||
|
|
cee16536cd | ||
|
|
811e75dc67 | ||
|
|
a4a77d70f7 | ||
|
|
13e2de3cb4 | ||
|
|
7925bbf454 | ||
|
|
39eb0a01d2 | ||
|
|
305e50004a | ||
|
|
9974784596 | ||
|
|
105c5a7fd4 | ||
|
|
7baad9c72e | ||
|
|
a5e8705ea3 | ||
|
|
7b2a5c3a30 | ||
|
|
3dcb33026a | ||
|
|
e956ea5482 | ||
|
|
64f50ab278 | ||
|
|
b152937f30 | ||
|
|
4e6a9d990c | ||
|
|
d6fb220b2c | ||
|
|
13fc539f8b | ||
|
|
d2b5d6342c | ||
|
|
eb644d4e6a | ||
|
|
d8a2efc691 | ||
|
|
163687aef3 | ||
|
|
b41f1a897d | ||
|
|
006a5723da | ||
|
|
daede9c9cf | ||
|
|
af7d14cd59 | ||
|
|
ba14fe3600 | ||
|
|
af726d1f15 | ||
|
|
3a2efb351d | ||
|
|
c9dc61c311 | ||
|
|
500f151d96 | ||
|
|
9708fc5d6c | ||
|
|
c697501285 | ||
|
|
e213cfc2ba | ||
|
|
270c1304a8 | ||
|
|
b887cf4612 | ||
|
|
e0573b721e | ||
|
|
22712c5dac | ||
|
|
7339b3d8cc | ||
|
|
0eda34b41f | ||
|
|
9d9e845198 | ||
|
|
6b7ead4461 | ||
|
|
3ce3056c4a | ||
|
|
aa73cbf459 | ||
|
|
fa3f109eb9 | ||
|
|
c22afa5725 | ||
|
|
318086571d | ||
|
|
260ef8edd5 | ||
|
|
a3a933a797 | ||
|
|
d2d298604c | ||
|
|
318f2ee003 | ||
|
|
657d59268c | ||
|
|
54b7931779 | ||
|
|
aadba55796 | ||
|
|
1a8d65d7a9 | ||
|
|
36f7d0957a | ||
|
|
85a33ff6aa | ||
|
|
ebef4feddb | ||
|
|
0a34bebd9c | ||
|
|
d6acd96dec | ||
|
|
3ac88acf11 | ||
|
|
2a7cffed2a | ||
|
|
08c2089b3d | ||
|
|
87521b07ce | ||
|
|
5de7efc1dc | ||
|
|
d4fb80772e | ||
|
|
97886bf62e | ||
|
|
83657e0a68 | ||
|
|
adf293f6c9 | ||
|
|
d35120feb9 | ||
|
|
00020f49fe | ||
|
|
206d1b231a | ||
|
|
008d8f491c | ||
|
|
ae5543b6fa | ||
|
|
2c2e6c5c25 | ||
|
|
718bed5758 | ||
|
|
d2c524e7fd | ||
|
|
c172221038 | ||
|
|
b006edf9c4 | ||
|
|
9f2dc4d6d1 | ||
|
|
8816d6c0c5 | ||
|
|
6dddad01fc | ||
|
|
7b7f0a7380 | ||
|
|
8f780e79e7 | ||
|
|
5a2b00fb95 | ||
|
|
25158d4d92 | ||
|
|
3c9e9fa746 | ||
|
|
e5a7e932cf | ||
|
|
84d15b0c7f | ||
|
|
79f216a491 | ||
|
|
b86645c1a7 | ||
|
|
3f359f1059 | ||
|
|
7c7117c39a | ||
|
|
92b8c68b94 | ||
|
|
7b7d722bae | ||
|
|
227c3197b4 | ||
|
|
cab1ba8970 | ||
|
|
7395c94d00 | ||
|
|
2a2dadb952 | ||
|
|
b60dcf8036 | ||
|
|
edeb706d19 | ||
|
|
cd76a2d217 | ||
|
|
2aaa6c3986 | ||
|
|
dfdd56dbec | ||
|
|
f521704ade | ||
|
|
0e0cf9cd3e | ||
|
|
60484bd45e | ||
|
|
5978ba32c0 | ||
|
|
45efa8f40d | ||
|
|
3392e1f0bf | ||
|
|
c734385f18 | ||
|
|
148434d833 | ||
|
|
d5e4a965cc | ||
|
|
c8ac4d9e1b | ||
|
|
1e17461601 | ||
|
|
4432244bc3 | ||
|
|
0d087f84a5 | ||
|
|
286deb277b | ||
|
|
3f2c1139ea | ||
|
|
1f3163942a | ||
|
|
28affcc60a | ||
|
|
f59c2cf7f5 | ||
|
|
86ff52ec36 | ||
|
|
17e7364bdb | ||
|
|
71e4b60a30 | ||
|
|
a3e1ff637d | ||
|
|
a3d2ba1444 | ||
|
|
9baa594a56 | ||
|
|
49de11c3ae | ||
|
|
690fbdc73d | ||
|
|
4d52824895 | ||
|
|
2e13e75fc6 | ||
|
|
f6f6462590 | ||
|
|
79fee16629 | ||
|
|
073f63251a | ||
|
|
20125dc04b | ||
|
|
9f1f8d62f0 | ||
|
|
99d432785c | ||
|
|
54e5401a96 | ||
|
|
6b5d7406d6 | ||
|
|
c6bfeba61a | ||
|
|
af1c8cc7e0 | ||
|
|
879161ed4c | ||
|
|
a15944774c | ||
|
|
42612a4f92 | ||
|
|
5c39935e88 | ||
|
|
35c941c540 | ||
|
|
92931b0d4d | ||
|
|
7c3d2f5578 | ||
|
|
9c809ba147 | ||
|
|
25fb1bb837 | ||
|
|
2d01086a3e | ||
|
|
f8af1e9014 | ||
|
|
2b34a5c646 | ||
|
|
22ca3db870 | ||
|
|
247ca97fbd | ||
|
|
b346b25a7b | ||
|
|
496cf3da4f | ||
|
|
f1b0130389 | ||
|
|
39db3be151 | ||
|
|
517ad7e77c | ||
|
|
5fa10a3f8e | ||
|
|
aa3986e9f2 | ||
|
|
9105c02681 | ||
|
|
6632727d00 | ||
|
|
34ccd5aa86 | ||
|
|
23979bdbee | ||
|
|
dda53292bf | ||
|
|
c98c5f13f7 | ||
|
|
2a69967863 | ||
|
|
781ef806de | ||
|
|
8794c51e42 | ||
|
|
9e89ddf2f1 | ||
|
|
f480a89e7a | ||
|
|
df44fb9827 | ||
|
|
fca9cacc4e | ||
|
|
fa7783972b | ||
|
|
e50b4280f2 | ||
|
|
17c9ccfdb0 | ||
|
|
d35af4c048 | ||
|
|
5fef9bbceb | ||
|
|
507acaa7c9 | ||
|
|
28eb9b62a8 | ||
|
|
061eeb809f | ||
|
|
0db5c6ac8e | ||
|
|
2fd6fd4624 | ||
|
|
db67ae2de4 | ||
|
|
fedcebbe4d | ||
|
|
eaaeb356d7 | ||
|
|
0cddbc2420 | ||
|
|
1b44520ff7 | ||
|
|
ee4dc86c15 | ||
|
|
cbc61daa56 | ||
|
|
f1cd6a06ec | ||
|
|
70248b9684 | ||
|
|
0a5cf8ae42 | ||
|
|
4dc5b18671 | ||
|
|
daffb950c3 | ||
|
|
1a6ebb9606 | ||
|
|
e397efb622 | ||
|
|
748cb0bb8d | ||
|
|
233a449e53 | ||
|
|
09ad8b6238 | ||
|
|
778f703d3d | ||
|
|
8234e4e168 | ||
|
|
08e588a6c4 | ||
|
|
43f550e48b | ||
|
|
5663029ba6 | ||
|
|
0869e23dc1 | ||
|
|
fc4779e095 | ||
|
|
1667603d1c | ||
|
|
81b210cf14 | ||
|
|
ca5fde8ef5 | ||
|
|
fef88734d0 | ||
|
|
aa1727d16f | ||
|
|
7c1afb6493 | ||
|
|
64e7757872 | ||
|
|
3b08250331 | ||
|
|
cd02638db6 | ||
|
|
5109811182 | ||
|
|
33ba8cabd1 | ||
|
|
dcac6028a2 | ||
|
|
25ab435129 | ||
|
|
cea5dc6216 | ||
|
|
bc525d29e1 | ||
|
|
cdfe0ca150 | ||
|
|
bd679e018d | ||
|
|
a6ee18448a | ||
|
|
7d96b3e89e | ||
|
|
e6723f194a | ||
|
|
e55541ea87 | ||
|
|
2676ff8ee3 | ||
|
|
f024fe4488 | ||
|
|
0d3dfb8d0f | ||
|
|
65e1951f5d | ||
|
|
fd6eb91f79 | ||
|
|
2aea0f2ac5 | ||
|
|
e1b5aa7011 | ||
|
|
cdc4d29745 | ||
|
|
5d00792e1f | ||
|
|
1c334f3231 | ||
|
|
9ba3182d19 | ||
|
|
285ad448d7 | ||
|
|
e81fedba43 | ||
|
|
6a47f973b7 | ||
|
|
7aa918cd46 | ||
|
|
183c9dd736 | ||
|
|
5621075cb7 | ||
|
|
b13d2087c2 | ||
|
|
89740af2ab | ||
|
|
4329dfd128 | ||
|
|
bb18a82a9c | ||
|
|
d073fe467d | ||
|
|
e37e885546 | ||
|
|
471ded85f7 | ||
|
|
e084655e69 | ||
|
|
330acb55f4 | ||
|
|
e1ace99e05 | ||
|
|
c0bfa07ea7 | ||
|
|
f4fceac372 | ||
|
|
c1f5345987 | ||
|
|
05992130d0 | ||
|
|
9c646712e0 | ||
|
|
0edff49957 | ||
|
|
0f709cb06a | ||
|
|
656dbbb9f1 | ||
|
|
9497a75c95 | ||
|
|
30c4ed87b5 | ||
|
|
e839765ddc | ||
|
|
055737a6e8 | ||
|
|
fbc609230a | ||
|
|
46f86a54c1 | ||
|
|
70f5231020 | ||
|
|
0ec0feed2c | ||
|
|
60cd505ee1 | ||
|
|
205a719649 | ||
|
|
af7d222a1e | ||
|
|
70f430f635 | ||
|
|
2a92a223f6 | ||
|
|
1ed43614c2 | ||
|
|
3a5295574f | ||
|
|
475b1cb1b8 | ||
|
|
55260a886d | ||
|
|
f77e03ceb0 | ||
|
|
4f7bf5ad58 | ||
|
|
460ea1aa07 | ||
|
|
7aa5f5cb80 | ||
|
|
9cc4133184 | ||
|
|
0d3721324d | ||
|
|
510249d282 | ||
|
|
77d840593b | ||
|
|
8c9a5c4ab5 | ||
|
|
ce497fff27 | ||
|
|
c3cada6bd5 | ||
|
|
bbacfe403c | ||
|
|
159031a071 | ||
|
|
4067927a23 | ||
|
|
d090343083 | ||
|
|
ed130366db | ||
|
|
d4ae40fec4 | ||
|
|
17c864dba3 | ||
|
|
398d6d1efd | ||
|
|
30b46b0f9b | ||
|
|
2d123fa11c | ||
|
|
352a651ac0 | ||
|
|
26b971978d | ||
|
|
478324ea62 | ||
|
|
fa447b2813 | ||
|
|
322790bfdb | ||
|
|
9ea7c18d66 | ||
|
|
2a0d16d57d | ||
|
|
033e2b27a9 | ||
|
|
c7af454bf5 | ||
|
|
d2be9df7a7 | ||
|
|
ae81f4e20a | ||
|
|
261c24b704 | ||
|
|
cc1995170e | ||
|
|
86c785fded | ||
|
|
31c2b1af19 | ||
|
|
90b973f529 | ||
|
|
1164763e1a | ||
|
|
b4319630b1 | ||
|
|
0586c6bdf2 | ||
|
|
c2f60f33e6 | ||
|
|
b118bc959d | ||
|
|
6ef5c2e0c3 | ||
|
|
728fd5b758 | ||
|
|
5bae455e4e | ||
|
|
9387903491 | ||
|
|
12b67bd556 | ||
|
|
9a30bcbd94 | ||
|
|
ab0f096bf2 | ||
|
|
2d8a29d2fd | ||
|
|
8d7de1543b | ||
|
|
b1b41a9b0c | ||
|
|
e9033040f6 | ||
|
|
4388f00607 | ||
|
|
9b8db06349 | ||
|
|
a4f55f6e5d | ||
|
|
d0cde66e92 | ||
|
|
f636c0eb88 | ||
|
|
5760d3180e | ||
|
|
8c4f98131b | ||
|
|
959a433e61 | ||
|
|
f72845a1b4 | ||
|
|
254f4ba574 | ||
|
|
d6f3b1b85f | ||
|
|
47b5b7c4b4 | ||
|
|
bcfdae62e3 | ||
|
|
c60f1c0031 | ||
|
|
d76802f563 | ||
|
|
7b39b31f6c | ||
|
|
a2585a8bb1 | ||
|
|
fe0c4767c7 | ||
|
|
e0b8b82a15 | ||
|
|
f90fa85e77 | ||
|
|
6d5b4e4471 | ||
|
|
1172a33117 | ||
|
|
8530f8ddcc | ||
|
|
e66e4fefed | ||
|
|
1237e839ca | ||
|
|
8ec08063f4 | ||
|
|
0d0004018b | ||
|
|
9124604dc4 | ||
|
|
4f05a7b8d0 | ||
|
|
db766bd9ae | ||
|
|
557603d2ae | ||
|
|
b46ca55c7d | ||
|
|
4788172206 | ||
|
|
6a118f172e | ||
|
|
d5abdfa3b0 | ||
|
|
1d24cb94b4 | ||
|
|
86e7f24238 | ||
|
|
0356f970f3 | ||
|
|
d340038f48 | ||
|
|
1d5e8e07c6 | ||
|
|
24f17479de | ||
|
|
84b8096cc9 | ||
|
|
5c38241e33 | ||
|
|
b590c73c08 | ||
|
|
68fcf9d2df | ||
|
|
bda579577c | ||
|
|
a16b555d47 | ||
|
|
6667c39c73 | ||
|
|
5219ac12a6 | ||
|
|
445f813fb9 | ||
|
|
87f9e59cfb | ||
|
|
8b03b39aa8 | ||
|
|
e59b6bb971 | ||
|
|
24a7ed467c | ||
|
|
f01f1033ac | ||
|
|
d35f515413 |
@@ -11,7 +11,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
||||
Batch,
|
||||
BatchStatus,
|
||||
CancelByBatchIDsResult,
|
||||
CancelByOriginResult,
|
||||
CancelByDestinationResult,
|
||||
ClearResult,
|
||||
EnqueueBatchResult,
|
||||
PruneResult,
|
||||
@@ -107,16 +107,18 @@ async def cancel_by_batch_ids(
|
||||
|
||||
|
||||
@session_queue_router.put(
|
||||
"/{queue_id}/cancel_by_origin",
|
||||
operation_id="cancel_by_origin",
|
||||
"/{queue_id}/cancel_by_destination",
|
||||
operation_id="cancel_by_destination",
|
||||
responses={200: {"model": CancelByBatchIDsResult}},
|
||||
)
|
||||
async def cancel_by_origin(
|
||||
async def cancel_by_destination(
|
||||
queue_id: str = Path(description="The queue id to perform this operation on"),
|
||||
origin: str = Query(description="The origin to cancel all queue items for"),
|
||||
) -> CancelByOriginResult:
|
||||
destination: str = Query(description="The destination to cancel all queue items for"),
|
||||
) -> CancelByDestinationResult:
|
||||
"""Immediately cancels all queue items with the given origin"""
|
||||
return ApiDependencies.invoker.services.session_queue.cancel_by_origin(queue_id=queue_id, origin=origin)
|
||||
return ApiDependencies.invoker.services.session_queue.cancel_by_destination(
|
||||
queue_id=queue_id, destination=destination
|
||||
)
|
||||
|
||||
|
||||
@session_queue_router.put(
|
||||
|
||||
@@ -6,7 +6,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
||||
Batch,
|
||||
BatchStatus,
|
||||
CancelByBatchIDsResult,
|
||||
CancelByOriginResult,
|
||||
CancelByDestinationResult,
|
||||
CancelByQueueIDResult,
|
||||
ClearResult,
|
||||
EnqueueBatchResult,
|
||||
@@ -97,8 +97,8 @@ class SessionQueueBase(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult:
|
||||
"""Cancels all queue items with the given batch origin"""
|
||||
def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDestinationResult:
|
||||
"""Cancels all queue items with the given batch destination"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -346,10 +346,10 @@ class CancelByBatchIDsResult(BaseModel):
|
||||
canceled: int = Field(..., description="Number of queue items canceled")
|
||||
|
||||
|
||||
class CancelByOriginResult(BaseModel):
|
||||
"""Result of canceling by list of batch ids"""
|
||||
class CancelByDestinationResult(CancelByBatchIDsResult):
|
||||
"""Result of canceling by a destination"""
|
||||
|
||||
canceled: int = Field(..., description="Number of queue items canceled")
|
||||
pass
|
||||
|
||||
|
||||
class CancelByQueueIDResult(CancelByBatchIDsResult):
|
||||
|
||||
@@ -10,7 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
||||
Batch,
|
||||
BatchStatus,
|
||||
CancelByBatchIDsResult,
|
||||
CancelByOriginResult,
|
||||
CancelByDestinationResult,
|
||||
CancelByQueueIDResult,
|
||||
ClearResult,
|
||||
EnqueueBatchResult,
|
||||
@@ -426,19 +426,19 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
self.__lock.release()
|
||||
return CancelByBatchIDsResult(canceled=count)
|
||||
|
||||
def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult:
|
||||
def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDestinationResult:
|
||||
try:
|
||||
current_queue_item = self.get_current(queue_id)
|
||||
self.__lock.acquire()
|
||||
where = """--sql
|
||||
WHERE
|
||||
queue_id == ?
|
||||
AND origin == ?
|
||||
AND destination == ?
|
||||
AND status != 'canceled'
|
||||
AND status != 'completed'
|
||||
AND status != 'failed'
|
||||
"""
|
||||
params = (queue_id, origin)
|
||||
params = (queue_id, destination)
|
||||
self.__cursor.execute(
|
||||
f"""--sql
|
||||
SELECT COUNT(*)
|
||||
@@ -457,14 +457,14 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
params,
|
||||
)
|
||||
self.__conn.commit()
|
||||
if current_queue_item is not None and current_queue_item.origin == origin:
|
||||
if current_queue_item is not None and current_queue_item.destination == destination:
|
||||
self._set_queue_item_status(current_queue_item.item_id, "canceled")
|
||||
except Exception:
|
||||
self.__conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
self.__lock.release()
|
||||
return CancelByOriginResult(canceled=count)
|
||||
return CancelByDestinationResult(canceled=count)
|
||||
|
||||
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
|
||||
try:
|
||||
|
||||
@@ -32,7 +32,9 @@ from invokeai.backend.model_manager.config import (
|
||||
)
|
||||
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.util.model_util import convert_bundle_to_flux_transformer_checkpoint
|
||||
from invokeai.backend.model_manager.util.model_util import (
|
||||
convert_bundle_to_flux_transformer_checkpoint,
|
||||
)
|
||||
from invokeai.backend.util.silence_warnings import SilenceWarnings
|
||||
|
||||
try:
|
||||
@@ -193,6 +195,11 @@ class FluxCheckpointModel(ModelLoader):
|
||||
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)
|
||||
new_sd_size = sum([ten.nelement() * torch.bfloat16.itemsize for ten in sd.values()])
|
||||
self._ram_cache.make_room(new_sd_size)
|
||||
for k in sd.keys():
|
||||
# We need to cast to bfloat16 due to it being the only currently supported dtype for inference
|
||||
sd[k] = sd[k].to(torch.bfloat16)
|
||||
model.load_state_dict(sd, assign=True)
|
||||
return model
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ const config: KnipConfig = {
|
||||
'src/features/nodes/types/v2/**',
|
||||
// TODO(psyche): maybe we can clean up these utils after canvas v2 release
|
||||
'src/features/controlLayers/konva/util.ts',
|
||||
// TODO(psyche): restore HRF functionality?
|
||||
'src/features/hrf/**',
|
||||
],
|
||||
ignoreBinaries: ['only-allow'],
|
||||
paths: {
|
||||
|
||||
@@ -127,7 +127,14 @@
|
||||
"bulkDownloadRequestedDesc": "Dein Download wird vorbereitet. Dies kann ein paar Momente dauern.",
|
||||
"bulkDownloadRequestFailed": "Problem beim Download vorbereiten",
|
||||
"bulkDownloadFailed": "Download fehlgeschlagen",
|
||||
"alwaysShowImageSizeBadge": "Zeige immer Bilder Größe Abzeichen"
|
||||
"alwaysShowImageSizeBadge": "Zeige immer Bilder Größe Abzeichen",
|
||||
"selectForCompare": "Zum Vergleichen auswählen",
|
||||
"compareImage": "Bilder vergleichen",
|
||||
"exitSearch": "Suche beenden",
|
||||
"newestFirst": "Neueste zuerst",
|
||||
"oldestFirst": "Älteste zuerst",
|
||||
"openInViewer": "Im Viewer öffnen",
|
||||
"swapImages": "Bilder tauschen"
|
||||
},
|
||||
"hotkeys": {
|
||||
"keyboardShortcuts": "Tastenkürzel",
|
||||
@@ -631,7 +638,8 @@
|
||||
"archived": "Archiviert",
|
||||
"noBoards": "Kein {boardType}} Ordner",
|
||||
"hideBoards": "Ordner verstecken",
|
||||
"viewBoards": "Ordner ansehen"
|
||||
"viewBoards": "Ordner ansehen",
|
||||
"deletedPrivateBoardsCannotbeRestored": "Gelöschte Boards können nicht wiederhergestellt werden. Wenn Sie „Nur Board löschen“ wählen, werden die Bilder in einen privaten, nicht kategorisierten Status für den Ersteller des Bildes versetzt."
|
||||
},
|
||||
"controlnet": {
|
||||
"showAdvanced": "Zeige Erweitert",
|
||||
@@ -781,7 +789,9 @@
|
||||
"batchFieldValues": "Stapelverarbeitungswerte",
|
||||
"batchQueued": "Stapelverarbeitung eingereiht",
|
||||
"graphQueued": "Graph eingereiht",
|
||||
"graphFailedToQueue": "Fehler beim Einreihen des Graphen"
|
||||
"graphFailedToQueue": "Fehler beim Einreihen des Graphen",
|
||||
"generations_one": "Generation",
|
||||
"generations_other": "Generationen"
|
||||
},
|
||||
"metadata": {
|
||||
"negativePrompt": "Negativ Beschreibung",
|
||||
@@ -1146,5 +1156,10 @@
|
||||
"noMatchingTriggers": "Keine passenden Trigger",
|
||||
"addPromptTrigger": "Prompt-Trigger hinzufügen",
|
||||
"compatibleEmbeddings": "Kompatible Einbettungen"
|
||||
},
|
||||
"ui": {
|
||||
"tabs": {
|
||||
"queue": "Warteschlange"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
"nodes": "Workflows",
|
||||
"notInstalled": "Not $t(common.installed)",
|
||||
"openInNewTab": "Open in New Tab",
|
||||
"openInViewer": "Open in Viewer",
|
||||
"orderBy": "Order By",
|
||||
"outpaint": "outpaint",
|
||||
"outputs": "Outputs",
|
||||
@@ -1014,6 +1015,8 @@
|
||||
"noModelForControlAdapter": "Control Adapter #{{number}} has no model selected.",
|
||||
"incompatibleBaseModelForControlAdapter": "Control Adapter #{{number}} model is incompatible with main model.",
|
||||
"noModelSelected": "No model selected",
|
||||
"canvasManagerNotLoaded": "Canvas Manager not loaded",
|
||||
"canvasBusy": "Canvas is busy",
|
||||
"noPrompts": "No prompts generated",
|
||||
"noNodesInGraph": "No nodes in graph",
|
||||
"systemDisconnected": "System disconnected",
|
||||
@@ -1049,8 +1052,7 @@
|
||||
"seamlessYAxis": "Seamless Tiling Y Axis",
|
||||
"seed": "Seed",
|
||||
"imageActions": "Image Actions",
|
||||
"sendToImg2Img": "Send to Image to Image",
|
||||
"sendToUnifiedCanvas": "Send To Unified Canvas",
|
||||
"sendToCanvas": "Send To Canvas",
|
||||
"sendToUpscale": "Send To Upscale",
|
||||
"showOptionsPanel": "Show Side Panel (O or T)",
|
||||
"shuffle": "Shuffle Seed",
|
||||
@@ -1191,8 +1193,8 @@
|
||||
"problemSavingMaskDesc": "Unable to export mask",
|
||||
"prunedQueue": "Pruned Queue",
|
||||
"resetInitialImage": "Reset Initial Image",
|
||||
"sentToImageToImage": "Sent To Image To Image",
|
||||
"sentToUnifiedCanvas": "Sent to Unified Canvas",
|
||||
"sentToCanvas": "Sent to Canvas",
|
||||
"sentToUpscale": "Sent to Upscale",
|
||||
"serverError": "Server Error",
|
||||
"sessionRef": "Session: {{sessionId}}",
|
||||
"setAsCanvasInitialImage": "Set as canvas initial image",
|
||||
@@ -1655,6 +1657,7 @@
|
||||
},
|
||||
"controlLayers": {
|
||||
"bookmark": "Bookmark for Quick Switch",
|
||||
"fitBboxToLayers": "Fit Bbox To Layers",
|
||||
"removeBookmark": "Remove Bookmark",
|
||||
"saveCanvasToGallery": "Save Canvas To Gallery",
|
||||
"saveBboxToGallery": "Save Bbox To Gallery",
|
||||
|
||||
@@ -86,15 +86,15 @@
|
||||
"loadMore": "Cargar más",
|
||||
"noImagesInGallery": "No hay imágenes para mostrar",
|
||||
"deleteImage_one": "Eliminar Imagen",
|
||||
"deleteImage_many": "",
|
||||
"deleteImage_other": "",
|
||||
"deleteImage_many": "Eliminar {{count}} Imágenes",
|
||||
"deleteImage_other": "Eliminar {{count}} Imágenes",
|
||||
"deleteImagePermanent": "Las imágenes eliminadas no se pueden restaurar.",
|
||||
"assets": "Activos",
|
||||
"autoAssignBoardOnClick": "Asignación automática de tableros al hacer clic"
|
||||
},
|
||||
"hotkeys": {
|
||||
"keyboardShortcuts": "Atajos de teclado",
|
||||
"appHotkeys": "Atajos de applicación",
|
||||
"appHotkeys": "Atajos de aplicación",
|
||||
"generalHotkeys": "Atajos generales",
|
||||
"galleryHotkeys": "Atajos de galería",
|
||||
"unifiedCanvasHotkeys": "Atajos de lienzo unificado",
|
||||
@@ -535,7 +535,7 @@
|
||||
"bottomMessage": "Al eliminar este panel y las imágenes que contiene, se restablecerán las funciones que los estén utilizando actualmente.",
|
||||
"deleteBoardAndImages": "Borrar el panel y las imágenes",
|
||||
"loading": "Cargando...",
|
||||
"deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar",
|
||||
"deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al Seleccionar 'Borrar Solo el Panel' transferirá las imágenes a un estado sin categorizar.",
|
||||
"move": "Mover",
|
||||
"menuItemAutoAdd": "Agregar automáticamente a este panel",
|
||||
"searchBoard": "Buscando paneles…",
|
||||
@@ -549,7 +549,13 @@
|
||||
"imagesWithCount_other": "{{count}} imágenes",
|
||||
"assetsWithCount_one": "{{count}} activo",
|
||||
"assetsWithCount_many": "{{count}} activos",
|
||||
"assetsWithCount_other": "{{count}} activos"
|
||||
"assetsWithCount_other": "{{count}} activos",
|
||||
"hideBoards": "Ocultar Paneles",
|
||||
"addPrivateBoard": "Agregar un tablero privado",
|
||||
"addSharedBoard": "Agregar Panel Compartido",
|
||||
"boards": "Paneles",
|
||||
"archiveBoard": "Archivar Panel",
|
||||
"archived": "Archivado"
|
||||
},
|
||||
"accordions": {
|
||||
"compositing": {
|
||||
|
||||
@@ -496,7 +496,9 @@
|
||||
"main": "Principali",
|
||||
"noModelsInstalledDesc1": "Installa i modelli con",
|
||||
"ipAdapters": "Adattatori IP",
|
||||
"noMatchingModels": "Nessun modello corrispondente"
|
||||
"noMatchingModels": "Nessun modello corrispondente",
|
||||
"starterModelsInModelManager": "I modelli iniziali possono essere trovati in Gestione Modelli",
|
||||
"spandrelImageToImage": "Immagine a immagine (Spandrel)"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Immagini",
|
||||
@@ -510,7 +512,7 @@
|
||||
"perlinNoise": "Rumore Perlin",
|
||||
"type": "Tipo",
|
||||
"strength": "Forza",
|
||||
"upscaling": "Ampliamento",
|
||||
"upscaling": "Amplia",
|
||||
"scale": "Scala",
|
||||
"imageFit": "Adatta l'immagine iniziale alle dimensioni di output",
|
||||
"scaleBeforeProcessing": "Scala prima dell'elaborazione",
|
||||
@@ -593,7 +595,7 @@
|
||||
"globalPositivePromptPlaceholder": "Prompt positivo globale",
|
||||
"globalNegativePromptPlaceholder": "Prompt negativo globale",
|
||||
"processImage": "Elabora Immagine",
|
||||
"sendToUpscale": "Invia a Ampliare",
|
||||
"sendToUpscale": "Invia a Amplia",
|
||||
"postProcessing": "Post-elaborazione (Shift + U)"
|
||||
},
|
||||
"settings": {
|
||||
@@ -1420,7 +1422,7 @@
|
||||
"paramUpscaleMethod": {
|
||||
"heading": "Metodo di ampliamento",
|
||||
"paragraphs": [
|
||||
"Metodo utilizzato per eseguire l'ampliamento dell'immagine per la correzione ad alta risoluzione."
|
||||
"Metodo utilizzato per ampliare l'immagine per la correzione ad alta risoluzione."
|
||||
]
|
||||
},
|
||||
"patchmatchDownScaleSize": {
|
||||
@@ -1528,7 +1530,7 @@
|
||||
},
|
||||
"upscaleModel": {
|
||||
"paragraphs": [
|
||||
"Il modello di ampliamento (Upscale), scala l'immagine alle dimensioni di uscita prima di aggiungere i dettagli. È possibile utilizzare qualsiasi modello di ampliamento supportato, ma alcuni sono specializzati per diversi tipi di immagini, come foto o disegni al tratto."
|
||||
"Il modello di ampliamento, scala l'immagine alle dimensioni di uscita prima di aggiungere i dettagli. È possibile utilizzare qualsiasi modello di ampliamento supportato, ma alcuni sono specializzati per diversi tipi di immagini, come foto o disegni al tratto."
|
||||
],
|
||||
"heading": "Modello di ampliamento"
|
||||
},
|
||||
@@ -1720,26 +1722,27 @@
|
||||
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
||||
"queue": "Coda",
|
||||
"queueTab": "$t(ui.tabs.queue) $t(common.tab)",
|
||||
"upscaling": "Ampliamento",
|
||||
"upscaling": "Amplia",
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
"creativity": "Creatività",
|
||||
"structure": "Struttura",
|
||||
"upscaleModel": "Modello di Ampliamento",
|
||||
"upscaleModel": "Modello di ampliamento",
|
||||
"scale": "Scala",
|
||||
"missingModelsWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare i modelli richiesti:",
|
||||
"mainModelDesc": "Modello principale (architettura SD1.5 o SDXL)",
|
||||
"tileControlNetModelDesc": "Modello Tile ControlNet per l'architettura del modello principale scelto",
|
||||
"upscaleModelDesc": "Modello per l'ampliamento (da immagine a immagine)",
|
||||
"upscaleModelDesc": "Modello per l'ampliamento (immagine a immagine)",
|
||||
"missingUpscaleInitialImage": "Immagine iniziale mancante per l'ampliamento",
|
||||
"missingUpscaleModel": "Modello per l’ampliamento mancante",
|
||||
"missingTileControlNetModel": "Nessun modello ControlNet Tile valido installato",
|
||||
"postProcessingModel": "Modello di post-elaborazione",
|
||||
"postProcessingMissingModelWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare un modello di post-elaborazione (da immagine a immagine).",
|
||||
"exceedsMaxSize": "Le impostazioni di ampliamento superano il limite massimo delle dimensioni",
|
||||
"exceedsMaxSizeDetails": "Il limite massimo di ampliamento è {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Prova un'immagine più piccola o diminuisci la scala selezionata."
|
||||
"exceedsMaxSizeDetails": "Il limite massimo di ampliamento è {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Prova un'immagine più piccola o diminuisci la scala selezionata.",
|
||||
"upscale": "Amplia"
|
||||
},
|
||||
"upsell": {
|
||||
"inviteTeammates": "Invita collaboratori",
|
||||
@@ -1789,6 +1792,7 @@
|
||||
"positivePromptColumn": "'prompt' o 'positive_prompt'",
|
||||
"noTemplates": "Nessun modello",
|
||||
"acceptedColumnsKeys": "Colonne/chiavi accettate:",
|
||||
"templateActions": "Azioni modello"
|
||||
"templateActions": "Azioni modello",
|
||||
"promptTemplateCleared": "Modello di prompt cancellato"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +501,8 @@
|
||||
"noModelsInstalled": "Нет установленных моделей",
|
||||
"noModelsInstalledDesc1": "Установите модели с помощью",
|
||||
"noMatchingModels": "Нет подходящих моделей",
|
||||
"ipAdapters": "IP адаптеры"
|
||||
"ipAdapters": "IP адаптеры",
|
||||
"starterModelsInModelManager": "Стартовые модели можно найти в Менеджере моделей"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "Изображения",
|
||||
@@ -1758,7 +1759,8 @@
|
||||
"postProcessingModel": "Модель постобработки",
|
||||
"tileControlNetModelDesc": "Модель ControlNet для выбранной архитектуры основной модели",
|
||||
"missingModelsWarning": "Зайдите в <LinkComponent>Менеджер моделей</LinkComponent> чтоб установить необходимые модели:",
|
||||
"postProcessingMissingModelWarning": "Посетите <LinkComponent>Менеджер моделей</LinkComponent>, чтобы установить модель постобработки (img2img)."
|
||||
"postProcessingMissingModelWarning": "Посетите <LinkComponent>Менеджер моделей</LinkComponent>, чтобы установить модель постобработки (img2img).",
|
||||
"upscale": "Увеличить"
|
||||
},
|
||||
"stylePresets": {
|
||||
"noMatchingTemplates": "Нет подходящих шаблонов",
|
||||
@@ -1804,7 +1806,8 @@
|
||||
"noTemplates": "Нет шаблонов",
|
||||
"promptTemplatesDesc2": "Используйте строку-заполнитель <Pre>{{placeholder}}</Pre>, чтобы указать место, куда должен быть включен ваш запрос в шаблоне.",
|
||||
"searchByName": "Поиск по имени",
|
||||
"shared": "Общий"
|
||||
"shared": "Общий",
|
||||
"promptTemplateCleared": "Шаблон запроса создан"
|
||||
},
|
||||
"upsell": {
|
||||
"inviteTeammates": "Пригласите членов команды",
|
||||
|
||||
@@ -154,7 +154,8 @@
|
||||
"displaySearch": "显示搜索",
|
||||
"stretchToFit": "拉伸以适应",
|
||||
"exitCompare": "退出对比",
|
||||
"compareHelp1": "在点击图库中的图片或使用箭头键切换比较图片时,请按住<Kbd>Alt</Kbd> 键。"
|
||||
"compareHelp1": "在点击图库中的图片或使用箭头键切换比较图片时,请按住<Kbd>Alt</Kbd> 键。",
|
||||
"go": "运行"
|
||||
},
|
||||
"hotkeys": {
|
||||
"keyboardShortcuts": "快捷键",
|
||||
@@ -494,7 +495,9 @@
|
||||
"huggingFacePlaceholder": "所有者或模型名称",
|
||||
"huggingFaceRepoID": "HuggingFace仓库ID",
|
||||
"loraTriggerPhrases": "LoRA 触发词",
|
||||
"ipAdapters": "IP适配器"
|
||||
"ipAdapters": "IP适配器",
|
||||
"spandrelImageToImage": "图生图(Spandrel)",
|
||||
"starterModelsInModelManager": "您可以在模型管理器中找到初始模型"
|
||||
},
|
||||
"parameters": {
|
||||
"images": "图像",
|
||||
@@ -695,7 +698,9 @@
|
||||
"outOfMemoryErrorDesc": "您当前的生成设置已超出系统处理能力.请调整设置后再次尝试.",
|
||||
"parametersSet": "参数已恢复",
|
||||
"errorCopied": "错误信息已复制",
|
||||
"modelImportCanceled": "模型导入已取消"
|
||||
"modelImportCanceled": "模型导入已取消",
|
||||
"importFailed": "导入失败",
|
||||
"importSuccessful": "导入成功"
|
||||
},
|
||||
"unifiedCanvas": {
|
||||
"layer": "图层",
|
||||
@@ -1705,12 +1710,55 @@
|
||||
"missingModelsWarning": "请访问<LinkComponent>模型管理器</LinkComponent> 安装所需的模型:",
|
||||
"mainModelDesc": "主模型(SD1.5或SDXL架构)",
|
||||
"exceedsMaxSize": "放大设置超出了最大尺寸限制",
|
||||
"exceedsMaxSizeDetails": "最大放大限制是 {{maxUpscaleDimension}}x{{maxUpscaleDimension}} 像素.请尝试一个较小的图像或减少您的缩放选择."
|
||||
"exceedsMaxSizeDetails": "最大放大限制是 {{maxUpscaleDimension}}x{{maxUpscaleDimension}} 像素.请尝试一个较小的图像或减少您的缩放选择.",
|
||||
"upscale": "放大"
|
||||
},
|
||||
"upsell": {
|
||||
"inviteTeammates": "邀请团队成员",
|
||||
"professional": "专业",
|
||||
"professionalUpsell": "可在 Invoke 的专业版中使用.点击此处或访问 invoke.com/pricing 了解更多详情.",
|
||||
"shareAccess": "共享访问权限"
|
||||
},
|
||||
"stylePresets": {
|
||||
"positivePrompt": "正向提示词",
|
||||
"preview": "预览",
|
||||
"deleteImage": "删除图像",
|
||||
"deleteTemplate": "删除模版",
|
||||
"deleteTemplate2": "您确定要删除这个模板吗?请注意,删除后无法恢复.",
|
||||
"importTemplates": "导入提示模板,支持CSV或JSON格式",
|
||||
"insertPlaceholder": "插入一个占位符",
|
||||
"myTemplates": "我的模版",
|
||||
"name": "名称",
|
||||
"type": "类型",
|
||||
"unableToDeleteTemplate": "无法删除提示模板",
|
||||
"updatePromptTemplate": "更新提示词模版",
|
||||
"exportPromptTemplates": "导出我的提示模板为CSV格式",
|
||||
"exportDownloaded": "导出已下载",
|
||||
"noMatchingTemplates": "无匹配的模版",
|
||||
"promptTemplatesDesc1": "提示模板可以帮助您在编写提示时添加预设的文本内容.",
|
||||
"promptTemplatesDesc3": "如果您没有使用占位符,那么模板的内容将会被添加到您提示的末尾.",
|
||||
"searchByName": "按名称搜索",
|
||||
"shared": "已分享",
|
||||
"sharedTemplates": "已分享的模版",
|
||||
"templateActions": "模版操作",
|
||||
"templateDeleted": "提示模版已删除",
|
||||
"toggleViewMode": "切换显示模式",
|
||||
"uploadImage": "上传图像",
|
||||
"active": "激活",
|
||||
"choosePromptTemplate": "选择提示词模板",
|
||||
"clearTemplateSelection": "清除模版选择",
|
||||
"copyTemplate": "拷贝模版",
|
||||
"createPromptTemplate": "创建提示词模版",
|
||||
"defaultTemplates": "默认模版",
|
||||
"editTemplate": "编辑模版",
|
||||
"exportFailed": "无法生成并下载CSV文件",
|
||||
"flatten": "将选定的模板内容合并到当前提示中",
|
||||
"negativePrompt": "反向提示词",
|
||||
"promptTemplateCleared": "提示模板已清除",
|
||||
"useForTemplate": "用于提示词模版",
|
||||
"viewList": "预览模版列表",
|
||||
"viewModeTooltip": "这是您的提示在当前选定的模板下的预览效果。如需编辑提示,请直接在文本框中点击进行修改.",
|
||||
"noTemplates": "无模版",
|
||||
"private": "私密"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import {
|
||||
sessionStagingAreaImageAccepted,
|
||||
sessionStagingAreaReset,
|
||||
} from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { canvasReset, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
@@ -16,14 +17,16 @@ import { assert } from 'tsafe';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
const matchCanvasOrStagingAreaRest = isAnyOf(sessionStagingAreaReset, canvasReset);
|
||||
|
||||
export const addStagingListeners = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: sessionStagingAreaReset,
|
||||
matcher: matchCanvasOrStagingAreaRest,
|
||||
effect: async (_, { dispatch }) => {
|
||||
try {
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.cancelByBatchOrigin.initiate(
|
||||
{ origin: 'canvas' },
|
||||
queueApi.endpoints.cancelByBatchDestination.initiate(
|
||||
{ destination: 'canvas' },
|
||||
{ fixedCacheKey: 'cancelByBatchOrigin' }
|
||||
)
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
|
||||
import { modelChanged, refinerModelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
|
||||
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
|
||||
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
|
||||
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
@@ -17,6 +18,7 @@ import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import i18n from 'i18next';
|
||||
import { forEach, upperFirst } from 'lodash-es';
|
||||
import { atom } from 'nanostores';
|
||||
import { useMemo } from 'react';
|
||||
import { getConnectedEdges } from 'reactflow';
|
||||
|
||||
@@ -28,7 +30,7 @@ const LAYER_TYPE_TO_TKEY = {
|
||||
control_layer: 'controlLayers.globalControlAdapter',
|
||||
} as const;
|
||||
|
||||
const createSelector = (templates: Templates, isConnected: boolean) =>
|
||||
const createSelector = (templates: Templates, isConnected: boolean, canvasIsBusy: boolean) =>
|
||||
createMemoizedSelector(
|
||||
[
|
||||
selectSystemSlice,
|
||||
@@ -117,6 +119,10 @@ const createSelector = (templates: Templates, isConnected: boolean) =>
|
||||
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
|
||||
}
|
||||
} else {
|
||||
if (canvasIsBusy) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.canvasBusy') });
|
||||
}
|
||||
|
||||
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
|
||||
}
|
||||
@@ -240,10 +246,17 @@ const createSelector = (templates: Templates, isConnected: boolean) =>
|
||||
}
|
||||
);
|
||||
|
||||
const dummyAtom = atom(true);
|
||||
|
||||
export const useIsReadyToEnqueue = () => {
|
||||
const templates = useStore($templates);
|
||||
const isConnected = useStore($isConnected);
|
||||
const selector = useMemo(() => createSelector(templates, isConnected), [templates, isConnected]);
|
||||
const canvasManager = useCanvasManagerSafe();
|
||||
const canvasIsBusy = useStore(canvasManager?.$isBusy ?? dummyAtom);
|
||||
const selector = useMemo(
|
||||
() => createSelector(templates, isConnected, canvasIsBusy),
|
||||
[templates, isConnected, canvasIsBusy]
|
||||
);
|
||||
const value = useAppSelector(selector);
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export const roundDownToMultiple = (num: number, multiple: number): number => {
|
||||
return Math.floor(num / multiple) * multiple;
|
||||
};
|
||||
export const roundUpToMultiple = (num: number, multiple: number): number => {
|
||||
return Math.ceil(num / multiple) * multiple;
|
||||
};
|
||||
|
||||
export const roundToMultiple = (num: number, multiple: number): number => {
|
||||
return Math.round(num / multiple) * multiple;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { EntityListActionBarAddLayerButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton';
|
||||
import { EntityListActionBarDeleteButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton';
|
||||
import { EntityListActionBarSelectedEntityFill } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill';
|
||||
import { SelectedEntityOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const EntityListActionBar = memo(() => {
|
||||
return (
|
||||
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
|
||||
<SelectedEntityOpacity />
|
||||
<Spacer />
|
||||
<EntityListActionBarSelectedEntityFill />
|
||||
<EntityListActionBarAddLayerButton />
|
||||
<EntityListActionBarDeleteButton />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListActionBar.displayName = 'EntityListActionBar';
|
||||
@@ -1,28 +0,0 @@
|
||||
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
export const EntityListActionBarAddLayerButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
size="sm"
|
||||
tooltip={t('controlLayers.addLayer')}
|
||||
aria-label={t('controlLayers.addLayer')}
|
||||
icon={<PiPlusBold />}
|
||||
variant="ghost"
|
||||
data-testid="control-layers-add-layer-menu-button"
|
||||
/>
|
||||
<MenuList>
|
||||
<CanvasEntityListMenuItems />
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListActionBarAddLayerButton.displayName = 'EntityListActionBarAddLayerButton';
|
||||
@@ -1,57 +0,0 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
inpaintMaskAdded,
|
||||
ipaAdded,
|
||||
rasterLayerAdded,
|
||||
rgAdded,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityListMenuItems = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useDefaultIPAdapter();
|
||||
const addInpaintMask = useCallback(() => {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addRegionalGuidance = useCallback(() => {
|
||||
dispatch(rgAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addRasterLayer = useCallback(() => {
|
||||
dispatch(rasterLayerAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addControlLayer = useCallback(() => {
|
||||
dispatch(controlLayerAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addIPAdapter = useCallback(() => {
|
||||
const overrides = { ipAdapter: defaultIPAdapter };
|
||||
dispatch(ipaAdded({ isSelected: true, overrides }));
|
||||
}, [defaultIPAdapter, dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||
{t('controlLayers.globalIPAdapter')}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityListMenuItems.displayName = 'CanvasEntityListMenu';
|
||||
@@ -1,39 +0,0 @@
|
||||
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { allEntitiesDeleted, entityDeleted } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectEntityCount, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleFill } from 'react-icons/pi';
|
||||
|
||||
export const EntityListActionBarDeleteButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const entityCount = useAppSelector(selectEntityCount);
|
||||
const shift = useShiftModifier();
|
||||
const onClick = useCallback(() => {
|
||||
if (shift) {
|
||||
dispatch(allEntitiesDeleted());
|
||||
return;
|
||||
}
|
||||
if (!selectedEntityIdentifier) {
|
||||
return;
|
||||
}
|
||||
dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier }));
|
||||
}, [dispatch, selectedEntityIdentifier, shift]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={shift ? entityCount === 0 : !selectedEntityIdentifier}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={shift ? t('controlLayers.deleteAll') : t('controlLayers.deleteSelected')}
|
||||
tooltip={shift ? t('controlLayers.deleteAll') : t('controlLayers.deleteSelected')}
|
||||
icon={<PiTrashSimpleFill />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListActionBarDeleteButton.displayName = 'EntityListActionBarDeleteButton';
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu';
|
||||
import { EntityListGlobalActionBarDenoisingStrength } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarDenoisingStrength';
|
||||
import { EntityListGlobalActionBarFitBboxToLayers } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarFitBboxToLayers';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const EntityListGlobalActionBar = memo(() => {
|
||||
return (
|
||||
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
|
||||
<EntityListGlobalActionBarDenoisingStrength />
|
||||
<Spacer />
|
||||
<Flex>
|
||||
<EntityListGlobalActionBarFitBboxToLayers />
|
||||
<EntityListGlobalActionBarAddLayerMenu />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListGlobalActionBar.displayName = 'EntityListGlobalActionBar';
|
||||
@@ -0,0 +1,69 @@
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
inpaintMaskAdded,
|
||||
ipaAdded,
|
||||
rasterLayerAdded,
|
||||
rgAdded,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useDefaultIPAdapter();
|
||||
const addInpaintMask = useCallback(() => {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addRegionalGuidance = useCallback(() => {
|
||||
dispatch(rgAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addRasterLayer = useCallback(() => {
|
||||
dispatch(rasterLayerAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addControlLayer = useCallback(() => {
|
||||
dispatch(controlLayerAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
const addIPAdapter = useCallback(() => {
|
||||
const overrides = { ipAdapter: defaultIPAdapter };
|
||||
dispatch(ipaAdded({ isSelected: true, overrides }));
|
||||
}, [defaultIPAdapter, dispatch]);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
tooltip={t('controlLayers.addLayer')}
|
||||
aria-label={t('controlLayers.addLayer')}
|
||||
icon={<PiPlusBold />}
|
||||
data-testid="control-layers-add-layer-menu-button"
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||
{t('controlLayers.globalIPAdapter')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListGlobalActionBarAddLayerMenu.displayName = 'EntityListGlobalActionBarAddLayerMenu';
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
CompositeSlider,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice';
|
||||
import { clamp } from 'lodash-es';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCaretDownBold } from 'react-icons/pi';
|
||||
|
||||
const marks = [0, 0.25, 0.5, 0.75, 1];
|
||||
|
||||
export const EntityListGlobalActionBarDenoisingStrength = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const strength = useAppSelector(selectImg2imgStrength);
|
||||
const config = useAppSelector(selectImg2imgStrengthConfig);
|
||||
|
||||
const [localStrength, setLocalStrength] = useState(strength);
|
||||
|
||||
const onChangeSlider = useCallback(
|
||||
(value: number) => {
|
||||
dispatch(setImg2imgStrength(value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
if (isNaN(Number(localStrength))) {
|
||||
setLocalStrength(config.initial);
|
||||
return;
|
||||
}
|
||||
dispatch(setImg2imgStrength(clamp(localStrength, 0, 1)));
|
||||
}, [config.initial, dispatch, localStrength]);
|
||||
|
||||
const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => {
|
||||
setLocalStrength(valueAsNumber);
|
||||
}, []);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onBlur();
|
||||
}
|
||||
},
|
||||
[onBlur]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStrength(strength);
|
||||
}, [strength]);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<FormControl w="min-content" gap={2}>
|
||||
<InformationalPopover feature="paramDenoisingStrength">
|
||||
<FormLabel m={0}>{`${t('parameters.denoisingStrength')}`}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<PopoverAnchor>
|
||||
<NumberInput
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
step={config.coarseStep}
|
||||
min={config.numberInputMin}
|
||||
max={config.numberInputMax}
|
||||
defaultValue={config.initial}
|
||||
value={localStrength}
|
||||
onChange={onChangeNumberInput}
|
||||
onBlur={onBlur}
|
||||
w="60px"
|
||||
onKeyDown={onKeyDown}
|
||||
clampValueOnBlur={false}
|
||||
variant="outline"
|
||||
>
|
||||
<NumberInputField paddingInlineEnd={7} _focusVisible={{ zIndex: 0 }} />
|
||||
<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
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
min={config.sliderMin}
|
||||
max={config.sliderMax}
|
||||
defaultValue={config.initial}
|
||||
onChange={onChangeSlider}
|
||||
value={localStrength}
|
||||
marks={marks}
|
||||
alwaysShowMarks
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListGlobalActionBarDenoisingStrength.displayName = 'EntityListGlobalActionBarDenoisingStrength';
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsOut } from 'react-icons/pi';
|
||||
|
||||
export const EntityListGlobalActionBarFitBboxToLayers = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const canvasManager = useCanvasManager();
|
||||
const onClick = useCallback(() => {
|
||||
canvasManager.bbox.fitToLayers();
|
||||
}, [canvasManager.bbox]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.fitBboxToLayers')}
|
||||
tooltip={t('controlLayers.fitBboxToLayers')}
|
||||
icon={<PiArrowsOut />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListGlobalActionBarFitBboxToLayers.displayName = 'EntityListGlobalActionBarFitBboxToLayers';
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton';
|
||||
import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill';
|
||||
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
|
||||
import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity';
|
||||
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const EntityListSelectedEntityActionBar = memo(() => {
|
||||
return (
|
||||
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
|
||||
<EntityListSelectedEntityActionBarOpacity />
|
||||
<Spacer />
|
||||
<EntityListSelectedEntityActionBarFill />
|
||||
<Flex>
|
||||
<EntityListSelectedEntityActionBarFilterButton />
|
||||
<EntityListSelectedEntityActionBarTransformButton />
|
||||
<EntityListSelectedEntityActionBarDuplicateButton />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBar.displayName = 'EntityListSelectedEntityActionBar';
|
||||
@@ -0,0 +1,34 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { entityDuplicated } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCopyFill } from 'react-icons/pi';
|
||||
|
||||
export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const onClick = useCallback(() => {
|
||||
if (!selectedEntityIdentifier) {
|
||||
return;
|
||||
}
|
||||
dispatch(entityDuplicated({ entityIdentifier: selectedEntityIdentifier }));
|
||||
}, [dispatch, selectedEntityIdentifier]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={!selectedEntityIdentifier}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.duplicate')}
|
||||
tooltip={t('controlLayers.duplicate')}
|
||||
icon={<PiCopyFill />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBarDuplicateButton.displayName = 'EntityListSelectedEntityActionBarDuplicateButton';
|
||||
@@ -9,7 +9,7 @@ import { type FillStyle, isMaskEntityIdentifier, type RgbColor } from 'features/
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const EntityListActionBarSelectedEntityFill = memo(() => {
|
||||
export const EntityListSelectedEntityActionBarFill = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
@@ -67,4 +67,4 @@ export const EntityListActionBarSelectedEntityFill = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
EntityListActionBarSelectedEntityFill.displayName = 'EntityListActionBarSelectedEntityFill';
|
||||
EntityListSelectedEntityActionBarFill.displayName = 'EntityListSelectedEntityActionBarFill';
|
||||
@@ -0,0 +1,52 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShootingStarBold } from 'react-icons/pi';
|
||||
|
||||
export const EntityListSelectedEntityActionBarFilterButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const canvasManager = useCanvasManager();
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!selectedEntityIdentifier) {
|
||||
return;
|
||||
}
|
||||
if (!isFilterableEntityIdentifier(selectedEntityIdentifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvasManager.filter.startFilter(selectedEntityIdentifier);
|
||||
}, [canvasManager, selectedEntityIdentifier]);
|
||||
|
||||
if (!selectedEntityIdentifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isFilterableEntityIdentifier(selectedEntityIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={isBusy || isStaging}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.filter.filter')}
|
||||
tooltip={t('controlLayers.filter.filter')}
|
||||
icon={<PiShootingStarBold />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBarFilterButton.displayName = 'EntityListSelectedEntityActionBarFilterButton';
|
||||
@@ -77,7 +77,7 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => {
|
||||
return selectedEntity.opacity;
|
||||
});
|
||||
|
||||
export const SelectedEntityOpacity = memo(() => {
|
||||
export const EntityListSelectedEntityActionBarOpacity = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
@@ -193,4 +193,4 @@ export const SelectedEntityOpacity = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
SelectedEntityOpacity.displayName = 'SelectedEntityOpacity';
|
||||
EntityListSelectedEntityActionBarOpacity.displayName = 'EntityListSelectedEntityActionBarOpacity';
|
||||
@@ -0,0 +1,55 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFrameCornersBold } from 'react-icons/pi';
|
||||
|
||||
export const EntityListSelectedEntityActionBarTransformButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const canvasManager = useCanvasManager();
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!selectedEntityIdentifier) {
|
||||
return;
|
||||
}
|
||||
if (!isTransformableEntityIdentifier(selectedEntityIdentifier)) {
|
||||
return;
|
||||
}
|
||||
const adapter = canvasManager.getAdapter(selectedEntityIdentifier);
|
||||
if (!adapter) {
|
||||
return;
|
||||
}
|
||||
adapter.transformer.startTransform();
|
||||
}, [canvasManager, selectedEntityIdentifier]);
|
||||
|
||||
if (!selectedEntityIdentifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isTransformableEntityIdentifier(selectedEntityIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={isBusy || isStaging}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.transform.transform')}
|
||||
tooltip={t('controlLayers.transform.transform')}
|
||||
icon={<PiFrameCornersBold />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBarTransformButton.displayName = 'EntityListSelectedEntityActionBarTransformButton';
|
||||
@@ -2,7 +2,8 @@ import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
|
||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
||||
import { EntityListActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBar';
|
||||
import { EntityListGlobalActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar';
|
||||
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||
import { memo } from 'react';
|
||||
@@ -13,7 +14,9 @@ export const CanvasPanelContent = memo(() => {
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||
<EntityListActionBar />
|
||||
<EntityListGlobalActionBar />
|
||||
<Divider py={0} />
|
||||
<EntityListSelectedEntityActionBar />
|
||||
<Divider py={0} />
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
{hasEntities && <CanvasEntityList />}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import { Flex, Grid, GridItem, IconButton } from '@invoke-ai/ui-library';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import {
|
||||
PiArrowDownBold,
|
||||
PiArrowDownLeftBold,
|
||||
PiArrowDownRightBold,
|
||||
PiArrowLeftBold,
|
||||
PiArrowRightBold,
|
||||
PiArrowUpBold,
|
||||
PiArrowUpLeftBold,
|
||||
PiArrowUpRightBold,
|
||||
PiSquareBold,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
type ResizeDirection =
|
||||
| 'up-left'
|
||||
| 'up'
|
||||
| 'up-right'
|
||||
| 'left'
|
||||
| 'center-out'
|
||||
| 'right'
|
||||
| 'down-left'
|
||||
| 'down'
|
||||
| 'down-right';
|
||||
|
||||
export const CanvasResizer = memo(() => {
|
||||
const [resizeDirection, setResizeDirection] = useState<ResizeDirection>('center-out');
|
||||
|
||||
const setDirUpLeft = useCallback(() => {
|
||||
setResizeDirection('up-left');
|
||||
}, []);
|
||||
|
||||
const setDirUp = useCallback(() => {
|
||||
setResizeDirection('up');
|
||||
}, []);
|
||||
|
||||
const setDirUpRight = useCallback(() => {
|
||||
setResizeDirection('up-right');
|
||||
}, []);
|
||||
|
||||
const setDirLeft = useCallback(() => {
|
||||
setResizeDirection('left');
|
||||
}, []);
|
||||
|
||||
const setDirCenterOut = useCallback(() => {
|
||||
setResizeDirection('center-out');
|
||||
}, []);
|
||||
|
||||
const setDirRight = useCallback(() => {
|
||||
setResizeDirection('right');
|
||||
}, []);
|
||||
|
||||
const setDirDownLeft = useCallback(() => {
|
||||
setResizeDirection('down-left');
|
||||
}, []);
|
||||
|
||||
const setDirDown = useCallback(() => {
|
||||
setResizeDirection('down');
|
||||
}, []);
|
||||
|
||||
const setDirDownRight = useCallback(() => {
|
||||
setResizeDirection('down-right');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex p={2}>
|
||||
<Grid gridTemplateRows="1fr 1fr 1fr" gridTemplateColumns="1fr 1fr 1fr" gap={2}>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
onClick={setDirUpLeft}
|
||||
aria-label="up-left"
|
||||
icon={<PiArrowUpLeftBold />}
|
||||
variant={resizeDirection === 'up-left' ? 'solid' : 'ghost'}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
onClick={setDirUp}
|
||||
aria-label="up"
|
||||
icon={<PiArrowUpBold />}
|
||||
variant={resizeDirection === 'up' ? 'solid' : 'ghost'}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
onClick={setDirUpRight}
|
||||
aria-label="up-right"
|
||||
icon={<PiArrowUpRightBold />}
|
||||
variant={resizeDirection === 'up-right' ? 'solid' : 'ghost'}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
onClick={setDirLeft}
|
||||
aria-label="left"
|
||||
icon={<PiArrowLeftBold />}
|
||||
variant={resizeDirection === 'left' ? 'solid' : 'ghost'}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
onClick={setDirCenterOut}
|
||||
aria-label="center-out"
|
||||
icon={<PiSquareBold />}
|
||||
variant={resizeDirection === 'center-out' ? 'solid' : 'ghost'}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
onClick={setDirRight}
|
||||
aria-label="right"
|
||||
icon={<PiArrowRightBold />}
|
||||
variant={resizeDirection === 'right' ? 'solid' : 'ghost'}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
onClick={setDirDownLeft}
|
||||
aria-label="down-left"
|
||||
icon={<PiArrowDownLeftBold />}
|
||||
variant={resizeDirection === 'down-left' ? 'solid' : 'ghost'}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
onClick={setDirDown}
|
||||
aria-label="down"
|
||||
icon={<PiArrowDownBold />}
|
||||
variant={resizeDirection === 'down' ? 'solid' : 'ghost'}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<IconButton
|
||||
onClick={setDirDownRight}
|
||||
aria-label="down-right"
|
||||
icon={<PiArrowDownRightBold />}
|
||||
variant={resizeDirection === 'down-right' ? 'solid' : 'ghost'}
|
||||
/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasResizer.displayName = 'CanvasResizer';
|
||||
@@ -4,7 +4,12 @@ import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { selectBase } from 'features/controlLayers/store/paramsSlice';
|
||||
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
IMAGE_FILTERS,
|
||||
isControlLayerEntityIdentifier,
|
||||
isFilterType,
|
||||
isRasterLayerEntityIdentifier,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType';
|
||||
@@ -39,6 +44,11 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
|
||||
|
||||
// Open the filter popup by setting this entity as the filtering entity
|
||||
if (!canvasManager.filter.$adapter.get()) {
|
||||
// Can only filter raster and control layers
|
||||
if (!isRasterLayerEntityIdentifier(entityIdentifier) && !isControlLayerEntityIdentifier(entityIdentifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the filter, preferring the model's default
|
||||
if (isFilterType(modelConfig.default_settings?.preprocessor)) {
|
||||
canvasManager.filter.$config.set(
|
||||
@@ -47,6 +57,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
|
||||
} else {
|
||||
canvasManager.filter.$config.set(IMAGE_FILTERS.canny_image_processor.buildDefaults(modelConfig.base));
|
||||
}
|
||||
|
||||
canvasManager.filter.startFilter(entityIdentifier);
|
||||
canvasManager.filter.previewFilter();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, ButtonGroup, Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import { Button, ButtonGroup, Flex, Heading, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { FilterSettings } from 'features/controlLayers/components/Filters/FilterSettings';
|
||||
import { FilterTypeSelect } from 'features/controlLayers/components/Filters/FilterTypeSelect';
|
||||
@@ -51,7 +51,7 @@ export const Filter = memo(() => {
|
||||
</Heading>
|
||||
<FilterTypeSelect filterType={config.type} onChange={onChangeFilterType} />
|
||||
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
|
||||
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
|
||||
<ButtonGroup isAttached={false} size="sm" w="full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiShootingStarBold />}
|
||||
@@ -61,6 +61,7 @@ export const Filter = memo(() => {
|
||||
>
|
||||
{t('controlLayers.filter.preview')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={<PiCheckBold />}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/stor
|
||||
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
|
||||
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { canvasReset } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -7,9 +8,11 @@ import { useTranslation } from 'react-i18next';
|
||||
export const CanvasSettingsResetButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const canvasManager = useCanvasManager();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(canvasReset());
|
||||
}, [dispatch]);
|
||||
canvasManager.stage.fitLayersToStage();
|
||||
}, [canvasManager.stage, dispatch]);
|
||||
return (
|
||||
<Button onClick={onClick} colorScheme="error" size="sm">
|
||||
{t('controlLayers.resetCanvas')}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full">
|
||||
<Flex w="full">
|
||||
<Flex w="full" px={1}>
|
||||
<Flex
|
||||
flexGrow={1}
|
||||
as={Button}
|
||||
@@ -59,8 +59,8 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
|
||||
<Spacer />
|
||||
</Flex>
|
||||
{canMergeVisible && <CanvasEntityMergeVisibleButton type={type} />}
|
||||
<CanvasEntityAddOfTypeButton type={type} />
|
||||
{canHideAll && <CanvasEntityTypeIsHiddenToggle type={type} />}
|
||||
<CanvasEntityAddOfTypeButton type={type} />
|
||||
</Flex>
|
||||
<Collapse in={collapse.isTrue}>
|
||||
<Flex flexDir="column" gap={2} pt={2}>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShootingStarBold } from 'react-icons/pi';
|
||||
@@ -10,14 +13,21 @@ export const CanvasEntityMenuItemsFilter = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const canvasManager = useCanvasManager();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!entityIdentifier) {
|
||||
return;
|
||||
}
|
||||
if (!isFilterableEntityIdentifier(entityIdentifier)) {
|
||||
return;
|
||||
}
|
||||
canvasManager.filter.startFilter(entityIdentifier);
|
||||
}, [canvasManager.filter, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiShootingStarBold />} isDisabled={isBusy}>
|
||||
<MenuItem onClick={onClick} icon={<PiShootingStarBold />} isDisabled={isBusy || isStaging}>
|
||||
{t('controlLayers.filter.filter')}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityAdapter } from 'features/controlLayers/hooks/useEntityAdapter';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFrameCornersBold } from 'react-icons/pi';
|
||||
@@ -10,14 +13,18 @@ export const CanvasEntityMenuItemsTransform = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const adapter = useEntityAdapter(entityIdentifier);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!isTransformableEntityIdentifier(entityIdentifier)) {
|
||||
return;
|
||||
}
|
||||
adapter.transformer.startTransform();
|
||||
}, [adapter.transformer]);
|
||||
}, [adapter.transformer, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiFrameCornersBold />} isDisabled={isBusy}>
|
||||
<MenuItem onClick={onClick} icon={<PiFrameCornersBold />} isDisabled={isBusy || isStaging}>
|
||||
{t('controlLayers.transform.transform')}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { isOk, withResultAsync } from 'common/util/result';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityTypeCount } from 'features/controlLayers/hooks/useEntityTypeCount';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
@@ -23,6 +25,8 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const canvasManager = useCanvasManager();
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const entityCount = useEntityTypeCount(type);
|
||||
const onClick = useCallback(async () => {
|
||||
if (type === 'raster_layer') {
|
||||
@@ -83,7 +87,7 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
|
||||
icon={<PiStackBold />}
|
||||
onClick={onClick}
|
||||
alignSelf="stretch"
|
||||
isDisabled={entityCount <= 1}
|
||||
isDisabled={entityCount <= 1 || isStaging || isBusy}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ export const CanvasEntityTypeIsHiddenToggle = memo(({ type }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isHidden = useEntityTypeIsHidden(type);
|
||||
const typeString = useEntityTypeString(type);
|
||||
const typeString = useEntityTypeString(type, true);
|
||||
const onClick = useCallback<MouseEventHandler>(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -18,8 +18,20 @@ export const CanvasManagerProviderGate = memo(({ children }: PropsWithChildren)
|
||||
|
||||
CanvasManagerProviderGate.displayName = 'CanvasManagerProviderGate';
|
||||
|
||||
/**
|
||||
* Consumes the CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise
|
||||
* it will throw an error.
|
||||
*/
|
||||
export const useCanvasManager = (): CanvasManager => {
|
||||
const canvasManager = useContext(CanvasManagerContext);
|
||||
assert(canvasManager, 'useCanvasManagerContext must be used within a CanvasManagerProviderGate');
|
||||
return canvasManager;
|
||||
};
|
||||
|
||||
/**
|
||||
* Consumes the CanvasManager from the context. If the CanvasManager is not available, it will return null.
|
||||
*/
|
||||
export const useCanvasManagerSafe = (): CanvasManager | null => {
|
||||
const canvasManager = useStore($canvasManager);
|
||||
return canvasManager;
|
||||
};
|
||||
|
||||
@@ -2,25 +2,25 @@ import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const useEntityTypeString = (type: CanvasEntityIdentifier['type']): string => {
|
||||
export const useEntityTypeString = (type: CanvasEntityIdentifier['type'], plural: boolean = false): string => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const typeString = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'control_layer':
|
||||
return t('controlLayers.controlLayer');
|
||||
return plural ? t('controlLayers.controlLayer_withCount_other') : t('controlLayers.controlLayer');
|
||||
case 'raster_layer':
|
||||
return t('controlLayers.rasterLayer');
|
||||
return plural ? t('controlLayers.rasterLayer_withCount_other') : t('controlLayers.rasterLayer');
|
||||
case 'inpaint_mask':
|
||||
return t('controlLayers.inpaintMask');
|
||||
return plural ? t('controlLayers.inpaintMask_withCount_other') : t('controlLayers.inpaintMask');
|
||||
case 'regional_guidance':
|
||||
return t('controlLayers.regionalGuidance');
|
||||
return plural ? t('controlLayers.regionalGuidance_withCount_other') : t('controlLayers.regionalGuidance');
|
||||
case 'ip_adapter':
|
||||
return t('controlLayers.globalIPAdapter');
|
||||
return plural ? t('controlLayers.globalIPAdapter_withCount_other') : t('controlLayers.globalIPAdapter');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}, [type, t]);
|
||||
}, [type, plural, t]);
|
||||
|
||||
return typeString;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { roundToMultiple, roundToMultipleMin } from 'common/util/roundDownToMultiple';
|
||||
import {
|
||||
roundDownToMultiple,
|
||||
roundToMultiple,
|
||||
roundToMultipleMin,
|
||||
roundUpToMultiple,
|
||||
} from 'common/util/roundDownToMultiple';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
@@ -295,6 +300,29 @@ export class CanvasBboxModule extends CanvasModuleBase {
|
||||
}
|
||||
};
|
||||
|
||||
fitToLayers = (): void => {
|
||||
const visibleRect = this.manager.stage.getVisibleRect();
|
||||
|
||||
// Can't fit the bbox to nothing
|
||||
if (visibleRect.height === 0 || visibleRect.width === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the bbox size that fits within the visible rect. The bbox must be at least 64px in width and height,
|
||||
// and its width and height must be multiples of 8px.
|
||||
const gridSize = 8;
|
||||
|
||||
// To be conservative, we will round up the x and y to the nearest grid size, and round down the width and height.
|
||||
// This ensures the bbox is never _larger_ than the visible rect. If the bbox is larger than the visible, we
|
||||
// will always trigger the outpainting workflow, which is not what the user wants.
|
||||
const x = roundUpToMultiple(visibleRect.x, gridSize);
|
||||
const y = roundUpToMultiple(visibleRect.y, gridSize);
|
||||
const width = roundDownToMultiple(visibleRect.width, gridSize);
|
||||
const height = roundDownToMultiple(visibleRect.height, gridSize);
|
||||
|
||||
this.manager.stateApi.setGenerationBbox({ x, y, width, height });
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -16,8 +16,8 @@ export class CanvasEntityAdapterControlLayer extends CanvasEntityAdapterBase<Can
|
||||
constructor(entityIdentifier: CanvasEntityIdentifier<'control_layer'>, manager: CanvasManager) {
|
||||
super(entityIdentifier, manager, CanvasEntityAdapterControlLayer.TYPE);
|
||||
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
this.renderer = new CanvasEntityObjectRenderer(this);
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export class CanvasEntityAdapterControlLayer extends CanvasEntityAdapterBase<Can
|
||||
// The opacity may have been changed in response to user selecting a different entity category, so we must restore
|
||||
// the original opacity before rendering the canvas
|
||||
const attrs: GroupConfig = { opacity: this.state.opacity };
|
||||
const canvas = this.renderer.getCanvas(rect, attrs);
|
||||
const canvas = this.renderer.getCanvas({ rect, attrs });
|
||||
return canvas;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase<Canv
|
||||
constructor(entityIdentifier: CanvasEntityIdentifier<'inpaint_mask'>, manager: CanvasManager) {
|
||||
super(entityIdentifier, manager, CanvasEntityAdapterInpaintMask.TYPE);
|
||||
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
this.renderer = new CanvasEntityObjectRenderer(this);
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export class CanvasEntityAdapterInpaintMask extends CanvasEntityAdapterBase<Canv
|
||||
// The opacity may have been changed in response to user selecting a different entity category, and the mask regions
|
||||
// should be fully opaque - set opacity to 1 before rendering the canvas
|
||||
const attrs: GroupConfig = { opacity: 1 };
|
||||
const canvas = this.renderer.getCanvas(rect, attrs);
|
||||
const canvas = this.renderer.getCanvas({ rect, attrs });
|
||||
return canvas;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase<Canv
|
||||
constructor(entityIdentifier: CanvasEntityIdentifier<'raster_layer'>, manager: CanvasManager) {
|
||||
super(entityIdentifier, manager, CanvasEntityAdapterRasterLayer.TYPE);
|
||||
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
this.renderer = new CanvasEntityObjectRenderer(this);
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export class CanvasEntityAdapterRasterLayer extends CanvasEntityAdapterBase<Canv
|
||||
// The opacity may have been changed in response to user selecting a different entity category, so we must restore
|
||||
// the original opacity before rendering the canvas
|
||||
const attrs: GroupConfig = { opacity: this.state.opacity };
|
||||
const canvas = this.renderer.getCanvas(rect, attrs);
|
||||
const canvas = this.renderer.getCanvas({ rect, attrs });
|
||||
return canvas;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase
|
||||
constructor(entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>, manager: CanvasManager) {
|
||||
super(entityIdentifier, manager, CanvasEntityAdapterRegionalGuidance.TYPE);
|
||||
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
this.renderer = new CanvasEntityObjectRenderer(this);
|
||||
this.transformer = new CanvasEntityTransformer(this);
|
||||
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(this.selectState, this.sync));
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export class CanvasEntityAdapterRegionalGuidance extends CanvasEntityAdapterBase
|
||||
// The opacity may have been changed in response to user selecting a different entity category, and the mask regions
|
||||
// should be fully opaque - set opacity to 1 before rendering the canvas
|
||||
const attrs: GroupConfig = { opacity: 1 };
|
||||
const canvas = this.renderer.getCanvas(rect, attrs);
|
||||
const canvas = this.renderer.getCanvas({ rect, attrs });
|
||||
return canvas;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
// to pan _before_ releasing the mouse button, which would cause the buffer to be lost if we didn't commit it.
|
||||
this.subscriptions.add(
|
||||
this.manager.tool.$tool.listen(() => {
|
||||
if (this.hasBuffer()) {
|
||||
if (this.hasBuffer() && !this.manager.$isBusy.get()) {
|
||||
this.commitBuffer();
|
||||
}
|
||||
})
|
||||
@@ -447,6 +447,7 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
this.bufferRenderer?.destroy();
|
||||
this.bufferRenderer = null;
|
||||
this.bufferState = null;
|
||||
this.syncCache(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -538,10 +539,16 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
* @param rect The rect to rasterize. If omitted, the entity's full rect will be used.
|
||||
* @returns A promise that resolves to the rasterized image DTO.
|
||||
*/
|
||||
rasterize = async (options: { rect: Rect; replaceObjects?: boolean; attrs?: GroupConfig }): Promise<ImageDTO> => {
|
||||
const { rect, replaceObjects, attrs } = { replaceObjects: false, attrs: {}, ...options };
|
||||
rasterize = async (options: {
|
||||
rect: Rect;
|
||||
replaceObjects?: boolean;
|
||||
attrs?: GroupConfig;
|
||||
bg?: string;
|
||||
}): Promise<ImageDTO> => {
|
||||
const { rect, replaceObjects, attrs, bg } = { replaceObjects: false, attrs: {}, ...options };
|
||||
let imageDTO: ImageDTO | null = null;
|
||||
const hash = this.parent.hash({ rect, attrs });
|
||||
const rasterizeArgs = { rect, attrs, bg };
|
||||
const hash = this.parent.hash(rasterizeArgs);
|
||||
const cachedImageName = this.manager.cache.imageNameCache.get(hash);
|
||||
|
||||
if (cachedImageName) {
|
||||
@@ -552,9 +559,9 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
}
|
||||
}
|
||||
|
||||
this.log.trace({ rect }, 'Rasterizing entity');
|
||||
this.log.trace({ rasterizeArgs }, 'Rasterizing entity');
|
||||
|
||||
const blob = await this.getBlob(rect, attrs);
|
||||
const blob = await this.getBlob(rasterizeArgs);
|
||||
if (this.manager._isDebugging) {
|
||||
previewBlob(blob, 'Rasterized entity');
|
||||
}
|
||||
@@ -607,32 +614,35 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
|
||||
}
|
||||
}, 300);
|
||||
|
||||
cloneObjectGroup = (attrs?: GroupConfig): Konva.Group => {
|
||||
cloneObjectGroup = (arg: { attrs?: GroupConfig } = {}): Konva.Group => {
|
||||
const { attrs } = arg;
|
||||
const clone = this.konva.objectGroup.clone();
|
||||
clone.cache();
|
||||
if (attrs) {
|
||||
clone.setAttrs(attrs);
|
||||
}
|
||||
clone.cache();
|
||||
return clone;
|
||||
};
|
||||
|
||||
getCanvas = (rect?: Rect, attrs?: GroupConfig): HTMLCanvasElement => {
|
||||
const clone = this.cloneObjectGroup(attrs);
|
||||
const canvas = konvaNodeToCanvas(clone, rect);
|
||||
getCanvas = (arg: { rect?: Rect; attrs?: GroupConfig; bg?: string } = {}): HTMLCanvasElement => {
|
||||
const { rect, attrs, bg } = arg;
|
||||
const clone = this.cloneObjectGroup({ attrs });
|
||||
const canvas = konvaNodeToCanvas({ node: clone, rect, bg });
|
||||
clone.destroy();
|
||||
return canvas;
|
||||
};
|
||||
|
||||
getBlob = async (rect?: Rect, attrs?: GroupConfig): Promise<Blob> => {
|
||||
const clone = this.cloneObjectGroup(attrs);
|
||||
const blob = await konvaNodeToBlob(clone, rect);
|
||||
clone.destroy();
|
||||
getBlob = async (arg: { rect?: Rect; attrs?: GroupConfig; bg?: string } = {}): Promise<Blob> => {
|
||||
const { rect, attrs, bg } = arg;
|
||||
const clone = this.cloneObjectGroup({ attrs });
|
||||
const blob = await konvaNodeToBlob({ node: clone, rect, bg });
|
||||
return blob;
|
||||
};
|
||||
|
||||
getImageData = (rect?: Rect, attrs?: GroupConfig): ImageData => {
|
||||
const clone = this.cloneObjectGroup(attrs);
|
||||
const imageData = konvaNodeToImageData(clone, rect);
|
||||
getImageData = (arg: { rect?: Rect; attrs?: GroupConfig; bg?: string } = {}): ImageData => {
|
||||
const { rect, attrs, bg } = arg;
|
||||
const clone = this.cloneObjectGroup({ attrs });
|
||||
const imageData = konvaNodeToImageData({ node: clone, rect, bg });
|
||||
clone.destroy();
|
||||
return imageData;
|
||||
};
|
||||
|
||||
@@ -172,8 +172,9 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
|
||||
rotateEnabled: true,
|
||||
// When dragging a transform anchor across either the x or y axis, the nodes will be flipped across the axis
|
||||
flipEnabled: true,
|
||||
// Transforming will retain aspect ratio only when shift is held
|
||||
keepRatio: false,
|
||||
// Transforming will allow free aspect ratio only when shift is held
|
||||
keepRatio: true,
|
||||
shiftBehavior: 'inverted',
|
||||
// The padding is the distance between the transformer bbox and the nodes
|
||||
padding: this.config.OUTLINE_PADDING,
|
||||
// This is `invokeBlue.400`
|
||||
@@ -705,7 +706,7 @@ export class CanvasEntityTransformer extends CanvasModuleBase {
|
||||
}
|
||||
|
||||
// We have eraser strokes - we must calculate the bbox using pixel data
|
||||
const canvas = this.parent.renderer.getCanvas(undefined, { opacity: 1 });
|
||||
const canvas = this.parent.renderer.getCanvas({ attrs: { opacity: 1, filters: [] } });
|
||||
const imageData = canvasToImageData(canvas);
|
||||
this.manager.worker.requestBbox(
|
||||
{ buffer: imageData.data.buffer, width: imageData.width, height: imageData.height },
|
||||
|
||||
@@ -46,7 +46,7 @@ export class CanvasFilterModule extends CanvasModuleBase {
|
||||
this.log.debug('Creating filter module');
|
||||
}
|
||||
|
||||
startFilter = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
startFilter = (entityIdentifier: CanvasEntityIdentifier<'raster_layer' | 'control_layer'>) => {
|
||||
this.log.trace('Initializing filter');
|
||||
const adapter = this.manager.getAdapter(entityIdentifier);
|
||||
if (!adapter) {
|
||||
@@ -70,7 +70,7 @@ export class CanvasFilterModule extends CanvasModuleBase {
|
||||
const config = this.$config.get();
|
||||
this.log.trace({ config }, 'Previewing filter');
|
||||
const rect = adapter.transformer.getRelativeRect();
|
||||
const imageDTO = await adapter.renderer.rasterize({ rect });
|
||||
const imageDTO = await adapter.renderer.rasterize({ rect, attrs: { filters: [] } });
|
||||
const nodeId = getPrefixedId('filter_node');
|
||||
const batch = this.buildBatchConfig(imageDTO, config, nodeId);
|
||||
|
||||
@@ -145,6 +145,7 @@ export class CanvasFilterModule extends CanvasModuleBase {
|
||||
adapter.renderer.clearBuffer();
|
||||
adapter.renderer.showObjects();
|
||||
adapter.transformer.updatePosition();
|
||||
adapter.renderer.syncCache(true);
|
||||
this.$adapter.set(null);
|
||||
}
|
||||
this.imageState = null;
|
||||
@@ -162,9 +163,8 @@ export class CanvasFilterModule extends CanvasModuleBase {
|
||||
nodes: {
|
||||
[node.id]: {
|
||||
...node,
|
||||
// Control images are always intermediate - do not save to gallery
|
||||
// is_intermediate: true,
|
||||
is_intermediate: false, // false for testing
|
||||
// filtered images are always intermediate - do not save to gallery
|
||||
is_intermediate: true,
|
||||
},
|
||||
},
|
||||
edges: [],
|
||||
|
||||
@@ -20,8 +20,8 @@ import { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStag
|
||||
import { CanvasToolModule } from 'features/controlLayers/konva/CanvasToolModule';
|
||||
import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
type CanvasEntityIdentifier,
|
||||
isControlLayerEntityIdentifier,
|
||||
isInpaintMaskEntityIdentifier,
|
||||
isRasterLayerEntityIdentifier,
|
||||
@@ -133,16 +133,38 @@ export class CanvasManager extends CanvasModuleBase {
|
||||
});
|
||||
}
|
||||
|
||||
getAdapter = (entityIdentifier: CanvasEntityIdentifier): CanvasEntityAdapter | null => {
|
||||
getAdapter = <T extends CanvasEntityType = CanvasEntityType>(
|
||||
entityIdentifier: CanvasEntityIdentifier<T>
|
||||
): Extract<CanvasEntityAdapter, { state: { type: T } }> | null => {
|
||||
switch (entityIdentifier.type) {
|
||||
case 'raster_layer':
|
||||
return this.adapters.rasterLayers.get(entityIdentifier.id) ?? null;
|
||||
return (
|
||||
(this.adapters.rasterLayers.get(entityIdentifier.id) as Extract<
|
||||
CanvasEntityAdapter,
|
||||
{ state: { type: T } }
|
||||
>) ?? null
|
||||
);
|
||||
case 'control_layer':
|
||||
return this.adapters.controlLayers.get(entityIdentifier.id) ?? null;
|
||||
return (
|
||||
(this.adapters.controlLayers.get(entityIdentifier.id) as Extract<
|
||||
CanvasEntityAdapter,
|
||||
{ state: { type: T } }
|
||||
>) ?? null
|
||||
);
|
||||
case 'regional_guidance':
|
||||
return this.adapters.regionMasks.get(entityIdentifier.id) ?? null;
|
||||
return (
|
||||
(this.adapters.regionMasks.get(entityIdentifier.id) as Extract<
|
||||
CanvasEntityAdapter,
|
||||
{ state: { type: T } }
|
||||
>) ?? null
|
||||
);
|
||||
case 'inpaint_mask':
|
||||
return this.adapters.inpaintMasks.get(entityIdentifier.id) ?? null;
|
||||
return (
|
||||
(this.adapters.inpaintMasks.get(entityIdentifier.id) as Extract<
|
||||
CanvasEntityAdapter,
|
||||
{ state: { type: T } }
|
||||
>) ?? null
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -100,10 +100,26 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
});
|
||||
};
|
||||
|
||||
fitStageToContainer = () => {
|
||||
/**
|
||||
* Fits the stage to the container element.
|
||||
*/
|
||||
fitStageToContainer = (): void => {
|
||||
this.log.trace('Fitting stage to container');
|
||||
this.konva.stage.width(this.konva.stage.container().offsetWidth);
|
||||
this.konva.stage.height(this.konva.stage.container().offsetHeight);
|
||||
const containerWidth = this.konva.stage.container().offsetWidth;
|
||||
const containerHeight = this.konva.stage.container().offsetHeight;
|
||||
|
||||
// If the container has no size, the following calculations will be reaallll funky and bork the stage
|
||||
if (containerWidth === 0 || containerHeight === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the stage _had_ no size just before this function was called, that means we've just mounted the stage or
|
||||
// maybe un-hidden it. In that case, the user is about to see the stage for the first time, so we should fit the
|
||||
// layers to the stage. If we don't do this, the layers will not be centered.
|
||||
const shouldFitLayersAfterFittingStage = this.konva.stage.width() === 0 || this.konva.stage.height() === 0;
|
||||
|
||||
this.konva.stage.width(containerWidth);
|
||||
this.konva.stage.height(containerHeight);
|
||||
this.$stageAttrs.set({
|
||||
x: this.konva.stage.x(),
|
||||
y: this.konva.stage.y(),
|
||||
@@ -111,6 +127,10 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
height: this.konva.stage.height(),
|
||||
scale: this.konva.stage.scaleX(),
|
||||
});
|
||||
|
||||
if (shouldFitLayersAfterFittingStage) {
|
||||
this.fitLayersToStage();
|
||||
}
|
||||
};
|
||||
|
||||
getVisibleRect = (type?: Exclude<CanvasEntityIdentifier['type'], 'ip_adapter'>): Rect => {
|
||||
@@ -129,13 +149,19 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
return getRectUnion(...rects);
|
||||
};
|
||||
|
||||
fitBboxToStage = () => {
|
||||
/**
|
||||
* Fits the bbox to the stage. This will center the bbox and scale it to fit the stage with some padding.
|
||||
*/
|
||||
fitBboxToStage = (): void => {
|
||||
const { rect } = this.manager.stateApi.getBbox();
|
||||
this.log.trace({ rect }, 'Fitting bbox to stage');
|
||||
this.fitRect(rect);
|
||||
};
|
||||
|
||||
fitLayersToStage() {
|
||||
/**
|
||||
* Fits the visible canvas to the stage. This will center the canvas and scale it to fit the stage with some padding.
|
||||
*/
|
||||
fitLayersToStage = (): void => {
|
||||
const rect = this.getVisibleRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
this.fitBboxToStage();
|
||||
@@ -143,17 +169,31 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
this.log.trace({ rect }, 'Fitting layers to stage');
|
||||
this.fitRect(rect);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fitRect = (rect: Rect) => {
|
||||
/**
|
||||
* Fits a rectangle to the stage. The rectangle will be centered and scaled to fit the stage with some padding.
|
||||
*
|
||||
* The max scale is 1, but the stage can be scaled down to fit the rect.
|
||||
*/
|
||||
fitRect = (rect: Rect): void => {
|
||||
const { width, height } = this.getSize();
|
||||
|
||||
// If the stage has no size, we can't fit anything to it
|
||||
if (width === 0 || height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const padding = 20; // Padding in absolute pixels
|
||||
|
||||
const availableWidth = width - padding * 2;
|
||||
const availableHeight = height - padding * 2;
|
||||
|
||||
const scale = Math.min(Math.min(availableWidth / rect.width, availableHeight / rect.height), 1);
|
||||
// Make sure we don't accidentally set the scale to something nonsensical, like a negative number, 0 or something
|
||||
// outside the valid range
|
||||
const scale = this.constrainScale(
|
||||
Math.min(Math.min(availableWidth / rect.width, availableHeight / rect.height), 1)
|
||||
);
|
||||
const x = -rect.x * scale + padding + (availableWidth - rect.width * scale) / 2;
|
||||
const y = -rect.y * scale + padding + (availableHeight - rect.height * scale) / 2;
|
||||
|
||||
@@ -193,14 +233,21 @@ export class CanvasStageModule extends CanvasModuleBase {
|
||||
return this.konva.stage.getAbsoluteTransform().point(center);
|
||||
};
|
||||
|
||||
/**
|
||||
* Constrains a scale to be within the valid range
|
||||
*/
|
||||
constrainScale = (scale: number): number => {
|
||||
return clamp(Math.round(scale * 100) / 100, this.config.MIN_SCALE, this.config.MAX_SCALE);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the scale of the stage. If center is provided, the stage will zoom in/out on that point.
|
||||
* @param scale The new scale to set
|
||||
* @param center The center of the stage to zoom in/out on
|
||||
*/
|
||||
setScale = (scale: number, center: Coordinate = this.getCenter(true)) => {
|
||||
setScale = (scale: number, center: Coordinate = this.getCenter(true)): void => {
|
||||
this.log.trace('Setting scale');
|
||||
const newScale = clamp(Math.round(scale * 100) / 100, this.config.MIN_SCALE, this.config.MAX_SCALE);
|
||||
const newScale = this.constrainScale(scale);
|
||||
|
||||
const { x, y } = this.getPosition();
|
||||
const oldScale = this.getScale();
|
||||
|
||||
@@ -636,7 +636,7 @@ export class CanvasToolModule extends CanvasModuleBase {
|
||||
this.$isMouseDown.set(false);
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
|
||||
|
||||
if (selectedEntity && selectedEntity.renderer.hasBuffer()) {
|
||||
if (selectedEntity && selectedEntity.renderer.hasBuffer() && !this.manager.$isBusy.get()) {
|
||||
selectedEntity.renderer.commitBuffer();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,8 +14,9 @@ export const LightnessToAlphaFilter = (imageData: ImageData): void => {
|
||||
const r = imageData.data[i * 4 + 0] as number;
|
||||
const g = imageData.data[i * 4 + 1] as number;
|
||||
const b = imageData.data[i * 4 + 2] as number;
|
||||
const a = imageData.data[i * 4 + 3] as number;
|
||||
const cMin = Math.min(r, g, b);
|
||||
const cMax = Math.max(r, g, b);
|
||||
imageData.data[i * 4 + 3] = (cMin + cMax) / 2;
|
||||
imageData.data[i * 4 + 3] = Math.min(a, (cMin + cMax) / 2);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -304,8 +304,24 @@ export const dataURLToImageData = (dataURL: string, width: number, height: numbe
|
||||
});
|
||||
};
|
||||
|
||||
export const konvaNodeToCanvas = (node: Konva.Node, bbox?: Rect): HTMLCanvasElement => {
|
||||
return node.toCanvas({ ...(bbox ?? {}) });
|
||||
export const konvaNodeToCanvas = (arg: { node: Konva.Node; rect?: Rect; bg?: string }): HTMLCanvasElement => {
|
||||
const { node, rect, bg } = arg;
|
||||
const canvas = node.toCanvas({ ...(rect ?? {}) });
|
||||
|
||||
if (!bg) {
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// We need to draw the canvas onto a new canvas with the specified background color
|
||||
const bgCanvas = document.createElement('canvas');
|
||||
bgCanvas.width = canvas.width;
|
||||
bgCanvas.height = canvas.height;
|
||||
const bgCtx = bgCanvas.getContext('2d');
|
||||
assert(bgCtx !== null, 'bgCtx is null');
|
||||
bgCtx.fillStyle = bg;
|
||||
bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
|
||||
bgCtx.drawImage(canvas, 0, 0);
|
||||
return bgCanvas;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -335,22 +351,24 @@ export const canvasToImageData = (canvas: HTMLCanvasElement): ImageData => {
|
||||
/**
|
||||
* Converts a Konva node to an ImageData object
|
||||
* @param node - The Konva node to convert to an ImageData object
|
||||
* @param bbox - The bounding box to crop to
|
||||
* @param rect - The bounding box to crop to
|
||||
* @returns A Promise that resolves with ImageData object of the node cropped to the bounding box
|
||||
*/
|
||||
export const konvaNodeToImageData = (node: Konva.Node, bbox?: Rect): ImageData => {
|
||||
const canvas = konvaNodeToCanvas(node, bbox);
|
||||
export const konvaNodeToImageData = (arg: { node: Konva.Node; rect?: Rect; bg?: string }): ImageData => {
|
||||
const { node, rect, bg } = arg;
|
||||
const canvas = konvaNodeToCanvas({ node, rect, bg });
|
||||
return canvasToImageData(canvas);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Konva node to a Blob
|
||||
* @param node - The Konva node to convert to a Blob
|
||||
* @param bbox - The bounding box to crop to
|
||||
* @param rect - The bounding box to crop to
|
||||
* @returns A Promise that resolves to the Blob or null,
|
||||
*/
|
||||
export const konvaNodeToBlob = (node: Konva.Node, bbox?: Rect): Promise<Blob> => {
|
||||
const canvas = konvaNodeToCanvas(node, bbox);
|
||||
export const konvaNodeToBlob = (arg: { node: Konva.Node; rect?: Rect; bg?: string }): Promise<Blob> => {
|
||||
const { node, rect, bg } = arg;
|
||||
const canvas = konvaNodeToCanvas({ node, rect, bg });
|
||||
return canvasToBlob(canvas);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createAction, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { canvasSlice } from 'features/controlLayers/store/canvasSlice';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { canvasReset, canvasSlice } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
|
||||
type CanvasSessionState = {
|
||||
@@ -50,6 +51,9 @@ export const canvasSessionSlice = createSlice({
|
||||
state.selectedStagedImageIndex = 0;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(canvasReset, () => deepClone(initialState));
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
|
||||
@@ -21,9 +21,9 @@ import type {
|
||||
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
||||
import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
|
||||
import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants';
|
||||
import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types';
|
||||
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
|
||||
import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/Bbox/constants';
|
||||
import type { AspectRatioID } from 'features/parameters/components/Bbox/types';
|
||||
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import { isEqual, merge, omit } from 'lodash-es';
|
||||
@@ -1078,16 +1078,18 @@ export const canvasSlice = createSlice({
|
||||
state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier);
|
||||
},
|
||||
canvasReset: (state) => {
|
||||
// We need to keep the bbox state that is dependent on the model/optimal dimension
|
||||
const { width, height } = state.bbox.rect;
|
||||
const optimalDimension = state.bbox.optimalDimension;
|
||||
const scaledSize = getScaledBoundingBoxDimensions({ width, height }, optimalDimension);
|
||||
const newState = deepClone(initialState);
|
||||
const rect = calculateNewSize(newState.bbox.aspectRatio.value, optimalDimension * optimalDimension);
|
||||
|
||||
// We need to retain the optimal dimension across resets, as it is changed only when the model changes. Copy it
|
||||
// from the old state, then recalculate the bbox size & scaled size.
|
||||
newState.bbox.optimalDimension = state.bbox.optimalDimension;
|
||||
const rect = calculateNewSize(
|
||||
newState.bbox.aspectRatio.value,
|
||||
newState.bbox.optimalDimension * newState.bbox.optimalDimension
|
||||
);
|
||||
newState.bbox.rect.width = rect.width;
|
||||
newState.bbox.rect.height = rect.height;
|
||||
newState.bbox.optimalDimension = optimalDimension;
|
||||
newState.bbox.scaledSize = scaledSize;
|
||||
syncScaledSize(newState);
|
||||
|
||||
return newState;
|
||||
},
|
||||
@@ -1145,7 +1147,7 @@ export const {
|
||||
entityArrangedBackwardOne,
|
||||
entityArrangedToBack,
|
||||
entityOpacityChanged,
|
||||
allEntitiesDeleted,
|
||||
// allEntitiesDeleted, // currently unused
|
||||
allEntitiesOfTypeIsHiddenToggled,
|
||||
// bbox
|
||||
bboxChanged,
|
||||
|
||||
@@ -29,7 +29,7 @@ export const selectCanvasSlice = (state: RootState) => state.canvas.present;
|
||||
*
|
||||
* It does not check for validity of the entities.
|
||||
*/
|
||||
export const selectEntityCount = createSelector(selectCanvasSlice, (canvas) => {
|
||||
const selectEntityCount = createSelector(selectCanvasSlice, (canvas) => {
|
||||
return (
|
||||
canvas.regions.entities.length +
|
||||
canvas.ipAdapters.entities.length +
|
||||
@@ -230,3 +230,8 @@ export const getIsHiddenSelector = (type: CanvasEntityType) => {
|
||||
assert(false, 'Unhandled entity type');
|
||||
}
|
||||
};
|
||||
|
||||
export const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width);
|
||||
export const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height);
|
||||
export const selectAspectRatioID = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.id);
|
||||
export const selectAspectRatioValue = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.value);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types';
|
||||
import type { AspectRatioState } from 'features/parameters/components/Bbox/types';
|
||||
import type { ParameterHeight, ParameterLoRAModel, ParameterWidth } from 'features/parameters/types/parameterSchemas';
|
||||
import { zParameterNegativePrompt, zParameterPositivePrompt } from 'features/parameters/types/parameterSchemas';
|
||||
import type { AnyInvocation, BaseModelType, ImageDTO, S } from 'services/api/types';
|
||||
@@ -778,7 +778,7 @@ export type EntityRasterizedPayload = EntityIdentifierPayload<{
|
||||
|
||||
export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint';
|
||||
|
||||
function isDrawableEntityType(
|
||||
function isRenderableEntityType(
|
||||
entityType: CanvasEntityState['type']
|
||||
): entityType is CanvasRenderableEntityState['type'] {
|
||||
return (
|
||||
@@ -813,8 +813,29 @@ export function isRegionalGuidanceEntityIdentifier(
|
||||
return entityIdentifier.type === 'regional_guidance';
|
||||
}
|
||||
|
||||
export function isFilterableEntityIdentifier(
|
||||
entityIdentifier: CanvasEntityIdentifier
|
||||
): entityIdentifier is CanvasEntityIdentifier<'raster_layer'> | CanvasEntityIdentifier<'control_layer'> {
|
||||
return isRasterLayerEntityIdentifier(entityIdentifier) || isControlLayerEntityIdentifier(entityIdentifier);
|
||||
}
|
||||
|
||||
export function isTransformableEntityIdentifier(
|
||||
entityIdentifier: CanvasEntityIdentifier
|
||||
): entityIdentifier is
|
||||
| CanvasEntityIdentifier<'raster_layer'>
|
||||
| CanvasEntityIdentifier<'control_layer'>
|
||||
| CanvasEntityIdentifier<'inpaint_mask'>
|
||||
| CanvasEntityIdentifier<'regional_guidance'> {
|
||||
return (
|
||||
isRasterLayerEntityIdentifier(entityIdentifier) ||
|
||||
isControlLayerEntityIdentifier(entityIdentifier) ||
|
||||
isInpaintMaskEntityIdentifier(entityIdentifier) ||
|
||||
isRegionalGuidanceEntityIdentifier(entityIdentifier)
|
||||
);
|
||||
}
|
||||
|
||||
export function isRenderableEntity(entity: CanvasEntityState): entity is CanvasRenderableEntityState {
|
||||
return isDrawableEntityType(entity.type);
|
||||
return isRenderableEntityType(entity.type);
|
||||
}
|
||||
|
||||
export const getEntityIdentifier = <T extends CanvasEntityType>(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFoldersBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemChangeBoard = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imagesToChangeSelected([imageDTO]));
|
||||
dispatch(isModalOpenChanged(true));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiFoldersBold />} onClickCapture={onClick}>
|
||||
{t('boards.changeBoard')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemChangeBoard.displayName = 'ImageMenuItemChangeBoard';
|
||||
@@ -0,0 +1,28 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCopyBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemCopy = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const { isClipboardAPIAvailable, copyImageToClipboard } = useCopyImageToClipboard();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
copyImageToClipboard(imageDTO.image_url);
|
||||
}, [copyImageToClipboard, imageDTO.image_url]);
|
||||
|
||||
if (!isClipboardAPIAvailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiCopyBold />} onClickCapture={onClick}>
|
||||
{t('parameters.copyImage')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemCopy.displayName = 'ImageMenuItemCopy';
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemDelete = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem isDestructive icon={<PiTrashSimpleBold />} onClickCapture={onClick}>
|
||||
{t('gallery.deleteImage', { count: 1 })}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemDelete.displayName = 'ImageMenuItemDelete';
|
||||
@@ -0,0 +1,24 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useDownloadImage } from 'common/hooks/useDownloadImage';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDownloadSimpleBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemDownload = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const { downloadImage } = useDownloadImage();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
downloadImage(imageDTO.image_url, imageDTO.image_name);
|
||||
}, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={onClick}>
|
||||
{t('parameters.downloadImage')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemDownload.displayName = 'ImageMenuItemDownload';
|
||||
@@ -0,0 +1,32 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { SpinnerIcon } from 'features/gallery/components/ImageContextMenu/SpinnerIcon';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
|
||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFlowArrowBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemLoadWorkflow = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
|
||||
const hasTemplates = useStore($hasTemplates);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
getAndLoadEmbeddedWorkflow(imageDTO.image_name);
|
||||
}, [getAndLoadEmbeddedWorkflow, imageDTO.image_name]);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
icon={getAndLoadEmbeddedWorkflowResult.isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
|
||||
onClickCapture={onClick}
|
||||
isDisabled={!imageDTO.has_workflow || !hasTemplates}
|
||||
>
|
||||
{t('nodes.loadWorkflow')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemLoadWorkflow.displayName = 'ImageMenuItemLoadWorkflow';
|
||||
@@ -0,0 +1,72 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { SpinnerIcon } from 'features/gallery/components/ImageContextMenu/SpinnerIcon';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiArrowsCounterClockwiseBold,
|
||||
PiAsteriskBold,
|
||||
PiPaintBrushBold,
|
||||
PiPlantBold,
|
||||
PiQuotesBold,
|
||||
} from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemMetadataRecallActions = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
const {
|
||||
recallAll,
|
||||
remix,
|
||||
recallSeed,
|
||||
recallPrompts,
|
||||
hasMetadata,
|
||||
hasSeed,
|
||||
hasPrompts,
|
||||
isLoadingMetadata,
|
||||
createAsPreset,
|
||||
} = useImageActions(imageDTO?.image_name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiArrowsCounterClockwiseBold />}
|
||||
onClickCapture={remix}
|
||||
isDisabled={isLoadingMetadata || !hasMetadata}
|
||||
>
|
||||
{t('parameters.remixImage')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiQuotesBold />}
|
||||
onClickCapture={recallPrompts}
|
||||
isDisabled={isLoadingMetadata || !hasPrompts}
|
||||
>
|
||||
{t('parameters.usePrompt')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPlantBold />}
|
||||
onClickCapture={recallSeed}
|
||||
isDisabled={isLoadingMetadata || !hasSeed}
|
||||
>
|
||||
{t('parameters.useSeed')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiAsteriskBold />}
|
||||
onClickCapture={recallAll}
|
||||
isDisabled={isLoadingMetadata || !hasMetadata}
|
||||
>
|
||||
{t('parameters.useAll')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
|
||||
onClickCapture={createAsPreset}
|
||||
isDisabled={isLoadingMetadata || !hasPrompts}
|
||||
>
|
||||
{t('stylePresets.useForTemplate')}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemMetadataRecallActions.displayName = 'ImageMenuItemMetadataRecallActions';
|
||||
@@ -0,0 +1,18 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowSquareOutBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemOpenInNewTab = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
return (
|
||||
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiArrowSquareOutBold />}>
|
||||
{t('common.openInNewTab')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemOpenInNewTab.displayName = 'ImageMenuItemOpenInNewTab';
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyeBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemOpenInViewer = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
dispatch(imageSelected(imageDTO));
|
||||
imageViewer.onOpen();
|
||||
}, [dispatch, imageDTO, imageViewer]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiEyeBold />} onClick={onClick}>
|
||||
{t('gallery.openInViewer')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemOpenInViewer.displayName = 'ImageMenuItemOpenInViewer';
|
||||
@@ -0,0 +1,31 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImagesBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemSelectForCompare = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const selectMaySelectForCompare = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name !== imageDTO.image_name),
|
||||
[imageDTO.image_name]
|
||||
);
|
||||
const maySelectForCompare = useAppSelector(selectMaySelectForCompare);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(imageDTO));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiImagesBold />} isDisabled={!maySelectForCompare} onClick={onClick}>
|
||||
{t('gallery.selectForCompare')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemSelectForCompare.displayName = 'ImageMenuItemSelectForCompare';
|
||||
@@ -0,0 +1,50 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { sentImageToCanvas } from 'features/gallery/store/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShareFatBold } from 'react-icons/pi';
|
||||
|
||||
const selectBboxRect = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect);
|
||||
|
||||
export const ImageMenuItemSendToCanvas = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const bboxRect = useAppSelector(selectBboxRect);
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
const handleSendToCanvas = useCallback(() => {
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const overrides: Partial<CanvasRasterLayerState> = {
|
||||
position: { x: bboxRect.x, y: bboxRect.y },
|
||||
objects: [imageObject],
|
||||
};
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
dispatch(setActiveTab('generation'));
|
||||
imageViewer.onClose();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [bboxRect.x, bboxRect.y, dispatch, imageDTO, imageViewer, t]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-canvas">
|
||||
{t('parameters.sendToCanvas')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemSendToCanvas.displayName = 'ImageMenuItemSendToCanvas';
|
||||
@@ -0,0 +1,33 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShareFatBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemSendToUpscale = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
const handleSendToCanvas = useCallback(() => {
|
||||
dispatch(upscaleInitialImageChanged(imageDTO));
|
||||
dispatch(setActiveTab('upscaling'));
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToUpscale'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [dispatch, imageDTO, t]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-upscale">
|
||||
{t('parameters.sendToUpscale')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemSendToUpscale.displayName = 'ImageMenuItemSendToUpscale';
|
||||
@@ -0,0 +1,44 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiStarBold, PiStarFill } from 'react-icons/pi';
|
||||
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
|
||||
|
||||
export const ImageMenuItemStarUnstar = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const customStarUi = useStore($customStarUI);
|
||||
const [starImages] = useStarImagesMutation();
|
||||
const [unstarImages] = useUnstarImagesMutation();
|
||||
|
||||
const starImage = useCallback(() => {
|
||||
if (imageDTO) {
|
||||
starImages({ imageDTOs: [imageDTO] });
|
||||
}
|
||||
}, [starImages, imageDTO]);
|
||||
|
||||
const unstarImage = useCallback(() => {
|
||||
if (imageDTO) {
|
||||
unstarImages({ imageDTOs: [imageDTO] });
|
||||
}
|
||||
}, [unstarImages, imageDTO]);
|
||||
|
||||
if (imageDTO.starred) {
|
||||
return (
|
||||
<MenuItem icon={customStarUi ? customStarUi.off.icon : <PiStarFill />} onClickCapture={unstarImage}>
|
||||
{customStarUi ? customStarUi.off.text : t('gallery.unstarImage')}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={starImage}>
|
||||
{customStarUi ? customStarUi.on.text : t('gallery.starImage')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
ImageMenuItemStarUnstar.displayName = 'ImageMenuItemStarUnstar';
|
||||
@@ -1,234 +1,45 @@
|
||||
import { Flex, MenuDivider, MenuItem, Spinner } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
|
||||
import { useDownloadImage } from 'common/hooks/useDownloadImage';
|
||||
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||
import { size } from 'lodash-es';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiArrowsCounterClockwiseBold,
|
||||
PiAsteriskBold,
|
||||
PiCopyBold,
|
||||
PiDownloadSimpleBold,
|
||||
PiFlowArrowBold,
|
||||
PiFoldersBold,
|
||||
PiImagesBold,
|
||||
PiPaintBrushBold,
|
||||
PiPlantBold,
|
||||
PiQuotesBold,
|
||||
PiShareFatBold,
|
||||
PiStarBold,
|
||||
PiStarFill,
|
||||
PiTrashSimpleBold,
|
||||
} from 'react-icons/pi';
|
||||
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { ImageMenuItemChangeBoard } from 'features/gallery/components/ImageContextMenu/ImageMenuItemChangeBoard';
|
||||
import { ImageMenuItemCopy } from 'features/gallery/components/ImageContextMenu/ImageMenuItemCopy';
|
||||
import { ImageMenuItemDelete } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDelete';
|
||||
import { ImageMenuItemDownload } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDownload';
|
||||
import { ImageMenuItemLoadWorkflow } from 'features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow';
|
||||
import { ImageMenuItemMetadataRecallActions } from 'features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions';
|
||||
import { ImageMenuItemOpenInNewTab } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInNewTab';
|
||||
import { ImageMenuItemOpenInViewer } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer';
|
||||
import { ImageMenuItemSelectForCompare } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare';
|
||||
import { ImageMenuItemSendToCanvas } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSendToCanvas';
|
||||
import { ImageMenuItemSendToUpscale } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSendToUpscale';
|
||||
import { ImageMenuItemStarUnstar } from 'features/gallery/components/ImageContextMenu/ImageMenuItemStarUnstar';
|
||||
import { ImageDTOContextProvider } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
type SingleSelectionMenuItemsProps = {
|
||||
imageDTO: ImageDTO;
|
||||
};
|
||||
|
||||
const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
const { imageDTO } = props;
|
||||
const selectMaySelectForCompare = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name !== imageDTO.image_name),
|
||||
[imageDTO.image_name]
|
||||
);
|
||||
const maySelectForCompare = useAppSelector(selectMaySelectForCompare);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const customStarUi = useStore($customStarUI);
|
||||
const { downloadImage } = useDownloadImage();
|
||||
const templates = useStore($templates);
|
||||
|
||||
const {
|
||||
recallAll,
|
||||
remix,
|
||||
recallSeed,
|
||||
recallPrompts,
|
||||
hasMetadata,
|
||||
hasSeed,
|
||||
hasPrompts,
|
||||
isLoadingMetadata,
|
||||
createAsPreset,
|
||||
} = useImageActions(imageDTO?.image_name);
|
||||
|
||||
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
|
||||
|
||||
const handleLoadWorkflow = useCallback(() => {
|
||||
getAndLoadEmbeddedWorkflow(imageDTO.image_name);
|
||||
}, [getAndLoadEmbeddedWorkflow, imageDTO.image_name]);
|
||||
|
||||
const [starImages] = useStarImagesMutation();
|
||||
const [unstarImages] = useUnstarImagesMutation();
|
||||
|
||||
const { isClipboardAPIAvailable, copyImageToClipboard } = useCopyImageToClipboard();
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const handleSendToImageToImage = useCallback(() => {
|
||||
// TODO(psyche): restore send to img2img functionality
|
||||
dispatch(sentImageToImg2Img());
|
||||
dispatch(setActiveTab('generation'));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSendToCanvas = useCallback(() => {
|
||||
// TODO(psyche): restore send to canvas functionality
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(setActiveTab('generation'));
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToUnifiedCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [dispatch, t]);
|
||||
|
||||
const handleChangeBoard = useCallback(() => {
|
||||
dispatch(imagesToChangeSelected([imageDTO]));
|
||||
dispatch(isModalOpenChanged(true));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const handleCopyImage = useCallback(() => {
|
||||
copyImageToClipboard(imageDTO.image_url);
|
||||
}, [copyImageToClipboard, imageDTO.image_url]);
|
||||
|
||||
const handleStarImage = useCallback(() => {
|
||||
if (imageDTO) {
|
||||
starImages({ imageDTOs: [imageDTO] });
|
||||
}
|
||||
}, [starImages, imageDTO]);
|
||||
|
||||
const handleUnstarImage = useCallback(() => {
|
||||
if (imageDTO) {
|
||||
unstarImages({ imageDTOs: [imageDTO] });
|
||||
}
|
||||
}, [unstarImages, imageDTO]);
|
||||
|
||||
const handleDownloadImage = useCallback(() => {
|
||||
downloadImage(imageDTO.image_url, imageDTO.image_name);
|
||||
}, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
|
||||
|
||||
const handleSelectImageForCompare = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(imageDTO));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const handleSendToUpscale = useCallback(() => {
|
||||
dispatch(upscaleInitialImageChanged(imageDTO));
|
||||
dispatch(setActiveTab('upscaling'));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) => {
|
||||
return (
|
||||
<>
|
||||
<MenuItem as="a" href={imageDTO.image_url} target="_blank" icon={<PiShareFatBold />}>
|
||||
{t('common.openInNewTab')}
|
||||
</MenuItem>
|
||||
{isClipboardAPIAvailable && (
|
||||
<MenuItem icon={<PiCopyBold />} onClickCapture={handleCopyImage}>
|
||||
{t('parameters.copyImage')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem icon={<PiDownloadSimpleBold />} onClickCapture={handleDownloadImage}>
|
||||
{t('parameters.downloadImage')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiImagesBold />} isDisabled={!maySelectForCompare} onClick={handleSelectImageForCompare}>
|
||||
{t('gallery.selectForCompare')}
|
||||
</MenuItem>
|
||||
<ImageDTOContextProvider value={imageDTO}>
|
||||
<ImageMenuItemOpenInNewTab />
|
||||
<ImageMenuItemCopy />
|
||||
<ImageMenuItemDownload />
|
||||
<ImageMenuItemOpenInViewer />
|
||||
<ImageMenuItemSelectForCompare />
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={getAndLoadEmbeddedWorkflowResult.isLoading ? <SpinnerIcon /> : <PiFlowArrowBold />}
|
||||
onClickCapture={handleLoadWorkflow}
|
||||
isDisabled={!imageDTO.has_workflow || !size(templates)}
|
||||
>
|
||||
{t('nodes.loadWorkflow')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiArrowsCounterClockwiseBold />}
|
||||
onClickCapture={remix}
|
||||
isDisabled={isLoadingMetadata || !hasMetadata}
|
||||
>
|
||||
{t('parameters.remixImage')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiQuotesBold />}
|
||||
onClickCapture={recallPrompts}
|
||||
isDisabled={isLoadingMetadata || !hasPrompts}
|
||||
>
|
||||
{t('parameters.usePrompt')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPlantBold />}
|
||||
onClickCapture={recallSeed}
|
||||
isDisabled={isLoadingMetadata || !hasSeed}
|
||||
>
|
||||
{t('parameters.useSeed')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiAsteriskBold />}
|
||||
onClickCapture={recallAll}
|
||||
isDisabled={isLoadingMetadata || !hasMetadata}
|
||||
>
|
||||
{t('parameters.useAll')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={isLoadingMetadata ? <SpinnerIcon /> : <PiPaintBrushBold />}
|
||||
onClickCapture={createAsPreset}
|
||||
isDisabled={isLoadingMetadata || !hasPrompts}
|
||||
>
|
||||
{t('stylePresets.useForTemplate')}
|
||||
</MenuItem>
|
||||
<ImageMenuItemLoadWorkflow />
|
||||
<ImageMenuItemMetadataRecallActions />
|
||||
<MenuDivider />
|
||||
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToImageToImage} id="send-to-img2img">
|
||||
{t('parameters.sendToImg2Img')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToUpscale} id="send-to-upscale">
|
||||
{t('parameters.sendToUpscale')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-canvas">
|
||||
{t('parameters.sendToUnifiedCanvas')}
|
||||
</MenuItem>
|
||||
<ImageMenuItemSendToUpscale />
|
||||
<ImageMenuItemSendToCanvas />
|
||||
<MenuDivider />
|
||||
<MenuItem icon={<PiFoldersBold />} onClickCapture={handleChangeBoard}>
|
||||
{t('boards.changeBoard')}
|
||||
</MenuItem>
|
||||
{imageDTO.starred ? (
|
||||
<MenuItem icon={customStarUi ? customStarUi.off.icon : <PiStarFill />} onClickCapture={handleUnstarImage}>
|
||||
{customStarUi ? customStarUi.off.text : t('gallery.unstarImage')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem icon={customStarUi ? customStarUi.on.icon : <PiStarBold />} onClickCapture={handleStarImage}>
|
||||
{customStarUi ? customStarUi.on.text : t('gallery.starImage')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<ImageMenuItemChangeBoard />
|
||||
<ImageMenuItemStarUnstar />
|
||||
<MenuDivider />
|
||||
<MenuItem color="error.300" icon={<PiTrashSimpleBold />} onClickCapture={handleDelete}>
|
||||
{t('gallery.deleteImage', { count: 1 })}
|
||||
</MenuItem>
|
||||
</>
|
||||
<ImageMenuItemDelete />
|
||||
</ImageDTOContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SingleSelectionMenuItems);
|
||||
|
||||
const SpinnerIcon = () => (
|
||||
<Flex w="14px" alignItems="center" justifyContent="center">
|
||||
<Spinner size="xs" />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Flex, Spinner } from '@invoke-ai/ui-library';
|
||||
|
||||
export const SpinnerIcon = () => (
|
||||
<Flex w="14px" alignItems="center" justifyContent="center">
|
||||
<Spinner size="xs" />
|
||||
</Flex>
|
||||
);
|
||||
@@ -9,14 +9,12 @@ import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteIm
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
|
||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||
import { size } from 'lodash-es';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@@ -65,14 +63,6 @@ const CurrentImageButtons = () => {
|
||||
const handleUseSize = useCallback(() => {
|
||||
parseAndRecallImageDimensions(lastSelectedImage);
|
||||
}, [lastSelectedImage]);
|
||||
const handleSendToImageToImage = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
// TODO(psyche): restore send to img2img functionality
|
||||
dispatch(sentImageToImg2Img());
|
||||
dispatch(setActiveTab('generation'));
|
||||
}, [dispatch, imageDTO]);
|
||||
const handleClickUpscale = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
@@ -93,9 +83,8 @@ const CurrentImageButtons = () => {
|
||||
useHotkeys('p', recallPrompts, { enabled: isImageViewerActive }, [recallPrompts, isImageViewerActive]);
|
||||
useHotkeys('r', remix, { enabled: isImageViewerActive }, [remix, isImageViewerActive]);
|
||||
useHotkeys('d', handleUseSize, { enabled: isImageViewerActive }, [handleUseSize, isImageViewerActive]);
|
||||
useHotkeys('shift+i', handleSendToImageToImage, { enabled: isImageViewerActive }, [imageDTO, isImageViewerActive]);
|
||||
useHotkeys(
|
||||
'Shift+U',
|
||||
'shift+u',
|
||||
handleClickUpscale,
|
||||
{ enabled: Boolean(isUpscalingEnabled && isImageViewerActive && isConnected) },
|
||||
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected, isImageViewerActive]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const ImageDTOContext = createContext<ImageDTO | null>(null);
|
||||
|
||||
export const ImageDTOContextProvider = ImageDTOContext.Provider;
|
||||
|
||||
export const useImageDTOContext = () => {
|
||||
const imageDTO = useContext(ImageDTOContext);
|
||||
assert(imageDTO !== null, 'useImageDTOContext must be used within ImageDTOContextProvider');
|
||||
return imageDTO;
|
||||
};
|
||||
@@ -2,6 +2,4 @@ import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
|
||||
|
||||
export const sentImageToImg2Img = createAction('gallery/sentImageToImg2Img');
|
||||
|
||||
export const imageDownloaded = createAction('gallery/imageDownloaded');
|
||||
|
||||
@@ -55,7 +55,7 @@ import {
|
||||
} from 'features/nodes/types/field';
|
||||
import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation';
|
||||
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
|
||||
import { atom } from 'nanostores';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { MouseEvent } from 'react';
|
||||
import type { Edge, EdgeChange, NodeChange, Viewport, XYPosition } from 'reactflow';
|
||||
import { applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow';
|
||||
@@ -435,6 +435,7 @@ export const {
|
||||
|
||||
export const $cursorPos = atom<XYPosition | null>(null);
|
||||
export const $templates = atom<Templates>({});
|
||||
export const $hasTemplates = computed($templates, (templates) => Object.keys(templates).length > 0);
|
||||
export const $copiedNodes = atom<AnyNode[]>([]);
|
||||
export const $copiedEdges = atom<InvocationNodeEdge[]>([]);
|
||||
export const $edgesToCopiedNodes = atom<InvocationNodeEdge[]>([]);
|
||||
|
||||
@@ -17,7 +17,7 @@ export const addControlNets = async (
|
||||
manager: CanvasManager,
|
||||
layers: CanvasControlLayerState[],
|
||||
g: Graph,
|
||||
bbox: Rect,
|
||||
rect: Rect,
|
||||
collector: Invocation<'collect'>,
|
||||
base: BaseModelType
|
||||
): Promise<AddControlNetsResult> => {
|
||||
@@ -35,7 +35,7 @@ export const addControlNets = async (
|
||||
|
||||
const adapter = manager.adapters.controlLayers.get(layer.id);
|
||||
assert(adapter, 'Adapter not found');
|
||||
const imageDTO = await adapter.renderer.rasterize({ rect: bbox, attrs: { opacity: 1, filters: [] } });
|
||||
const imageDTO = await adapter.renderer.rasterize({ rect, attrs: { opacity: 1, filters: [] }, bg: 'black' });
|
||||
addControlNetToGraph(g, layer, imageDTO, collector);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export const addT2IAdapters = async (
|
||||
manager: CanvasManager,
|
||||
layers: CanvasControlLayerState[],
|
||||
g: Graph,
|
||||
bbox: Rect,
|
||||
rect: Rect,
|
||||
collector: Invocation<'collect'>,
|
||||
base: BaseModelType
|
||||
): Promise<AddT2IAdaptersResult> => {
|
||||
@@ -68,7 +68,7 @@ export const addT2IAdapters = async (
|
||||
|
||||
const adapter = manager.adapters.controlLayers.get(layer.id);
|
||||
assert(adapter, 'Adapter not found');
|
||||
const imageDTO = await adapter.renderer.rasterize({ rect: bbox, attrs: { opacity: 1, filters: [] } });
|
||||
const imageDTO = await adapter.renderer.rasterize({ rect, attrs: { opacity: 1, filters: [], bg: 'black' } });
|
||||
addT2IAdapterToGraph(g, layer, imageDTO, collector);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import type { ComboboxOption, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { SingleValue } from 'chakra-react-select';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/DocumentSize/constants';
|
||||
import { isAspectRatioID } from 'features/parameters/components/DocumentSize/types';
|
||||
import { selectAspectRatioID } from 'features/controlLayers/store/selectors';
|
||||
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/Bbox/constants';
|
||||
import { isAspectRatioID } from 'features/parameters/components/Bbox/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectAspectRatioID = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.id);
|
||||
|
||||
export const AspectRatioSelect = memo(() => {
|
||||
export const BboxAspectRatioSelect = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const id = useAppSelector(selectAspectRatioID);
|
||||
@@ -40,6 +37,6 @@ export const AspectRatioSelect = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
AspectRatioSelect.displayName = 'AspectRatioSelect';
|
||||
BboxAspectRatioSelect.displayName = 'BboxAspectRatioSelect';
|
||||
|
||||
const selectStyles: SystemStyleObject = { minW: 24 };
|
||||
@@ -1,17 +1,13 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { bboxHeightChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { selectHeight, selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
||||
import { selectHeightConfig } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height);
|
||||
const selectHeightConfig = createSelector(selectConfigSlice, (config) => config.sd.height);
|
||||
|
||||
export const ParamHeight = memo(() => {
|
||||
export const BboxHeight = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||
@@ -58,4 +54,4 @@ export const ParamHeight = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ParamHeight.displayName = 'ParamHeight';
|
||||
BboxHeight.displayName = 'BboxHeight';
|
||||
@@ -9,7 +9,7 @@ import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi';
|
||||
|
||||
const selectAspectRatioIsLocked = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.isLocked);
|
||||
|
||||
export const LockAspectRatioButton = memo(() => {
|
||||
export const BboxLockAspectRatioButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isLocked = useAppSelector(selectAspectRatioIsLocked);
|
||||
@@ -29,4 +29,4 @@ export const LockAspectRatioButton = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
LockAspectRatioButton.displayName = 'LockAspectRatioButton';
|
||||
BboxLockAspectRatioButton.displayName = 'BboxLockAspectRatioButton';
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectAspectRatioValue, selectHeight, selectWidth } from 'features/controlLayers/store/selectors';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
export const BboxPreview = memo(() => {
|
||||
const bboxWidth = useAppSelector(selectWidth);
|
||||
const bboxHeight = useAppSelector(selectHeight);
|
||||
const aspectRatioValue = useAppSelector(selectAspectRatioValue);
|
||||
const [ref, dims] = useMeasure<HTMLDivElement>();
|
||||
|
||||
const previewBoxSize = useMemo(() => {
|
||||
if (!dims) {
|
||||
return { width: 0, height: 0 };
|
||||
}
|
||||
|
||||
let width = bboxWidth;
|
||||
let height = bboxHeight;
|
||||
|
||||
if (bboxWidth > bboxHeight) {
|
||||
width = dims.width;
|
||||
height = width / aspectRatioValue;
|
||||
} else {
|
||||
height = dims.height;
|
||||
width = height * aspectRatioValue;
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
}, [dims, bboxWidth, bboxHeight, aspectRatioValue]);
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center" ref={ref}>
|
||||
<Flex
|
||||
position="relative"
|
||||
borderRadius="base"
|
||||
borderColor="base.600"
|
||||
borderWidth="3px"
|
||||
width={`${previewBoxSize.width}px`}
|
||||
height={`${previewBoxSize.height}px`}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Grid
|
||||
borderRadius="base"
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
gridTemplateColumns="1fr 1fr 1fr"
|
||||
gridTemplateRows="1fr 1fr 1fr"
|
||||
gap="1px"
|
||||
bg="base.700"
|
||||
>
|
||||
<GridItem bg="base.800" />
|
||||
<GridItem bg="base.800" />
|
||||
<GridItem bg="base.800" />
|
||||
<GridItem bg="base.800" />
|
||||
<GridItem bg="base.800" />
|
||||
<GridItem bg="base.800" />
|
||||
<GridItem bg="base.800" />
|
||||
<GridItem bg="base.800" />
|
||||
<GridItem bg="base.800" />
|
||||
</Grid>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
BboxPreview.displayName = 'BboxPreview';
|
||||
@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectScaleMethod = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.scaleMethod);
|
||||
|
||||
const ParamScaleBeforeProcessing = () => {
|
||||
const BboxScaleMethod = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const scaleMethod = useAppSelector(selectScaleMethod);
|
||||
@@ -47,4 +47,4 @@ const ParamScaleBeforeProcessing = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamScaleBeforeProcessing);
|
||||
export default memo(BboxScaleMethod);
|
||||
@@ -14,7 +14,7 @@ const selectScaledBoundingBoxHeightConfig = createSelector(
|
||||
(config) => config.sd.scaledBoundingBoxHeight
|
||||
);
|
||||
|
||||
const ParamScaledHeight = () => {
|
||||
const BboxScaledHeight = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||
@@ -55,4 +55,4 @@ const ParamScaledHeight = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamScaledHeight);
|
||||
export default memo(BboxScaledHeight);
|
||||
@@ -14,7 +14,7 @@ const selectScaledBoundingBoxWidthConfig = createSelector(
|
||||
(config) => config.sd.scaledBoundingBoxWidth
|
||||
);
|
||||
|
||||
const ParamScaledWidth = () => {
|
||||
const BboxScaledWidth = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||
@@ -54,4 +54,4 @@ const ParamScaledWidth = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ParamScaledWidth);
|
||||
export default memo(BboxScaledWidth);
|
||||
@@ -11,7 +11,7 @@ import { RiSparklingFill } from 'react-icons/ri';
|
||||
const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width);
|
||||
const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height);
|
||||
|
||||
export const SetOptimalSizeButton = memo(() => {
|
||||
export const BboxSetOptimalSizeButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const width = useAppSelector(selectWidth);
|
||||
@@ -51,4 +51,4 @@ export const SetOptimalSizeButton = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
SetOptimalSizeButton.displayName = 'SetOptimalSizeButton';
|
||||
BboxSetOptimalSizeButton.displayName = 'BboxSetOptimalSizeButton';
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { FormLabelProps } from '@invoke-ai/ui-library';
|
||||
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
|
||||
import { BboxAspectRatioSelect } from 'features/parameters/components/Bbox/BboxAspectRatioSelect';
|
||||
import { BboxHeight } from 'features/parameters/components/Bbox/BboxHeight';
|
||||
import { BboxLockAspectRatioButton } from 'features/parameters/components/Bbox/BboxLockAspectRatioButton';
|
||||
import { BboxPreview } from 'features/parameters/components/Bbox/BboxPreview';
|
||||
import { BboxSetOptimalSizeButton } from 'features/parameters/components/Bbox/BboxSetOptimalSizeButton';
|
||||
import { BboxSwapDimensionsButton } from 'features/parameters/components/Bbox/BboxSwapDimensionsButton';
|
||||
import { BboxWidth } from 'features/parameters/components/Bbox/BboxWidth';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const BboxSettings = memo(() => {
|
||||
return (
|
||||
<Flex gap={4} alignItems="center">
|
||||
<Flex gap={4} flexDirection="column" width="full">
|
||||
<FormControlGroup formLabelProps={formLabelProps}>
|
||||
<Flex gap={4}>
|
||||
<BboxAspectRatioSelect />
|
||||
<BboxSwapDimensionsButton />
|
||||
<BboxLockAspectRatioButton />
|
||||
<BboxSetOptimalSizeButton />
|
||||
</Flex>
|
||||
<BboxWidth />
|
||||
<BboxHeight />
|
||||
</FormControlGroup>
|
||||
</Flex>
|
||||
<Flex w="108px" h="108px" flexShrink={0} flexGrow={0} alignItems="center" justifyContent="center">
|
||||
<BboxPreview />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
BboxSettings.displayName = 'BboxSettings';
|
||||
|
||||
const formLabelProps: FormLabelProps = {
|
||||
minW: 14,
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowsDownUpBold } from 'react-icons/pi';
|
||||
|
||||
export const SwapDimensionsButton = memo(() => {
|
||||
export const BboxSwapDimensionsButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
@@ -23,4 +23,4 @@ export const SwapDimensionsButton = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
SwapDimensionsButton.displayName = 'SwapDimensionsButton';
|
||||
BboxSwapDimensionsButton.displayName = 'BboxSwapDimensionsButton';
|
||||
@@ -1,17 +1,13 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { selectOptimalDimension, selectWidth } from 'features/controlLayers/store/selectors';
|
||||
import { selectWidthConfig } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width);
|
||||
const selectWidthConfig = createSelector(selectConfigSlice, (config) => config.sd.width);
|
||||
|
||||
export const ParamWidth = memo(() => {
|
||||
export const BboxWidth = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const width = useAppSelector(selectWidth);
|
||||
@@ -58,4 +54,4 @@ export const ParamWidth = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ParamWidth.displayName = 'ParamWidth';
|
||||
BboxWidth.displayName = 'BboxWidth';
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice';
|
||||
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
const ParamImageToImageStrength = () => {
|
||||
const img2imgStrength = useAppSelector(selectImg2imgStrength);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(setImg2imgStrength(v));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return <ImageToImageStrength value={img2imgStrength} onChange={onChange} />;
|
||||
};
|
||||
|
||||
export default memo(ParamImageToImageStrength);
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { FormLabelProps } from '@invoke-ai/ui-library';
|
||||
import { Flex, FormControlGroup } from '@invoke-ai/ui-library';
|
||||
import { CanvasResizer } from 'features/controlLayers/components/CanvasResizer';
|
||||
import { ParamHeight } from 'features/parameters/components/Core/ParamHeight';
|
||||
import { ParamWidth } from 'features/parameters/components/Core/ParamWidth';
|
||||
import { AspectRatioSelect } from 'features/parameters/components/DocumentSize/AspectRatioSelect';
|
||||
import { LockAspectRatioButton } from 'features/parameters/components/DocumentSize/LockAspectRatioButton';
|
||||
import { SetOptimalSizeButton } from 'features/parameters/components/DocumentSize/SetOptimalSizeButton';
|
||||
import { SwapDimensionsButton } from 'features/parameters/components/DocumentSize/SwapDimensionsButton';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const DocumentSize = memo(() => {
|
||||
return (
|
||||
<Flex gap={4} alignItems="center">
|
||||
<Flex gap={4} flexDirection="column" width="full">
|
||||
<FormControlGroup formLabelProps={formLabelProps}>
|
||||
<Flex gap={4}>
|
||||
<AspectRatioSelect />
|
||||
<SwapDimensionsButton />
|
||||
<LockAspectRatioButton />
|
||||
<SetOptimalSizeButton />
|
||||
</Flex>
|
||||
<ParamWidth />
|
||||
<ParamHeight />
|
||||
</FormControlGroup>
|
||||
</Flex>
|
||||
<Flex w="108px" h="108px" flexShrink={0} flexGrow={0} bg="base.850" alignItems="center" justifyContent="center">
|
||||
<CanvasResizer />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
DocumentSize.displayName = 'DocumentSize';
|
||||
|
||||
const formLabelProps: FormLabelProps = {
|
||||
minW: 14,
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const marks = [0, 0.5, 1];
|
||||
|
||||
type Props = {
|
||||
value: number;
|
||||
onChange: (v: number) => void;
|
||||
};
|
||||
|
||||
const ImageToImageStrength = ({ value, onChange }: Props) => {
|
||||
const config = useAppSelector(selectImg2imgStrengthConfig);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<InformationalPopover feature="paramDenoisingStrength">
|
||||
<FormLabel>{`${t('parameters.denoisingStrength')}`}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<CompositeSlider
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
min={config.sliderMin}
|
||||
max={config.sliderMax}
|
||||
defaultValue={config.initial}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
marks={marks}
|
||||
/>
|
||||
<CompositeNumberInput
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
min={config.numberInputMin}
|
||||
max={config.numberInputMax}
|
||||
defaultValue={config.initial}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ImageToImageStrength);
|
||||
@@ -1,16 +1,14 @@
|
||||
import type { FormLabelProps } from '@invoke-ai/ui-library';
|
||||
import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { HrfSettings } from 'features/hrf/components/HrfSettings';
|
||||
import { selectHrfSlice } from 'features/hrf/store/hrfSlice';
|
||||
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';
|
||||
import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight';
|
||||
import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth';
|
||||
import ParamImageToImageStrength from 'features/parameters/components/Canvas/ParamImageToImageStrength';
|
||||
import { DocumentSize } from 'features/parameters/components/DocumentSize/DocumentSize';
|
||||
import BboxScaledHeight from 'features/parameters/components/Bbox/BboxScaledHeight';
|
||||
import BboxScaledWidth from 'features/parameters/components/Bbox/BboxScaledWidth';
|
||||
import BboxScaleMethod from 'features/parameters/components/Bbox/BboxScaleMethod';
|
||||
import { BboxSettings } from 'features/parameters/components/Bbox/BboxSettings';
|
||||
import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput';
|
||||
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
|
||||
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
|
||||
@@ -19,34 +17,30 @@ import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selector = createMemoizedSelector(
|
||||
[selectHrfSlice, selectCanvasSlice, selectParamsSlice],
|
||||
(hrf, canvas, params) => {
|
||||
const { shouldRandomizeSeed, model } = params;
|
||||
const { hrfEnabled } = hrf;
|
||||
const badges: string[] = [];
|
||||
const isSDXL = model?.base === 'sdxl';
|
||||
const selectBadges = createMemoizedSelector([selectCanvasSlice, selectParamsSlice], (canvas, params) => {
|
||||
const { shouldRandomizeSeed } = params;
|
||||
const badges: string[] = [];
|
||||
|
||||
const { aspectRatio } = canvas.bbox;
|
||||
const { width, height } = canvas.bbox.rect;
|
||||
const { aspectRatio } = canvas.bbox;
|
||||
const { width, height } = canvas.bbox.rect;
|
||||
|
||||
badges.push(`${width}×${height}`);
|
||||
badges.push(aspectRatio.id);
|
||||
badges.push(`${width}×${height}`);
|
||||
badges.push(aspectRatio.id);
|
||||
|
||||
if (aspectRatio.isLocked) {
|
||||
badges.push('locked');
|
||||
}
|
||||
|
||||
if (!shouldRandomizeSeed) {
|
||||
badges.push('Manual Seed');
|
||||
}
|
||||
|
||||
if (hrfEnabled && !isSDXL) {
|
||||
badges.push('HiRes Fix');
|
||||
}
|
||||
return { badges, isSDXL };
|
||||
if (aspectRatio.isLocked) {
|
||||
badges.push('locked');
|
||||
}
|
||||
);
|
||||
|
||||
if (!shouldRandomizeSeed) {
|
||||
badges.push('Manual Seed');
|
||||
}
|
||||
|
||||
if (badges.length === 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
badges;
|
||||
});
|
||||
|
||||
const scalingLabelProps: FormLabelProps = {
|
||||
minW: '4.5rem',
|
||||
@@ -54,7 +48,7 @@ const scalingLabelProps: FormLabelProps = {
|
||||
|
||||
export const ImageSettingsAccordion = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { badges, isSDXL } = useAppSelector(selector);
|
||||
const badges = useAppSelector(selectBadges);
|
||||
const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({
|
||||
id: 'image-settings',
|
||||
defaultIsOpen: true,
|
||||
@@ -72,22 +66,18 @@ export const ImageSettingsAccordion = memo(() => {
|
||||
onToggle={onToggleAccordion}
|
||||
>
|
||||
<Flex px={4} pt={4} w="full" h="full" flexDir="column" data-testid="image-settings-accordion">
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<DocumentSize />
|
||||
<ParamImageToImageStrength />
|
||||
<BboxSettings />
|
||||
<Flex pt={4} gap={4} alignItems="center">
|
||||
<ParamSeedNumberInput />
|
||||
<ParamSeedShuffle />
|
||||
<ParamSeedRandomize />
|
||||
</Flex>
|
||||
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
|
||||
<Flex gap={4} pb={4} flexDir="column">
|
||||
<Flex gap={4} alignItems="center">
|
||||
<ParamSeedNumberInput />
|
||||
<ParamSeedShuffle />
|
||||
<ParamSeedRandomize />
|
||||
</Flex>
|
||||
{!isSDXL && <HrfSettings />}
|
||||
<ParamScaleBeforeProcessing />
|
||||
<BboxScaleMethod />
|
||||
<FormControlGroup formLabelProps={scalingLabelProps}>
|
||||
<ParamScaledWidth />
|
||||
<ParamScaledHeight />
|
||||
<BboxScaledWidth />
|
||||
<BboxScaledHeight />
|
||||
</FormControlGroup>
|
||||
</Flex>
|
||||
</Expander>
|
||||
|
||||
@@ -16,10 +16,10 @@ type UseGetAndLoadEmbeddedWorkflowReturn = {
|
||||
};
|
||||
|
||||
type UseGetAndLoadEmbeddedWorkflow = (
|
||||
options: UseGetAndLoadEmbeddedWorkflowOptions
|
||||
options?: UseGetAndLoadEmbeddedWorkflowOptions
|
||||
) => UseGetAndLoadEmbeddedWorkflowReturn;
|
||||
|
||||
export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ onSuccess, onError }) => {
|
||||
export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = (options) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [_getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult] = useLazyGetImageWorkflowQuery();
|
||||
@@ -30,7 +30,7 @@ export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ o
|
||||
if (data) {
|
||||
dispatch(workflowLoadRequested({ data, asCopy: true }));
|
||||
// No toast - the listener for this action does that after the workflow is loaded
|
||||
onSuccess && onSuccess();
|
||||
options?.onSuccess && options?.onSuccess();
|
||||
} else {
|
||||
toast({
|
||||
id: 'PROBLEM_RETRIEVING_WORKFLOW',
|
||||
@@ -44,10 +44,10 @@ export const useGetAndLoadEmbeddedWorkflow: UseGetAndLoadEmbeddedWorkflow = ({ o
|
||||
title: t('toast.problemRetrievingWorkflow'),
|
||||
status: 'error',
|
||||
});
|
||||
onError && onError();
|
||||
options?.onError && options?.onError();
|
||||
}
|
||||
},
|
||||
[_getAndLoadEmbeddedWorkflow, dispatch, onSuccess, t, onError]
|
||||
[_getAndLoadEmbeddedWorkflow, dispatch, options, t]
|
||||
);
|
||||
|
||||
return { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult };
|
||||
|
||||
@@ -276,12 +276,12 @@ export const queueApi = api.injectEndpoints({
|
||||
},
|
||||
invalidatesTags: ['SessionQueueStatus', 'BatchStatus'],
|
||||
}),
|
||||
cancelByBatchOrigin: build.mutation<
|
||||
paths['/api/v1/queue/{queue_id}/cancel_by_origin']['put']['responses']['200']['content']['application/json'],
|
||||
paths['/api/v1/queue/{queue_id}/cancel_by_origin']['put']['parameters']['query']
|
||||
cancelByBatchDestination: build.mutation<
|
||||
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['responses']['200']['content']['application/json'],
|
||||
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['parameters']['query']
|
||||
>({
|
||||
query: (params) => ({
|
||||
url: buildQueueUrl('cancel_by_origin'),
|
||||
url: buildQueueUrl('cancel_by_destination'),
|
||||
method: 'PUT',
|
||||
params,
|
||||
}),
|
||||
|
||||
@@ -1104,7 +1104,7 @@ export type paths = {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/queue/{queue_id}/cancel_by_origin": {
|
||||
"/api/v1/queue/{queue_id}/cancel_by_destination": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
@@ -1113,10 +1113,10 @@ export type paths = {
|
||||
};
|
||||
get?: never;
|
||||
/**
|
||||
* Cancel By Origin
|
||||
* Cancel By Destination
|
||||
* @description Immediately cancels all queue items with the given origin
|
||||
*/
|
||||
put: operations["cancel_by_origin"];
|
||||
put: operations["cancel_by_destination"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
@@ -3052,10 +3052,10 @@ export type components = {
|
||||
canceled: number;
|
||||
};
|
||||
/**
|
||||
* CancelByOriginResult
|
||||
* @description Result of canceling by list of batch ids
|
||||
* CancelByDestinationResult
|
||||
* @description Result of canceling by a destination
|
||||
*/
|
||||
CancelByOriginResult: {
|
||||
CancelByDestinationResult: {
|
||||
/**
|
||||
* Canceled
|
||||
* @description Number of queue items canceled
|
||||
@@ -18561,11 +18561,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
cancel_by_origin: {
|
||||
cancel_by_destination: {
|
||||
parameters: {
|
||||
query: {
|
||||
/** @description The origin to cancel all queue items for */
|
||||
origin: string;
|
||||
/** @description The destination to cancel all queue items for */
|
||||
destination: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user