mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-15 19:58:13 -05:00
Compare commits
544 Commits
v4.2.9.dev
...
v4.2.9.dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37ed567aad | ||
|
|
9692a5f996 | ||
|
|
5db7b48cd8 | ||
|
|
ea014c66ac | ||
|
|
25918c28aa | ||
|
|
0c60469401 | ||
|
|
f1aa50f447 | ||
|
|
a413b261f0 | ||
|
|
4a1a6639f6 | ||
|
|
201c370ca1 | ||
|
|
d070c7c726 | ||
|
|
e38e20a992 | ||
|
|
39a94ec70e | ||
|
|
c7bfae2d1e | ||
|
|
e7944c427d | ||
|
|
48ed4e120d | ||
|
|
a5b038a1b1 | ||
|
|
dc752c98b0 | ||
|
|
85a47cc6fe | ||
|
|
6450f42cfa | ||
|
|
3876f71ff4 | ||
|
|
cf819e8eab | ||
|
|
2217fb8485 | ||
|
|
43652e830a | ||
|
|
a3417bf81d | ||
|
|
06637161e3 | ||
|
|
c4f4b16a36 | ||
|
|
3001718f9f | ||
|
|
ac16fa65a3 | ||
|
|
bc0b5335ff | ||
|
|
e91c7c5a30 | ||
|
|
74791cc490 | ||
|
|
68409c6a0f | ||
|
|
85613b220c | ||
|
|
80085ad854 | ||
|
|
b6bfa65104 | ||
|
|
0bfa033089 | ||
|
|
6f0974b5bc | ||
|
|
c3b53fc4f6 | ||
|
|
8f59a32d81 | ||
|
|
3b4f20f433 | ||
|
|
73da6e9628 | ||
|
|
2fc482141d | ||
|
|
a41ec5f3fc | ||
|
|
c906225d03 | ||
|
|
2985ea3716 | ||
|
|
4f151c6c6f | ||
|
|
c4f5252c1a | ||
|
|
77c13f2cf3 | ||
|
|
3270d36fca | ||
|
|
b6b30ff01f | ||
|
|
aa9bfdff35 | ||
|
|
80308cc3b8 | ||
|
|
f6db73bf1f | ||
|
|
ef9f61a39f | ||
|
|
a1a0881133 | ||
|
|
9956919ab6 | ||
|
|
abc07f57d6 | ||
|
|
1a1cae79f1 | ||
|
|
bcfafe7b06 | ||
|
|
34e8ced592 | ||
|
|
1fdada65b6 | ||
|
|
433f3e1971 | ||
|
|
a60e23f825 | ||
|
|
f69de3148e | ||
|
|
cbcd36ef54 | ||
|
|
aa76134340 | ||
|
|
55758acae8 | ||
|
|
196e43b5e5 | ||
|
|
38b9828441 | ||
|
|
0048a7077e | ||
|
|
527a39a3ad | ||
|
|
30ce4c55c7 | ||
|
|
ca082d4288 | ||
|
|
5e59a4f43a | ||
|
|
9f86605049 | ||
|
|
79058a7894 | ||
|
|
bb3ad8c2f1 | ||
|
|
799688514b | ||
|
|
b7344b0df2 | ||
|
|
7e382c5f3f | ||
|
|
9cf357e184 | ||
|
|
95b6c773d4 | ||
|
|
89d8c5ba00 | ||
|
|
59580cf6ed | ||
|
|
2b0c084f5b | ||
|
|
4d896073ff | ||
|
|
9f69503a80 | ||
|
|
0311e852a0 | ||
|
|
7003a3d546 | ||
|
|
dc73072e27 | ||
|
|
e549c44ad7 | ||
|
|
45a4231cbe | ||
|
|
81f046ebac | ||
|
|
6ef6c593c4 | ||
|
|
5b53eefef7 | ||
|
|
9a9919c0af | ||
|
|
10661b33d4 | ||
|
|
52193d604d | ||
|
|
2568441e6a | ||
|
|
1a14860b3b | ||
|
|
9ff7647ec5 | ||
|
|
b49106e8fe | ||
|
|
906d0902a3 | ||
|
|
fbde6f5a7f | ||
|
|
b388268987 | ||
|
|
3b4164bd62 | ||
|
|
b7fc6fe573 | ||
|
|
2954a19d27 | ||
|
|
aa45ce7fbd | ||
|
|
77e5078e4a | ||
|
|
603cc7bf2e | ||
|
|
cd517a102d | ||
|
|
9a442918b5 | ||
|
|
f9c03d85a5 | ||
|
|
10d07c71c4 | ||
|
|
cd05a78219 | ||
|
|
f8ee572abc | ||
|
|
d918654509 | ||
|
|
582e30c542 | ||
|
|
34a6555301 | ||
|
|
fff860090b | ||
|
|
f4971197c1 | ||
|
|
621d5e0462 | ||
|
|
0b68a69a6c | ||
|
|
9a599ce595 | ||
|
|
1467ba276f | ||
|
|
708facf707 | ||
|
|
9c6c6adb1f | ||
|
|
c335b8581c | ||
|
|
f1348e45bd | ||
|
|
ce6cf9b079 | ||
|
|
13ec80736a | ||
|
|
c9690a4b21 | ||
|
|
489e875a6e | ||
|
|
8651396048 | ||
|
|
2bab5a6179 | ||
|
|
006f06b615 | ||
|
|
d603923d1b | ||
|
|
86878e855b | ||
|
|
35de60a8fa | ||
|
|
2c444a1941 | ||
|
|
3dfef01889 | ||
|
|
a845a2daa5 | ||
|
|
df41f4fbce | ||
|
|
76482da6f5 | ||
|
|
8205abbbbf | ||
|
|
926873de26 | ||
|
|
00cb1903ba | ||
|
|
58ba38b9c7 | ||
|
|
2f6a5617f9 | ||
|
|
e0d84743be | ||
|
|
ee7c62acc4 | ||
|
|
daf3e58bd9 | ||
|
|
c5b9209057 | ||
|
|
2a4d6d98e2 | ||
|
|
cfdf59d906 | ||
|
|
f91ce1a47c | ||
|
|
4af2888168 | ||
|
|
8471c6fe86 | ||
|
|
fe65a5a2db | ||
|
|
0df26e967c | ||
|
|
d4822b305e | ||
|
|
8df5447563 | ||
|
|
7b5a43df9b | ||
|
|
61ef630175 | ||
|
|
4eda2ef555 | ||
|
|
57f4489520 | ||
|
|
fb6cf9e3da | ||
|
|
f776326cff | ||
|
|
5be32d5733 | ||
|
|
1f73435241 | ||
|
|
3251a00631 | ||
|
|
49c4ad1dd7 | ||
|
|
5857e95c4a | ||
|
|
85be2532c6 | ||
|
|
8b81a00def | ||
|
|
8544595c27 | ||
|
|
a6a5d1470c | ||
|
|
febcc12ec9 | ||
|
|
ab64078b76 | ||
|
|
0ff3459b07 | ||
|
|
2abd7c9bfe | ||
|
|
8e5330bdc9 | ||
|
|
1ecec4ea3a | ||
|
|
700dbe69f3 | ||
|
|
af7ba3b7e4 | ||
|
|
1f7144d62e | ||
|
|
4e389e415b | ||
|
|
2db29fb6ab | ||
|
|
79653fcff5 | ||
|
|
9e39180fbc | ||
|
|
dc0f832d8f | ||
|
|
0dcd6aa5d9 | ||
|
|
9f4a8f11f8 | ||
|
|
6f9579d6ec | ||
|
|
2b2aabb234 | ||
|
|
a79b9633ab | ||
|
|
7b628c908b | ||
|
|
181703a709 | ||
|
|
c439e3c204 | ||
|
|
11e81eb456 | ||
|
|
3e24bf640e | ||
|
|
a17664fb75 | ||
|
|
5aed23dc91 | ||
|
|
7f389716d0 | ||
|
|
eca13b674a | ||
|
|
7c982a1bdf | ||
|
|
bdfe6870fd | ||
|
|
7eebbc0dd9 | ||
|
|
6ae46d7c8b | ||
|
|
8aae30566e | ||
|
|
4b7c3e221c | ||
|
|
4015795b7f | ||
|
|
132dd61d8d | ||
|
|
6f2b548dd1 | ||
|
|
49dd316f17 | ||
|
|
e0a8bb149d | ||
|
|
020b6db34b | ||
|
|
f6d2f0bf8c | ||
|
|
1280cce803 | ||
|
|
82463d12e2 | ||
|
|
ba6c1b84e4 | ||
|
|
92b6d3198a | ||
|
|
693ae1af50 | ||
|
|
3873a3096c | ||
|
|
4a9f6ab5ef | ||
|
|
2fc29c5125 | ||
|
|
5fbc876cfd | ||
|
|
89b0673ac9 | ||
|
|
3179a16189 | ||
|
|
3ce216b391 | ||
|
|
8a3a94e21a | ||
|
|
f61af188f9 | ||
|
|
73fc52bfed | ||
|
|
4eeff4eef8 | ||
|
|
cff2c43030 | ||
|
|
504b1f2425 | ||
|
|
eff5b56990 | ||
|
|
0e673f1a18 | ||
|
|
4fea22aea4 | ||
|
|
c2478c9ac3 | ||
|
|
ed8243825e | ||
|
|
c7ac2b5278 | ||
|
|
a783003556 | ||
|
|
44ba1c6113 | ||
|
|
a02d67fcc6 | ||
|
|
14c8f7c4f5 | ||
|
|
a487ecb50f | ||
|
|
fc55862823 | ||
|
|
e5400601d6 | ||
|
|
735f9f1483 | ||
|
|
d139db0a0f | ||
|
|
a35bb450b1 | ||
|
|
26c01dfa48 | ||
|
|
2b1839374a | ||
|
|
59f5f18e1d | ||
|
|
e41fcb081c | ||
|
|
3032042b35 | ||
|
|
544db61044 | ||
|
|
67a3aa6dff | ||
|
|
34f4468b20 | ||
|
|
d99ae58001 | ||
|
|
d60ec53762 | ||
|
|
0ece9361d5 | ||
|
|
69987a2f00 | ||
|
|
7346bfccb9 | ||
|
|
b705083ce2 | ||
|
|
4b3c82df6f | ||
|
|
46290205d5 | ||
|
|
7b15585b80 | ||
|
|
cbfeeb9079 | ||
|
|
fdac20b43e | ||
|
|
ae2312104e | ||
|
|
74b6674af6 | ||
|
|
81adce3238 | ||
|
|
48b7f460a8 | ||
|
|
38a8232341 | ||
|
|
39a51c4f4c | ||
|
|
220bfeb37d | ||
|
|
e766279950 | ||
|
|
db82406525 | ||
|
|
2d44f332d9 | ||
|
|
ea1526689a | ||
|
|
200338ed72 | ||
|
|
88003a61bd | ||
|
|
b82089b30b | ||
|
|
efd780d395 | ||
|
|
3f90f783de | ||
|
|
4f5b755117 | ||
|
|
a455f12581 | ||
|
|
de516db383 | ||
|
|
6781575293 | ||
|
|
65b0e40fc8 | ||
|
|
19e78a07b7 | ||
|
|
e3b60dda07 | ||
|
|
a9696d3193 | ||
|
|
336d72873f | ||
|
|
111a380bce | ||
|
|
012a8351af | ||
|
|
deeb80ea9b | ||
|
|
1e97a917d6 | ||
|
|
a15ba925db | ||
|
|
6e5a968aad | ||
|
|
915edaa02f | ||
|
|
f6b0fa7c18 | ||
|
|
3f1fba0f35 | ||
|
|
d9fa85a4c6 | ||
|
|
022bb8649c | ||
|
|
673bc33a01 | ||
|
|
579a64928d | ||
|
|
5316df7d7d | ||
|
|
62fe61dc30 | ||
|
|
ea819a4a2f | ||
|
|
868a25dae2 | ||
|
|
3a25d00cb6 | ||
|
|
6301b74d87 | ||
|
|
b43b90c299 | ||
|
|
0a43444ab3 | ||
|
|
75ea4d2155 | ||
|
|
9d281388e0 | ||
|
|
178e1cc50b | ||
|
|
6eafe53b2d | ||
|
|
7514b2b7d4 | ||
|
|
51dee2dba2 | ||
|
|
a81c3d841c | ||
|
|
154487f966 | ||
|
|
588daafcf5 | ||
|
|
ab00097aed | ||
|
|
5aefae71a2 | ||
|
|
0edd598970 | ||
|
|
0bb485031f | ||
|
|
01ffd86367 | ||
|
|
e2e02f31b6 | ||
|
|
0008617348 | ||
|
|
f8f21c0edd | ||
|
|
010916158b | ||
|
|
df0ba004ca | ||
|
|
038b29e15b | ||
|
|
763ab73923 | ||
|
|
9a060b4437 | ||
|
|
2307a30892 | ||
|
|
2006f84f6e | ||
|
|
23b15fef6a | ||
|
|
41e72f929d | ||
|
|
29fc49bb3b | ||
|
|
6f65b6a40f | ||
|
|
1e9b22e3a4 | ||
|
|
3dc2c723c3 | ||
|
|
6f007cbd48 | ||
|
|
9a402dd10e | ||
|
|
4249d0e13b | ||
|
|
f6050bad67 | ||
|
|
d3a4b7b51b | ||
|
|
3f5f9ac764 | ||
|
|
e7c1299a7f | ||
|
|
56a3918a1e | ||
|
|
dabf7718cf | ||
|
|
ef22c29288 | ||
|
|
74f06074f7 | ||
|
|
b97bf52faa | ||
|
|
0a77f5cec8 | ||
|
|
f8e92f7b73 | ||
|
|
530c6e3a59 | ||
|
|
11b95cfaf4 | ||
|
|
707c005a26 | ||
|
|
19fa8e7e33 | ||
|
|
b252ded366 | ||
|
|
84305d4e73 | ||
|
|
1150b41e14 | ||
|
|
88d8ccb34b | ||
|
|
4111b3f1aa | ||
|
|
36862be2aa | ||
|
|
425665e0d9 | ||
|
|
9abd604f69 | ||
|
|
59bdc288b5 | ||
|
|
eb37d2958e | ||
|
|
2a9738a341 | ||
|
|
6aac1cf33a | ||
|
|
9ca4d072ab | ||
|
|
7aaf14c26b | ||
|
|
cf598ca175 | ||
|
|
a722790afc | ||
|
|
320151a040 | ||
|
|
c090f511c3 | ||
|
|
86dd1475b3 | ||
|
|
0b71ac258c | ||
|
|
54e1eae509 | ||
|
|
bf57b2dc77 | ||
|
|
de3c27b44f | ||
|
|
05717fea93 | ||
|
|
191584d229 | ||
|
|
6069169e6b | ||
|
|
07438587f3 | ||
|
|
913e36d6fd | ||
|
|
139004b976 | ||
|
|
ef4269d585 | ||
|
|
954cb129a4 | ||
|
|
02c4b28de5 | ||
|
|
febea88b58 | ||
|
|
40ccfac514 | ||
|
|
831fb814cc | ||
|
|
bf166fdd61 | ||
|
|
384bde3539 | ||
|
|
6f1d238d0a | ||
|
|
ac524153a7 | ||
|
|
2cad2b15cf | ||
|
|
fd63e202fe | ||
|
|
a0250e47e3 | ||
|
|
7d8ece45bb | ||
|
|
ffb8f053da | ||
|
|
fb46f457f9 | ||
|
|
6d4f4152a7 | ||
|
|
d3d0ac7327 | ||
|
|
f57df46995 | ||
|
|
a747171745 | ||
|
|
e9ae9e80d4 | ||
|
|
3b9a59b98d | ||
|
|
8a381e7f74 | ||
|
|
61513fc800 | ||
|
|
1d26c49e92 | ||
|
|
77be9836d2 | ||
|
|
4427960acb | ||
|
|
84aa4fb7bc | ||
|
|
01df96cbe0 | ||
|
|
1ac0634f57 | ||
|
|
8e7d3634b1 | ||
|
|
fadafe5c77 | ||
|
|
b2ea1f6690 | ||
|
|
0a03c1f882 | ||
|
|
ce8b490ed8 | ||
|
|
86eccba80d | ||
|
|
97453e7c6c | ||
|
|
9e1084b701 | ||
|
|
41aec81f3f | ||
|
|
a5741a0551 | ||
|
|
72f73c231a | ||
|
|
b8fcaa274e | ||
|
|
a33bbf48bb | ||
|
|
98d9490fb9 | ||
|
|
51c643c4f8 | ||
|
|
2d7370ca6c | ||
|
|
0d68141387 | ||
|
|
e88a8c6639 | ||
|
|
2d04bb286e | ||
|
|
6d9ba24c32 | ||
|
|
7645a1c86e | ||
|
|
dc284b9a48 | ||
|
|
8a38332d44 | ||
|
|
2407d7d148 | ||
|
|
23275cf607 | ||
|
|
8a0e02d335 | ||
|
|
913873623c | ||
|
|
5d81e7dd4d | ||
|
|
418650fdf3 | ||
|
|
7c24e56d9f | ||
|
|
f6faed46c3 | ||
|
|
1d393eecf1 | ||
|
|
03a72240c0 | ||
|
|
acf62450fb | ||
|
|
d0269310cf | ||
|
|
43618a74e7 | ||
|
|
56fed637ec | ||
|
|
944ae4a604 | ||
|
|
f3da609102 | ||
|
|
11c8a8cf72 | ||
|
|
656978676f | ||
|
|
d216e6519f | ||
|
|
0a2bcae0e3 | ||
|
|
31cf244420 | ||
|
|
d761effac1 | ||
|
|
5efaf2c661 | ||
|
|
b79161fbec | ||
|
|
89a2f2134b | ||
|
|
343c3b19b1 | ||
|
|
e6ec646b2c | ||
|
|
b557fe4e40 | ||
|
|
45908dfbd2 | ||
|
|
1cf0673a22 | ||
|
|
189847f8a5 | ||
|
|
b27929f702 | ||
|
|
b1a6a9835d | ||
|
|
5564a16d4b | ||
|
|
b2aa447d50 | ||
|
|
8666f9a848 | ||
|
|
e7dc7c4917 | ||
|
|
513d0f1e5c | ||
|
|
c6ce7618cf | ||
|
|
9b78b8dc91 | ||
|
|
3bae233f40 | ||
|
|
0a305c4291 | ||
|
|
54fe4ddf3e | ||
|
|
aa877b981c | ||
|
|
b8ec4348a5 | ||
|
|
0e3e27668c | ||
|
|
e8c8025119 | ||
|
|
6a72eda5d2 | ||
|
|
35c8c54466 | ||
|
|
2eda45ca5b | ||
|
|
ecd6e7960c | ||
|
|
0d3a61cbdb | ||
|
|
6737c275d7 | ||
|
|
201a3e5838 | ||
|
|
85cb239219 | ||
|
|
002f45e383 | ||
|
|
470e5ba290 | ||
|
|
b6722b3a10 | ||
|
|
6a624916ca | ||
|
|
65653e0932 | ||
|
|
a7e59d6697 | ||
|
|
f1356105c1 | ||
|
|
8acc6379fb | ||
|
|
d13014c5d9 | ||
|
|
db11d8ba90 | ||
|
|
46a4ee2360 | ||
|
|
a19c053b88 | ||
|
|
5b47a32d31 | ||
|
|
32ae9efb6a | ||
|
|
b81bee551a | ||
|
|
8c9dffd082 | ||
|
|
cdfae643e4 | ||
|
|
cb93108206 | ||
|
|
c11343dc1c | ||
|
|
3733c6f89d | ||
|
|
3c23a0eac0 | ||
|
|
7fd69ab1f1 | ||
|
|
3f511774de | ||
|
|
3600100879 | ||
|
|
8fae372103 | ||
|
|
2fce1fe04c | ||
|
|
22f3e975c7 | ||
|
|
c6ced5a210 | ||
|
|
5d66e85205 | ||
|
|
649f163bf7 | ||
|
|
4c821bd930 | ||
|
|
6359de4e25 | ||
|
|
2c610c8cd4 | ||
|
|
7efe8a249b | ||
|
|
bd421d184e | ||
|
|
78f5844ba0 | ||
|
|
f6bb4d5051 | ||
|
|
56642c3e87 | ||
|
|
9fd8678d3d | ||
|
|
8b89518fd6 |
@@ -45,13 +45,11 @@ class UIType(str, Enum, metaclass=MetaEnum):
|
||||
SDXLRefinerModel = "SDXLRefinerModelField"
|
||||
ONNXModel = "ONNXModelField"
|
||||
VAEModel = "VAEModelField"
|
||||
FluxVAEModel = "FluxVAEModelField"
|
||||
LoRAModel = "LoRAModelField"
|
||||
ControlNetModel = "ControlNetModelField"
|
||||
IPAdapterModel = "IPAdapterModelField"
|
||||
T2IAdapterModel = "T2IAdapterModelField"
|
||||
T5EncoderModel = "T5EncoderModelField"
|
||||
CLIPEmbedModel = "CLIPEmbedModelField"
|
||||
SpandrelImageToImageModel = "SpandrelImageToImageModelField"
|
||||
# endregion
|
||||
|
||||
@@ -130,7 +128,6 @@ class FieldDescriptions:
|
||||
noise = "Noise tensor"
|
||||
clip = "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count"
|
||||
t5_encoder = "T5 tokenizer and text encoder"
|
||||
clip_embed_model = "CLIP Embed loader"
|
||||
unet = "UNet (scheduler, LoRAs)"
|
||||
transformer = "Transformer"
|
||||
vae = "VAE"
|
||||
|
||||
@@ -40,10 +40,7 @@ class FluxTextEncoderInvocation(BaseInvocation):
|
||||
|
||||
@torch.no_grad()
|
||||
def invoke(self, context: InvocationContext) -> FluxConditioningOutput:
|
||||
# Note: The T5 and CLIP encoding are done in separate functions to ensure that all model references are locally
|
||||
# scoped. This ensures that the T5 model can be freed and gc'd before loading the CLIP model (if necessary).
|
||||
t5_embeddings = self._t5_encode(context)
|
||||
clip_embeddings = self._clip_encode(context)
|
||||
t5_embeddings, clip_embeddings = self._encode_prompt(context)
|
||||
conditioning_data = ConditioningFieldData(
|
||||
conditionings=[FLUXConditioningInfo(clip_embeds=clip_embeddings, t5_embeds=t5_embeddings)]
|
||||
)
|
||||
@@ -51,7 +48,12 @@ class FluxTextEncoderInvocation(BaseInvocation):
|
||||
conditioning_name = context.conditioning.save(conditioning_data)
|
||||
return FluxConditioningOutput.build(conditioning_name)
|
||||
|
||||
def _t5_encode(self, context: InvocationContext) -> torch.Tensor:
|
||||
def _encode_prompt(self, context: InvocationContext) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
# Load CLIP.
|
||||
clip_tokenizer_info = context.models.load(self.clip.tokenizer)
|
||||
clip_text_encoder_info = context.models.load(self.clip.text_encoder)
|
||||
|
||||
# Load T5.
|
||||
t5_tokenizer_info = context.models.load(self.t5_encoder.tokenizer)
|
||||
t5_text_encoder_info = context.models.load(self.t5_encoder.text_encoder)
|
||||
|
||||
@@ -68,15 +70,6 @@ class FluxTextEncoderInvocation(BaseInvocation):
|
||||
|
||||
prompt_embeds = t5_encoder(prompt)
|
||||
|
||||
assert isinstance(prompt_embeds, torch.Tensor)
|
||||
return prompt_embeds
|
||||
|
||||
def _clip_encode(self, context: InvocationContext) -> torch.Tensor:
|
||||
clip_tokenizer_info = context.models.load(self.clip.tokenizer)
|
||||
clip_text_encoder_info = context.models.load(self.clip.text_encoder)
|
||||
|
||||
prompt = [self.prompt]
|
||||
|
||||
with (
|
||||
clip_text_encoder_info as clip_text_encoder,
|
||||
clip_tokenizer_info as clip_tokenizer,
|
||||
@@ -88,5 +81,6 @@ class FluxTextEncoderInvocation(BaseInvocation):
|
||||
|
||||
pooled_prompt_embeds = clip_encoder(prompt)
|
||||
|
||||
assert isinstance(prompt_embeds, torch.Tensor)
|
||||
assert isinstance(pooled_prompt_embeds, torch.Tensor)
|
||||
return pooled_prompt_embeds
|
||||
return prompt_embeds, pooled_prompt_embeds
|
||||
|
||||
@@ -58,7 +58,13 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
|
||||
@torch.no_grad()
|
||||
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||
latents = self._run_diffusion(context)
|
||||
# Load the conditioning data.
|
||||
cond_data = context.conditioning.load(self.positive_text_conditioning.conditioning_name)
|
||||
assert len(cond_data.conditionings) == 1
|
||||
flux_conditioning = cond_data.conditionings[0]
|
||||
assert isinstance(flux_conditioning, FLUXConditioningInfo)
|
||||
|
||||
latents = self._run_diffusion(context, flux_conditioning.clip_embeds, flux_conditioning.t5_embeds)
|
||||
image = self._run_vae_decoding(context, latents)
|
||||
image_dto = context.images.save(image=image)
|
||||
return ImageOutput.build(image_dto)
|
||||
@@ -66,19 +72,11 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
def _run_diffusion(
|
||||
self,
|
||||
context: InvocationContext,
|
||||
clip_embeddings: torch.Tensor,
|
||||
t5_embeddings: torch.Tensor,
|
||||
):
|
||||
inference_dtype = torch.bfloat16
|
||||
|
||||
# Load the conditioning data.
|
||||
cond_data = context.conditioning.load(self.positive_text_conditioning.conditioning_name)
|
||||
assert len(cond_data.conditionings) == 1
|
||||
flux_conditioning = cond_data.conditionings[0]
|
||||
assert isinstance(flux_conditioning, FLUXConditioningInfo)
|
||||
flux_conditioning = flux_conditioning.to(dtype=inference_dtype)
|
||||
t5_embeddings = flux_conditioning.t5_embeds
|
||||
clip_embeddings = flux_conditioning.clip_embeds
|
||||
|
||||
transformer_info = context.models.load(self.transformer.transformer)
|
||||
inference_dtype = torch.bfloat16
|
||||
|
||||
# Prepare input noise.
|
||||
x = get_noise(
|
||||
@@ -90,19 +88,24 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
seed=self.seed,
|
||||
)
|
||||
|
||||
x, img_ids = prepare_latent_img_patches(x)
|
||||
img, img_ids = prepare_latent_img_patches(x)
|
||||
|
||||
is_schnell = "schnell" in transformer_info.config.config_path
|
||||
|
||||
timesteps = get_schedule(
|
||||
num_steps=self.num_steps,
|
||||
image_seq_len=x.shape[1],
|
||||
image_seq_len=img.shape[1],
|
||||
shift=not is_schnell,
|
||||
)
|
||||
|
||||
bs, t5_seq_len, _ = t5_embeddings.shape
|
||||
txt_ids = torch.zeros(bs, t5_seq_len, 3, dtype=inference_dtype, device=TorchDevice.choose_torch_device())
|
||||
|
||||
# HACK(ryand): Manually empty the cache. Currently we don't check the size of the model before loading it from
|
||||
# disk. Since the transformer model is large (24GB), there's a good chance that it will OOM on 32GB RAM systems
|
||||
# if the cache is not empty.
|
||||
context.models._services.model_manager.load.ram_cache.make_room(24 * 2**30)
|
||||
|
||||
with transformer_info as transformer:
|
||||
assert isinstance(transformer, Flux)
|
||||
|
||||
@@ -137,7 +140,7 @@ class FluxTextToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
|
||||
x = denoise(
|
||||
model=transformer,
|
||||
img=x,
|
||||
img=img,
|
||||
img_ids=img_ids,
|
||||
txt=t5_embeddings,
|
||||
txt_ids=txt_ids,
|
||||
|
||||
@@ -157,7 +157,7 @@ class FluxModelLoaderOutput(BaseInvocationOutput):
|
||||
title="Flux Main Model",
|
||||
tags=["model", "flux"],
|
||||
category="model",
|
||||
version="1.0.4",
|
||||
version="1.0.3",
|
||||
classification=Classification.Prototype,
|
||||
)
|
||||
class FluxModelLoaderInvocation(BaseInvocation):
|
||||
@@ -169,35 +169,23 @@ class FluxModelLoaderInvocation(BaseInvocation):
|
||||
input=Input.Direct,
|
||||
)
|
||||
|
||||
t5_encoder_model: ModelIdentifierField = InputField(
|
||||
description=FieldDescriptions.t5_encoder, ui_type=UIType.T5EncoderModel, input=Input.Direct, title="T5 Encoder"
|
||||
)
|
||||
|
||||
clip_embed_model: ModelIdentifierField = InputField(
|
||||
description=FieldDescriptions.clip_embed_model,
|
||||
ui_type=UIType.CLIPEmbedModel,
|
||||
t5_encoder: ModelIdentifierField = InputField(
|
||||
description=FieldDescriptions.t5_encoder,
|
||||
ui_type=UIType.T5EncoderModel,
|
||||
input=Input.Direct,
|
||||
title="CLIP Embed",
|
||||
)
|
||||
|
||||
vae_model: ModelIdentifierField = InputField(
|
||||
description=FieldDescriptions.vae_model, ui_type=UIType.FluxVAEModel, title="VAE"
|
||||
)
|
||||
|
||||
def invoke(self, context: InvocationContext) -> FluxModelLoaderOutput:
|
||||
for key in [self.model.key, self.t5_encoder_model.key, self.clip_embed_model.key, self.vae_model.key]:
|
||||
if not context.models.exists(key):
|
||||
raise ValueError(f"Unknown model: {key}")
|
||||
|
||||
transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
|
||||
vae = self.vae_model.model_copy(update={"submodel_type": SubModelType.VAE})
|
||||
|
||||
tokenizer = self.clip_embed_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
|
||||
clip_encoder = self.clip_embed_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
|
||||
|
||||
tokenizer2 = self.t5_encoder_model.model_copy(update={"submodel_type": SubModelType.Tokenizer2})
|
||||
t5_encoder = self.t5_encoder_model.model_copy(update={"submodel_type": SubModelType.TextEncoder2})
|
||||
model_key = self.model.key
|
||||
|
||||
if not context.models.exists(model_key):
|
||||
raise ValueError(f"Unknown model: {model_key}")
|
||||
transformer = self._get_model(context, SubModelType.Transformer)
|
||||
tokenizer = self._get_model(context, SubModelType.Tokenizer)
|
||||
tokenizer2 = self._get_model(context, SubModelType.Tokenizer2)
|
||||
clip_encoder = self._get_model(context, SubModelType.TextEncoder)
|
||||
t5_encoder = self._get_model(context, SubModelType.TextEncoder2)
|
||||
vae = self._get_model(context, SubModelType.VAE)
|
||||
transformer_config = context.models.get_config(transformer)
|
||||
assert isinstance(transformer_config, CheckpointConfigBase)
|
||||
|
||||
@@ -209,6 +197,52 @@ class FluxModelLoaderInvocation(BaseInvocation):
|
||||
max_seq_len=max_seq_lengths[transformer_config.config_path],
|
||||
)
|
||||
|
||||
def _get_model(self, context: InvocationContext, submodel: SubModelType) -> ModelIdentifierField:
|
||||
match submodel:
|
||||
case SubModelType.Transformer:
|
||||
return self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
|
||||
case SubModelType.VAE:
|
||||
return self._pull_model_from_mm(
|
||||
context,
|
||||
SubModelType.VAE,
|
||||
"FLUX.1-schnell_ae",
|
||||
ModelType.VAE,
|
||||
BaseModelType.Flux,
|
||||
)
|
||||
case submodel if submodel in [SubModelType.Tokenizer, SubModelType.TextEncoder]:
|
||||
return self._pull_model_from_mm(
|
||||
context,
|
||||
submodel,
|
||||
"clip-vit-large-patch14",
|
||||
ModelType.CLIPEmbed,
|
||||
BaseModelType.Any,
|
||||
)
|
||||
case submodel if submodel in [SubModelType.Tokenizer2, SubModelType.TextEncoder2]:
|
||||
return self._pull_model_from_mm(
|
||||
context,
|
||||
submodel,
|
||||
self.t5_encoder.name,
|
||||
ModelType.T5Encoder,
|
||||
BaseModelType.Any,
|
||||
)
|
||||
case _:
|
||||
raise Exception(f"{submodel.value} is not a supported submodule for a flux model")
|
||||
|
||||
def _pull_model_from_mm(
|
||||
self,
|
||||
context: InvocationContext,
|
||||
submodel: SubModelType,
|
||||
name: str,
|
||||
type: ModelType,
|
||||
base: BaseModelType,
|
||||
):
|
||||
if models := context.models.search_by_attrs(name=name, base=base, type=type):
|
||||
if len(models) != 1:
|
||||
raise Exception(f"Multiple models detected for selected model with name {name}")
|
||||
return ModelIdentifierField.from_config(models[0]).model_copy(update={"submodel_type": submodel})
|
||||
else:
|
||||
raise ValueError(f"Please install the {base}:{type} model named {name} via starter models")
|
||||
|
||||
|
||||
@invocation(
|
||||
"main_model_loader",
|
||||
|
||||
@@ -88,8 +88,7 @@ class QueueItemEventBase(QueueEventBase):
|
||||
|
||||
item_id: int = Field(description="The ID of the queue item")
|
||||
batch_id: str = Field(description="The ID of the queue batch")
|
||||
origin: str | None = Field(default=None, description="The origin of the queue item")
|
||||
destination: str | None = Field(default=None, description="The destination of the queue item")
|
||||
origin: str | None = Field(default=None, description="The origin of the batch")
|
||||
|
||||
|
||||
class InvocationEventBase(QueueItemEventBase):
|
||||
@@ -115,7 +114,6 @@ class InvocationStartedEvent(InvocationEventBase):
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
destination=queue_item.destination,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@@ -150,7 +148,6 @@ class InvocationDenoiseProgressEvent(InvocationEventBase):
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
destination=queue_item.destination,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@@ -189,7 +186,6 @@ class InvocationCompleteEvent(InvocationEventBase):
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
destination=queue_item.destination,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@@ -223,7 +219,6 @@ class InvocationErrorEvent(InvocationEventBase):
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
destination=queue_item.destination,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@@ -262,7 +257,6 @@ class QueueItemStatusChangedEvent(QueueItemEventBase):
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
destination=queue_item.destination,
|
||||
session_id=queue_item.session_id,
|
||||
status=queue_item.status,
|
||||
error_type=queue_item.error_type,
|
||||
|
||||
@@ -77,14 +77,7 @@ BatchDataCollection: TypeAlias = list[list[BatchDatum]]
|
||||
|
||||
class Batch(BaseModel):
|
||||
batch_id: str = Field(default_factory=uuid_string, description="The ID of the batch")
|
||||
origin: str | None = Field(
|
||||
default=None,
|
||||
description="The origin of this queue item. This data is used by the frontend to determine how to handle results.",
|
||||
)
|
||||
destination: str | None = Field(
|
||||
default=None,
|
||||
description="The origin of this queue item. This data is used by the frontend to determine how to handle results",
|
||||
)
|
||||
origin: str | None = Field(default=None, description="The origin of this batch.")
|
||||
data: Optional[BatchDataCollection] = Field(default=None, description="The batch data collection.")
|
||||
graph: Graph = Field(description="The graph to initialize the session with")
|
||||
workflow: Optional[WorkflowWithoutID] = Field(
|
||||
@@ -203,14 +196,7 @@ class SessionQueueItemWithoutGraph(BaseModel):
|
||||
status: QUEUE_ITEM_STATUS = Field(default="pending", description="The status of this queue item")
|
||||
priority: int = Field(default=0, description="The priority of this queue item")
|
||||
batch_id: str = Field(description="The ID of the batch associated with this queue item")
|
||||
origin: str | None = Field(
|
||||
default=None,
|
||||
description="The origin of this queue item. This data is used by the frontend to determine how to handle results.",
|
||||
)
|
||||
destination: str | None = Field(
|
||||
default=None,
|
||||
description="The origin of this queue item. This data is used by the frontend to determine how to handle results",
|
||||
)
|
||||
origin: str | None = Field(default=None, description="The origin of this queue item. ")
|
||||
session_id: str = Field(
|
||||
description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed."
|
||||
)
|
||||
@@ -311,7 +297,6 @@ class BatchStatus(BaseModel):
|
||||
queue_id: str = Field(..., description="The ID of the queue")
|
||||
batch_id: str = Field(..., description="The ID of the batch")
|
||||
origin: str | None = Field(..., description="The origin of the batch")
|
||||
destination: str | None = Field(..., description="The destination of the batch")
|
||||
pending: int = Field(..., description="Number of queue items with status 'pending'")
|
||||
in_progress: int = Field(..., description="Number of queue items with status 'in_progress'")
|
||||
completed: int = Field(..., description="Number of queue items with status 'complete'")
|
||||
@@ -458,7 +443,6 @@ class SessionQueueValueToInsert(NamedTuple):
|
||||
priority: int # priority
|
||||
workflow: Optional[str] # workflow json
|
||||
origin: str | None
|
||||
destination: str | None
|
||||
|
||||
|
||||
ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert]
|
||||
@@ -480,7 +464,6 @@ def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new
|
||||
priority, # priority
|
||||
json.dumps(workflow, default=to_jsonable_python) if workflow else None, # workflow (json)
|
||||
batch.origin, # origin
|
||||
batch.destination, # destination
|
||||
)
|
||||
)
|
||||
return values_to_insert
|
||||
|
||||
@@ -128,8 +128,8 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
|
||||
self.__cursor.executemany(
|
||||
"""--sql
|
||||
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
values_to_insert,
|
||||
)
|
||||
@@ -579,8 +579,7 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
session_id,
|
||||
batch_id,
|
||||
queue_id,
|
||||
origin,
|
||||
destination
|
||||
origin
|
||||
FROM session_queue
|
||||
WHERE queue_id = ?
|
||||
"""
|
||||
@@ -660,7 +659,7 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
self.__lock.acquire()
|
||||
self.__cursor.execute(
|
||||
"""--sql
|
||||
SELECT status, count(*), origin, destination
|
||||
SELECT status, count(*), origin
|
||||
FROM session_queue
|
||||
WHERE
|
||||
queue_id = ?
|
||||
@@ -673,7 +672,6 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
total = sum(row[1] for row in result)
|
||||
counts: dict[str, int] = {row[0]: row[1] for row in result}
|
||||
origin = result[0]["origin"] if result else None
|
||||
destination = result[0]["destination"] if result else None
|
||||
except Exception:
|
||||
self.__conn.rollback()
|
||||
raise
|
||||
@@ -683,7 +681,6 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
return BatchStatus(
|
||||
batch_id=batch_id,
|
||||
origin=origin,
|
||||
destination=destination,
|
||||
queue_id=queue_id,
|
||||
pending=counts.get("pending", 0),
|
||||
in_progress=counts.get("in_progress", 0),
|
||||
|
||||
@@ -10,11 +10,9 @@ class Migration15Callback:
|
||||
def _add_origin_col(self, cursor: sqlite3.Cursor) -> None:
|
||||
"""
|
||||
- Adds `origin` column to the session queue table.
|
||||
- Adds `destination` column to the session queue table.
|
||||
"""
|
||||
|
||||
cursor.execute("ALTER TABLE session_queue ADD COLUMN origin TEXT;")
|
||||
cursor.execute("ALTER TABLE session_queue ADD COLUMN destination TEXT;")
|
||||
|
||||
|
||||
def build_migration_15() -> Migration:
|
||||
@@ -23,7 +21,6 @@ def build_migration_15() -> Migration:
|
||||
|
||||
This migration does the following:
|
||||
- Adds `origin` column to the session queue table.
|
||||
- Adds `destination` column to the session queue table.
|
||||
"""
|
||||
migration_15 = Migration(
|
||||
from_version=14,
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
"name": "FLUX Text to Image",
|
||||
"author": "InvokeAI",
|
||||
"description": "A simple text-to-image workflow using FLUX dev or schnell models. Prerequisite model downloads: T5 Encoder, CLIP-L Encoder, and FLUX VAE. Quantized and un-quantized versions can be found in the starter models tab within your Model Manager. We recommend 4 steps for FLUX schnell models and 30 steps for FLUX dev models.",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.0",
|
||||
"contact": "",
|
||||
"tags": "text2image, flux",
|
||||
"notes": "Prerequisite model downloads: T5 Encoder, CLIP-L Encoder, and FLUX VAE. Quantized and un-quantized versions can be found in the starter models tab within your Model Manager. We recommend 4 steps for FLUX schnell models and 30 steps for FLUX dev models.",
|
||||
"exposedFields": [
|
||||
{
|
||||
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||
"nodeId": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
|
||||
"fieldName": "model"
|
||||
},
|
||||
{
|
||||
@@ -20,8 +20,8 @@
|
||||
"fieldName": "num_steps"
|
||||
},
|
||||
{
|
||||
"nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||
"fieldName": "t5_encoder_model"
|
||||
"nodeId": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
|
||||
"fieldName": "t5_encoder"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
@@ -30,12 +30,12 @@
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||
"id": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
|
||||
"type": "invocation",
|
||||
"data": {
|
||||
"id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||
"id": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
|
||||
"type": "flux_model_loader",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.3",
|
||||
"label": "",
|
||||
"notes": "",
|
||||
"isOpen": true,
|
||||
@@ -44,25 +44,31 @@
|
||||
"inputs": {
|
||||
"model": {
|
||||
"name": "model",
|
||||
"label": ""
|
||||
"label": "Model (Starter Models can be found in Model Manager)",
|
||||
"value": {
|
||||
"key": "f04a7a2f-c74d-4538-8d5e-879a53501662",
|
||||
"hash": "random:4875da7a9508444ffa706f61961c260d0c6729f6181a86b31fad06df1277b850",
|
||||
"name": "FLUX Dev (Quantized)",
|
||||
"base": "flux",
|
||||
"type": "main"
|
||||
}
|
||||
},
|
||||
"t5_encoder_model": {
|
||||
"name": "t5_encoder_model",
|
||||
"label": ""
|
||||
},
|
||||
"clip_embed_model": {
|
||||
"name": "clip_embed_model",
|
||||
"label": ""
|
||||
},
|
||||
"vae_model": {
|
||||
"name": "vae_model",
|
||||
"label": ""
|
||||
"t5_encoder": {
|
||||
"name": "t5_encoder",
|
||||
"label": "T 5 Encoder (Starter Models can be found in Model Manager)",
|
||||
"value": {
|
||||
"key": "20dcd9ec-5fbb-4012-8401-049e707da5e5",
|
||||
"hash": "random:f986be43ff3502169e4adbdcee158afb0e0a65a1edc4cab16ae59963630cfd8f",
|
||||
"name": "t5_bnb_int8_quantized_encoder",
|
||||
"base": "any",
|
||||
"type": "t5_encoder"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"x": 381.1882713063478,
|
||||
"y": -95.89663532854017
|
||||
"x": 337.09365228062825,
|
||||
"y": 40.63469521079861
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -201,45 +207,45 @@
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90max_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
|
||||
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33amax_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
|
||||
"type": "default",
|
||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
|
||||
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
||||
"sourceHandle": "max_seq_len",
|
||||
"targetHandle": "t5_max_seq_len"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90vae-159bdf1b-79e7-4174-b86e-d40e646964c8vae",
|
||||
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33avae-159bdf1b-79e7-4174-b86e-d40e646964c8vae",
|
||||
"type": "default",
|
||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
|
||||
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
|
||||
"sourceHandle": "vae",
|
||||
"targetHandle": "vae"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90t5_encoder-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_encoder",
|
||||
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33atransformer-159bdf1b-79e7-4174-b86e-d40e646964c8transformer",
|
||||
"type": "default",
|
||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
|
||||
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
|
||||
"sourceHandle": "transformer",
|
||||
"targetHandle": "transformer"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33at5_encoder-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_encoder",
|
||||
"type": "default",
|
||||
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
|
||||
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
||||
"sourceHandle": "t5_encoder",
|
||||
"targetHandle": "t5_encoder"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90clip-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cclip",
|
||||
"id": "reactflow__edge-4f0207c2-ff40-41fd-b047-ad33fbb1c33aclip-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cclip",
|
||||
"type": "default",
|
||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||
"source": "4f0207c2-ff40-41fd-b047-ad33fbb1c33a",
|
||||
"target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
|
||||
"sourceHandle": "clip",
|
||||
"targetHandle": "clip"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90transformer-159bdf1b-79e7-4174-b86e-d40e646964c8transformer",
|
||||
"type": "default",
|
||||
"source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
|
||||
"target": "159bdf1b-79e7-4174-b86e-d40e646964c8",
|
||||
"sourceHandle": "transformer",
|
||||
"targetHandle": "transformer"
|
||||
},
|
||||
{
|
||||
"id": "reactflow__edge-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cconditioning-159bdf1b-79e7-4174-b86e-d40e646964c8positive_text_conditioning",
|
||||
"type": "default",
|
||||
|
||||
@@ -111,7 +111,16 @@ def denoise(
|
||||
step_callback: Callable[[], None],
|
||||
guidance: float = 4.0,
|
||||
):
|
||||
# guidance_vec is ignored for schnell.
|
||||
dtype = model.txt_in.bias.dtype
|
||||
|
||||
# TODO(ryand): This shouldn't be necessary if we manage the dtypes properly in the caller.
|
||||
img = img.to(dtype=dtype)
|
||||
img_ids = img_ids.to(dtype=dtype)
|
||||
txt = txt.to(dtype=dtype)
|
||||
txt_ids = txt_ids.to(dtype=dtype)
|
||||
vec = vec.to(dtype=dtype)
|
||||
|
||||
# this is ignored for schnell
|
||||
guidance_vec = torch.full((img.shape[0],), guidance, device=img.device, dtype=img.dtype)
|
||||
for t_curr, t_prev in tqdm(list(zip(timesteps[:-1], timesteps[1:], strict=True))):
|
||||
t_vec = torch.full((img.shape[0],), t_curr, dtype=img.dtype, device=img.device)
|
||||
@@ -159,9 +168,9 @@ def prepare_latent_img_patches(latent_img: torch.Tensor) -> tuple[torch.Tensor,
|
||||
img = repeat(img, "1 ... -> bs ...", bs=bs)
|
||||
|
||||
# Generate patch position ids.
|
||||
img_ids = torch.zeros(h // 2, w // 2, 3, device=img.device, dtype=img.dtype)
|
||||
img_ids[..., 1] = img_ids[..., 1] + torch.arange(h // 2, device=img.device, dtype=img.dtype)[:, None]
|
||||
img_ids[..., 2] = img_ids[..., 2] + torch.arange(w // 2, device=img.device, dtype=img.dtype)[None, :]
|
||||
img_ids = torch.zeros(h // 2, w // 2, 3, device=img.device)
|
||||
img_ids[..., 1] = img_ids[..., 1] + torch.arange(h // 2, device=img.device)[:, None]
|
||||
img_ids[..., 2] = img_ids[..., 2] + torch.arange(w // 2, device=img.device)[None, :]
|
||||
img_ids = repeat(img_ids, "h w c -> b (h w) c", b=bs)
|
||||
|
||||
return img, img_ids
|
||||
|
||||
@@ -72,7 +72,6 @@ class ModelLoader(ModelLoaderBase):
|
||||
pass
|
||||
|
||||
config.path = str(self._get_model_path(config))
|
||||
self._ram_cache.make_room(self.get_size_fs(config, Path(config.path), submodel_type))
|
||||
loaded_model = self._load_model(config, submodel_type)
|
||||
|
||||
self._ram_cache.put(
|
||||
|
||||
@@ -193,6 +193,15 @@ class ModelCacheBase(ABC, Generic[T]):
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def exists(
|
||||
self,
|
||||
key: str,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> bool:
|
||||
"""Return true if the model identified by key and submodel_type is in the cache."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cache_size(self) -> int:
|
||||
"""Get the total size of the models currently cached."""
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development team
|
||||
# TODO: Add Stalker's proper name to copyright
|
||||
""" """
|
||||
"""
|
||||
Manage a RAM cache of diffusion/transformer models for fast switching.
|
||||
They are moved between GPU VRAM and CPU RAM as necessary. If the cache
|
||||
grows larger than a preset maximum, then the least recently used
|
||||
model will be cleared and (re)loaded from disk when next needed.
|
||||
|
||||
The cache returns context manager generators designed to load the
|
||||
model into the GPU within the context, and unload outside the
|
||||
context. Use like this:
|
||||
|
||||
cache = ModelCache(max_cache_size=7.5)
|
||||
with cache.get_model('runwayml/stable-diffusion-1-5') as SD1,
|
||||
cache.get_model('stabilityai/stable-diffusion-2') as SD2:
|
||||
do_something_in_GPU(SD1,SD2)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import gc
|
||||
import math
|
||||
@@ -24,64 +40,45 @@ from invokeai.backend.model_manager.load.model_util import calc_model_size_by_da
|
||||
from invokeai.backend.util.devices import TorchDevice
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
|
||||
# Size of a GB in bytes.
|
||||
GB = 2**30
|
||||
# Maximum size of the cache, in gigs
|
||||
# Default is roughly enough to hold three fp16 diffusers models in RAM simultaneously
|
||||
DEFAULT_MAX_CACHE_SIZE = 6.0
|
||||
|
||||
# amount of GPU memory to hold in reserve for use by generations (GB)
|
||||
DEFAULT_MAX_VRAM_CACHE_SIZE = 2.75
|
||||
|
||||
# actual size of a gig
|
||||
GIG = 1073741824
|
||||
|
||||
# Size of a MB in bytes.
|
||||
MB = 2**20
|
||||
|
||||
|
||||
class ModelCache(ModelCacheBase[AnyModel]):
|
||||
"""A cache for managing models in memory.
|
||||
|
||||
The cache is based on two levels of model storage:
|
||||
- execution_device: The device where most models are executed (typically "cuda", "mps", or "cpu").
|
||||
- storage_device: The device where models are offloaded when not in active use (typically "cpu").
|
||||
|
||||
The model cache is based on the following assumptions:
|
||||
- storage_device_mem_size > execution_device_mem_size
|
||||
- disk_to_storage_device_transfer_time >> storage_device_to_execution_device_transfer_time
|
||||
|
||||
A copy of all models in the cache is always kept on the storage_device. A subset of the models also have a copy on
|
||||
the execution_device.
|
||||
|
||||
Models are moved between the storage_device and the execution_device as necessary. Cache size limits are enforced
|
||||
on both the storage_device and the execution_device. The execution_device cache uses a smallest-first offload
|
||||
policy. The storage_device cache uses a least-recently-used (LRU) offload policy.
|
||||
|
||||
Note: Neither of these offload policies has really been compared against alternatives. It's likely that different
|
||||
policies would be better, although the optimal policies are likely heavily dependent on usage patterns and HW
|
||||
configuration.
|
||||
|
||||
The cache returns context manager generators designed to load the model into the execution device (often GPU) within
|
||||
the context, and unload outside the context.
|
||||
|
||||
Example usage:
|
||||
```
|
||||
cache = ModelCache(max_cache_size=7.5, max_vram_cache_size=6.0)
|
||||
with cache.get_model('runwayml/stable-diffusion-1-5') as SD1:
|
||||
do_something_on_gpu(SD1)
|
||||
```
|
||||
"""
|
||||
"""Implementation of ModelCacheBase."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_cache_size: float,
|
||||
max_vram_cache_size: float,
|
||||
max_cache_size: float = DEFAULT_MAX_CACHE_SIZE,
|
||||
max_vram_cache_size: float = DEFAULT_MAX_VRAM_CACHE_SIZE,
|
||||
execution_device: torch.device = torch.device("cuda"),
|
||||
storage_device: torch.device = torch.device("cpu"),
|
||||
precision: torch.dtype = torch.float16,
|
||||
sequential_offload: bool = False,
|
||||
lazy_offloading: bool = True,
|
||||
sha_chunksize: int = 16777216,
|
||||
log_memory_usage: bool = False,
|
||||
logger: Optional[Logger] = None,
|
||||
):
|
||||
"""
|
||||
Initialize the model RAM cache.
|
||||
|
||||
:param max_cache_size: Maximum size of the storage_device cache in GBs.
|
||||
:param max_vram_cache_size: Maximum size of the execution_device cache in GBs.
|
||||
:param max_cache_size: Maximum size of the RAM cache [6.0 GB]
|
||||
:param execution_device: Torch device to load active model into [torch.device('cuda')]
|
||||
:param storage_device: Torch device to save inactive model in [torch.device('cpu')]
|
||||
:param lazy_offloading: Keep model in VRAM until another model needs to be loaded.
|
||||
:param precision: Precision for loaded models [torch.float16]
|
||||
:param lazy_offloading: Keep model in VRAM until another model needs to be loaded
|
||||
:param sequential_offload: Conserve VRAM by loading and unloading each stage of the pipeline sequentially
|
||||
:param log_memory_usage: If True, a memory snapshot will be captured before and after every model cache
|
||||
operation, and the result will be logged (at debug level). There is a time cost to capturing the memory
|
||||
snapshots, so it is recommended to disable this feature unless you are actively inspecting the model cache's
|
||||
@@ -89,6 +86,7 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
||||
"""
|
||||
# allow lazy offloading only when vram cache enabled
|
||||
self._lazy_offloading = lazy_offloading and max_vram_cache_size > 0
|
||||
self._precision: torch.dtype = precision
|
||||
self._max_cache_size: float = max_cache_size
|
||||
self._max_vram_cache_size: float = max_vram_cache_size
|
||||
self._execution_device: torch.device = execution_device
|
||||
@@ -147,6 +145,15 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
||||
total += cache_record.size
|
||||
return total
|
||||
|
||||
def exists(
|
||||
self,
|
||||
key: str,
|
||||
submodel_type: Optional[SubModelType] = None,
|
||||
) -> bool:
|
||||
"""Return true if the model identified by key and submodel_type is in the cache."""
|
||||
key = self._make_cache_key(key, submodel_type)
|
||||
return key in self._cached_models
|
||||
|
||||
def put(
|
||||
self,
|
||||
key: str,
|
||||
@@ -196,7 +203,7 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
||||
# more stats
|
||||
if self.stats:
|
||||
stats_name = stats_name or key
|
||||
self.stats.cache_size = int(self._max_cache_size * GB)
|
||||
self.stats.cache_size = int(self._max_cache_size * GIG)
|
||||
self.stats.high_watermark = max(self.stats.high_watermark, self.cache_size())
|
||||
self.stats.in_cache = len(self._cached_models)
|
||||
self.stats.loaded_model_sizes[stats_name] = max(
|
||||
@@ -224,13 +231,10 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
||||
return model_key
|
||||
|
||||
def offload_unlocked_models(self, size_required: int) -> None:
|
||||
"""Offload models from the execution_device to make room for size_required.
|
||||
|
||||
:param size_required: The amount of space to clear in the execution_device cache, in bytes.
|
||||
"""
|
||||
reserved = self._max_vram_cache_size * GB
|
||||
"""Move any unused models from VRAM."""
|
||||
reserved = self._max_vram_cache_size * GIG
|
||||
vram_in_use = torch.cuda.memory_allocated() + size_required
|
||||
self.logger.debug(f"{(vram_in_use/GB):.2f}GB VRAM needed for models; max allowed={(reserved/GB):.2f}GB")
|
||||
self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM needed for models; max allowed={(reserved/GIG):.2f}GB")
|
||||
for _, cache_entry in sorted(self._cached_models.items(), key=lambda x: x[1].size):
|
||||
if vram_in_use <= reserved:
|
||||
break
|
||||
@@ -241,7 +245,7 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
||||
cache_entry.loaded = False
|
||||
vram_in_use = torch.cuda.memory_allocated() + size_required
|
||||
self.logger.debug(
|
||||
f"Removing {cache_entry.key} from VRAM to free {(cache_entry.size/GB):.2f}GB; vram free = {(torch.cuda.memory_allocated()/GB):.2f}GB"
|
||||
f"Removing {cache_entry.key} from VRAM to free {(cache_entry.size/GIG):.2f}GB; vram free = {(torch.cuda.memory_allocated()/GIG):.2f}GB"
|
||||
)
|
||||
|
||||
TorchDevice.empty_cache()
|
||||
@@ -299,7 +303,7 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
||||
self.logger.debug(
|
||||
f"Moved model '{cache_entry.key}' from {source_device} to"
|
||||
f" {target_device} in {(end_model_to_time-start_model_to_time):.2f}s."
|
||||
f"Estimated model size: {(cache_entry.size/GB):.3f} GB."
|
||||
f"Estimated model size: {(cache_entry.size/GIG):.3f} GB."
|
||||
f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}"
|
||||
)
|
||||
|
||||
@@ -322,14 +326,14 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
||||
f"Moving model '{cache_entry.key}' from {source_device} to"
|
||||
f" {target_device} caused an unexpected change in VRAM usage. The model's"
|
||||
" estimated size may be incorrect. Estimated model size:"
|
||||
f" {(cache_entry.size/GB):.3f} GB.\n"
|
||||
f" {(cache_entry.size/GIG):.3f} GB.\n"
|
||||
f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}"
|
||||
)
|
||||
|
||||
def print_cuda_stats(self) -> None:
|
||||
"""Log CUDA diagnostics."""
|
||||
vram = "%4.2fG" % (torch.cuda.memory_allocated() / GB)
|
||||
ram = "%4.2fG" % (self.cache_size() / GB)
|
||||
vram = "%4.2fG" % (torch.cuda.memory_allocated() / GIG)
|
||||
ram = "%4.2fG" % (self.cache_size() / GIG)
|
||||
|
||||
in_ram_models = 0
|
||||
in_vram_models = 0
|
||||
@@ -349,20 +353,17 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
||||
)
|
||||
|
||||
def make_room(self, size: int) -> None:
|
||||
"""Make enough room in the cache to accommodate a new model of indicated size.
|
||||
|
||||
Note: This function deletes all of the cache's internal references to a model in order to free it. If there are
|
||||
external references to the model, there's nothing that the cache can do about it, and those models will not be
|
||||
garbage-collected.
|
||||
"""
|
||||
"""Make enough room in the cache to accommodate a new model of indicated size."""
|
||||
# calculate how much memory this model will require
|
||||
# multiplier = 2 if self.precision==torch.float32 else 1
|
||||
bytes_needed = size
|
||||
maximum_size = self.max_cache_size * GB # stored in GB, convert to bytes
|
||||
maximum_size = self.max_cache_size * GIG # stored in GB, convert to bytes
|
||||
current_size = self.cache_size()
|
||||
|
||||
if current_size + bytes_needed > maximum_size:
|
||||
self.logger.debug(
|
||||
f"Max cache size exceeded: {(current_size/GB):.2f}/{self.max_cache_size:.2f} GB, need an additional"
|
||||
f" {(bytes_needed/GB):.2f} GB"
|
||||
f"Max cache size exceeded: {(current_size/GIG):.2f}/{self.max_cache_size:.2f} GB, need an additional"
|
||||
f" {(bytes_needed/GIG):.2f} GB"
|
||||
)
|
||||
|
||||
self.logger.debug(f"Before making_room: cached_models={len(self._cached_models)}")
|
||||
@@ -379,7 +380,7 @@ class ModelCache(ModelCacheBase[AnyModel]):
|
||||
|
||||
if not cache_entry.locked:
|
||||
self.logger.debug(
|
||||
f"Removing {model_key} from RAM cache to free at least {(size/GB):.2f} GB (-{(cache_entry.size/GB):.2f} GB)"
|
||||
f"Removing {model_key} from RAM cache to free at least {(size/GIG):.2f} GB (-{(cache_entry.size/GIG):.2f} GB)"
|
||||
)
|
||||
current_size -= cache_entry.size
|
||||
models_cleared += 1
|
||||
|
||||
@@ -54,10 +54,8 @@ class InvokeLinear8bitLt(bnb.nn.Linear8bitLt):
|
||||
|
||||
# See `bnb.nn.Linear8bitLt._save_to_state_dict()` for the serialization logic of SCB and weight_format.
|
||||
scb = state_dict.pop(prefix + "SCB", None)
|
||||
|
||||
# Currently, we only support weight_format=0.
|
||||
weight_format = state_dict.pop(prefix + "weight_format", None)
|
||||
assert weight_format == 0
|
||||
# weight_format is unused, but we pop it so we can validate that there are no unexpected keys.
|
||||
_weight_format = state_dict.pop(prefix + "weight_format", None)
|
||||
|
||||
# TODO(ryand): Technically, we should be using `strict`, `missing_keys`, `unexpected_keys`, and `error_msgs`
|
||||
# rather than raising an exception to correctly implement this API.
|
||||
@@ -91,14 +89,6 @@ class InvokeLinear8bitLt(bnb.nn.Linear8bitLt):
|
||||
)
|
||||
self.bias = bias if bias is None else torch.nn.Parameter(bias)
|
||||
|
||||
# Reset the state. The persisted fields are based on the initialization behaviour in
|
||||
# `bnb.nn.Linear8bitLt.__init__()`.
|
||||
new_state = bnb.MatmulLtState()
|
||||
new_state.threshold = self.state.threshold
|
||||
new_state.has_fp16_weights = False
|
||||
new_state.use_pool = self.state.use_pool
|
||||
self.state = new_state
|
||||
|
||||
|
||||
def _convert_linear_layers_to_llm_8bit(
|
||||
module: torch.nn.Module, ignore_modules: set[str], outlier_threshold: float, prefix: str = ""
|
||||
|
||||
@@ -43,11 +43,6 @@ class FLUXConditioningInfo:
|
||||
clip_embeds: torch.Tensor
|
||||
t5_embeds: torch.Tensor
|
||||
|
||||
def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None):
|
||||
self.clip_embeds = self.clip_embeds.to(device=device, dtype=dtype)
|
||||
self.t5_embeds = self.t5_embeds.to(device=device, dtype=dtype)
|
||||
return self
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConditioningFieldData:
|
||||
|
||||
@@ -3,9 +3,10 @@ Initialization file for invokeai.backend.util
|
||||
"""
|
||||
|
||||
from invokeai.backend.util.logging import InvokeAILogger
|
||||
from invokeai.backend.util.util import Chdir, directory_size
|
||||
from invokeai.backend.util.util import GIG, Chdir, directory_size
|
||||
|
||||
__all__ = [
|
||||
"GIG",
|
||||
"directory_size",
|
||||
"Chdir",
|
||||
"InvokeAILogger",
|
||||
|
||||
@@ -7,6 +7,9 @@ from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
# actual size of a gig
|
||||
GIG = 1073741824
|
||||
|
||||
|
||||
def slugify(value: str, allow_unicode: bool = False) -> str:
|
||||
"""
|
||||
|
||||
@@ -164,10 +164,10 @@
|
||||
"alpha": "Alpha",
|
||||
"selected": "Selected",
|
||||
"tab": "Tab",
|
||||
"view": "View",
|
||||
"viewDesc": "Review images in a large gallery view",
|
||||
"edit": "Edit",
|
||||
"editDesc": "Edit on the Canvas",
|
||||
"viewing": "Viewing",
|
||||
"viewingDesc": "Review images in a large gallery view",
|
||||
"editing": "Editing",
|
||||
"editingDesc": "Edit on the Control Layers canvas",
|
||||
"comparing": "Comparing",
|
||||
"comparingDesc": "Comparing two images",
|
||||
"enabled": "Enabled",
|
||||
@@ -328,13 +328,9 @@
|
||||
"completedIn": "Completed in",
|
||||
"batch": "Batch",
|
||||
"origin": "Origin",
|
||||
"destination": "Destination",
|
||||
"upscaling": "Upscaling",
|
||||
"canvas": "Canvas",
|
||||
"generation": "Generation",
|
||||
"workflows": "Workflows",
|
||||
"other": "Other",
|
||||
"gallery": "Gallery",
|
||||
"originCanvas": "Canvas",
|
||||
"originWorkflows": "Workflows",
|
||||
"originOther": "Other",
|
||||
"batchFieldValues": "Batch Field Values",
|
||||
"item": "Item",
|
||||
"session": "Session",
|
||||
@@ -706,8 +702,6 @@
|
||||
"availableModels": "Available Models",
|
||||
"baseModel": "Base Model",
|
||||
"cancel": "Cancel",
|
||||
"clipEmbed": "CLIP Embed",
|
||||
"clipVision": "CLIP Vision",
|
||||
"config": "Config",
|
||||
"convert": "Convert",
|
||||
"convertingModelBegin": "Converting Model. Please wait.",
|
||||
@@ -795,7 +789,6 @@
|
||||
"settings": "Settings",
|
||||
"simpleModelPlaceholder": "URL or path to a local file or diffusers folder",
|
||||
"source": "Source",
|
||||
"spandrelImageToImage": "Image to Image (Spandrel)",
|
||||
"starterModels": "Starter Models",
|
||||
"starterModelsInModelManager": "Starter Models can be found in Model Manager",
|
||||
"syncModels": "Sync Models",
|
||||
@@ -804,7 +797,6 @@
|
||||
"loraTriggerPhrases": "LoRA Trigger Phrases",
|
||||
"mainModelTriggerPhrases": "Main Model Trigger Phrases",
|
||||
"typePhraseHere": "Type phrase here",
|
||||
"t5Encoder": "T5 Encoder",
|
||||
"upcastAttention": "Upcast Attention",
|
||||
"uploadImage": "Upload Image",
|
||||
"urlOrLocalPath": "URL or Local Path",
|
||||
@@ -1654,13 +1646,6 @@
|
||||
"storeNotInitialized": "Store is not initialized"
|
||||
},
|
||||
"controlLayers": {
|
||||
"saveCanvasToGallery": "Save Canvas To Gallery",
|
||||
"saveBboxToGallery": "Save Bbox To Gallery",
|
||||
"savedToGalleryOk": "Saved to Gallery",
|
||||
"savedToGalleryError": "Error saving to gallery",
|
||||
"mergeVisible": "Merge Visible",
|
||||
"mergeVisibleOk": "Merged visible layers",
|
||||
"mergeVisibleError": "Error merging visible layers",
|
||||
"clearHistory": "Clear History",
|
||||
"generateMode": "Generate",
|
||||
"generateModeDesc": "Create individual images. Generated images are added directly to the gallery.",
|
||||
@@ -1690,44 +1675,32 @@
|
||||
"deletePrompt": "Delete Prompt",
|
||||
"resetRegion": "Reset Region",
|
||||
"debugLayers": "Debug Layers",
|
||||
"showHUD": "Show HUD",
|
||||
"rectangle": "Rectangle",
|
||||
"maskFill": "Mask Fill",
|
||||
"addPositivePrompt": "Add $t(common.positivePrompt)",
|
||||
"addNegativePrompt": "Add $t(common.negativePrompt)",
|
||||
"addIPAdapter": "Add $t(common.ipAdapter)",
|
||||
"addRasterLayer": "Add $t(controlLayers.rasterLayer)",
|
||||
"addControlLayer": "Add $t(controlLayers.controlLayer)",
|
||||
"addInpaintMask": "Add $t(controlLayers.inpaintMask)",
|
||||
"addRegionalGuidance": "Add $t(controlLayers.regionalGuidance)",
|
||||
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
|
||||
"raster": "Raster",
|
||||
"rasterLayer": "Raster Layer",
|
||||
"controlLayer": "Control Layer",
|
||||
"inpaintMask": "Inpaint Mask",
|
||||
"regionalGuidance": "Regional Guidance",
|
||||
"ipAdapter": "IP Adapter",
|
||||
"sendToGallery": "Send To Gallery",
|
||||
"sendToGalleryDesc": "Generations will be sent to the gallery.",
|
||||
"sendToCanvas": "Send To Canvas",
|
||||
"sendToCanvasDesc": "Generations will be staged onto the canvas.",
|
||||
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
|
||||
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
|
||||
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
|
||||
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
|
||||
"ipAdapter_withCount_one": "$t(controlLayers.ipAdapter)",
|
||||
"rasterLayer_withCount_other": "Raster Layers",
|
||||
"controlLayer_withCount_other": "Control Layers",
|
||||
"inpaintMask_withCount_other": "Inpaint Masks",
|
||||
"regionalGuidance_withCount_other": "Regional Guidance",
|
||||
"ipAdapter_withCount_other": "IP Adapters",
|
||||
"rasterLayer_one": "Raster Layer",
|
||||
"controlLayer_one": "Control Layer",
|
||||
"inpaintMask_one": "Inpaint Mask",
|
||||
"regionalGuidance_one": "Regional Guidance",
|
||||
"ipAdapter_one": "IP Adapter",
|
||||
"rasterLayer_other": "Raster Layers",
|
||||
"controlLayer_other": "Control Layers",
|
||||
"inpaintMask_other": "Inpaint Masks",
|
||||
"regionalGuidance_other": "Regional Guidance",
|
||||
"ipAdapter_other": "IP Adapters",
|
||||
"opacity": "Opacity",
|
||||
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
|
||||
"controlAdapters_withCount_hidden": "Control Adapters ({{count}} hidden)",
|
||||
"controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
|
||||
"rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
|
||||
"ipAdapters_withCount_hidden": "IP Adapters ({{count}} hidden)",
|
||||
"inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
|
||||
"regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
|
||||
"controlAdapters_withCount_visible": "Control Adapters ({{count}})",
|
||||
"controlLayers_withCount_visible": "Control Layers ({{count}})",
|
||||
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
|
||||
"ipAdapters_withCount_visible": "IP Adapters ({{count}})",
|
||||
@@ -1764,7 +1737,6 @@
|
||||
"flipHorizontal": "Flip Horizontal",
|
||||
"flipVertical": "Flip Vertical",
|
||||
"fill": {
|
||||
"fillColor": "Fill Color",
|
||||
"fillStyle": "Fill Style",
|
||||
"solid": "Solid",
|
||||
"grid": "Grid",
|
||||
|
||||
@@ -16,7 +16,6 @@ import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicP
|
||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
||||
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
|
||||
import SettingsModal from 'features/system/components/SettingsModal/SettingsModal';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
@@ -44,17 +43,10 @@ interface Props {
|
||||
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
|
||||
};
|
||||
selectedWorkflowId?: string;
|
||||
selectedStylePresetId?: string;
|
||||
destination?: TabName;
|
||||
destination?: TabName | undefined;
|
||||
}
|
||||
|
||||
const App = ({
|
||||
config = DEFAULT_CONFIG,
|
||||
selectedImage,
|
||||
selectedWorkflowId,
|
||||
selectedStylePresetId,
|
||||
destination,
|
||||
}: Props) => {
|
||||
const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, destination }: Props) => {
|
||||
const language = useAppSelector(selectLanguage);
|
||||
const logger = useLogger('system');
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -93,12 +85,6 @@ const App = ({
|
||||
}
|
||||
}, [selectedWorkflowId, getAndLoadWorkflow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedStylePresetId) {
|
||||
dispatch(activeStylePresetIdChanged(selectedStylePresetId));
|
||||
}
|
||||
}, [dispatch, selectedStylePresetId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (destination) {
|
||||
dispatch(setActiveTab(destination));
|
||||
|
||||
@@ -45,7 +45,6 @@ interface Props extends PropsWithChildren {
|
||||
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
|
||||
};
|
||||
selectedWorkflowId?: string;
|
||||
selectedStylePresetId?: string;
|
||||
destination?: TabName;
|
||||
customStarUi?: CustomStarUi;
|
||||
socketOptions?: Partial<ManagerOptions & SocketOptions>;
|
||||
@@ -67,7 +66,6 @@ const InvokeAIUI = ({
|
||||
queueId,
|
||||
selectedImage,
|
||||
selectedWorkflowId,
|
||||
selectedStylePresetId,
|
||||
destination,
|
||||
customStarUi,
|
||||
socketOptions,
|
||||
@@ -229,7 +227,6 @@ const InvokeAIUI = ({
|
||||
config={config}
|
||||
selectedImage={selectedImage}
|
||||
selectedWorkflowId={selectedWorkflowId}
|
||||
selectedStylePresetId={selectedStylePresetId}
|
||||
destination={destination}
|
||||
/>
|
||||
</AppDndContext>
|
||||
|
||||
@@ -68,7 +68,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
|
||||
objects: [imageObject],
|
||||
};
|
||||
|
||||
api.dispatch(rasterLayerAdded({ overrides, isSelected: false }));
|
||||
api.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
api.dispatch(sessionStagingAreaReset());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
|
||||
let didStartStaging = false;
|
||||
|
||||
if (!state.canvasSession.isStaging && state.canvasSession.sendToCanvas) {
|
||||
if (!state.canvasSession.isStaging && state.canvasSession.mode === 'compose') {
|
||||
dispatch(sessionStartedStaging());
|
||||
didStartStaging = true;
|
||||
}
|
||||
@@ -70,11 +70,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
|
||||
const { g, noise, posCond } = buildGraphResult.value;
|
||||
|
||||
const destination = state.canvasSession.sendToCanvas ? 'canvas' : 'gallery';
|
||||
|
||||
const prepareBatchResult = withResult(() =>
|
||||
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination)
|
||||
);
|
||||
const prepareBatchResult = withResult(() => prepareLinearUIBatch(state, g, prepend, noise, posCond));
|
||||
|
||||
if (isErr(prepareBatchResult)) {
|
||||
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');
|
||||
|
||||
@@ -32,7 +32,6 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
|
||||
workflow: builtWorkflow,
|
||||
runs: state.params.iterations,
|
||||
origin: 'workflows',
|
||||
destination: 'gallery',
|
||||
},
|
||||
prepend: action.payload.prepend,
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
|
||||
|
||||
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
|
||||
|
||||
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery');
|
||||
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond);
|
||||
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, IconButton, Tooltip, useToken } from '@invoke-ai/ui-library';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
type IconSwitchProps = {
|
||||
isChecked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
iconChecked: ReactElement;
|
||||
tooltipChecked?: ReactNode;
|
||||
iconUnchecked: ReactElement;
|
||||
tooltipUnchecked?: ReactNode;
|
||||
ariaLabel: string;
|
||||
};
|
||||
|
||||
const getSx = (padding: string | number): SystemStyleObject => ({
|
||||
transition: 'left 0.1s ease-in-out, transform 0.1s ease-in-out',
|
||||
'&[data-checked="true"]': {
|
||||
left: `calc(100% - ${padding})`,
|
||||
transform: 'translateX(-100%)',
|
||||
},
|
||||
'&[data-checked="false"]': {
|
||||
left: padding,
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
});
|
||||
|
||||
export const IconSwitch = memo(
|
||||
({
|
||||
isChecked,
|
||||
onChange,
|
||||
iconChecked,
|
||||
tooltipChecked,
|
||||
iconUnchecked,
|
||||
tooltipUnchecked,
|
||||
ariaLabel,
|
||||
}: IconSwitchProps) => {
|
||||
const onUncheck = useCallback(() => {
|
||||
onChange(false);
|
||||
}, [onChange]);
|
||||
const onCheck = useCallback(() => {
|
||||
onChange(true);
|
||||
}, [onChange]);
|
||||
|
||||
const gap = useToken('space', 1.5);
|
||||
const sx = useMemo(() => getSx(gap), [gap]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position="relative"
|
||||
bg="base.800"
|
||||
borderRadius="base"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
h="full"
|
||||
p={gap}
|
||||
gap={gap}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
borderRadius="base"
|
||||
bg="invokeBlue.400"
|
||||
w={12}
|
||||
top={gap}
|
||||
bottom={gap}
|
||||
data-checked={isChecked}
|
||||
sx={sx}
|
||||
/>
|
||||
<Tooltip hasArrow label={tooltipUnchecked}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
fontSize={16}
|
||||
icon={iconUnchecked}
|
||||
onClick={onUncheck}
|
||||
variant={!isChecked ? 'solid' : 'ghost'}
|
||||
colorScheme={!isChecked ? 'invokeBlue' : 'base'}
|
||||
aria-label={ariaLabel}
|
||||
data-checked={!isChecked}
|
||||
w={12}
|
||||
alignSelf="stretch"
|
||||
h="auto"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip hasArrow label={tooltipChecked}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
fontSize={16}
|
||||
icon={iconChecked}
|
||||
onClick={onCheck}
|
||||
variant={isChecked ? 'solid' : 'ghost'}
|
||||
colorScheme={isChecked ? 'invokeBlue' : 'base'}
|
||||
aria-label={ariaLabel}
|
||||
data-checked={isChecked}
|
||||
w={12}
|
||||
alignSelf="stretch"
|
||||
h="auto"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IconSwitch.displayName = 'IconSwitch';
|
||||
@@ -1,53 +1,52 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { WritableAtom } from 'nanostores';
|
||||
import { atom } from 'nanostores';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
type UseBoolean = {
|
||||
isTrue: boolean;
|
||||
setTrue: () => void;
|
||||
setFalse: () => void;
|
||||
set: (value: boolean) => void;
|
||||
toggle: () => void;
|
||||
};
|
||||
export const useBoolean = (initialValue: boolean) => {
|
||||
const [isTrue, set] = useState(initialValue);
|
||||
const setTrue = useCallback(() => set(true), []);
|
||||
const setFalse = useCallback(() => set(false), []);
|
||||
const toggle = useCallback(() => set((v) => !v), []);
|
||||
|
||||
/**
|
||||
* Creates a hook to manage a boolean state. The boolean is stored in a nanostores atom.
|
||||
* Returns a tuple containing the hook and the atom. Use this for global boolean state.
|
||||
* @param initialValue Initial value of the boolean
|
||||
*/
|
||||
export const buildUseBoolean = (initialValue: boolean): [() => UseBoolean, WritableAtom<boolean>] => {
|
||||
const $boolean = atom(initialValue);
|
||||
|
||||
const setTrue = () => {
|
||||
$boolean.set(true);
|
||||
};
|
||||
const setFalse = () => {
|
||||
$boolean.set(false);
|
||||
};
|
||||
const set = (value: boolean) => {
|
||||
$boolean.set(value);
|
||||
};
|
||||
const toggle = () => {
|
||||
$boolean.set(!$boolean.get());
|
||||
};
|
||||
|
||||
const useBoolean = () => {
|
||||
const isTrue = useStore($boolean);
|
||||
|
||||
return {
|
||||
const api = useMemo(
|
||||
() => ({
|
||||
isTrue,
|
||||
set,
|
||||
setTrue,
|
||||
setFalse,
|
||||
set,
|
||||
toggle,
|
||||
};
|
||||
};
|
||||
}),
|
||||
[isTrue, set, setTrue, setFalse, toggle]
|
||||
);
|
||||
|
||||
return [useBoolean, $boolean] as const;
|
||||
return api;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to manage a boolean state. Use this for a local boolean state.
|
||||
* @param initialValue Initial value of the boolean
|
||||
*/
|
||||
export const useBoolean = (initialValue: boolean) => buildUseBoolean(initialValue)[0]();
|
||||
export const buildUseBoolean = ($boolean: WritableAtom<boolean>) => {
|
||||
return () => {
|
||||
const setTrue = useCallback(() => {
|
||||
$boolean.set(true);
|
||||
}, []);
|
||||
const setFalse = useCallback(() => {
|
||||
$boolean.set(false);
|
||||
}, []);
|
||||
const set = useCallback((value: boolean) => {
|
||||
$boolean.set(value);
|
||||
}, []);
|
||||
const toggle = useCallback(() => {
|
||||
$boolean.set(!$boolean.get());
|
||||
}, []);
|
||||
|
||||
const api = useMemo(
|
||||
() => ({
|
||||
setTrue,
|
||||
setFalse,
|
||||
set,
|
||||
toggle,
|
||||
$boolean,
|
||||
}),
|
||||
[set, setFalse, setTrue, toggle]
|
||||
);
|
||||
|
||||
return api;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
|
||||
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||
import { useQueueFront } from 'features/queue/hooks/useQueueFront';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
|
||||
@@ -31,22 +31,22 @@ export const CanvasAddEntityButtons = memo(() => {
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" alignItems="center">
|
||||
<ButtonGroup position="relative" orientation="vertical" isAttached={false} top="20%">
|
||||
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<ButtonGroup orientation="vertical" isAttached={false}>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
{t('controlLayers.inpaintMask', { count: 1 })}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
{t('controlLayers.regionalGuidance', { count: 1 })}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
{t('controlLayers.rasterLayer', { count: 1 })}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addControlLayer}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
{t('controlLayers.controlLayer', { count: 1 })}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||
{t('controlLayers.ipAdapter')}
|
||||
{t('controlLayers.ipAdapter', { count: 1 })}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
|
||||
@@ -33,19 +33,19 @@ export const CanvasEntityListMenuItems = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
{t('controlLayers.inpaintMask', { count: 1 })}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
{t('controlLayers.regionalGuidance', { count: 1 })}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
{t('controlLayers.rasterLayer', { count: 1 })}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
{t('controlLayers.controlLayer', { count: 1 })}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||
{t('controlLayers.ipAdapter')}
|
||||
{t('controlLayers.ipAdapter', { count: 1 })}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -157,7 +157,7 @@ export const SelectedEntityOpacity = memo(() => {
|
||||
clampValueOnBlur={false}
|
||||
variant="outline"
|
||||
>
|
||||
<NumberInputField paddingInlineEnd={7} _focusVisible={{ zIndex: 0 }} />
|
||||
<NumberInputField paddingInlineEnd={7} />
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label="open-slider"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectCanvasSessionSlice, sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectCanvasMode = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.mode);
|
||||
|
||||
export const CanvasModeSwitcher = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const mode = useAppSelector(selectCanvasMode);
|
||||
const onClickGenerate = useCallback(() => dispatch(sessionModeChanged({ mode: 'generate' })), [dispatch]);
|
||||
const onClickCompose = useCallback(() => dispatch(sessionModeChanged({ mode: 'compose' })), [dispatch]);
|
||||
|
||||
return (
|
||||
<ButtonGroup variant="outline">
|
||||
<Button onClick={onClickGenerate} colorScheme={mode === 'generate' ? 'invokeBlue' : 'base'}>
|
||||
{t('controlLayers.generateMode')}
|
||||
</Button>
|
||||
<Button onClick={onClickCompose} colorScheme={mode === 'compose' ? 'invokeBlue' : 'base'}>
|
||||
{t('controlLayers.composeMode')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasModeSwitcher.displayName = 'CanvasModeSwitcher';
|
||||
@@ -1,37 +1,21 @@
|
||||
import { Box, ContextMenu, Divider, Flex, MenuList } from '@invoke-ai/ui-library';
|
||||
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 { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const CanvasPanelContent = memo(() => {
|
||||
const hasEntities = useAppSelector(selectHasEntities);
|
||||
const renderMenu = useCallback(
|
||||
() => (
|
||||
<MenuList>
|
||||
<CanvasEntityListMenuItems />
|
||||
</MenuList>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||
<EntityListActionBar />
|
||||
<Divider py={0} />
|
||||
<ContextMenu<HTMLDivElement> renderMenu={renderMenu}>
|
||||
{(ref) => (
|
||||
<Box ref={ref} w="full" h="full">
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
{hasEntities && <CanvasEntityList />}
|
||||
</Box>
|
||||
)}
|
||||
</ContextMenu>
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
{hasEntities && <CanvasEntityList />}
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IconSwitch } from 'common/components/IconSwitch';
|
||||
import { selectIsComposing, sessionSendToCanvasChanged } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
|
||||
|
||||
const TooltipSendToGallery = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('controlLayers.sendToGallery')}</Text>
|
||||
<Text fontWeight="normal">{t('controlLayers.sendToGalleryDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
TooltipSendToGallery.displayName = 'TooltipSendToGallery';
|
||||
|
||||
const TooltipSendToCanvas = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('controlLayers.sendToCanvas')}</Text>
|
||||
<Text fontWeight="normal">{t('controlLayers.sendToCanvasDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
TooltipSendToCanvas.displayName = 'TooltipSendToCanvas';
|
||||
|
||||
export const CanvasSendToToggle = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isComposing = useAppSelector(selectIsComposing);
|
||||
|
||||
const onChange = useCallback(
|
||||
(isChecked: boolean) => {
|
||||
dispatch(sessionSendToCanvasChanged(isChecked));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<IconSwitch
|
||||
isChecked={isComposing}
|
||||
onChange={onChange}
|
||||
iconUnchecked={<PiImageBold />}
|
||||
tooltipUnchecked={<TooltipSendToGallery />}
|
||||
iconChecked={<PiPaintBrushBold />}
|
||||
tooltipChecked={<TooltipSendToCanvas />}
|
||||
ariaLabel="Toggle canvas mode"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasSendToToggle.displayName = 'CanvasSendToToggle';
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
@@ -28,7 +29,8 @@ export const ControlLayer = memo(({ id }: Props) => {
|
||||
<CanvasEntityEditableTitle />
|
||||
<Spacer />
|
||||
<ControlLayerBadges />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
<CanvasEntityIsLockedToggle />
|
||||
<CanvasEntityEnabledToggle />
|
||||
</CanvasEntityHeader>
|
||||
<CanvasEntitySettingsWrapper>
|
||||
<ControlLayerControlAdapter />
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasModeSwitcher } from 'features/controlLayers/components/CanvasModeSwitcher';
|
||||
import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton';
|
||||
import { CanvasScale } from 'features/controlLayers/components/CanvasScale';
|
||||
import { SaveToGalleryButton } from 'features/controlLayers/components/SaveToGalleryButton';
|
||||
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
|
||||
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
||||
import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
|
||||
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
|
||||
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useCanvasUndoRedo } from 'features/controlLayers/hooks/useCanvasUndoRedo';
|
||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||
import { ViewerToggle } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
||||
import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const ControlLayersToolbar = memo(() => {
|
||||
useCanvasUndoRedo();
|
||||
|
||||
return (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex w="full" gap={2} alignItems="center">
|
||||
@@ -28,9 +26,10 @@ export const ControlLayersToolbar = memo(() => {
|
||||
<CanvasResetViewButton />
|
||||
<Spacer />
|
||||
<ToolFillColorPicker />
|
||||
<SaveToGalleryButton />
|
||||
<CanvasModeSwitcher />
|
||||
<UndoRedoButtonGroup />
|
||||
<CanvasSettingsPopover />
|
||||
<ViewerToggle />
|
||||
<ViewerToggleMenu />
|
||||
</Flex>
|
||||
</CanvasManagerProviderGate>
|
||||
);
|
||||
|
||||
@@ -1,27 +1,53 @@
|
||||
import { Grid, GridItem, Text } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { round } from 'lodash-es';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectBbox = createSelector(selectCanvasSlice, (canvas) => canvas.bbox);
|
||||
|
||||
export const HeadsUpDisplay = memo(() => {
|
||||
const canvasManager = useCanvasManager();
|
||||
const stageAttrs = useStore(canvasManager.stateApi.$stageAttrs);
|
||||
const cursorPos = useStore(canvasManager.stateApi.$lastCursorPos);
|
||||
const isDrawing = useStore(canvasManager.stateApi.$isDrawing);
|
||||
const isMouseDown = useStore(canvasManager.stateApi.$isMouseDown);
|
||||
const lastMouseDownPos = useStore(canvasManager.stateApi.$lastMouseDownPos);
|
||||
const lastAddedPoint = useStore(canvasManager.stateApi.$lastAddedPoint);
|
||||
const bbox = useAppSelector(selectBbox);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
bg="base.900"
|
||||
borderBottomEndRadius="base"
|
||||
p={2}
|
||||
gap={2}
|
||||
borderRadius="base"
|
||||
templateColumns="auto auto"
|
||||
opacity={0.6}
|
||||
>
|
||||
<HUDItem label="BBox" value={`${bbox.rect.width}×${bbox.rect.height} px`} />
|
||||
<HUDItem label="Scaled BBox" value={`${bbox.scaledSize.width}×${bbox.scaledSize.height} px`} />
|
||||
</Grid>
|
||||
<Flex flexDir="column" bg="blackAlpha.400" borderBottomEndRadius="base" p={2} minW={64} gap={2}>
|
||||
<HUDItem label="Zoom" value={`${round(stageAttrs.scale * 100, 2)}%`} />
|
||||
<HUDItem label="Stage Pos" value={`${round(stageAttrs.x, 3)}, ${round(stageAttrs.y, 3)}`} />
|
||||
<HUDItem
|
||||
label="Stage Size"
|
||||
value={`${round(stageAttrs.width / stageAttrs.scale, 2)}×${round(stageAttrs.height / stageAttrs.scale, 2)} px`}
|
||||
/>
|
||||
<HUDItem label="BBox Size" value={`${bbox.rect.width}×${bbox.rect.height} px`} />
|
||||
<HUDItem label="BBox Position" value={`${bbox.rect.x}, ${bbox.rect.y}`} />
|
||||
<HUDItem label="BBox Width % 8" value={round(bbox.rect.width % 8, 2)} />
|
||||
<HUDItem label="BBox Height % 8" value={round(bbox.rect.height % 8, 2)} />
|
||||
<HUDItem label="BBox X % 8" value={round(bbox.rect.x % 8, 2)} />
|
||||
<HUDItem label="BBox Y % 8" value={round(bbox.rect.y % 8, 2)} />
|
||||
<HUDItem
|
||||
label="Cursor Position"
|
||||
value={cursorPos ? `${round(cursorPos.x, 2)}, ${round(cursorPos.y, 2)}` : '?, ?'}
|
||||
/>
|
||||
<HUDItem label="Is Drawing" value={isDrawing ? 'True' : 'False'} />
|
||||
<HUDItem label="Is Mouse Down" value={isMouseDown ? 'True' : 'False'} />
|
||||
<HUDItem
|
||||
label="Last Mouse Down Pos"
|
||||
value={lastMouseDownPos ? `${round(lastMouseDownPos.x, 2)}, ${round(lastMouseDownPos.y, 2)}` : '?, ?'}
|
||||
/>
|
||||
<HUDItem
|
||||
label="Last Added Point"
|
||||
value={lastAddedPoint ? `${round(lastAddedPoint.x, 2)}, ${round(lastAddedPoint.y, 2)}` : '?, ?'}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -29,14 +55,12 @@ HeadsUpDisplay.displayName = 'HeadsUpDisplay';
|
||||
|
||||
const HUDItem = memo(({ label, value }: { label: string; value: string | number }) => {
|
||||
return (
|
||||
<>
|
||||
<GridItem>
|
||||
<Text textAlign="end">{label}: </Text>
|
||||
</GridItem>
|
||||
<GridItem fontWeight="semibold">
|
||||
<Text>{value}</Text>
|
||||
</GridItem>
|
||||
</>
|
||||
<Box display="inline-block" lineHeight={1}>
|
||||
<Text as="span">{label}: </Text>
|
||||
<Text as="span" fontWeight="semibold">
|
||||
{value}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
import { IPAdapterSettings } from 'features/controlLayers/components/IPAdapter/IPAdapterSettings';
|
||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
@@ -18,10 +18,10 @@ export const IPAdapter = memo(({ id }: Props) => {
|
||||
return (
|
||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||
<CanvasEntityContainer>
|
||||
<CanvasEntityHeader ps={4} py={5}>
|
||||
<CanvasEntityHeader ps={4}>
|
||||
<CanvasEntityEditableTitle />
|
||||
<Spacer />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
<CanvasEntityEnabledToggle />
|
||||
</CanvasEntityHeader>
|
||||
<IPAdapterSettings />
|
||||
</CanvasEntityContainer>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
@@ -24,7 +25,8 @@ export const InpaintMask = memo(({ id }: Props) => {
|
||||
<CanvasEntityPreviewImage />
|
||||
<CanvasEntityEditableTitle />
|
||||
<Spacer />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
<CanvasEntityIsLockedToggle />
|
||||
<CanvasEntityEnabledToggle />
|
||||
</CanvasEntityHeader>
|
||||
</CanvasEntityContainer>
|
||||
</EntityMaskAdapterGate>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
@@ -24,7 +25,8 @@ export const RasterLayer = memo(({ id }: Props) => {
|
||||
<CanvasEntityPreviewImage />
|
||||
<CanvasEntityEditableTitle />
|
||||
<Spacer />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
<CanvasEntityIsLockedToggle />
|
||||
<CanvasEntityEnabledToggle />
|
||||
</CanvasEntityHeader>
|
||||
</CanvasEntityContainer>
|
||||
</EntityLayerAdapterGate>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges';
|
||||
@@ -27,7 +28,8 @@ export const RegionalGuidance = memo(({ id }: Props) => {
|
||||
<CanvasEntityEditableTitle />
|
||||
<Spacer />
|
||||
<RegionalGuidanceBadges />
|
||||
<CanvasEntityHeaderCommonActions />
|
||||
<CanvasEntityIsLockedToggle />
|
||||
<CanvasEntityEnabledToggle />
|
||||
</CanvasEntityHeader>
|
||||
<RegionalGuidanceSettings />
|
||||
</CanvasEntityContainer>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { isOk, withResultAsync } from 'common/util/result';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFloppyDiskBold } from 'react-icons/pi';
|
||||
import { serializeError } from 'serialize-error';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
const [useIsSaving] = buildUseBoolean(false);
|
||||
|
||||
export const SaveToGalleryButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const shift = useShiftModifier();
|
||||
const canvasManager = useCanvasManager();
|
||||
const isSaving = useIsSaving();
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
isSaving.setTrue();
|
||||
|
||||
const rect = shift ? canvasManager.stateApi.getBbox().rect : canvasManager.stage.getVisibleRect('raster_layer');
|
||||
|
||||
const result = await withResultAsync(() =>
|
||||
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, true)
|
||||
);
|
||||
|
||||
if (isOk(result)) {
|
||||
toast({ title: t('controlLayers.savedToGalleryOk') });
|
||||
} else {
|
||||
log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery');
|
||||
toast({ title: t('controlLayers.savedToGalleryError'), status: 'error' });
|
||||
}
|
||||
|
||||
isSaving.setFalse();
|
||||
}, [canvasManager.compositor, canvasManager.stage, canvasManager.stateApi, isSaving, shift, t]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
onClick={onClick}
|
||||
icon={<PiFloppyDiskBold />}
|
||||
isLoading={isSaving.isTrue}
|
||||
aria-label={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
|
||||
tooltip={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SaveToGalleryButton.displayName = 'SaveToGalleryButton';
|
||||
@@ -18,7 +18,6 @@ import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/compo
|
||||
import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo';
|
||||
import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton';
|
||||
import { CanvasSettingsResetButton } from 'features/controlLayers/components/Settings/CanvasSettingsResetButton';
|
||||
import { CanvasSettingsShowHUDSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsShowHUDSwitch';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiSettings4Fill } from 'react-icons/ri';
|
||||
@@ -38,7 +37,6 @@ export const CanvasSettingsPopover = memo(() => {
|
||||
<CanvasSettingsInvertScrollCheckbox />
|
||||
<CanvasSettingsClipToBboxCheckbox />
|
||||
<CanvasSettingsDynamicGridSwitch />
|
||||
<CanvasSettingsShowHUDSwitch />
|
||||
<CanvasSettingsResetButton />
|
||||
<DebugSettings />
|
||||
</Flex>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectCanvasSettingsSlice, settingsShowHUDToggled } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD);
|
||||
|
||||
export const CanvasSettingsShowHUDSwitch = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
const onChange = useCallback(() => {
|
||||
dispatch(settingsShowHUDToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormLabel m={0} flexGrow={1}>
|
||||
{t('controlLayers.showHUD')}
|
||||
</FormLabel>
|
||||
<Switch size="sm" isChecked={showHUD} onChange={onChange} />
|
||||
</FormControl>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasSettingsShowHUDSwitch.displayName = 'CanvasSettingsShowHUDSwitch';
|
||||
@@ -8,18 +8,20 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import Konva from 'konva';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
const showHud = false;
|
||||
|
||||
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
|
||||
Konva.showWarnings = false;
|
||||
|
||||
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null) => {
|
||||
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => {
|
||||
const store = useAppStore();
|
||||
const socket = useStore($socket);
|
||||
const dpr = useDevicePixelRatio({ round: false });
|
||||
@@ -40,25 +42,28 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null)
|
||||
const manager = new CanvasManager(stage, container, store, socket);
|
||||
manager.initialize();
|
||||
return manager.destroy;
|
||||
}, [container, socket, stage, store]);
|
||||
}, [asPreview, container, socket, stage, store]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
Konva.pixelRatio = dpr;
|
||||
}, [dpr]);
|
||||
};
|
||||
|
||||
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
|
||||
const selectShowHUD = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.showHUD);
|
||||
type Props = {
|
||||
asPreview?: boolean;
|
||||
};
|
||||
|
||||
export const StageComponent = memo(() => {
|
||||
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
|
||||
|
||||
export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||
const showHUD = useAppSelector(selectShowHUD);
|
||||
|
||||
const [stage] = useState(
|
||||
() =>
|
||||
new Konva.Stage({
|
||||
id: getPrefixedId('konva_stage'),
|
||||
id: uuidv4(),
|
||||
container: document.createElement('div'),
|
||||
listening: !asPreview,
|
||||
})
|
||||
);
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
@@ -67,7 +72,7 @@ export const StageComponent = memo(() => {
|
||||
setContainer(el);
|
||||
}, []);
|
||||
|
||||
useStageRenderer(stage, container);
|
||||
useStageRenderer(stage, container, asPreview);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -101,9 +106,9 @@ export const StageComponent = memo(() => {
|
||||
overflow="hidden"
|
||||
data-testid="control-layers-canvas"
|
||||
/>
|
||||
{showHUD && (
|
||||
<Flex position="absolute" top={1} insetInlineStart={1} pointerEvents="none">
|
||||
<HeadsUpDisplay />
|
||||
{!asPreview && (
|
||||
<Flex position="absolute" top={0} insetInlineStart={0} pointerEvents="none">
|
||||
{showHud && <HeadsUpDisplay />}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -98,9 +98,9 @@ export const StagingAreaToolbar = memo(() => {
|
||||
onPrev,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasActive,
|
||||
},
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
[isCanvasActive]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
@@ -108,9 +108,9 @@ export const StagingAreaToolbar = memo(() => {
|
||||
onNext,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasActive,
|
||||
},
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
[isCanvasActive]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
@@ -118,9 +118,9 @@ export const StagingAreaToolbar = memo(() => {
|
||||
onAccept,
|
||||
{
|
||||
preventDefault: true,
|
||||
enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
|
||||
enabled: isCanvasActive,
|
||||
},
|
||||
[isCanvasActive, shouldShowStagedImage, imageCount]
|
||||
[isCanvasActive]
|
||||
);
|
||||
|
||||
const counterText = useMemo(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||
@@ -23,13 +23,17 @@ export const ToolFillColorPicker = memo(() => {
|
||||
return (
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<Flex role="button" aria-label={t('controlLayers.fill.fillColor')} tabIndex={-1} w={8} h={8}>
|
||||
<Tooltip label={t('controlLayers.fill.fillColor')}>
|
||||
<Flex w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Box borderRadius="full" w={6} h={6} borderWidth={1} bg={rgbaColorToString(fill)} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Flex
|
||||
as="button"
|
||||
aria-label={t('controlLayers.brushColor')}
|
||||
borderRadius="full"
|
||||
borderWidth={1}
|
||||
bg={rgbaColorToString(fill)}
|
||||
w={8}
|
||||
h={8}
|
||||
cursor="pointer"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverBody minH={64}>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { canvasRedo, canvasUndo } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasMayRedo, selectCanvasMayUndo } from 'features/controlLayers/store/selectors';
|
||||
import { useCallback } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowClockwiseBold, PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export const useCanvasUndoRedo = () => {
|
||||
useAssertSingleton('useCanvasUndoRedo');
|
||||
export const UndoRedoButtonGroup = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const mayUndo = useAppSelector(selectCanvasMayUndo);
|
||||
@@ -25,4 +27,27 @@ export const useCanvasUndoRedo = () => {
|
||||
mayRedo,
|
||||
handleRedo,
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<ButtonGroup isAttached={false}>
|
||||
<IconButton
|
||||
aria-label={t('unifiedCanvas.undo')}
|
||||
tooltip={t('unifiedCanvas.undo')}
|
||||
onClick={handleUndo}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
isDisabled={!mayUndo}
|
||||
variant="ghost"
|
||||
/>
|
||||
<IconButton
|
||||
aria-label={t('unifiedCanvas.redo')}
|
||||
tooltip={t('unifiedCanvas.redo')}
|
||||
onClick={handleRedo}
|
||||
icon={<PiArrowClockwiseBold />}
|
||||
isDisabled={!mayRedo}
|
||||
variant="ghost"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
|
||||
UndoRedoButtonGroup.displayName = 'UndoRedoButtonGroup';
|
||||
@@ -1,70 +0,0 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
inpaintMaskAdded,
|
||||
ipaAdded,
|
||||
rasterLayerAdded,
|
||||
rgAdded,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
type: CanvasEntityIdentifier['type'];
|
||||
};
|
||||
|
||||
export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
switch (type) {
|
||||
case 'inpaint_mask':
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
dispatch(rgAdded({ isSelected: true }));
|
||||
break;
|
||||
case 'raster_layer':
|
||||
dispatch(rasterLayerAdded({ isSelected: true }));
|
||||
break;
|
||||
case 'control_layer':
|
||||
dispatch(controlLayerAdded({ isSelected: true }));
|
||||
break;
|
||||
case 'ip_adapter':
|
||||
dispatch(ipaAdded({ isSelected: true }));
|
||||
break;
|
||||
}
|
||||
}, [dispatch, type]);
|
||||
|
||||
const label = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'inpaint_mask':
|
||||
return t('controlLayers.addInpaintMask');
|
||||
case 'regional_guidance':
|
||||
return t('controlLayers.addRegionalGuidance');
|
||||
case 'raster_layer':
|
||||
return t('controlLayers.addRasterLayer');
|
||||
case 'control_layer':
|
||||
return t('controlLayers.addControlLayer');
|
||||
case 'ip_adapter':
|
||||
return t('controlLayers.addIPAdapter');
|
||||
}
|
||||
}, [type, t]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size="sm"
|
||||
aria-label={label}
|
||||
tooltip={label}
|
||||
variant="link"
|
||||
icon={<PiPlusBold />}
|
||||
onClick={onClick}
|
||||
alignSelf="stretch"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityAddOfTypeButton.displayName = 'CanvasEntityAddOfTypeButton';
|
||||
@@ -1,31 +0,0 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleFill } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityDeleteButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(entityDeleted({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size="sm"
|
||||
aria-label={t('common.delete')}
|
||||
tooltip={t('common.delete')}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
icon={<PiTrashSimpleFill />}
|
||||
onClick={onClick}
|
||||
colorScheme="error"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityDeleteButton.displayName = 'CanvasEntityDeleteButton';
|
||||
@@ -21,8 +21,7 @@ export const CanvasEntityEnabledToggle = memo(() => {
|
||||
size="sm"
|
||||
aria-label={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
||||
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
variant="ghost"
|
||||
icon={isEnabled ? <PiCircleFill /> : <PiCircleBold />}
|
||||
onClick={onClick}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { useBoolean } from 'common/hooks/useBoolean';
|
||||
import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton';
|
||||
import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/components/common/CanvasEntityMergeVisibleButton';
|
||||
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
|
||||
import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { PiCaretDownBold } from 'react-icons/pi';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
@@ -22,9 +20,6 @@ const _hover: SystemStyleObject = {
|
||||
export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props) => {
|
||||
const title = useEntityTypeTitle(type);
|
||||
const collapse = useBoolean(true);
|
||||
const canMergeVisible = useMemo(() => type === 'raster_layer' || type === 'inpaint_mask', [type]);
|
||||
const canHideAll = useMemo(() => type !== 'ip_adapter', [type]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full">
|
||||
<Flex w="full">
|
||||
@@ -58,9 +53,7 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
|
||||
</Text>
|
||||
<Spacer />
|
||||
</Flex>
|
||||
{canMergeVisible && <CanvasEntityMergeVisibleButton type={type} />}
|
||||
<CanvasEntityAddOfTypeButton type={type} />
|
||||
{canHideAll && <CanvasEntityTypeIsHiddenToggle type={type} />}
|
||||
{type !== 'ip_adapter' && <CanvasEntityTypeIsHiddenToggle type={type} />}
|
||||
</Flex>
|
||||
<Collapse in={collapse.isTrue}>
|
||||
<Flex flexDir="column" gap={2} pt={2}>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
|
||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const CanvasEntityHeaderCommonActions = memo(() => {
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
|
||||
return (
|
||||
<Flex alignSelf="stretch">
|
||||
{entityIdentifier.type !== 'ip_adapter' && <CanvasEntityIsLockedToggle />}
|
||||
<CanvasEntityEnabledToggle />
|
||||
<CanvasEntityDeleteButton />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityHeaderCommonActions.displayName = 'CanvasEntityHeaderCommonActions';
|
||||
@@ -21,8 +21,7 @@ export const CanvasEntityIsLockedToggle = memo(() => {
|
||||
size="sm"
|
||||
aria-label={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
|
||||
tooltip={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
variant="ghost"
|
||||
icon={isLocked ? <PiLockSimpleFill /> : <PiLockSimpleOpenBold />}
|
||||
onClick={onClick}
|
||||
/>
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { isOk, withResultAsync } from 'common/util/result';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiStackBold } from 'react-icons/pi';
|
||||
import { serializeError } from 'serialize-error';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
type Props = {
|
||||
type: CanvasEntityIdentifier['type'];
|
||||
};
|
||||
|
||||
export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const canvasManager = useCanvasManager();
|
||||
const onClick = useCallback(async () => {
|
||||
if (type === 'raster_layer') {
|
||||
const rect = canvasManager.stage.getVisibleRect('raster_layer');
|
||||
const result = await withResultAsync(() =>
|
||||
canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, false)
|
||||
);
|
||||
|
||||
if (isOk(result)) {
|
||||
dispatch(
|
||||
rasterLayerAdded({
|
||||
isSelected: true,
|
||||
overrides: {
|
||||
objects: [imageDTOToImageObject(result.value)],
|
||||
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
|
||||
},
|
||||
deleteOthers: true,
|
||||
})
|
||||
);
|
||||
toast({ title: t('controlLayers.mergeVisibleOk') });
|
||||
} else {
|
||||
log.error({ error: serializeError(result.error) }, 'Failed to merge visible');
|
||||
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
|
||||
}
|
||||
} else if (type === 'inpaint_mask') {
|
||||
const rect = canvasManager.stage.getVisibleRect('inpaint_mask');
|
||||
const result = await withResultAsync(() =>
|
||||
canvasManager.compositor.rasterizeAndUploadCompositeInpaintMask(rect, false)
|
||||
);
|
||||
|
||||
if (isOk(result)) {
|
||||
dispatch(
|
||||
inpaintMaskAdded({
|
||||
isSelected: true,
|
||||
overrides: {
|
||||
objects: [imageDTOToImageObject(result.value)],
|
||||
position: { x: Math.floor(rect.x), y: Math.floor(rect.y) },
|
||||
},
|
||||
deleteOthers: true,
|
||||
})
|
||||
);
|
||||
toast({ title: t('controlLayers.mergeVisibleOk') });
|
||||
} else {
|
||||
log.error({ error: serializeError(result.error) }, 'Failed to merge visible');
|
||||
toast({ title: t('controlLayers.mergeVisibleError'), status: 'error' });
|
||||
}
|
||||
} else {
|
||||
log.error({ type }, 'Unsupported type for merge visible');
|
||||
}
|
||||
}, [canvasManager.compositor, canvasManager.stage, dispatch, t, type]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size="sm"
|
||||
aria-label={t('controlLayers.mergeVisible')}
|
||||
tooltip={t('controlLayers.mergeVisible')}
|
||||
variant="link"
|
||||
icon={<PiStackBold />}
|
||||
onClick={onClick}
|
||||
alignSelf="stretch"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityMergeVisibleButton.displayName = 'CanvasEntityMergeVisibleButton';
|
||||
@@ -29,15 +29,15 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
|
||||
const parts: string[] = [];
|
||||
if (entityIdentifier.type === 'inpaint_mask') {
|
||||
parts.push(t('controlLayers.inpaintMask'));
|
||||
parts.push(t('controlLayers.inpaintMask', { count: 1 }));
|
||||
} else if (entityIdentifier.type === 'control_layer') {
|
||||
parts.push(t('controlLayers.controlLayer'));
|
||||
parts.push(t('controlLayers.controlLayer', { count: 1 }));
|
||||
} else if (entityIdentifier.type === 'raster_layer') {
|
||||
parts.push(t('controlLayers.rasterLayer'));
|
||||
parts.push(t('controlLayers.rasterLayer', { count: 1 }));
|
||||
} else if (entityIdentifier.type === 'ip_adapter') {
|
||||
parts.push(t('common.ipAdapter'));
|
||||
parts.push(t('common.ipAdapter', { count: 1 }));
|
||||
} else if (entityIdentifier.type === 'regional_guidance') {
|
||||
parts.push(t('controlLayers.regionalGuidance'));
|
||||
parts.push(t('controlLayers.regionalGuidance', { count: 1 }));
|
||||
} else {
|
||||
assert(false, 'Unexpected entity type');
|
||||
}
|
||||
|
||||
@@ -8,15 +8,15 @@ export const useEntityTypeString = (type: CanvasEntityIdentifier['type']): strin
|
||||
const typeString = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'control_layer':
|
||||
return t('controlLayers.controlLayer');
|
||||
return t('controlLayers.controlLayer', { count: 0 });
|
||||
case 'raster_layer':
|
||||
return t('controlLayers.rasterLayer');
|
||||
return t('controlLayers.rasterLayer', { count: 0 });
|
||||
case 'inpaint_mask':
|
||||
return t('controlLayers.inpaintMask');
|
||||
return t('controlLayers.inpaintMask', { count: 0 });
|
||||
case 'regional_guidance':
|
||||
return t('controlLayers.regionalGuidance');
|
||||
return t('controlLayers.regionalGuidance', { count: 0 });
|
||||
case 'ip_adapter':
|
||||
return t('controlLayers.ipAdapter');
|
||||
return t('controlLayers.ipAdapter', { count: 0 });
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -147,19 +147,6 @@ export class CanvasCompositorModule extends CanvasModuleABC {
|
||||
return stableHash(data);
|
||||
};
|
||||
|
||||
rasterizeAndUploadCompositeRasterLayer = async (rect: Rect, saveToGallery: boolean) => {
|
||||
this.log.trace({ rect }, 'Rasterizing composite raster layer');
|
||||
|
||||
const canvas = this.getCompositeRasterLayerCanvas(rect);
|
||||
const blob = await canvasToBlob(canvas);
|
||||
|
||||
if (this.manager._isDebugging) {
|
||||
previewBlob(blob, 'Composite raster layer canvas');
|
||||
}
|
||||
|
||||
return uploadImage(blob, 'composite-raster-layer.png', 'general', !saveToGallery);
|
||||
};
|
||||
|
||||
getCompositeRasterLayerImageDTO = async (rect: Rect): Promise<ImageDTO> => {
|
||||
let imageDTO: ImageDTO | null = null;
|
||||
|
||||
@@ -174,21 +161,17 @@ export class CanvasCompositorModule extends CanvasModuleABC {
|
||||
}
|
||||
}
|
||||
|
||||
imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, false);
|
||||
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
|
||||
return imageDTO;
|
||||
};
|
||||
this.log.trace({ rect }, 'Rasterizing composite raster layer');
|
||||
|
||||
rasterizeAndUploadCompositeInpaintMask = async (rect: Rect, saveToGallery: boolean) => {
|
||||
this.log.trace({ rect }, 'Rasterizing composite inpaint mask');
|
||||
|
||||
const canvas = this.getCompositeInpaintMaskCanvas(rect);
|
||||
const canvas = this.getCompositeRasterLayerCanvas(rect);
|
||||
const blob = await canvasToBlob(canvas);
|
||||
if (this.manager._isDebugging) {
|
||||
previewBlob(blob, 'Composite inpaint mask canvas');
|
||||
previewBlob(blob, 'Composite raster layer canvas');
|
||||
}
|
||||
|
||||
return uploadImage(blob, 'composite-inpaint-mask.png', 'general', !saveToGallery);
|
||||
imageDTO = await uploadImage(blob, 'composite-raster-layer.png', 'general', true);
|
||||
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
|
||||
return imageDTO;
|
||||
};
|
||||
|
||||
getCompositeInpaintMaskImageDTO = async (rect: Rect): Promise<ImageDTO> => {
|
||||
@@ -205,7 +188,15 @@ export class CanvasCompositorModule extends CanvasModuleABC {
|
||||
}
|
||||
}
|
||||
|
||||
imageDTO = await this.rasterizeAndUploadCompositeInpaintMask(rect, false);
|
||||
this.log.trace({ rect }, 'Rasterizing composite inpaint mask');
|
||||
|
||||
const canvas = this.getCompositeInpaintMaskCanvas(rect);
|
||||
const blob = await canvasToBlob(canvas);
|
||||
if (this.manager._isDebugging) {
|
||||
previewBlob(blob, 'Composite inpaint mask canvas');
|
||||
}
|
||||
|
||||
imageDTO = await uploadImage(blob, 'composite-inpaint-mask.png', 'general', true);
|
||||
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
|
||||
return imageDTO;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleABC } from 'features/controlLayers/konva/CanvasModuleABC';
|
||||
import { CANVAS_SCALE_BY } from 'features/controlLayers/konva/constants';
|
||||
import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasEntityIdentifier, Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types';
|
||||
import type { Coordinate, Dimensions, Rect } from 'features/controlLayers/store/types';
|
||||
import type Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { clamp } from 'lodash-es';
|
||||
@@ -76,36 +76,35 @@ export class CanvasStageModule extends CanvasModuleABC {
|
||||
});
|
||||
};
|
||||
|
||||
getVisibleRect = (type?: Exclude<CanvasEntityIdentifier['type'], 'ip_adapter'>): Rect => {
|
||||
getVisibleRect = (): Rect => {
|
||||
const rects = [];
|
||||
|
||||
for (const adapter of this.manager.adapters.getAll()) {
|
||||
if (!adapter.state.isEnabled) {
|
||||
continue;
|
||||
if (adapter.state.isEnabled) {
|
||||
rects.push(adapter.transformer.getRelativeRect());
|
||||
}
|
||||
if (type && adapter.state.type !== type) {
|
||||
continue;
|
||||
}
|
||||
rects.push(adapter.transformer.getRelativeRect());
|
||||
}
|
||||
|
||||
return getRectUnion(...rects);
|
||||
const rectUnion = getRectUnion(...rects);
|
||||
|
||||
if (rectUnion.width === 0 || rectUnion.height === 0) {
|
||||
// fall back to the bbox if there is no content
|
||||
return this.manager.stateApi.getBbox().rect;
|
||||
} else {
|
||||
return rectUnion;
|
||||
}
|
||||
};
|
||||
|
||||
fitBboxToStage = () => {
|
||||
const { rect } = this.manager.stateApi.getBbox();
|
||||
this.log.trace({ rect }, 'Fitting bbox to stage');
|
||||
this.fitRect(rect);
|
||||
this.log.trace('Fitting bbox to stage');
|
||||
const bbox = this.manager.stateApi.getBbox();
|
||||
this.fitRect(bbox.rect);
|
||||
};
|
||||
|
||||
fitLayersToStage() {
|
||||
this.log.trace('Fitting layers to stage');
|
||||
const rect = this.getVisibleRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
this.fitBboxToStage();
|
||||
} else {
|
||||
this.log.trace({ rect }, 'Fitting layers to stage');
|
||||
this.fitRect(rect);
|
||||
}
|
||||
this.fitRect(rect);
|
||||
}
|
||||
|
||||
fitRect = (rect: Rect) => {
|
||||
|
||||
@@ -250,10 +250,12 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
this.konva.colorPicker.group.visible(tool === 'colorPicker');
|
||||
};
|
||||
|
||||
syncCursorStyle = () => {
|
||||
render = () => {
|
||||
const stage = this.manager.stage;
|
||||
const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount();
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
||||
const isMouseDown = this.manager.stateApi.$isMouseDown.get();
|
||||
const tool = this.manager.stateApi.$tool.get();
|
||||
|
||||
@@ -292,158 +294,6 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
// Non-drawable layers don't have tools
|
||||
stage.container.style.cursor = 'not-allowed';
|
||||
}
|
||||
};
|
||||
|
||||
renderBrushTool = (cursorPos: Coordinate) => {
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill();
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width);
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
const radius = toolState.brush.width / 2;
|
||||
|
||||
// The circle is scaled
|
||||
this.konva.brush.fillCircle.setAttrs({
|
||||
x: alignedCursorPos.x,
|
||||
y: alignedCursorPos.y,
|
||||
radius,
|
||||
fill: rgbaColorToString(brushPreviewFill),
|
||||
});
|
||||
|
||||
// But the borders are in screen-pixels
|
||||
this.konva.brush.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius,
|
||||
outerRadius: radius + onePixel,
|
||||
});
|
||||
this.konva.brush.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
};
|
||||
|
||||
renderEraserTool = (cursorPos: Coordinate) => {
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
const radius = toolState.eraser.width / 2;
|
||||
|
||||
// The circle is scaled
|
||||
this.konva.eraser.fillCircle.setAttrs({
|
||||
x: alignedCursorPos.x,
|
||||
y: alignedCursorPos.y,
|
||||
radius,
|
||||
fill: 'white',
|
||||
});
|
||||
|
||||
// But the borders are in screen-pixels
|
||||
this.konva.eraser.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius,
|
||||
outerRadius: radius + onePixel,
|
||||
});
|
||||
this.konva.eraser.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
};
|
||||
|
||||
renderColorPicker = (cursorPos: Coordinate) => {
|
||||
const toolState = this.manager.stateApi.getToolState();
|
||||
const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get();
|
||||
const colorPickerInnerRadius = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_RADIUS);
|
||||
const colorPickerOuterRadius = this.manager.stage.getScaledPixels(
|
||||
CanvasToolModule.COLOR_PICKER_RADIUS + CanvasToolModule.COLOR_PICKER_THICKNESS
|
||||
);
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
|
||||
this.konva.colorPicker.newColor.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
fill: rgbColorToString(colorUnderCursor),
|
||||
innerRadius: colorPickerInnerRadius,
|
||||
outerRadius: colorPickerOuterRadius,
|
||||
});
|
||||
this.konva.colorPicker.oldColor.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
fill: rgbColorToString(toolState.fill),
|
||||
innerRadius: colorPickerInnerRadius,
|
||||
outerRadius: colorPickerOuterRadius,
|
||||
});
|
||||
this.konva.colorPicker.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: colorPickerOuterRadius,
|
||||
outerRadius: colorPickerOuterRadius + onePixel,
|
||||
});
|
||||
this.konva.colorPicker.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: colorPickerOuterRadius + onePixel,
|
||||
outerRadius: colorPickerOuterRadius + twoPixels,
|
||||
});
|
||||
|
||||
const size = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SIZE);
|
||||
const space = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SPACE);
|
||||
const innerThickness = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_INNER_THICKNESS);
|
||||
const outerThickness = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_OUTER_THICKNESS);
|
||||
this.konva.colorPicker.crosshairNorthOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
|
||||
});
|
||||
this.konva.colorPicker.crosshairNorthInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
|
||||
});
|
||||
this.konva.colorPicker.crosshairEastOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
|
||||
});
|
||||
this.konva.colorPicker.crosshairEastInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
|
||||
});
|
||||
this.konva.colorPicker.crosshairSouthOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
|
||||
});
|
||||
this.konva.colorPicker.crosshairSouthInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
|
||||
});
|
||||
this.konva.colorPicker.crosshairWestOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
|
||||
});
|
||||
this.konva.colorPicker.crosshairWestInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
|
||||
});
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const stage = this.manager.stage;
|
||||
const renderedEntityCount = this.manager.stateApi.getRenderedEntityCount();
|
||||
const selectedEntity = this.manager.stateApi.getSelectedEntity();
|
||||
const cursorPos = this.manager.stateApi.$lastCursorPos.get();
|
||||
const tool = this.manager.stateApi.$tool.get();
|
||||
|
||||
const isDrawable =
|
||||
!!selectedEntity &&
|
||||
selectedEntity.state.isEnabled &&
|
||||
!selectedEntity.state.isLocked &&
|
||||
isDrawableEntity(selectedEntity.state);
|
||||
|
||||
this.syncCursorStyle();
|
||||
|
||||
stage.setIsDraggable(tool === 'view');
|
||||
|
||||
@@ -455,11 +305,136 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
|
||||
// No need to render the brush preview if the cursor position or color is missing
|
||||
if (cursorPos && tool === 'brush') {
|
||||
this.renderBrushTool(cursorPos);
|
||||
const brushPreviewFill = this.manager.stateApi.getBrushPreviewFill();
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.brush.width);
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
const radius = toolState.brush.width / 2;
|
||||
|
||||
// The circle is scaled
|
||||
this.konva.brush.fillCircle.setAttrs({
|
||||
x: alignedCursorPos.x,
|
||||
y: alignedCursorPos.y,
|
||||
radius,
|
||||
fill: rgbaColorToString(brushPreviewFill),
|
||||
});
|
||||
|
||||
// But the borders are in screen-pixels
|
||||
this.konva.brush.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius,
|
||||
outerRadius: radius + onePixel,
|
||||
});
|
||||
this.konva.brush.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
} else if (cursorPos && tool === 'eraser') {
|
||||
this.renderEraserTool(cursorPos);
|
||||
const alignedCursorPos = alignCoordForTool(cursorPos, toolState.eraser.width);
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
const radius = toolState.eraser.width / 2;
|
||||
|
||||
// The circle is scaled
|
||||
this.konva.eraser.fillCircle.setAttrs({
|
||||
x: alignedCursorPos.x,
|
||||
y: alignedCursorPos.y,
|
||||
radius,
|
||||
fill: 'white',
|
||||
});
|
||||
|
||||
// But the borders are in screen-pixels
|
||||
this.konva.eraser.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius,
|
||||
outerRadius: radius + onePixel,
|
||||
});
|
||||
this.konva.eraser.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: radius + onePixel,
|
||||
outerRadius: radius + twoPixels,
|
||||
});
|
||||
} else if (cursorPos && tool === 'colorPicker') {
|
||||
this.renderColorPicker(cursorPos);
|
||||
const colorUnderCursor = this.manager.stateApi.$colorUnderCursor.get();
|
||||
const colorPickerInnerRadius = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_RADIUS);
|
||||
const colorPickerOuterRadius = this.manager.stage.getScaledPixels(
|
||||
CanvasToolModule.COLOR_PICKER_RADIUS + CanvasToolModule.COLOR_PICKER_THICKNESS
|
||||
);
|
||||
const onePixel = this.manager.stage.getScaledPixels(1);
|
||||
const twoPixels = this.manager.stage.getScaledPixels(2);
|
||||
|
||||
this.konva.colorPicker.newColor.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
fill: rgbColorToString(colorUnderCursor),
|
||||
innerRadius: colorPickerInnerRadius,
|
||||
outerRadius: colorPickerOuterRadius,
|
||||
});
|
||||
this.konva.colorPicker.oldColor.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
fill: rgbColorToString(toolState.fill),
|
||||
innerRadius: colorPickerInnerRadius,
|
||||
outerRadius: colorPickerOuterRadius,
|
||||
});
|
||||
this.konva.colorPicker.innerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: colorPickerOuterRadius,
|
||||
outerRadius: colorPickerOuterRadius + onePixel,
|
||||
});
|
||||
this.konva.colorPicker.outerBorder.setAttrs({
|
||||
x: cursorPos.x,
|
||||
y: cursorPos.y,
|
||||
innerRadius: colorPickerOuterRadius + onePixel,
|
||||
outerRadius: colorPickerOuterRadius + twoPixels,
|
||||
});
|
||||
|
||||
const size = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SIZE);
|
||||
const space = this.manager.stage.getScaledPixels(CanvasToolModule.COLOR_PICKER_CROSSHAIR_SPACE);
|
||||
const innerThickness = this.manager.stage.getScaledPixels(
|
||||
CanvasToolModule.COLOR_PICKER_CROSSHAIR_INNER_THICKNESS
|
||||
);
|
||||
const outerThickness = this.manager.stage.getScaledPixels(
|
||||
CanvasToolModule.COLOR_PICKER_CROSSHAIR_OUTER_THICKNESS
|
||||
);
|
||||
this.konva.colorPicker.crosshairNorthOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
|
||||
});
|
||||
this.konva.colorPicker.crosshairNorthInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x, cursorPos.y - size, cursorPos.x, cursorPos.y - space],
|
||||
});
|
||||
this.konva.colorPicker.crosshairEastOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
|
||||
});
|
||||
this.konva.colorPicker.crosshairEastInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x + space, cursorPos.y, cursorPos.x + size, cursorPos.y],
|
||||
});
|
||||
this.konva.colorPicker.crosshairSouthOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
|
||||
});
|
||||
this.konva.colorPicker.crosshairSouthInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x, cursorPos.y + space, cursorPos.x, cursorPos.y + size],
|
||||
});
|
||||
this.konva.colorPicker.crosshairWestOuter.setAttrs({
|
||||
strokeWidth: outerThickness,
|
||||
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
|
||||
});
|
||||
this.konva.colorPicker.crosshairWestInner.setAttrs({
|
||||
strokeWidth: innerThickness,
|
||||
points: [cursorPos.x - space, cursorPos.y, cursorPos.x - size, cursorPos.y],
|
||||
});
|
||||
}
|
||||
|
||||
this.setToolVisibility(tool, isDrawable);
|
||||
@@ -889,10 +864,6 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
this.manager.stateApi.$spaceKey.set(true);
|
||||
this.manager.stateApi.$lastCursorPos.set(null);
|
||||
this.manager.stateApi.$lastMouseDownPos.set(null);
|
||||
} else if (e.key === 'Alt') {
|
||||
// Select the color picker on alt key down
|
||||
this.manager.stateApi.$toolBuffer.set(this.manager.stateApi.$tool.get());
|
||||
this.manager.stateApi.$tool.set('colorPicker');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -909,11 +880,6 @@ export class CanvasToolModule extends CanvasModuleABC {
|
||||
this.manager.stateApi.$tool.set(toolBuffer ?? 'move');
|
||||
this.manager.stateApi.$toolBuffer.set(null);
|
||||
this.manager.stateApi.$spaceKey.set(false);
|
||||
} else if (e.key === 'Alt') {
|
||||
// Revert the tool to the previous tool on alt key up
|
||||
const toolBuffer = this.manager.stateApi.$toolBuffer.get();
|
||||
this.manager.stateApi.$tool.set(toolBuffer ?? 'move');
|
||||
this.manager.stateApi.$toolBuffer.set(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { getPrefixedId, getRectUnion } from 'features/controlLayers/konva/util';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('util', () => {
|
||||
describe('getPrefixedId', () => {
|
||||
it('should return a prefixed id', () => {
|
||||
expect(getPrefixedId('foo').split(':')[0]).toBe('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRectUnion', () => {
|
||||
it('should return the union of rects (2 rects)', () => {
|
||||
const rect1 = { x: 0, y: 0, width: 10, height: 10 };
|
||||
const rect2 = { x: 5, y: 5, width: 10, height: 10 };
|
||||
const union = getRectUnion(rect1, rect2);
|
||||
expect(union).toEqual({ x: 0, y: 0, width: 15, height: 15 });
|
||||
});
|
||||
it('should return the union of rects (3 rects)', () => {
|
||||
const rect1 = { x: 0, y: 0, width: 10, height: 10 };
|
||||
const rect2 = { x: 5, y: 5, width: 10, height: 10 };
|
||||
const rect3 = { x: 10, y: 10, width: 10, height: 10 };
|
||||
const union = getRectUnion(rect1, rect2, rect3);
|
||||
expect(union).toEqual({ x: 0, y: 0, width: 20, height: 20 });
|
||||
});
|
||||
it('should return the union of rects (2 rects none from zero)', () => {
|
||||
const rect1 = { x: 5, y: 5, width: 10, height: 10 };
|
||||
const rect2 = { x: 10, y: 10, width: 10, height: 10 };
|
||||
const union = getRectUnion(rect1, rect2);
|
||||
expect(union).toEqual({ x: 5, y: 5, width: 15, height: 15 });
|
||||
});
|
||||
it('should return the union of rects (2 rects with negative x/y)', () => {
|
||||
const rect1 = { x: -5, y: -5, width: 10, height: 10 };
|
||||
const rect2 = { x: 0, y: 0, width: 10, height: 10 };
|
||||
const union = getRectUnion(rect1, rect2);
|
||||
expect(union).toEqual({ x: -5, y: -5, width: 15, height: 15 });
|
||||
});
|
||||
it('should return the union of the first rect if only one rect is provided', () => {
|
||||
const rect = { x: 0, y: 0, width: 10, height: 10 };
|
||||
const union = getRectUnion(rect);
|
||||
expect(union).toEqual(rect);
|
||||
});
|
||||
it('should fall back on an empty rect if no rects are provided', () => {
|
||||
const union = getRectUnion();
|
||||
expect(union).toEqual({ x: 0, y: 0, width: 0, height: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -302,13 +302,10 @@ export const konvaNodeToCanvas = (node: Konva.Node, bbox?: Rect): HTMLCanvasElem
|
||||
* @returns A Promise that resolves with Blob of the node cropped to the bounding box
|
||||
*/
|
||||
export const canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
reject('Failed to convert canvas to blob');
|
||||
} else {
|
||||
resolve(blob);
|
||||
}
|
||||
assert(blob, 'blob is null');
|
||||
resolve(blob);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -421,25 +418,19 @@ export function snapToNearest(value: number, candidateValues: number[], threshol
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the union of any number of rects.
|
||||
* @params rects The rects to union
|
||||
* Gets the union of two rects
|
||||
* @param rect1 The first rect
|
||||
* @param rect2 The second rect
|
||||
* @returns The union of the two rects
|
||||
*/
|
||||
export const getRectUnion = (...rects: Rect[]): Rect => {
|
||||
const firstRect = rects.shift();
|
||||
|
||||
if (!firstRect) {
|
||||
return getEmptyRect();
|
||||
}
|
||||
|
||||
const rect = rects.reduce<Rect>((acc, r) => {
|
||||
const x = Math.min(acc.x, r.x);
|
||||
const y = Math.min(acc.y, r.y);
|
||||
const width = Math.max(acc.x + acc.width, r.x + r.width) - x;
|
||||
const height = Math.max(acc.y + acc.height, r.y + r.height) - y;
|
||||
return { x, y, width, height };
|
||||
}, firstRect);
|
||||
|
||||
}, getEmptyRect());
|
||||
return rect;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
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 type { StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import type { SessionMode, StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
|
||||
export type CanvasSessionState = {
|
||||
sendToCanvas: boolean;
|
||||
mode: SessionMode;
|
||||
isStaging: boolean;
|
||||
stagedImages: StagingAreaImage[];
|
||||
selectedStagedImageIndex: number;
|
||||
};
|
||||
|
||||
const initialState: CanvasSessionState = {
|
||||
sendToCanvas: false,
|
||||
mode: 'generate',
|
||||
isStaging: false,
|
||||
stagedImages: [],
|
||||
selectedStagedImageIndex: 0,
|
||||
@@ -27,7 +27,6 @@ export const canvasSessionSlice = createSlice({
|
||||
},
|
||||
sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
|
||||
const { stagingAreaImage } = action.payload;
|
||||
state.isStaging = true;
|
||||
state.stagedImages.push(stagingAreaImage);
|
||||
state.selectedStagedImageIndex = state.stagedImages.length - 1;
|
||||
},
|
||||
@@ -51,8 +50,9 @@ export const canvasSessionSlice = createSlice({
|
||||
state.stagedImages = [];
|
||||
state.selectedStagedImageIndex = 0;
|
||||
},
|
||||
sessionSendToCanvasChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.sendToCanvas = action.payload;
|
||||
sessionModeChanged: (state, action: PayloadAction<{ mode: SessionMode }>) => {
|
||||
const { mode } = action.payload;
|
||||
state.mode = mode;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -64,7 +64,7 @@ export const {
|
||||
sessionStagingAreaReset,
|
||||
sessionNextStagedImageSelected,
|
||||
sessionPrevStagedImageSelected,
|
||||
sessionSendToCanvasChanged,
|
||||
sessionModeChanged,
|
||||
} = canvasSessionSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
@@ -85,7 +85,3 @@ export const sessionStagingAreaImageAccepted = createAction<{ index: number }>(
|
||||
export const selectCanvasSessionSlice = (s: RootState) => s.canvasSession;
|
||||
|
||||
export const selectIsStaging = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.isStaging);
|
||||
export const selectIsComposing = createSelector(
|
||||
selectCanvasSessionSlice,
|
||||
(canvasSession) => canvasSession.sendToCanvas
|
||||
);
|
||||
|
||||
@@ -35,14 +35,10 @@ export const canvasSettingsSlice = createSlice({
|
||||
settingsAutoSaveToggled: (state) => {
|
||||
state.autoSave = !state.autoSave;
|
||||
},
|
||||
settingsShowHUDToggled: (state) => {
|
||||
state.showHUD = !state.showHUD;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled, settingsShowHUDToggled } =
|
||||
canvasSettingsSlice.actions;
|
||||
export const { clipToBboxChanged, settingsAutoSaveToggled, settingsDynamicGridToggled } = canvasSettingsSlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const migrate = (state: any): any => {
|
||||
|
||||
@@ -123,14 +123,9 @@ export const canvasSlice = createSlice({
|
||||
rasterLayerAdded: {
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
overrides?: Partial<CanvasRasterLayerState>;
|
||||
isSelected?: boolean;
|
||||
deleteOthers?: boolean;
|
||||
}>
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }>
|
||||
) => {
|
||||
const { id, overrides, isSelected, deleteOthers } = action.payload;
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const entity: CanvasRasterLayerState = {
|
||||
id,
|
||||
name: null,
|
||||
@@ -142,25 +137,12 @@ export const canvasSlice = createSlice({
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
merge(entity, overrides);
|
||||
|
||||
if (deleteOthers) {
|
||||
state.rasterLayers.entities = [entity];
|
||||
} else {
|
||||
state.rasterLayers.entities.push(entity);
|
||||
}
|
||||
|
||||
state.rasterLayers.entities.push(entity);
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
}
|
||||
},
|
||||
prepare: (payload: {
|
||||
overrides?: Partial<CanvasRasterLayerState>;
|
||||
isSelected?: boolean;
|
||||
/**
|
||||
* asdf
|
||||
*/
|
||||
deleteOthers?: boolean;
|
||||
}) => ({
|
||||
prepare: (payload: { overrides?: Partial<CanvasRasterLayerState>; isSelected?: boolean }) => ({
|
||||
payload: { ...payload, id: getPrefixedId('raster_layer') },
|
||||
}),
|
||||
},
|
||||
@@ -621,14 +603,9 @@ export const canvasSlice = createSlice({
|
||||
inpaintMaskAdded: {
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
overrides?: Partial<CanvasInpaintMaskState>;
|
||||
isSelected?: boolean;
|
||||
deleteOthers?: boolean;
|
||||
}>
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }>
|
||||
) => {
|
||||
const { id, overrides, isSelected, deleteOthers } = action.payload;
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const entity: CanvasInpaintMaskState = {
|
||||
id,
|
||||
name: null,
|
||||
@@ -644,22 +621,12 @@ export const canvasSlice = createSlice({
|
||||
},
|
||||
};
|
||||
merge(entity, overrides);
|
||||
|
||||
if (deleteOthers) {
|
||||
state.inpaintMasks.entities = [entity];
|
||||
} else {
|
||||
state.inpaintMasks.entities.push(entity);
|
||||
}
|
||||
|
||||
state.inpaintMasks.entities.push(entity);
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
}
|
||||
},
|
||||
prepare: (payload?: {
|
||||
overrides?: Partial<CanvasInpaintMaskState>;
|
||||
isSelected?: boolean;
|
||||
deleteOthers?: boolean;
|
||||
}) => ({
|
||||
prepare: (payload?: { overrides?: Partial<CanvasInpaintMaskState>; isSelected?: boolean }) => ({
|
||||
payload: { ...payload, id: getPrefixedId('inpaint_mask') },
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -685,6 +685,8 @@ export type StagingAreaImage = {
|
||||
offsetY: number;
|
||||
};
|
||||
|
||||
export type SessionMode = 'generate' | 'compose';
|
||||
|
||||
export type CanvasState = {
|
||||
_version: 3;
|
||||
selectedEntityIdentifier: CanvasEntityIdentifier | null;
|
||||
|
||||
@@ -1,61 +1,55 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { IconSwitch } from 'common/components/IconSwitch';
|
||||
import { ButtonGroup, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
||||
|
||||
const TooltipEdit = memo(() => {
|
||||
export const ViewerToggleMenu = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('common.edit')}</Text>
|
||||
<Text fontWeight="normal">{t('common.editDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
TooltipEdit.displayName = 'TooltipEdit';
|
||||
|
||||
const TooltipView = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('common.view')}</Text>
|
||||
<Text fontWeight="normal">{t('common.viewDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
TooltipView.displayName = 'TooltipView';
|
||||
|
||||
export const ViewerToggle = memo(() => {
|
||||
const imageViewer = useImageViewer();
|
||||
useHotkeys('z', imageViewer.onToggle, [imageViewer]);
|
||||
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
|
||||
const onChange = useCallback(
|
||||
(isChecked: boolean) => {
|
||||
if (isChecked) {
|
||||
imageViewer.onClose();
|
||||
} else {
|
||||
imageViewer.onOpen();
|
||||
}
|
||||
},
|
||||
[imageViewer]
|
||||
);
|
||||
|
||||
return (
|
||||
<IconSwitch
|
||||
isChecked={!imageViewer.isOpen}
|
||||
onChange={onChange}
|
||||
iconUnchecked={<PiEyeBold />}
|
||||
tooltipUnchecked={<TooltipView />}
|
||||
iconChecked={<PiPencilBold />}
|
||||
tooltipChecked={<TooltipEdit />}
|
||||
ariaLabel="Toggle viewer"
|
||||
/>
|
||||
<Flex gap={4} alignItems="center" justifyContent="center">
|
||||
<ButtonGroup size="md">
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('common.viewing')}</Text>
|
||||
<Text fontWeight="normal">{t('common.viewingDesc')}</Text>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
icon={<PiEyeBold />}
|
||||
onClick={imageViewer.onOpen}
|
||||
variant={imageViewer.isOpen ? 'solid' : 'outline'}
|
||||
colorScheme={imageViewer.isOpen ? 'invokeBlue' : 'base'}
|
||||
aria-label={t('common.viewing')}
|
||||
w={12}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('common.editing')}</Text>
|
||||
<Text fontWeight="normal">{t('common.editingDesc')}</Text>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
icon={<PiPencilBold />}
|
||||
onClick={imageViewer.onClose}
|
||||
variant={!imageViewer.isOpen ? 'solid' : 'outline'}
|
||||
colorScheme={!imageViewer.isOpen ? 'invokeBlue' : 'base'}
|
||||
aria-label={t('common.editing')}
|
||||
w={12}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToggle.displayName = 'ViewerToggle';
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import { ViewerToggle } from './ViewerToggleMenu';
|
||||
import { ViewerToggleMenu } from './ViewerToggleMenu';
|
||||
|
||||
const selectShowToggle = createSelector(selectActiveTab, (tab) => {
|
||||
if (tab === 'upscaling' || tab === 'workflows') {
|
||||
@@ -31,7 +31,7 @@ export const ViewerToolbar = memo(() => {
|
||||
</Flex>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
<Flex gap={2} marginInlineStart="auto">
|
||||
{showToggle && <ViewerToggle />}
|
||||
{showToggle && <ViewerToggleMenu />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -179,12 +179,12 @@ const ModelList = () => {
|
||||
{/* T5 Encoders List */}
|
||||
{isLoadingT5EncoderModels && <FetchingModelsLoader loadingMessage="Loading T5 Encoder Models..." />}
|
||||
{!isLoadingT5EncoderModels && filteredT5EncoderModels.length > 0 && (
|
||||
<ModelListWrapper title={t('modelManager.t5Encoder')} modelList={filteredT5EncoderModels} key="t5-encoder" />
|
||||
<ModelListWrapper title="T5 Encoder" modelList={filteredT5EncoderModels} key="t5-encoder" />
|
||||
)}
|
||||
{/* Clip Embed List */}
|
||||
{isLoadingClipEmbedModels && <FetchingModelsLoader loadingMessage="Loading Clip Embed Models..." />}
|
||||
{!isLoadingClipEmbedModels && filteredClipEmbedModels.length > 0 && (
|
||||
<ModelListWrapper title={t('modelManager.clipEmbed')} modelList={filteredClipEmbedModels} key="clip-embed" />
|
||||
<ModelListWrapper title="Clip Embed" modelList={filteredClipEmbedModels} key="clip-embed" />
|
||||
)}
|
||||
{/* Spandrel Image to Image List */}
|
||||
{isLoadingSpandrelImageToImageModels && (
|
||||
@@ -192,7 +192,7 @@ const ModelList = () => {
|
||||
)}
|
||||
{!isLoadingSpandrelImageToImageModels && filteredSpandrelImageToImageModels.length > 0 && (
|
||||
<ModelListWrapper
|
||||
title={t('modelManager.spandrelImageToImage')}
|
||||
title="Image-to-Image"
|
||||
modelList={filteredSpandrelImageToImageModels}
|
||||
key="spandrel-image-to-image"
|
||||
/>
|
||||
|
||||
@@ -19,10 +19,11 @@ export const ModelTypeFilter = memo(() => {
|
||||
controlnet: 'ControlNet',
|
||||
vae: 'VAE',
|
||||
t2i_adapter: t('common.t2iAdapter'),
|
||||
t5_encoder: t('modelManager.t5Encoder'),
|
||||
clip_embed: t('modelManager.clipEmbed'),
|
||||
t5_encoder: 'T5Encoder',
|
||||
clip_embed: 'Clip Embed',
|
||||
ip_adapter: t('common.ipAdapter'),
|
||||
spandrel_image_to_image: t('modelManager.spandrelImageToImage'),
|
||||
clip_vision: 'Clip Vision',
|
||||
spandrel_image_to_image: 'Image-to-Image',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Flex } from '@invoke-ai/ui-library';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
|
||||
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
||||
import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings';
|
||||
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
|
||||
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
|
||||
import { memo } from 'react';
|
||||
@@ -40,7 +39,6 @@ const NodeEditor = () => {
|
||||
<LoadWorkflowFromGraphModal />
|
||||
</>
|
||||
)}
|
||||
<WorkflowEditorSettings />
|
||||
{isLoading && <IAINoContentFallback label={t('nodes.loadingNodes')} icon={MdDeviceHub} />}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Text,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
@@ -33,7 +33,6 @@ import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
|
||||
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memoize } from 'lodash-es';
|
||||
import { computed } from 'nanostores';
|
||||
import type { ChangeEvent } from 'react';
|
||||
@@ -163,13 +162,13 @@ const cmdkRootSx: SystemStyleObject = {
|
||||
export const AddNodeCmdk = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const addNodeCmdk = useAddNodeCmdk();
|
||||
const addNodeCmdkIsOpen = useStore(addNodeCmdk.$boolean);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const addNode = useAddNode();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const throttledSearchTerm = useThrottle(searchTerm, 100);
|
||||
|
||||
useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { enabled: tab === 'workflows', preventDefault: true }, [tab]);
|
||||
useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { preventDefault: true });
|
||||
|
||||
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(e.target.value);
|
||||
@@ -191,7 +190,7 @@ export const AddNodeCmdk = memo(() => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={addNodeCmdk.isTrue}
|
||||
isOpen={addNodeCmdkIsOpen}
|
||||
onClose={onClose}
|
||||
useInert={false}
|
||||
initialFocusRef={inputRef}
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
isBoardFieldInputTemplate,
|
||||
isBooleanFieldInputInstance,
|
||||
isBooleanFieldInputTemplate,
|
||||
isCLIPEmbedModelFieldInputInstance,
|
||||
isCLIPEmbedModelFieldInputTemplate,
|
||||
isColorFieldInputInstance,
|
||||
isColorFieldInputTemplate,
|
||||
isControlNetModelFieldInputInstance,
|
||||
@@ -18,8 +16,6 @@ import {
|
||||
isFloatFieldInputTemplate,
|
||||
isFluxMainModelFieldInputInstance,
|
||||
isFluxMainModelFieldInputTemplate,
|
||||
isFluxVAEModelFieldInputInstance,
|
||||
isFluxVAEModelFieldInputTemplate,
|
||||
isImageFieldInputInstance,
|
||||
isImageFieldInputTemplate,
|
||||
isIntegerFieldInputInstance,
|
||||
@@ -53,12 +49,10 @@ import { memo } from 'react';
|
||||
|
||||
import BoardFieldInputComponent from './inputs/BoardFieldInputComponent';
|
||||
import BooleanFieldInputComponent from './inputs/BooleanFieldInputComponent';
|
||||
import CLIPEmbedModelFieldInputComponent from './inputs/CLIPEmbedModelFieldInputComponent';
|
||||
import ColorFieldInputComponent from './inputs/ColorFieldInputComponent';
|
||||
import ControlNetModelFieldInputComponent from './inputs/ControlNetModelFieldInputComponent';
|
||||
import EnumFieldInputComponent from './inputs/EnumFieldInputComponent';
|
||||
import FluxMainModelFieldInputComponent from './inputs/FluxMainModelFieldInputComponent';
|
||||
import FluxVAEModelFieldInputComponent from './inputs/FluxVAEModelFieldInputComponent';
|
||||
import ImageFieldInputComponent from './inputs/ImageFieldInputComponent';
|
||||
import IPAdapterModelFieldInputComponent from './inputs/IPAdapterModelFieldInputComponent';
|
||||
import LoRAModelFieldInputComponent from './inputs/LoRAModelFieldInputComponent';
|
||||
@@ -128,13 +122,6 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => {
|
||||
if (isT5EncoderModelFieldInputInstance(fieldInstance) && isT5EncoderModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <T5EncoderModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
}
|
||||
if (isCLIPEmbedModelFieldInputInstance(fieldInstance) && isCLIPEmbedModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <CLIPEmbedModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
}
|
||||
|
||||
if (isFluxVAEModelFieldInputInstance(fieldInstance) && isFluxVAEModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <FluxVAEModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
}
|
||||
|
||||
if (isLoRAModelFieldInputInstance(fieldInstance) && isLoRAModelFieldInputTemplate(fieldTemplate)) {
|
||||
return <LoRAModelFieldInputComponent nodeId={nodeId} field={fieldInstance} fieldTemplate={fieldTemplate} />;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
|
||||
import { fieldCLIPEmbedValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { CLIPEmbedModelFieldInputInstance, CLIPEmbedModelFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useClipEmbedModels } from 'services/api/hooks/modelsByType';
|
||||
import type { ClipEmbedModelConfig } from 'services/api/types';
|
||||
|
||||
import type { FieldComponentProps } from './types';
|
||||
|
||||
type Props = FieldComponentProps<CLIPEmbedModelFieldInputInstance, CLIPEmbedModelFieldInputTemplate>;
|
||||
|
||||
const CLIPEmbedModelFieldInputComponent = (props: Props) => {
|
||||
const { nodeId, field } = props;
|
||||
const { t } = useTranslation();
|
||||
const disabledTabs = useAppSelector((s) => s.config.disabledTabs);
|
||||
const dispatch = useAppDispatch();
|
||||
const [modelConfigs, { isLoading }] = useClipEmbedModels();
|
||||
const _onChange = useCallback(
|
||||
(value: ClipEmbedModelConfig | null) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
fieldCLIPEmbedValueChanged({
|
||||
nodeId,
|
||||
fieldName: field.name,
|
||||
value,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, field.name, nodeId]
|
||||
);
|
||||
const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
|
||||
modelConfigs,
|
||||
onChange: _onChange,
|
||||
isLoading,
|
||||
selectedModel: field.value,
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex w="full" alignItems="center" gap={2}>
|
||||
<Tooltip label={!disabledTabs.includes('models') && t('modelManager.starterModelsInModelManager')}>
|
||||
<FormControl className="nowheel nodrag" isDisabled={!options.length} isInvalid={!value}>
|
||||
<Combobox
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
/>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CLIPEmbedModelFieldInputComponent);
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
|
||||
import { fieldFluxVAEModelValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { FluxVAEModelFieldInputInstance, FluxVAEModelFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFluxVAEModels } from 'services/api/hooks/modelsByType';
|
||||
import type { VAEModelConfig } from 'services/api/types';
|
||||
|
||||
import type { FieldComponentProps } from './types';
|
||||
|
||||
type Props = FieldComponentProps<FluxVAEModelFieldInputInstance, FluxVAEModelFieldInputTemplate>;
|
||||
|
||||
const FluxVAEModelFieldInputComponent = (props: Props) => {
|
||||
const { nodeId, field } = props;
|
||||
const { t } = useTranslation();
|
||||
const disabledTabs = useAppSelector((s) => s.config.disabledTabs);
|
||||
const dispatch = useAppDispatch();
|
||||
const [modelConfigs, { isLoading }] = useFluxVAEModels();
|
||||
const _onChange = useCallback(
|
||||
(value: VAEModelConfig | null) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
fieldFluxVAEModelValueChanged({
|
||||
nodeId,
|
||||
fieldName: field.name,
|
||||
value,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, field.name, nodeId]
|
||||
);
|
||||
const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({
|
||||
modelConfigs,
|
||||
onChange: _onChange,
|
||||
isLoading,
|
||||
selectedModel: field.value,
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex w="full" alignItems="center" gap={2}>
|
||||
<Tooltip label={!disabledTabs.includes('models') && t('modelManager.starterModelsInModelManager')}>
|
||||
<FormControl className="nowheel nodrag" isDisabled={!options.length} isInvalid={!value}>
|
||||
<Combobox
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
/>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FluxVAEModelFieldInputComponent);
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Switch,
|
||||
useDisclosure,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopRightPanel/ReloadSchemaButton';
|
||||
import {
|
||||
selectionModeChanged,
|
||||
@@ -32,17 +32,20 @@ import {
|
||||
shouldSnapToGridChanged,
|
||||
shouldValidateGraphChanged,
|
||||
} from 'features/nodes/store/workflowSettingsSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type { ChangeEvent, ReactNode } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectionMode } from 'reactflow';
|
||||
|
||||
const formLabelProps: FormLabelProps = { flexGrow: 1 };
|
||||
export const [useWorkflowEditorSettingsModal] = buildUseBoolean(false);
|
||||
|
||||
const WorkflowEditorSettings = () => {
|
||||
type Props = {
|
||||
children: (props: { onOpen: () => void }) => ReactNode;
|
||||
};
|
||||
|
||||
const WorkflowEditorSettings = ({ children }: Props) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const modal = useWorkflowEditorSettingsModal();
|
||||
|
||||
const shouldSnapToGrid = useAppSelector(selectShouldSnapToGrid);
|
||||
const selectionMode = useAppSelector(selectSelectionMode);
|
||||
@@ -96,72 +99,76 @@ const WorkflowEditorSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal isOpen={modal.isTrue} onClose={modal.setFalse} size="2xl" isCentered useInert={false}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Flex flexDirection="column" gap={4} py={4}>
|
||||
<Heading size="sm">{t('parameters.general')}</Heading>
|
||||
<FormControlGroup orientation="vertical" formLabelProps={formLabelProps}>
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.animatedEdges')}</FormLabel>
|
||||
<Switch onChange={handleChangeShouldAnimate} isChecked={shouldAnimateEdges} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.animatedEdgesHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.snapToGrid')}</FormLabel>
|
||||
<Switch isChecked={shouldSnapToGrid} onChange={handleChangeShouldSnap} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.snapToGridHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.colorCodeEdges')}</FormLabel>
|
||||
<Switch isChecked={shouldColorEdges} onChange={handleChangeShouldColor} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.colorCodeEdgesHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.fullyContainNodes')}</FormLabel>
|
||||
<Switch isChecked={selectionMode === SelectionMode.Full} onChange={handleChangeSelectionMode} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.fullyContainNodesHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.showEdgeLabels')}</FormLabel>
|
||||
<Switch isChecked={shouldShowEdgeLabels} onChange={handleChangeShouldShowEdgeLabels} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.showEdgeLabelsHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<Heading size="sm" pt={4}>
|
||||
{t('common.advanced')}
|
||||
</Heading>
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.validateConnections')}</FormLabel>
|
||||
<Switch isChecked={shouldValidateGraph} onChange={handleChangeShouldValidate} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.validateConnectionsHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
</FormControlGroup>
|
||||
<ReloadNodeTemplatesButton />
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<>
|
||||
{children({ onOpen })}
|
||||
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered useInert={false}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Flex flexDirection="column" gap={4} py={4}>
|
||||
<Heading size="sm">{t('parameters.general')}</Heading>
|
||||
<FormControlGroup orientation="vertical" formLabelProps={formLabelProps}>
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.animatedEdges')}</FormLabel>
|
||||
<Switch onChange={handleChangeShouldAnimate} isChecked={shouldAnimateEdges} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.animatedEdgesHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.snapToGrid')}</FormLabel>
|
||||
<Switch isChecked={shouldSnapToGrid} onChange={handleChangeShouldSnap} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.snapToGridHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.colorCodeEdges')}</FormLabel>
|
||||
<Switch isChecked={shouldColorEdges} onChange={handleChangeShouldColor} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.colorCodeEdgesHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.fullyContainNodes')}</FormLabel>
|
||||
<Switch isChecked={selectionMode === SelectionMode.Full} onChange={handleChangeSelectionMode} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.fullyContainNodesHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.showEdgeLabels')}</FormLabel>
|
||||
<Switch isChecked={shouldShowEdgeLabels} onChange={handleChangeShouldShowEdgeLabels} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.showEdgeLabelsHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
<Heading size="sm" pt={4}>
|
||||
{t('common.advanced')}
|
||||
</Heading>
|
||||
<FormControl>
|
||||
<Flex w="full">
|
||||
<FormLabel>{t('nodes.validateConnections')}</FormLabel>
|
||||
<Switch isChecked={shouldValidateGraph} onChange={handleChangeShouldValidate} />
|
||||
</Flex>
|
||||
<FormHelperText>{t('nodes.validateConnectionsHelp')}</FormHelperText>
|
||||
</FormControl>
|
||||
<Divider />
|
||||
</FormControlGroup>
|
||||
<ReloadNodeTemplatesButton />
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'reactflow/dist/style.css';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
import QueueControls from 'features/queue/components/QueueControls';
|
||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||
@@ -33,6 +34,7 @@ const NodeEditorPanelGroup = () => {
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
<QueueControls />
|
||||
<Flex w="full" justifyContent="space-between" alignItems="center" gap="4" padding={1}>
|
||||
<Flex justifyContent="space-between" alignItems="center" gap="4">
|
||||
<WorkflowLibraryButton />
|
||||
|
||||
@@ -2,12 +2,12 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import {
|
||||
$addNodeCmdk,
|
||||
$didUpdateEdge,
|
||||
$edgePendingUpdate,
|
||||
$pendingConnection,
|
||||
$templates,
|
||||
edgesChanged,
|
||||
useAddNodeCmdk,
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodes, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
|
||||
@@ -21,7 +21,6 @@ export const useConnection = () => {
|
||||
const store = useAppStore();
|
||||
const templates = useStore($templates);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const addNodeCmdk = useAddNodeCmdk();
|
||||
|
||||
const onConnectStart = useCallback<OnConnectStart>(
|
||||
(event, { nodeId, handleId, handleType }) => {
|
||||
@@ -108,9 +107,9 @@ export const useConnection = () => {
|
||||
$pendingConnection.set(null);
|
||||
} else {
|
||||
// The mouse is not over a node - we should open the add node popover
|
||||
addNodeCmdk.setTrue();
|
||||
$addNodeCmdk.set(true);
|
||||
}
|
||||
}, [addNodeCmdk, store, templates, updateNodeInternals]);
|
||||
}, [store, templates, updateNodeInternals]);
|
||||
|
||||
const api = useMemo(() => ({ onConnectStart, onConnect, onConnectEnd }), [onConnectStart, onConnect, onConnectEnd]);
|
||||
return api;
|
||||
|
||||
@@ -7,13 +7,11 @@ import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
||||
import type {
|
||||
BoardFieldValue,
|
||||
BooleanFieldValue,
|
||||
CLIPEmbedModelFieldValue,
|
||||
ColorFieldValue,
|
||||
ControlNetModelFieldValue,
|
||||
EnumFieldValue,
|
||||
FieldValue,
|
||||
FloatFieldValue,
|
||||
FluxVAEModelFieldValue,
|
||||
ImageFieldValue,
|
||||
IntegerFieldValue,
|
||||
IPAdapterModelFieldValue,
|
||||
@@ -32,12 +30,10 @@ import type {
|
||||
import {
|
||||
zBoardFieldValue,
|
||||
zBooleanFieldValue,
|
||||
zCLIPEmbedModelFieldValue,
|
||||
zColorFieldValue,
|
||||
zControlNetModelFieldValue,
|
||||
zEnumFieldValue,
|
||||
zFloatFieldValue,
|
||||
zFluxVAEModelFieldValue,
|
||||
zImageFieldValue,
|
||||
zIntegerFieldValue,
|
||||
zIPAdapterModelFieldValue,
|
||||
@@ -351,12 +347,6 @@ export const nodesSlice = createSlice({
|
||||
fieldT5EncoderValueChanged: (state, action: FieldValueAction<T5EncoderModelFieldValue>) => {
|
||||
fieldValueReducer(state, action, zT5EncoderModelFieldValue);
|
||||
},
|
||||
fieldCLIPEmbedValueChanged: (state, action: FieldValueAction<CLIPEmbedModelFieldValue>) => {
|
||||
fieldValueReducer(state, action, zCLIPEmbedModelFieldValue);
|
||||
},
|
||||
fieldFluxVAEModelValueChanged: (state, action: FieldValueAction<FluxVAEModelFieldValue>) => {
|
||||
fieldValueReducer(state, action, zFluxVAEModelFieldValue);
|
||||
},
|
||||
fieldEnumModelValueChanged: (state, action: FieldValueAction<EnumFieldValue>) => {
|
||||
fieldValueReducer(state, action, zEnumFieldValue);
|
||||
},
|
||||
@@ -419,8 +409,6 @@ export const {
|
||||
fieldStringValueChanged,
|
||||
fieldVaeModelValueChanged,
|
||||
fieldT5EncoderValueChanged,
|
||||
fieldCLIPEmbedValueChanged,
|
||||
fieldFluxVAEModelValueChanged,
|
||||
nodeEditorReset,
|
||||
nodeIsIntermediateChanged,
|
||||
nodeIsOpenChanged,
|
||||
@@ -444,7 +432,8 @@ export const $didUpdateEdge = atom(false);
|
||||
export const $lastEdgeUpdateMouseEvent = atom<MouseEvent | null>(null);
|
||||
|
||||
export const $viewport = atom<Viewport>({ x: 0, y: 0, zoom: 1 });
|
||||
export const [useAddNodeCmdk, $addNodeCmdk] = buildUseBoolean(false);
|
||||
export const $addNodeCmdk = atom(false);
|
||||
export const useAddNodeCmdk = buildUseBoolean($addNodeCmdk);
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const migrateNodesState = (state: any): any => {
|
||||
@@ -525,8 +514,6 @@ export const isAnyNodeOrEdgeMutation = isAnyOf(
|
||||
fieldStringValueChanged,
|
||||
fieldVaeModelValueChanged,
|
||||
fieldT5EncoderValueChanged,
|
||||
fieldCLIPEmbedValueChanged,
|
||||
fieldFluxVAEModelValueChanged,
|
||||
nodesChanged,
|
||||
nodeIsIntermediateChanged,
|
||||
nodeIsOpenChanged,
|
||||
|
||||
@@ -151,14 +151,6 @@ const zT5EncoderModelFieldType = zFieldTypeBase.extend({
|
||||
name: z.literal('T5EncoderModelField'),
|
||||
originalType: zStatelessFieldType.optional(),
|
||||
});
|
||||
const zCLIPEmbedModelFieldType = zFieldTypeBase.extend({
|
||||
name: z.literal('CLIPEmbedModelField'),
|
||||
originalType: zStatelessFieldType.optional(),
|
||||
});
|
||||
const zFluxVAEModelFieldType = zFieldTypeBase.extend({
|
||||
name: z.literal('FluxVAEModelField'),
|
||||
originalType: zStatelessFieldType.optional(),
|
||||
});
|
||||
const zSchedulerFieldType = zFieldTypeBase.extend({
|
||||
name: z.literal('SchedulerField'),
|
||||
originalType: zStatelessFieldType.optional(),
|
||||
@@ -183,8 +175,6 @@ const zStatefulFieldType = z.union([
|
||||
zT2IAdapterModelFieldType,
|
||||
zSpandrelImageToImageModelFieldType,
|
||||
zT5EncoderModelFieldType,
|
||||
zCLIPEmbedModelFieldType,
|
||||
zFluxVAEModelFieldType,
|
||||
zColorFieldType,
|
||||
zSchedulerFieldType,
|
||||
]);
|
||||
@@ -677,53 +667,7 @@ export const isT5EncoderModelFieldInputInstance = (val: unknown): val is T5Encod
|
||||
export const isT5EncoderModelFieldInputTemplate = (val: unknown): val is T5EncoderModelFieldInputTemplate =>
|
||||
zT5EncoderModelFieldInputTemplate.safeParse(val).success;
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region FluxVAEModelField
|
||||
|
||||
export const zFluxVAEModelFieldValue = zModelIdentifierField.optional();
|
||||
const zFluxVAEModelFieldInputInstance = zFieldInputInstanceBase.extend({
|
||||
value: zFluxVAEModelFieldValue,
|
||||
});
|
||||
const zFluxVAEModelFieldInputTemplate = zFieldInputTemplateBase.extend({
|
||||
type: zFluxVAEModelFieldType,
|
||||
originalType: zFieldType.optional(),
|
||||
default: zFluxVAEModelFieldValue,
|
||||
});
|
||||
|
||||
export type FluxVAEModelFieldValue = z.infer<typeof zFluxVAEModelFieldValue>;
|
||||
|
||||
export type FluxVAEModelFieldInputInstance = z.infer<typeof zFluxVAEModelFieldInputInstance>;
|
||||
export type FluxVAEModelFieldInputTemplate = z.infer<typeof zFluxVAEModelFieldInputTemplate>;
|
||||
export const isFluxVAEModelFieldInputInstance = (val: unknown): val is FluxVAEModelFieldInputInstance =>
|
||||
zFluxVAEModelFieldInputInstance.safeParse(val).success;
|
||||
export const isFluxVAEModelFieldInputTemplate = (val: unknown): val is FluxVAEModelFieldInputTemplate =>
|
||||
zFluxVAEModelFieldInputTemplate.safeParse(val).success;
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region CLIPEmbedModelField
|
||||
|
||||
export const zCLIPEmbedModelFieldValue = zModelIdentifierField.optional();
|
||||
const zCLIPEmbedModelFieldInputInstance = zFieldInputInstanceBase.extend({
|
||||
value: zCLIPEmbedModelFieldValue,
|
||||
});
|
||||
const zCLIPEmbedModelFieldInputTemplate = zFieldInputTemplateBase.extend({
|
||||
type: zCLIPEmbedModelFieldType,
|
||||
originalType: zFieldType.optional(),
|
||||
default: zCLIPEmbedModelFieldValue,
|
||||
});
|
||||
|
||||
export type CLIPEmbedModelFieldValue = z.infer<typeof zCLIPEmbedModelFieldValue>;
|
||||
|
||||
export type CLIPEmbedModelFieldInputInstance = z.infer<typeof zCLIPEmbedModelFieldInputInstance>;
|
||||
export type CLIPEmbedModelFieldInputTemplate = z.infer<typeof zCLIPEmbedModelFieldInputTemplate>;
|
||||
export const isCLIPEmbedModelFieldInputInstance = (val: unknown): val is CLIPEmbedModelFieldInputInstance =>
|
||||
zCLIPEmbedModelFieldInputInstance.safeParse(val).success;
|
||||
export const isCLIPEmbedModelFieldInputTemplate = (val: unknown): val is CLIPEmbedModelFieldInputTemplate =>
|
||||
zCLIPEmbedModelFieldInputTemplate.safeParse(val).success;
|
||||
|
||||
// #endregion
|
||||
// #endregio
|
||||
|
||||
// #region SchedulerField
|
||||
|
||||
@@ -814,8 +758,6 @@ export const zStatefulFieldValue = z.union([
|
||||
zT2IAdapterModelFieldValue,
|
||||
zSpandrelImageToImageModelFieldValue,
|
||||
zT5EncoderModelFieldValue,
|
||||
zFluxVAEModelFieldValue,
|
||||
zCLIPEmbedModelFieldValue,
|
||||
zColorFieldValue,
|
||||
zSchedulerFieldValue,
|
||||
]);
|
||||
@@ -846,8 +788,6 @@ const zStatefulFieldInputInstance = z.union([
|
||||
zT2IAdapterModelFieldInputInstance,
|
||||
zSpandrelImageToImageModelFieldInputInstance,
|
||||
zT5EncoderModelFieldInputInstance,
|
||||
zFluxVAEModelFieldInputInstance,
|
||||
zCLIPEmbedModelFieldInputInstance,
|
||||
zColorFieldInputInstance,
|
||||
zSchedulerFieldInputInstance,
|
||||
]);
|
||||
@@ -879,8 +819,6 @@ const zStatefulFieldInputTemplate = z.union([
|
||||
zT2IAdapterModelFieldInputTemplate,
|
||||
zSpandrelImageToImageModelFieldInputTemplate,
|
||||
zT5EncoderModelFieldInputTemplate,
|
||||
zFluxVAEModelFieldInputTemplate,
|
||||
zCLIPEmbedModelFieldInputTemplate,
|
||||
zColorFieldInputTemplate,
|
||||
zSchedulerFieldInputTemplate,
|
||||
zStatelessFieldInputTemplate,
|
||||
|
||||
@@ -10,9 +10,7 @@ export const prepareLinearUIBatch = (
|
||||
g: Graph,
|
||||
prepend: boolean,
|
||||
noise: Invocation<'noise'>,
|
||||
posCond: Invocation<'compel' | 'sdxl_compel_prompt'>,
|
||||
origin: 'generation' | 'workflows' | 'upscaling',
|
||||
destination: 'canvas' | 'gallery'
|
||||
posCond: Invocation<'compel' | 'sdxl_compel_prompt'>
|
||||
): BatchConfig => {
|
||||
const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.params;
|
||||
const { prompts, seedBehaviour } = state.dynamicPrompts;
|
||||
@@ -105,8 +103,7 @@ export const prepareLinearUIBatch = (
|
||||
graph: g.getGraph(),
|
||||
runs: 1,
|
||||
data,
|
||||
origin,
|
||||
destination,
|
||||
origin: 'canvas',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export const addInpaint = async (
|
||||
const canvas = selectCanvasSlice(state);
|
||||
|
||||
const { bbox } = canvas;
|
||||
const { sendToCanvas: isComposing } = canvasSession;
|
||||
const { mode } = canvasSession;
|
||||
|
||||
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
||||
@@ -99,7 +99,7 @@ export const addInpaint = async (
|
||||
g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
||||
g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
||||
|
||||
if (!isComposing) {
|
||||
if (mode === 'generate') {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export const addInpaint = async (
|
||||
|
||||
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
||||
|
||||
if (!isComposing) {
|
||||
if (mode === 'generate') {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export const addOutpaint = async (
|
||||
const canvas = selectCanvasSlice(state);
|
||||
|
||||
const { bbox } = canvas;
|
||||
const { sendToCanvas: isComposing } = canvasSession;
|
||||
const { mode } = canvasSession;
|
||||
|
||||
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
||||
@@ -123,7 +123,7 @@ export const addOutpaint = async (
|
||||
g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
||||
g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
||||
|
||||
if (!isComposing) {
|
||||
if (mode === 'generate') {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ export const addOutpaint = async (
|
||||
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
|
||||
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
||||
|
||||
if (!isComposing) {
|
||||
if (mode === 'generate') {
|
||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||
}
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ export const buildSD1Graph = async (
|
||||
canvasOutput = addWatermarker(g, canvasOutput);
|
||||
}
|
||||
|
||||
const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave;
|
||||
const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave;
|
||||
|
||||
g.updateNode(canvasOutput, {
|
||||
id: getPrefixedId('canvas_output'),
|
||||
|
||||
@@ -285,7 +285,7 @@ export const buildSDXLGraph = async (
|
||||
canvasOutput = addWatermarker(g, canvasOutput);
|
||||
}
|
||||
|
||||
const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave;
|
||||
const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave;
|
||||
|
||||
g.updateNode(canvasOutput, {
|
||||
id: getPrefixedId('canvas_output'),
|
||||
|
||||
@@ -23,8 +23,6 @@ const FIELD_VALUE_FALLBACK_MAP: Record<StatefulFieldType['name'], FieldValue> =
|
||||
VAEModelField: undefined,
|
||||
ControlNetModelField: undefined,
|
||||
T5EncoderModelField: undefined,
|
||||
FluxVAEModelField: undefined,
|
||||
CLIPEmbedModelField: undefined,
|
||||
};
|
||||
|
||||
export const buildFieldInputInstance = (id: string, template: FieldInputTemplate): FieldInputInstance => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { FieldParseError } from 'features/nodes/types/error';
|
||||
import type {
|
||||
BoardFieldInputTemplate,
|
||||
BooleanFieldInputTemplate,
|
||||
CLIPEmbedModelFieldInputTemplate,
|
||||
ColorFieldInputTemplate,
|
||||
ControlNetModelFieldInputTemplate,
|
||||
EnumFieldInputTemplate,
|
||||
@@ -10,7 +9,6 @@ import type {
|
||||
FieldType,
|
||||
FloatFieldInputTemplate,
|
||||
FluxMainModelFieldInputTemplate,
|
||||
FluxVAEModelFieldInputTemplate,
|
||||
ImageFieldInputTemplate,
|
||||
IntegerFieldInputTemplate,
|
||||
IPAdapterModelFieldInputTemplate,
|
||||
@@ -240,34 +238,6 @@ const buildT5EncoderModelFieldInputTemplate: FieldInputTemplateBuilder<T5Encoder
|
||||
return template;
|
||||
};
|
||||
|
||||
const buildCLIPEmbedModelFieldInputTemplate: FieldInputTemplateBuilder<CLIPEmbedModelFieldInputTemplate> = ({
|
||||
schemaObject,
|
||||
baseField,
|
||||
fieldType,
|
||||
}) => {
|
||||
const template: CLIPEmbedModelFieldInputTemplate = {
|
||||
...baseField,
|
||||
type: fieldType,
|
||||
default: schemaObject.default ?? undefined,
|
||||
};
|
||||
|
||||
return template;
|
||||
};
|
||||
|
||||
const buildFluxVAEModelFieldInputTemplate: FieldInputTemplateBuilder<FluxVAEModelFieldInputTemplate> = ({
|
||||
schemaObject,
|
||||
baseField,
|
||||
fieldType,
|
||||
}) => {
|
||||
const template: FluxVAEModelFieldInputTemplate = {
|
||||
...baseField,
|
||||
type: fieldType,
|
||||
default: schemaObject.default ?? undefined,
|
||||
};
|
||||
|
||||
return template;
|
||||
};
|
||||
|
||||
const buildLoRAModelFieldInputTemplate: FieldInputTemplateBuilder<LoRAModelFieldInputTemplate> = ({
|
||||
schemaObject,
|
||||
baseField,
|
||||
@@ -453,8 +423,6 @@ export const TEMPLATE_BUILDER_MAP: Record<StatefulFieldType['name'], FieldInputT
|
||||
SpandrelImageToImageModelField: buildSpandrelImageToImageModelFieldInputTemplate,
|
||||
VAEModelField: buildVAEModelFieldInputTemplate,
|
||||
T5EncoderModelField: buildT5EncoderModelFieldInputTemplate,
|
||||
CLIPEmbedModelField: buildCLIPEmbedModelFieldInputTemplate,
|
||||
FluxVAEModelField: buildFluxVAEModelFieldInputTemplate,
|
||||
} as const;
|
||||
|
||||
export const buildFieldInputTemplate = (
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import type { ButtonProps } from '@invoke-ai/ui-library';
|
||||
import { Button } from '@invoke-ai/ui-library';
|
||||
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleFill } from 'react-icons/pi';
|
||||
|
||||
import { useClearQueue } from './ClearQueueConfirmationAlertDialog';
|
||||
|
||||
type Props = ButtonProps;
|
||||
|
||||
const ClearQueueButton = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const clearQueue = useClearQueue();
|
||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
||||
const { isLoading, isDisabled } = useClearQueue();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
isDisabled={clearQueue.isDisabled}
|
||||
isLoading={clearQueue.isLoading}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
tooltip={t('queue.clearTooltip')}
|
||||
leftIcon={<PiTrashSimpleFill />}
|
||||
colorScheme="error"
|
||||
onClick={clearQueue.openDialog}
|
||||
onClick={dialogState.setTrue}
|
||||
data-testid={t('queue.clear')}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,72 +1,26 @@
|
||||
import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { buildUseBoolean } from 'common/hooks/useBoolean';
|
||||
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { atom } from 'nanostores';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
const [useClearQueueConfirmationAlertDialog] = buildUseBoolean(false);
|
||||
|
||||
export const useClearQueue = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const dialog = useClearQueueConfirmationAlertDialog();
|
||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||
const isConnected = useStore($isConnected);
|
||||
const [trigger, { isLoading }] = useClearQueueMutation({
|
||||
fixedCacheKey: 'clearQueue',
|
||||
});
|
||||
|
||||
const clearQueue = useCallback(async () => {
|
||||
if (!queueStatus?.queue.total) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await trigger().unwrap();
|
||||
toast({
|
||||
id: 'QUEUE_CLEAR_SUCCEEDED',
|
||||
title: t('queue.clearSucceeded'),
|
||||
status: 'success',
|
||||
});
|
||||
dispatch(listCursorChanged(undefined));
|
||||
dispatch(listPriorityChanged(undefined));
|
||||
} catch {
|
||||
toast({
|
||||
id: 'QUEUE_CLEAR_FAILED',
|
||||
title: t('queue.clearFailed'),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
}, [queueStatus?.queue.total, trigger, dispatch, t]);
|
||||
|
||||
const isDisabled = useMemo(() => !isConnected || !queueStatus?.queue.total, [isConnected, queueStatus?.queue.total]);
|
||||
|
||||
return {
|
||||
clearQueue,
|
||||
isOpen: dialog.isTrue,
|
||||
openDialog: dialog.setTrue,
|
||||
closeDialog: dialog.setFalse,
|
||||
isLoading,
|
||||
queueStatus,
|
||||
isDisabled,
|
||||
};
|
||||
};
|
||||
const $boolean = atom(false);
|
||||
export const useClearQueueConfirmationAlertDialog = buildUseBoolean($boolean);
|
||||
|
||||
export const ClearQueueConfirmationsAlertDialog = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const clearQueue = useClearQueue();
|
||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
||||
const isOpen = useStore(dialogState.$boolean);
|
||||
const { clearQueue } = useClearQueue();
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
isOpen={clearQueue.isOpen}
|
||||
onClose={clearQueue.closeDialog}
|
||||
isOpen={isOpen}
|
||||
onClose={dialogState.setFalse}
|
||||
title={t('queue.clearTooltip')}
|
||||
acceptCallback={clearQueue.clearQueue}
|
||||
acceptCallback={clearQueue}
|
||||
acceptButtonText={t('queue.clear')}
|
||||
useInert={false}
|
||||
>
|
||||
|
||||
@@ -1,40 +1,67 @@
|
||||
import type { IconButtonProps } from '@invoke-ai/ui-library';
|
||||
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
|
||||
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
import { useClearQueue } from './ClearQueueConfirmationAlertDialog';
|
||||
type ClearQueueButtonProps = Omit<IconButtonProps, 'aria-label'>;
|
||||
|
||||
export const ClearQueueIconButton = memo((_) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
export const ClearAllQueueIconButton = memo((props: ClearQueueButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const clearQueue = useClearQueue();
|
||||
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
|
||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
||||
const { isLoading, isDisabled } = useClearQueue();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
aria-label={t('queue.clear')}
|
||||
tooltip={t('queue.clearTooltip')}
|
||||
icon={<PiTrashSimpleBold size="16px" />}
|
||||
colorScheme="error"
|
||||
onClick={dialogState.setTrue}
|
||||
data-testid={t('queue.clear')}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ClearAllQueueIconButton.displayName = 'ClearAllQueueIconButton';
|
||||
|
||||
const ClearSingleQueueItemIconButton = memo((props: ClearQueueButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { cancelQueueItem, isLoading, isDisabled } = useCancelCurrentQueueItem();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
aria-label={t('queue.cancel')}
|
||||
tooltip={t('queue.cancelTooltip')}
|
||||
icon={<PiXBold size="16px" />}
|
||||
colorScheme="error"
|
||||
onClick={cancelQueueItem}
|
||||
data-testid={t('queue.cancel')}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ClearSingleQueueItemIconButton.displayName = 'ClearSingleQueueItemIconButton';
|
||||
|
||||
export const ClearQueueIconButton = memo((props: ClearQueueButtonProps) => {
|
||||
// Show the single item clear button when shift is pressed
|
||||
// Otherwise show the clear queue button
|
||||
const shift = useShiftModifier();
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size="lg"
|
||||
isDisabled={shift ? clearQueue.isDisabled : cancelCurrentQueueItem.isDisabled}
|
||||
isLoading={shift ? clearQueue.isLoading : cancelCurrentQueueItem.isLoading}
|
||||
aria-label={shift ? t('queue.clear') : t('queue.cancel')}
|
||||
tooltip={shift ? t('queue.clearTooltip') : t('queue.cancelTooltip')}
|
||||
icon={shift ? <PiTrashSimpleBold /> : <PiXBold />}
|
||||
colorScheme="error"
|
||||
onClick={shift ? clearQueue.openDialog : cancelCurrentQueueItem.cancelQueueItem}
|
||||
data-testid={shift ? t('queue.clear') : t('queue.cancel')}
|
||||
/>
|
||||
{/* The badge is dynamically positioned, needs a ref to the target element */}
|
||||
<QueueCountBadge targetRef={ref} />
|
||||
</>
|
||||
);
|
||||
if (shift) {
|
||||
return <ClearAllQueueIconButton {...props} />;
|
||||
}
|
||||
|
||||
return <ClearSingleQueueItemIconButton {...props} />;
|
||||
});
|
||||
|
||||
ClearQueueIconButton.displayName = 'ClearQueueIconButton';
|
||||
|
||||
@@ -15,7 +15,7 @@ export const InvokeQueueBackButton = memo(() => {
|
||||
const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading);
|
||||
|
||||
return (
|
||||
<Flex pos="relative" w="192px">
|
||||
<Flex pos="relative" flexGrow={1} minW="240px">
|
||||
<QueueIterationsNumberInput />
|
||||
<QueueButtonTooltip>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Portal,
|
||||
useDisclosure,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import type { Coordinate } from 'features/controlLayers/store/types';
|
||||
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
|
||||
import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPauseFill, PiPlayFill, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { RiListCheck, RiPlayList2Fill } from 'react-icons/ri';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
export const QueueActionsMenuButton = memo(() => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [badgePos, setBadgePos] = useState<Coordinate | null>(null);
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
||||
const isPauseEnabled = useFeatureStatus('pauseQueue');
|
||||
const isResumeEnabled = useFeatureStatus('resumeQueue');
|
||||
const { queueSize } = useGetQueueStatusQuery(undefined, {
|
||||
selectFromResult: (res) => ({
|
||||
queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0,
|
||||
}),
|
||||
});
|
||||
const { isLoading: isLoadingClearQueue, isDisabled: isDisabledClearQueue } = useClearQueue();
|
||||
const {
|
||||
resumeProcessor,
|
||||
isLoading: isLoadingResumeProcessor,
|
||||
isDisabled: isDisabledResumeProcessor,
|
||||
} = useResumeProcessor();
|
||||
const {
|
||||
pauseProcessor,
|
||||
isLoading: isLoadingPauseProcessor,
|
||||
isDisabled: isDisabledPauseProcessor,
|
||||
} = usePauseProcessor();
|
||||
const openQueue = useCallback(() => {
|
||||
dispatch(setActiveTab('queue'));
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuButtonRef.current) {
|
||||
const { x, y } = menuButtonRef.current.getBoundingClientRect();
|
||||
setBadgePos({ x: x - 10, y: y - 10 });
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box pos="relative">
|
||||
<Menu isOpen={isOpen} onOpen={onOpen} onClose={onClose} placement="bottom-end">
|
||||
<MenuButton ref={menuButtonRef} as={IconButton} aria-label="Queue Actions Menu" icon={<RiListCheck />} />
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
isDestructive
|
||||
icon={<PiTrashSimpleBold size="16px" />}
|
||||
onClick={dialogState.setTrue}
|
||||
isLoading={isLoadingClearQueue}
|
||||
isDisabled={isDisabledClearQueue}
|
||||
>
|
||||
{t('queue.clearTooltip')}
|
||||
</MenuItem>
|
||||
{isResumeEnabled && (
|
||||
<MenuItem
|
||||
icon={<PiPlayFill size="14px" />}
|
||||
onClick={resumeProcessor}
|
||||
isLoading={isLoadingResumeProcessor}
|
||||
isDisabled={isDisabledResumeProcessor}
|
||||
>
|
||||
{t('queue.resumeTooltip')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isPauseEnabled && (
|
||||
<MenuItem
|
||||
icon={<PiPauseFill size="14px" />}
|
||||
onClick={pauseProcessor}
|
||||
isLoading={isLoadingPauseProcessor}
|
||||
isDisabled={isDisabledPauseProcessor}
|
||||
>
|
||||
{t('queue.pauseTooltip')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuDivider />
|
||||
<MenuItem icon={<RiPlayList2Fill />} onClick={openQueue}>
|
||||
{t('queue.openQueue')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
{queueSize > 0 && badgePos !== null && (
|
||||
<Portal>
|
||||
<Badge
|
||||
pos="absolute"
|
||||
insetInlineStart={badgePos.x}
|
||||
insetBlockStart={badgePos.y}
|
||||
colorScheme="invokeYellow"
|
||||
zIndex="docked"
|
||||
>
|
||||
{queueSize}
|
||||
</Badge>
|
||||
</Portal>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
QueueActionsMenuButton.displayName = 'QueueActionsMenuButton';
|
||||
@@ -1,27 +1,24 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
|
||||
import { ButtonGroup, Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
||||
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
|
||||
import ProgressBar from 'features/system/components/ProgressBar';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { InvokeQueueBackButton } from './InvokeQueueBackButton';
|
||||
import { QueueActionsMenuButton } from './QueueActionsMenuButton';
|
||||
|
||||
const QueueControls = () => {
|
||||
const isPrependEnabled = useFeatureStatus('prependQueue');
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
return (
|
||||
<Flex w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
|
||||
<Flex gap={2}>
|
||||
<ButtonGroup size="lg" isAttached={false}>
|
||||
{isPrependEnabled && <QueueFrontButton />}
|
||||
<InvokeQueueBackButton />
|
||||
<Spacer />
|
||||
{tab === 'generation' && <CanvasSendToToggle />}
|
||||
<QueueActionsMenuButton />
|
||||
<ClearQueueIconButton />
|
||||
</Flex>
|
||||
</ButtonGroup>
|
||||
<ProgressBar />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Badge, Portal } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $isParametersPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import type { RefObject } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
type Props = {
|
||||
targetRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const QueueCountBadge = memo(({ targetRef }: Props) => {
|
||||
const [badgePos, setBadgePos] = useState<{ x: string; y: string } | null>(null);
|
||||
const isParametersPanelOpen = useStore($isParametersPanelOpen);
|
||||
const { queueSize } = useGetQueueStatusQuery(undefined, {
|
||||
selectFromResult: (res) => ({
|
||||
queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0,
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = targetRef.current;
|
||||
const parent = target.parentElement;
|
||||
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cb = () => {
|
||||
if (!$isParametersPanelOpen.get()) {
|
||||
return;
|
||||
}
|
||||
const { x, y } = target.getBoundingClientRect();
|
||||
setBadgePos({ x: `${x - 7}px`, y: `${y - 5}px` });
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(cb);
|
||||
resizeObserver.observe(parent);
|
||||
cb();
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [targetRef]);
|
||||
|
||||
if (queueSize === 0) {
|
||||
return null;
|
||||
}
|
||||
if (!badgePos) {
|
||||
return null;
|
||||
}
|
||||
if (!isParametersPanelOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Badge
|
||||
pos="absolute"
|
||||
insetInlineStart={badgePos.x}
|
||||
insetBlockStart={badgePos.y}
|
||||
colorScheme="invokeYellow"
|
||||
zIndex="docked"
|
||||
shadow="dark-lg"
|
||||
userSelect="none"
|
||||
>
|
||||
{queueSize}
|
||||
</Badge>
|
||||
</Portal>
|
||||
);
|
||||
});
|
||||
|
||||
QueueCountBadge.displayName = 'QueueCountBadge';
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ChakraProps, CollapseProps } from '@invoke-ai/ui-library';
|
||||
import { ButtonGroup, Collapse, Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||
import QueueStatusBadge from 'features/queue/components/common/QueueStatusBadge';
|
||||
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
|
||||
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
|
||||
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
||||
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
|
||||
@@ -53,7 +52,6 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
|
||||
|
||||
const isCanceled = useMemo(() => ['canceled', 'completed', 'failed'].includes(item.status), [item.status]);
|
||||
const originText = useOriginText(item.origin);
|
||||
const destinationText = useDestinationText(item.destination);
|
||||
|
||||
const icon = useMemo(() => <PiXBold />, []);
|
||||
return (
|
||||
@@ -78,11 +76,6 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
|
||||
{originText}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex w={COLUMN_WIDTHS.destination} flexShrink={0}>
|
||||
<Text overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" alignItems="center">
|
||||
{destinationText}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex w={COLUMN_WIDTHS.time} alignItems="center" flexShrink={0}>
|
||||
{executionTime || '-'}
|
||||
</Flex>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Button, ButtonGroup, Flex, Heading, Spinner, Text } from '@invoke-ai/ui-library';
|
||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
|
||||
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
|
||||
import { useCancelBatch } from 'features/queue/hooks/useCancelBatch';
|
||||
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
||||
@@ -18,7 +17,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
||||
const { session_id, batch_id, item_id, origin, destination } = queueItemDTO;
|
||||
const { session_id, batch_id, item_id, origin } = queueItemDTO;
|
||||
const { t } = useTranslation();
|
||||
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled } = useCancelBatch(batch_id);
|
||||
|
||||
@@ -27,7 +26,6 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
||||
const { data: queueItem } = useGetQueueItemQuery(item_id);
|
||||
|
||||
const originText = useOriginText(origin);
|
||||
const destinationText = useDestinationText(destination);
|
||||
|
||||
const statusAndTiming = useMemo(() => {
|
||||
if (!queueItem) {
|
||||
@@ -56,7 +54,6 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
||||
>
|
||||
<QueueItemData label={t('queue.status')} data={statusAndTiming} />
|
||||
<QueueItemData label={t('queue.origin')} data={originText} />
|
||||
<QueueItemData label={t('queue.destination')} data={destinationText} />
|
||||
<QueueItemData label={t('queue.item')} data={item_id} />
|
||||
<QueueItemData label={t('queue.batch')} data={batch_id} />
|
||||
<QueueItemData label={t('queue.session')} data={session_id} />
|
||||
|
||||
@@ -25,9 +25,6 @@ const QueueListHeader = () => {
|
||||
<Flex ps={0.5} w={COLUMN_WIDTHS.origin} alignItems="center">
|
||||
<Text variant="subtext">{t('queue.origin')}</Text>
|
||||
</Flex>
|
||||
<Flex ps={0.5} w={COLUMN_WIDTHS.destination} alignItems="center">
|
||||
<Text variant="subtext">{t('queue.destination')}</Text>
|
||||
</Flex>
|
||||
<Flex ps={0.5} w={COLUMN_WIDTHS.time} alignItems="center">
|
||||
<Text variant="subtext">{t('queue.time')}</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -4,8 +4,7 @@ export const COLUMN_WIDTHS = {
|
||||
statusDot: 2,
|
||||
time: '4rem',
|
||||
origin: '5rem',
|
||||
destination: '6rem',
|
||||
batchId: '5rem',
|
||||
fieldValues: 'auto',
|
||||
actions: 'auto',
|
||||
} as const;
|
||||
};
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { SessionQueueItemDTO } from 'services/api/types';
|
||||
|
||||
export const useDestinationText = (destination: SessionQueueItemDTO['destination']) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (destination === 'canvas') {
|
||||
return t('queue.canvas');
|
||||
}
|
||||
|
||||
if (destination === 'gallery') {
|
||||
return t('queue.gallery');
|
||||
}
|
||||
|
||||
return t('queue.other');
|
||||
};
|
||||
@@ -4,17 +4,13 @@ import type { SessionQueueItemDTO } from 'services/api/types';
|
||||
export const useOriginText = (origin: SessionQueueItemDTO['origin']) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (origin === 'generation') {
|
||||
return t('queue.generation');
|
||||
if (origin === 'canvas') {
|
||||
return t('queue.originCanvas');
|
||||
}
|
||||
|
||||
if (origin === 'workflows') {
|
||||
return t('queue.workflows');
|
||||
return t('queue.originWorkflows');
|
||||
}
|
||||
|
||||
if (origin === 'upscaling') {
|
||||
return t('queue.upscaling');
|
||||
}
|
||||
|
||||
return t('queue.other');
|
||||
return t('queue.originOther');
|
||||
};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
export const useClearQueue = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||
const isConnected = useStore($isConnected);
|
||||
const [trigger, { isLoading }] = useClearQueueMutation({
|
||||
fixedCacheKey: 'clearQueue',
|
||||
});
|
||||
|
||||
const clearQueue = useCallback(async () => {
|
||||
if (!queueStatus?.queue.total) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await trigger().unwrap();
|
||||
toast({
|
||||
id: 'QUEUE_CLEAR_SUCCEEDED',
|
||||
title: t('queue.clearSucceeded'),
|
||||
status: 'success',
|
||||
});
|
||||
dispatch(listCursorChanged(undefined));
|
||||
dispatch(listPriorityChanged(undefined));
|
||||
} catch {
|
||||
toast({
|
||||
id: 'QUEUE_CLEAR_FAILED',
|
||||
title: t('queue.clearFailed'),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
}, [queueStatus?.queue.total, trigger, dispatch, t]);
|
||||
|
||||
const isDisabled = useMemo(() => !isConnected || !queueStatus?.queue.total, [isConnected, queueStatus?.queue.total]);
|
||||
|
||||
return { clearQueue, isLoading, queueStatus, isDisabled };
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user