Compare commits
780 Commits
camera-ite
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e00b487167 | ||
|
|
771a79cd33 | ||
|
|
a3afb08664 | ||
|
|
4de91ef20b | ||
|
|
94630c451a | ||
|
|
8ab891edb5 | ||
|
|
9f89d1a9ec | ||
|
|
c8e8ae2506 | ||
|
|
929c061f36 | ||
|
|
189c09ed79 | ||
|
|
ccbd773cee | ||
|
|
87857090b4 | ||
|
|
5f152df9f7 | ||
|
|
b70a188e67 | ||
|
|
5a2bb6faed | ||
|
|
65c426f981 | ||
|
|
ec468b6752 | ||
|
|
23f0b5f880 | ||
|
|
8bfb01808a | ||
|
|
e621f4b60e | ||
|
|
7e0b956f49 | ||
|
|
4d229b95b6 | ||
|
|
392eaf4ee5 | ||
|
|
6e2de6ac87 | ||
|
|
04e20a3bbf | ||
|
|
429a6efcee | ||
|
|
b1ab37c8dc | ||
|
|
181224767d | ||
|
|
2b07345286 | ||
|
|
d5034b4034 | ||
|
|
891d90fc5e | ||
|
|
ba97528ed6 | ||
|
|
536fafb4cf | ||
|
|
59e6a11079 | ||
|
|
0e4f5de529 | ||
|
|
05d4670609 | ||
|
|
08cec3c700 | ||
|
|
0b29f2f6c9 | ||
|
|
38a0520299 | ||
|
|
a080560c9c | ||
|
|
024ae4ddd7 | ||
|
|
6742caa967 | ||
|
|
ba5415653e | ||
|
|
ddef55879a | ||
|
|
c63416d1f3 | ||
|
|
b0ee7e9f54 | ||
|
|
ec871f5963 | ||
|
|
418e3a2ccb | ||
|
|
75bdb44371 | ||
|
|
6678681c2c | ||
|
|
3edaaf2dfc | ||
|
|
0ebfc1153e | ||
|
|
2d6bc06cbe | ||
|
|
bbf12b9e31 | ||
|
|
621a9c6d85 | ||
|
|
96e910c450 | ||
|
|
25b0760913 | ||
|
|
b542dc1b23 | ||
|
|
3e4be41d3f | ||
|
|
76fc1bfab5 | ||
|
|
928886563d | ||
|
|
e3a3db246e | ||
|
|
b4dccf6f89 | ||
|
|
0599eece82 | ||
|
|
27196e2dda | ||
|
|
0a4d6adf74 | ||
|
|
cf1cb90642 | ||
|
|
2343bbc974 | ||
|
|
fd7cad1ab4 | ||
|
|
0c3efe9477 | ||
|
|
065214c873 | ||
|
|
795f111ebc | ||
|
|
f40c6dcbd4 | ||
|
|
2632a0c5f3 | ||
|
|
1bfd9c95f0 | ||
|
|
a753001f15 | ||
|
|
cbd77fe704 | ||
|
|
c5246c9dc8 | ||
|
|
209171abfc | ||
|
|
7e53db3d33 | ||
|
|
4bc71b01cb | ||
|
|
c4ccda2118 | ||
|
|
3a55aaa2cf | ||
|
|
0650956ab4 | ||
|
|
45ec292b99 | ||
|
|
550087841f | ||
|
|
bd1b54cf03 | ||
|
|
707452462d | ||
|
|
1c8aa93850 | ||
|
|
a0b014576e | ||
|
|
50e1921cd9 | ||
|
|
14c5c355b5 | ||
|
|
4641f4f724 | ||
|
|
053d468035 | ||
|
|
f44abad5b0 | ||
|
|
9e8f09fe97 | ||
|
|
ae68d0e135 | ||
|
|
7314428ce7 | ||
|
|
c5440bc426 | ||
|
|
ad1c97aad6 | ||
|
|
51b0942c49 | ||
|
|
c665f33d25 | ||
|
|
a6a1195922 | ||
|
|
d6ad00bf53 | ||
|
|
b6524e90e1 | ||
|
|
6f113a9ec4 | ||
|
|
bf85140b65 | ||
|
|
2b9ba2b957 | ||
|
|
8df1ba549e | ||
|
|
dd0e45cffe | ||
|
|
a14457ecc1 | ||
|
|
eac44ded78 | ||
|
|
2357cba5f9 | ||
|
|
e69d85cc56 | ||
|
|
a5b4939b57 | ||
|
|
d9a6ca9bef | ||
|
|
2e3e944f99 | ||
|
|
fefa66060a | ||
|
|
6b93a1a095 | ||
|
|
5f6b48c9b4 | ||
|
|
bda2d06e81 | ||
|
|
87907984f6 | ||
|
|
83dbde9bc0 | ||
|
|
c69b2652c8 | ||
|
|
ec50f38dfd | ||
|
|
8e9ac71ebb | ||
|
|
b95ea8e90f | ||
|
|
712678a666 | ||
|
|
1c6b42e69a | ||
|
|
51de346f6b | ||
|
|
71b1792ee2 | ||
|
|
30f340fe41 | ||
|
|
040f86d50d | ||
|
|
f0a494392a | ||
|
|
a7aa609651 | ||
|
|
7a3322efad | ||
|
|
877a32ab09 | ||
|
|
4ccee1bf89 | ||
|
|
6b78684650 | ||
|
|
4f75aa5edf | ||
|
|
d92309d0ef | ||
|
|
c8d9af5217 | ||
|
|
127641064a | ||
|
|
76d58fe2f5 | ||
|
|
565cb29b85 | ||
|
|
5383bbedcd | ||
|
|
41d6b02506 | ||
|
|
2719c0b00a | ||
|
|
c475ed04a7 | ||
|
|
00086221cd | ||
|
|
b31f6bc983 | ||
|
|
9e72edae67 | ||
|
|
bb0f023040 | ||
|
|
8e143f97d4 | ||
|
|
e1e773befb | ||
|
|
c5b1869f42 | ||
|
|
0384bb8014 | ||
|
|
6c498c60f2 | ||
|
|
07ea966c01 | ||
|
|
fb0f8d4115 | ||
|
|
24beda4a4a | ||
|
|
de045a1c68 | ||
|
|
923c1a3cc1 | ||
|
|
b26d82c991 | ||
|
|
2d91ae6162 | ||
|
|
173f32706d | ||
|
|
d234d6f765 | ||
|
|
f4c75c5705 | ||
|
|
72c5312c5e | ||
|
|
00c5190200 | ||
|
|
2acb75a3fc | ||
|
|
c6754bd689 | ||
|
|
28613f8585 | ||
|
|
aadf6f6f07 | ||
|
|
fe2410e92a | ||
|
|
0b9f2c47fd | ||
|
|
6dc6628d7e | ||
|
|
9c936eb277 | ||
|
|
8b3d4e611a | ||
|
|
f2eac318da | ||
|
|
840fada1e1 | ||
|
|
cd3cf3721a | ||
|
|
e5cfa249df | ||
|
|
3fbc659904 | ||
|
|
38ea600846 | ||
|
|
292e3988c5 | ||
|
|
218b5f959a | ||
|
|
9015c3e6c4 | ||
|
|
ec3ced272a | ||
|
|
030fd6ed53 | ||
|
|
c9d2f92142 | ||
|
|
9466e97dd1 | ||
|
|
eca5bb4bf2 | ||
|
|
4426d14bc5 | ||
|
|
66a3ae22cc | ||
|
|
6cb46cf4f3 | ||
|
|
119c7026b4 | ||
|
|
a8b5ff47a6 | ||
|
|
a8144dbd22 | ||
|
|
826b2413c9 | ||
|
|
a29a04d3f5 | ||
|
|
0b4c7edc92 | ||
|
|
81a741ad36 | ||
|
|
d9dded54ca | ||
|
|
72d1173d2e | ||
|
|
12a3dc26af | ||
|
|
7fab72f903 | ||
|
|
856d738267 | ||
|
|
f855ca9820 | ||
|
|
3571fa96aa | ||
|
|
e4186a90fc | ||
|
|
d360a57f6e | ||
|
|
58328cd63b | ||
|
|
00fdaf5b9f | ||
|
|
3225819121 | ||
|
|
b0d623e9de | ||
|
|
6ec09809d9 | ||
|
|
0ec465ac39 | ||
|
|
c59198b4a2 | ||
|
|
49224ad6b5 | ||
|
|
4b68544a0b | ||
|
|
42d36627ff | ||
|
|
63cf0db448 | ||
|
|
929ea054f3 | ||
|
|
c052ad22ba | ||
|
|
e37d43eeb8 | ||
|
|
14544bfd3e | ||
|
|
c1d3a4cbcb | ||
|
|
dfb59ee7fa | ||
|
|
cb9460c344 | ||
|
|
e9078bfaf8 | ||
|
|
5a042bf11c | ||
|
|
a8d3b1fda1 | ||
|
|
a4a43d3ceb | ||
|
|
f4d08dac9c | ||
|
|
7b126557df | ||
|
|
859d38f1b8 | ||
|
|
98ae69c858 | ||
|
|
e31d3e2197 | ||
|
|
017a7d4c7b | ||
|
|
13210f858d | ||
|
|
692fc13863 | ||
|
|
4bcca6af99 | ||
|
|
165dc1d259 | ||
|
|
3a91ce8158 | ||
|
|
4b0969d9cf | ||
|
|
d0f88c13cd | ||
|
|
07e9474c79 | ||
|
|
0abc38c83a | ||
|
|
3b4a42c0cb | ||
|
|
c22bf9b511 | ||
|
|
806857b738 | ||
|
|
3446ae5a4e | ||
|
|
5a5695f2cc | ||
|
|
cc8b9c0378 | ||
|
|
ac2f4a9e27 | ||
|
|
c93036278f | ||
|
|
600eab9d2a | ||
|
|
85bb21414d | ||
|
|
2bf244bb6e | ||
|
|
3374473265 | ||
|
|
2e87fceae1 | ||
|
|
e678ca38ae | ||
|
|
cbb54fd3d2 | ||
|
|
aeec63c070 | ||
|
|
fd1c6ed0cf | ||
|
|
13c6dbfd4d | ||
|
|
4fb7c3df84 | ||
|
|
9d0e1ce021 | ||
|
|
dd38b590c1 | ||
|
|
24b7195297 | ||
|
|
7d8993b640 | ||
|
|
4c70ded2b0 | ||
|
|
7f7a8b5264 | ||
|
|
43246900db | ||
|
|
b7a123f9c9 | ||
|
|
900316968a | ||
|
|
db30f8e61b | ||
|
|
0a005b9864 | ||
|
|
f4ed8bc37d | ||
|
|
49ee4d3b67 | ||
|
|
c2f1f737a0 | ||
|
|
c9a57773d1 | ||
|
|
68a7c06b9e | ||
|
|
b196cbfa3a | ||
|
|
7067d048d6 | ||
|
|
ef09b69378 | ||
|
|
c5dea3b7c9 | ||
|
|
5ea147f9fe | ||
|
|
3d6fceb0dd | ||
|
|
b1900de652 | ||
|
|
76ff476112 | ||
|
|
57f9516a81 | ||
|
|
fe6ed81255 | ||
|
|
173b033963 | ||
|
|
fe9ae8225c | ||
|
|
a149098044 | ||
|
|
2a5bddbb5a | ||
|
|
a7d5992b6b | ||
|
|
94b9b9ddcf | ||
|
|
cfb193251f | ||
|
|
14e899db0e | ||
|
|
42fc3c3f44 | ||
|
|
9bfbade9a2 | ||
|
|
3f4002881c | ||
|
|
8a8121f1a8 | ||
|
|
8fc017a6fc | ||
|
|
7016904adb | ||
|
|
93692f365e | ||
|
|
b765b68d41 | ||
|
|
5d82e42820 | ||
|
|
15fe997237 | ||
|
|
44a3480022 | ||
|
|
f0c50ba237 | ||
|
|
30a05a3218 | ||
|
|
779706d792 | ||
|
|
d65163553e | ||
|
|
33ccc66411 | ||
|
|
fb1627dccc | ||
|
|
9f49ba729d | ||
|
|
a44f36641e | ||
|
|
34fd7e6e49 | ||
|
|
a5f087d29f | ||
|
|
3b996c46e2 | ||
|
|
2e50491144 | ||
|
|
c3e4b2ed2a | ||
|
|
c0141da88b | ||
|
|
8f18d83694 | ||
|
|
6822e4bdb7 | ||
|
|
3c66109c45 | ||
|
|
d27ec84496 | ||
|
|
239e0c0318 | ||
|
|
ab913a8d84 | ||
|
|
a5653853f8 | ||
|
|
58a17fedba | ||
|
|
be709efbdd | ||
|
|
4f01bd5eec | ||
|
|
242eed37fe | ||
|
|
fe22d3954a | ||
|
|
96b4e22e10 | ||
|
|
a51db703fd | ||
|
|
7e755a44b8 | ||
|
|
80c9386f79 | ||
|
|
4730f0732b | ||
|
|
79935befb7 | ||
|
|
b2e452326b | ||
|
|
3e31905b5b | ||
|
|
c830becbff | ||
|
|
e8e485b472 | ||
|
|
bf68b87813 | ||
|
|
9546a36200 | ||
|
|
c12300e756 | ||
|
|
4683710f09 | ||
|
|
1ca054c2b3 | ||
|
|
5f7e9d4eb6 | ||
|
|
c9fe1be10b | ||
|
|
f58c748f80 | ||
|
|
499aa2be28 | ||
|
|
7ba710e8b7 | ||
|
|
62802367eb | ||
|
|
853f779930 | ||
|
|
21d10a15ee | ||
|
|
6146a5b63a | ||
|
|
01485e2020 | ||
|
|
6e77c3667d | ||
|
|
003dd9fcc1 | ||
|
|
42738b739f | ||
|
|
fe83a69bb9 | ||
|
|
e0a7d4f44a | ||
|
|
14ebe85493 | ||
|
|
08135b03a2 | ||
|
|
4827561f88 | ||
|
|
72f7fd8e9d | ||
|
|
827c630a70 | ||
|
|
98a6ad6f32 | ||
|
|
c09afba366 | ||
|
|
48f15e848d | ||
|
|
78912c8f1b | ||
|
|
13815d4d20 | ||
|
|
36d91fd999 | ||
|
|
b7191c3183 | ||
|
|
796ee1a34e | ||
|
|
de10a7101e | ||
|
|
41d22da691 | ||
|
|
73ed1210ed | ||
|
|
a8e16a8411 | ||
|
|
4d8d38e4e5 | ||
|
|
01f4a8fbcd | ||
|
|
f639a1662a | ||
|
|
6350444559 | ||
|
|
ecc7d7b912 | ||
|
|
ae40158dec | ||
|
|
1b28290cbb | ||
|
|
a3bcff035a | ||
|
|
71ca32a560 | ||
|
|
d28b477c8a | ||
|
|
643697ea33 | ||
|
|
f5259f2977 | ||
|
|
7bedd2c27f | ||
|
|
707b81871c | ||
|
|
1fddcd9033 | ||
|
|
7ff470e1db | ||
|
|
a7df6d8a08 | ||
|
|
a9ff97e39c | ||
|
|
126e9f4ca6 | ||
|
|
e379dae4ad | ||
|
|
b3fa529f77 | ||
|
|
1e8d3a3d4c | ||
|
|
2988ae3c28 | ||
|
|
9c04b5710e | ||
|
|
1324c26c74 | ||
|
|
0b9f79158c | ||
|
|
e5d0906d73 | ||
|
|
f08f3e4e4b | ||
|
|
d17841ec7a | ||
|
|
ebaf113d94 | ||
|
|
5be25da46f | ||
|
|
f5e8156400 | ||
|
|
02bf210c51 | ||
|
|
eba317ace9 | ||
|
|
84e42499e7 | ||
|
|
9b6b450d18 | ||
|
|
53979ce167 | ||
|
|
e2c9e06cd3 | ||
|
|
65a9957be1 | ||
|
|
fbe941749a | ||
|
|
36fd3216c7 | ||
|
|
f73c8f968d | ||
|
|
83e3a913c5 | ||
|
|
de56aa53d6 | ||
|
|
9f0043f406 | ||
|
|
99eb0ea547 | ||
|
|
04535895e8 | ||
|
|
69311125ba | ||
|
|
45b832455b | ||
|
|
b590e82b05 | ||
|
|
f770357010 | ||
|
|
3cbc10d91e | ||
|
|
9425d30cb3 | ||
|
|
8d2aac12d6 | ||
|
|
5f9d9d4714 | ||
|
|
eda50b7d51 | ||
|
|
01c5ad3b04 | ||
|
|
d7fba79be9 | ||
|
|
89fcc55cca | ||
|
|
a831fcb99f | ||
|
|
8e37d27130 | ||
|
|
149131f322 | ||
|
|
4a90b936b1 | ||
|
|
ab6998407b | ||
|
|
be2f5b8e37 | ||
|
|
226554fadc | ||
|
|
47cbc2b8fc | ||
|
|
90f60c9178 | ||
|
|
49253567cb | ||
|
|
350007cbaf | ||
|
|
7318d5317c | ||
|
|
15f771ff93 | ||
|
|
302a64785d | ||
|
|
34aab266a3 | ||
|
|
fba4e10611 | ||
|
|
807928a8d3 | ||
|
|
f74c891907 | ||
|
|
2dc0fdadfd | ||
|
|
8e317a8e08 | ||
|
|
c41179a437 | ||
|
|
43910cce78 | ||
|
|
643986e03d | ||
|
|
a2b061ed43 | ||
|
|
c458624157 | ||
|
|
1d2b149395 | ||
|
|
c84848ea9c | ||
|
|
b34e5f7a19 | ||
|
|
af3d49c9ef | ||
|
|
d5d63b2f89 | ||
|
|
d4b7c3f61a | ||
|
|
36dacb3322 | ||
|
|
eec38dd60f | ||
|
|
1e0ea6ae92 | ||
|
|
31f45760f0 | ||
|
|
f76863a79b | ||
|
|
43b3499ed7 | ||
|
|
dfaaa2a3a9 | ||
|
|
b8d17ac7ca | ||
|
|
ed28914f86 | ||
|
|
f8fab6bf5a | ||
|
|
1b37d77304 | ||
|
|
6eb36516b0 | ||
|
|
f0200be990 | ||
|
|
cfd3d0016b | ||
|
|
fbf0f4f672 | ||
|
|
387fa96a60 | ||
|
|
d549018024 | ||
|
|
5cd92f2c03 | ||
|
|
2450411e21 | ||
|
|
b53bfde23b | ||
|
|
15eb73af41 | ||
|
|
3c82ae309c | ||
|
|
4adcfca2fe | ||
|
|
f893491bed | ||
|
|
9d1181aaca | ||
|
|
b0f9bf24cf | ||
|
|
3527ce2507 | ||
|
|
f045bfb93d | ||
|
|
3d4ab27a14 | ||
|
|
54ff87c146 | ||
|
|
2bc988fe3d | ||
|
|
051893858b | ||
|
|
1ac211c23f | ||
|
|
4874f010dd | ||
|
|
da894143f9 | ||
|
|
109affec81 | ||
|
|
665e6b034f | ||
|
|
fc45b4e400 | ||
|
|
af75d2a451 | ||
|
|
3136a8f2b7 | ||
|
|
e97368f3e2 | ||
|
|
4da7ed1ccf | ||
|
|
bf248e192f | ||
|
|
167c3d1cbf | ||
|
|
b39d8f37f4 | ||
|
|
1198797db8 | ||
|
|
823a71a286 | ||
|
|
f78da4b198 | ||
|
|
558fb1aa4e | ||
|
|
abe32a8dbd | ||
|
|
d9e040bf8b | ||
|
|
0b8da1d280 | ||
|
|
455f3a0e9c | ||
|
|
411b30294b | ||
|
|
1d54b75a53 | ||
|
|
4c56c31bfc | ||
|
|
b21e031668 | ||
|
|
7c6b2f5949 | ||
|
|
1f66d7dee4 | ||
|
|
53b035ef92 | ||
|
|
237b084a65 | ||
|
|
707e56cb6f | ||
|
|
6f4da69e36 | ||
|
|
5634729445 | ||
|
|
b9603dc0a1 | ||
|
|
0323fbfb86 | ||
|
|
433568da63 | ||
|
|
4e199dd43f | ||
|
|
a4430b9a95 | ||
|
|
136383326e | ||
|
|
5a86f11beb | ||
|
|
6ab2bb2d52 | ||
|
|
ce8877b67e | ||
|
|
74a2f7a249 | ||
|
|
11ecb0aebe | ||
|
|
281f5e91ff | ||
|
|
5dc7cfbe83 | ||
|
|
3432c63a6e | ||
|
|
02ac96b0dc | ||
|
|
0736f99631 | ||
|
|
4bbe3ad8d0 | ||
|
|
1cf9d0f8c5 | ||
|
|
51d0cab832 | ||
|
|
062c63af2b | ||
|
|
3ce3fadb8d | ||
|
|
f0e39195b7 | ||
|
|
0d639760e9 | ||
|
|
6109a6bde6 | ||
|
|
000b633767 | ||
|
|
e3b1276d77 | ||
|
|
1cb8e1b7a4 | ||
|
|
e879b724ec | ||
|
|
08010efa46 | ||
|
|
e682931e3e | ||
|
|
bf89b73e8f | ||
|
|
2e252279bb | ||
|
|
9cf43ec535 | ||
|
|
f15578f7df | ||
|
|
f63ca4a25b | ||
|
|
89741508e3 | ||
|
|
81be807749 | ||
|
|
fcdb8a7edf | ||
|
|
90ce185f29 | ||
|
|
d6353035ae | ||
|
|
ceb1b23264 | ||
|
|
199e6f70b3 | ||
|
|
641db4face | ||
|
|
80d292b511 | ||
|
|
7fbcc92d02 | ||
|
|
de46726d01 | ||
|
|
6efa3a909e | ||
|
|
ea0f174da7 | ||
|
|
444146fa21 | ||
|
|
a7930760e8 | ||
|
|
668c32f583 | ||
|
|
927e1c48e2 | ||
|
|
8cc20e054c | ||
|
|
b12b171b53 | ||
|
|
0e61d51072 | ||
|
|
e61c04960e | ||
|
|
5dc82be1a3 | ||
|
|
6861b92972 | ||
|
|
f68f350cd2 | ||
|
|
2820415373 | ||
|
|
f721a57604 | ||
|
|
fb4149eaa7 | ||
|
|
d3746ef787 | ||
|
|
9d4b4bee4d | ||
|
|
5390b0effb | ||
|
|
13da4c8d60 | ||
|
|
1740d55f9c | ||
|
|
52d5e97de6 | ||
|
|
633df045d4 | ||
|
|
415bf5bb04 | ||
|
|
4e2d4a15c9 | ||
|
|
ef0142183d | ||
|
|
e5dfd9d3df | ||
|
|
6773fe2cf3 | ||
|
|
5a6e7e02ca | ||
|
|
c5d8ec2ef1 | ||
|
|
a31781146e | ||
|
|
cb9368cb8e | ||
|
|
629c05e507 | ||
|
|
9123153bf3 | ||
|
|
f3dc632ffc | ||
|
|
c369100d8a | ||
|
|
e10d40ef5e | ||
|
|
d6ca367187 | ||
|
|
88ac8a8b86 | ||
|
|
70fef89e2d | ||
|
|
4d79a59b20 | ||
|
|
c776c1b712 | ||
|
|
d4de15e0b3 | ||
|
|
9d220ec235 | ||
|
|
09c6c010e2 | ||
|
|
c9e185e9f3 | ||
|
|
4737149c85 | ||
|
|
648451b47e | ||
|
|
a9ef2d6f4b | ||
|
|
d6ffcadd76 | ||
|
|
448b7489e8 | ||
|
|
622cf60a65 | ||
|
|
1ab11f1660 | ||
|
|
9654e6cc59 | ||
|
|
44d177858f | ||
|
|
be41f11b83 | ||
|
|
dd2bffc2b1 | ||
|
|
d2ef508607 | ||
|
|
0cc7c7bc24 | ||
|
|
8d65e304cb | ||
|
|
b0dbccbd18 | ||
|
|
a823fbfb57 | ||
|
|
4a864e6519 | ||
|
|
e23d847449 | ||
|
|
8ba199f053 | ||
|
|
23925b4475 | ||
|
|
7ea0220f33 | ||
|
|
969e4de882 | ||
|
|
e5acb3013f | ||
|
|
3d57bed1c0 | ||
|
|
45839ebf60 | ||
|
|
0bb5b53e02 | ||
|
|
4f966b6d71 | ||
|
|
2687a393b5 | ||
|
|
5c605932ef | ||
|
|
bccdc8221e | ||
|
|
bddf17bbef | ||
|
|
e8511e8759 | ||
|
|
85296f538b | ||
|
|
f8ff80b781 | ||
|
|
932fef87f5 | ||
|
|
742780e34e | ||
|
|
41ede661f7 | ||
|
|
8d85460803 | ||
|
|
ac71581db8 | ||
|
|
2f894c4058 | ||
|
|
2b97c98a6e | ||
|
|
98d9f491ba | ||
|
|
16d710e106 | ||
|
|
9ad202e778 | ||
|
|
b3747d623c | ||
|
|
d5ff9281e6 | ||
|
|
18ebd8c522 | ||
|
|
411f71c217 | ||
|
|
3027ae20a9 | ||
|
|
fbce321426 | ||
|
|
74bc3a306e | ||
|
|
a950ad59f1 | ||
|
|
cf840da444 | ||
|
|
173fba35fa | ||
|
|
97fbbec0fe | ||
|
|
400f3a23bf | ||
|
|
1ad3336b6f | ||
|
|
8e9956abe1 | ||
|
|
6ca93ea24c | ||
|
|
49810e8fd1 | ||
|
|
afd0946558 | ||
|
|
fd31d0cd99 | ||
|
|
2f380da62f | ||
|
|
1c6971cd11 | ||
|
|
da4cf084d2 | ||
|
|
ca95ed5461 | ||
|
|
1b06cb6952 | ||
|
|
df89c826bb | ||
|
|
21f7f66440 | ||
|
|
3f1bad18c8 | ||
|
|
0c190cc833 | ||
|
|
5f0df8e7c1 | ||
|
|
d715f2f9ed | ||
|
|
d7fa4e62b8 | ||
|
|
568ebb0060 | ||
|
|
42efc67625 | ||
|
|
dd4b0097a1 | ||
|
|
a483cdbc7d | ||
|
|
84a7546442 | ||
|
|
dd43f238ff | ||
|
|
4f057358c8 | ||
|
|
e20971aef7 | ||
|
|
eb0d3c5bcf | ||
|
|
a18222e3df | ||
|
|
93ca932da9 | ||
|
|
c512b2fef5 | ||
|
|
f53c684377 | ||
|
|
3552166207 | ||
|
|
0b6eb147c5 | ||
|
|
1833ed50f3 | ||
|
|
6cdbfb3bad | ||
|
|
a5e0cb19c9 | ||
|
|
519ab94312 | ||
|
|
f2a8c0ed13 | ||
|
|
b3998f315d | ||
|
|
40bcd61b98 | ||
|
|
7a4d557c61 | ||
|
|
303b3f3ca2 | ||
|
|
ac5e949614 | ||
|
|
60baabb850 | ||
|
|
248e48fd8e | ||
|
|
b9baee7e5f | ||
|
|
3598ffdbb1 | ||
|
|
c171d9e6db | ||
|
|
d7a607a663 | ||
|
|
03ccf9127d | ||
|
|
6a3024657f | ||
|
|
a0058fcc2e | ||
|
|
410cc13c4f | ||
|
|
bc259dbe01 | ||
|
|
3730e20248 | ||
|
|
1be413065d | ||
|
|
2721c20c8b | ||
|
|
80130f6ffd | ||
|
|
8da778eaba | ||
|
|
3c708d1aaf | ||
|
|
1ae1bef3c1 | ||
|
|
7fd0c1fddb | ||
|
|
fd9957b784 | ||
|
|
57da02dbad | ||
|
|
83a5b36ddc | ||
|
|
8c36d73775 | ||
|
|
8396b98f67 | ||
|
|
46d989ce7f | ||
|
|
75ac570486 | ||
|
|
242def9d06 | ||
|
|
959f32009b | ||
|
|
57796c217d | ||
|
|
cea3390e36 | ||
|
|
a756e80064 | ||
|
|
1f61d32877 | ||
|
|
da7e2c5e6e | ||
|
|
fd0940bfe9 | ||
|
|
2b4a495c07 | ||
|
|
cffb968d2e | ||
|
|
201ab3aee8 | ||
|
|
cfa3584dda | ||
|
|
f19ba93958 | ||
|
|
09a2572c3b | ||
|
|
c6316b4e8b | ||
|
|
9cd900b23f | ||
|
|
a6d30cb366 | ||
|
|
3e94a3f62a | ||
|
|
060f8264ff | ||
|
|
b7b3907647 | ||
|
|
1550555df1 | ||
|
|
c42a29a034 | ||
|
|
621bf6b82a |
@@ -1,2 +1,2 @@
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
|
||||
34
.github/workflows/build.yml
vendored
34
.github/workflows/build.yml
vendored
@@ -1,46 +1,26 @@
|
||||
name: Build
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build_and_package:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install runtime dependencies
|
||||
run: sudo apt install -y --no-install-recommends libxkbcommon-dev libstdc++6 libopenxr-dev libx11-dev libxfixes-dev libgl1-mesa-dev libegl1-mesa-dev libgbm-dev libfontconfig-dev libjsoncpp-dev libxcb1-dev libglx-dev libxcb-glx0-dev libdrm-dev libwayland-dev libfreetype-dev libpng-dev
|
||||
|
||||
- name: Install build dependencies
|
||||
run: sudo apt install -y --no-install-recommends cmake ninja-build
|
||||
run: sudo apt install -y --no-install-recommends libopenxr-dev libwayland-dev libasound2-dev
|
||||
|
||||
- name: Set up Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.88.0
|
||||
override: true
|
||||
|
||||
- name: Build server
|
||||
run: cargo build --release
|
||||
|
||||
|
||||
- name: Install appimagetool
|
||||
run: |
|
||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$(uname -m).AppImage -O /usr/local/bin/appimagetool; \
|
||||
chmod +x /usr/local/bin/appimagetool; \
|
||||
sed -i 's|AI\x02|\x00\x00\x00|' /usr/local/bin/appimagetool
|
||||
- name: Install cargo-appimage
|
||||
run: cargo install cargo-appimage
|
||||
|
||||
- name: Generate AppImage
|
||||
run: cargo appimage
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: appimage
|
||||
path: '*.AppImage'
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,4 +12,5 @@
|
||||
|
||||
/libs/
|
||||
*.AppImage
|
||||
*.blend1
|
||||
*.blend1
|
||||
anchors.txt
|
||||
|
||||
29
.lapce/run.toml
Normal file
29
.lapce/run.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
# The run config is used for both run mode and debug mode
|
||||
|
||||
[[configs]]
|
||||
# the name of this task
|
||||
name = "task"
|
||||
|
||||
# the type of the debugger. If not set, it can't be debugged but can still be run
|
||||
# type = "lldb"
|
||||
|
||||
# the program to run
|
||||
program = "./target/debug/stardust-xr-server"
|
||||
|
||||
# the program arguments, e.g. args = ["arg1", "arg2"], optional
|
||||
# args = []
|
||||
|
||||
# current working directory, optional
|
||||
cwd = "${workspace}"
|
||||
|
||||
# enviroment variables, optional
|
||||
# [configs.env]
|
||||
# VAR1 = "VAL1"
|
||||
# VAR2 = "VAL2"
|
||||
|
||||
# task to run before the run/debug session is started, optional
|
||||
# [configs.prelaunch]
|
||||
# program = "cargo"
|
||||
# args = [
|
||||
# "build",
|
||||
# ]
|
||||
94
.zed/debug.json
Normal file
94
.zed/debug.json
Normal file
@@ -0,0 +1,94 @@
|
||||
[
|
||||
{
|
||||
"label": "Debug",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "target/debug/stardust-xr-server",
|
||||
"args": [],
|
||||
"env": {
|
||||
"RUST_LOG": "debug"
|
||||
},
|
||||
"request": "launch",
|
||||
"build": {
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=stardust-xr-server",
|
||||
"--package=stardust-xr-server"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Debug (Overlay)",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "target/debug/stardust-xr-server",
|
||||
"args": ["-o", "1"],
|
||||
"env": {
|
||||
"RUST_LOG": "debug"
|
||||
},
|
||||
"request": "launch",
|
||||
"build": {
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=stardust-xr-server",
|
||||
"--package=stardust-xr-server"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Debug (Headless)",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "target/debug/stardust-xr-server",
|
||||
"args": [],
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
"DISPLAY": "",
|
||||
"WAYLAND_DISPLAY": ""
|
||||
},
|
||||
"request": "launch",
|
||||
"build": {
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=stardust-xr-server",
|
||||
"--package=stardust-xr-server"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Debug (Flatscreen)",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "target/debug/stardust-xr-server",
|
||||
"args": ["-f"],
|
||||
"env": {
|
||||
"RUST_LOG": "debug"
|
||||
},
|
||||
"request": "launch",
|
||||
"build": {
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=stardust-xr-server",
|
||||
"--package=stardust-xr-server"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Debug (Flatscreen, Restore Latest)",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "target/debug/stardust-xr-server",
|
||||
"args": ["-f", "--restore", "latest"],
|
||||
"env": {
|
||||
"RUST_LOG": "debug"
|
||||
},
|
||||
"request": "launch",
|
||||
"build": {
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=stardust-xr-server",
|
||||
"--package=stardust-xr-server"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
6539
Cargo.lock
generated
6539
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
219
Cargo.toml
219
Cargo.toml
@@ -1,102 +1,177 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
rust-version = "1.89"
|
||||
name = "stardust-xr-server"
|
||||
version = "0.43.0"
|
||||
version = "0.45.0"
|
||||
authors = ["Nova King <technobaboo@proton.me>"]
|
||||
description = "Stardust XR reference display server"
|
||||
license = "GPLv2"
|
||||
repository = "https://github.com/StardustXR/stardust-xr-server/"
|
||||
homepage = "https://stardustxr.org"
|
||||
|
||||
[workspace]
|
||||
members = ["codegen"]
|
||||
|
||||
[workspace.dependencies.stardust-xr]
|
||||
git = "https://github.com/StardustXR/core.git"
|
||||
branch = "dev"
|
||||
|
||||
[[bin]]
|
||||
name = "stardust-xr-server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["wayland"]
|
||||
wayland = ["dep:smithay", "dep:xkbcommon"]
|
||||
xwayland = ["smithay/xwayland"]
|
||||
wayland = [
|
||||
"dep:waynest",
|
||||
"dep:waynest-protocols",
|
||||
"dep:waynest-server",
|
||||
"dep:tokio-stream",
|
||||
"dep:memmap2",
|
||||
"dep:drm-fourcc",
|
||||
"dep:memfd",
|
||||
"dep:vulkano",
|
||||
"dep:wgpu-hal",
|
||||
"dep:ash",
|
||||
]
|
||||
profile_tokio = ["dep:console-subscriber", "tokio/tracing"]
|
||||
profile_app = ["dep:tracing-tracy"]
|
||||
profile_app = [
|
||||
"dep:tracing-tracy",
|
||||
"bevy/trace_tracy",
|
||||
"bevy/trace",
|
||||
"dep:tracy-client",
|
||||
]
|
||||
bevy_debugging = ["bevy/bevy_remote", "bevy/track_location"]
|
||||
|
||||
[package.metadata.appimage]
|
||||
auto_link = true
|
||||
auto_link_exclude_list = [
|
||||
"libc*",
|
||||
"libdl*",
|
||||
"libpthread*",
|
||||
"ld-linux*",
|
||||
"libGL*",
|
||||
"libEGL*",
|
||||
]
|
||||
auto_link_exclude_list = ["libc*", "libdl*", "libpthread*", "ld-linux*"]
|
||||
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
# strip = "symbols"
|
||||
debug = true
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
|
||||
[patch.crates-io]
|
||||
bevy_mod_openxr = { git = "https://github.com/awtterpip/bevy_oxr", rev = "9fd0c797" }
|
||||
bevy_mod_xr = { git = "https://github.com/awtterpip/bevy_oxr", rev = "9fd0c797" }
|
||||
bevy_gltf = { git = "https://github.com/Schmarni-Dev/bevy", branch = "gltf_backport" }
|
||||
# TODO: figure out how to not need this horrifing patch
|
||||
wgpu = { git = "https://github.com/Schmarni-Dev/wgpu", branch = "bad_dmabuf_workaround" }
|
||||
wgpu-types = { git = "https://github.com/Schmarni-Dev/wgpu", branch = "bad_dmabuf_workaround" }
|
||||
wgpu-core = { git = "https://github.com/Schmarni-Dev/wgpu", branch = "bad_dmabuf_workaround" }
|
||||
naga = { git = "https://github.com/Schmarni-Dev/wgpu", branch = "bad_dmabuf_workaround" }
|
||||
wgpu-hal = { git = "https://github.com/Schmarni-Dev/wgpu", branch = "bad_dmabuf_workaround" }
|
||||
bevy_pbr = { git = "https://github.com/Schmarni-Dev/bevy", branch = "premul_oit_016" }
|
||||
bevy_core_pipeline = { git = "https://github.com/Schmarni-Dev/bevy", branch = "premul_oit_016" }
|
||||
bevy_render = { git = "https://github.com/Schmarni-Dev/bevy", branch = "render_thread_init_callback" }
|
||||
|
||||
[dependencies]
|
||||
color-eyre = { version = "0.6.2", default-features = false }
|
||||
clap = { version = "4.2.4", features = ["derive"] }
|
||||
glam = { version = "0.23.0", features = ["mint"] }
|
||||
lazy_static = "1.4.0"
|
||||
mint = "0.5.9"
|
||||
# small utility thingys
|
||||
nanoid = "0.4.0"
|
||||
once_cell = "1.17.1"
|
||||
parking_lot = "0.12.1"
|
||||
portable-atomic = { version = "1.2.0", features = ["float", "std"] }
|
||||
rustc-hash = "1.1.0"
|
||||
tokio = { version = "1.27.0", features = ["rt-multi-thread", "signal", "time"] }
|
||||
send_wrapper = "0.6.0"
|
||||
prisma = "0.1.1"
|
||||
directories = "5.0.0"
|
||||
serde = { version = "1.0.160", features = ["derive"] }
|
||||
serde_repr = "0.1.16"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
global_counter = "0.2.2"
|
||||
rand = "0.8.5"
|
||||
atty = "0.2.14"
|
||||
xkbcommon = { version = "0.6.0", default-features = false, optional = true }
|
||||
ctrlc = "3.4.1"
|
||||
libc = "0.2.148"
|
||||
input-event-codes = "5.16.8"
|
||||
drm-fourcc = { version = "2.2.0", features = ["serde"] }
|
||||
lazy_static = "1.5.0"
|
||||
rand = "0.9.2"
|
||||
rustc-hash = "2.0.0"
|
||||
slotmap = "1.0.7"
|
||||
global_counter = "=0.2.2"
|
||||
parking_lot = "0.12.3"
|
||||
dashmap = "6.1.0"
|
||||
|
||||
[dependencies.smithay]
|
||||
# git = "https://github.com/technobaboo/smithay.git" # Until we get stereokit to understand OES samplers and external textures
|
||||
git = "https://github.com/smithay/smithay.git" # Until we get stereokit to understand OES samplers and external textures
|
||||
# path = "../smithay"
|
||||
default-features = false
|
||||
features = [
|
||||
"desktop",
|
||||
"backend_drm",
|
||||
"backend_egl",
|
||||
"renderer_gl",
|
||||
"wayland_frontend",
|
||||
]
|
||||
version = "*"
|
||||
optional = true
|
||||
# rust errors/logging
|
||||
color-eyre = { version = "0.6.3", default-features = false }
|
||||
clap = { version = "4.5.13", features = ["derive"] }
|
||||
console-subscriber = { version = "0.4.0", optional = true }
|
||||
thiserror = "2.0.11"
|
||||
tracing = { version = "0.1.40", features = ["release_max_level_warn"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracing-tracy = { version = "0.11.1", optional = true }
|
||||
tracy-client = { version = "=0.18.0", optional = true }
|
||||
|
||||
[dependencies.stereokit]
|
||||
default-features = false
|
||||
features = ["linux-egl"]
|
||||
version = "0.16.9"
|
||||
# (de)serialization
|
||||
serde = { version = "1.0.205", features = ["derive"] }
|
||||
serde_repr = "0.1.19"
|
||||
toml = "0.9.7"
|
||||
|
||||
[dependencies.console-subscriber]
|
||||
version = "0.1.8"
|
||||
optional = true
|
||||
# mathy stuffs
|
||||
glam = { version = "0.29.0", features = ["mint", "serde"] }
|
||||
mint = "0.5.9"
|
||||
tokio = { version = "1.39.2", features = ["rt-multi-thread", "signal", "time"] }
|
||||
|
||||
[dependencies.tracing-tracy]
|
||||
version = "0.10.4"
|
||||
optional = true
|
||||
# bevy
|
||||
bevy = { version = "0.16", default-features = false, features = [
|
||||
"bevy_asset",
|
||||
"bevy_audio",
|
||||
"bevy_color",
|
||||
"bevy_core_pipeline",
|
||||
"bevy_gizmos",
|
||||
"bevy_gltf",
|
||||
"bevy_log",
|
||||
"bevy_pbr",
|
||||
"bevy_render",
|
||||
"bevy_window",
|
||||
"bevy_winit",
|
||||
"std",
|
||||
"wayland",
|
||||
"mp3",
|
||||
"wav",
|
||||
"qoi",
|
||||
"png",
|
||||
"hdr",
|
||||
"jpeg",
|
||||
"multi_threaded",
|
||||
] }
|
||||
bevy_mod_xr = "0.3"
|
||||
bevy_mod_openxr = "0.3"
|
||||
# bevy_sk.git = "https://github.com/MalekiRe/bevy_sk"
|
||||
bevy_sk = { git = "https://github.com/technobaboo/bevy_sk", branch = "stochastic" }
|
||||
# bevy-dmabuf = "0.2.0"
|
||||
bevy-dmabuf.git = "https://github.com/Schmarni-Dev/bevy-dmabuf"
|
||||
bevy-equirect = "0.1.1"
|
||||
|
||||
# bevy-mesh-text-3d.git = "https://github.com/terhechte/bevy-mesh-text-3d"
|
||||
# use my fork until my pr to use minimal bevy features was merged
|
||||
bevy-mesh-text-3d.git = "https://github.com/Schmarni-Dev/bevy-mesh-text-3d"
|
||||
|
||||
openxr = "0.19"
|
||||
|
||||
# linux stuffs
|
||||
input-event-codes = "6.2.0"
|
||||
zbus = { version = "5.11.0", features = [
|
||||
"blocking-api",
|
||||
"tokio",
|
||||
], default-features = false }
|
||||
directories = "6.0.0"
|
||||
xkbcommon-rs = "0.1.0"
|
||||
cosmic-text = "0.14.2"
|
||||
|
||||
# Wayland
|
||||
waynest = { git = "https://github.com/verdiwm/waynest.git", default-features = false, optional = true }
|
||||
waynest-protocols = { git = "https://github.com/verdiwm/waynest.git", features = [
|
||||
"server",
|
||||
"stable",
|
||||
"mesa",
|
||||
"tracing",
|
||||
], default-features = false, optional = true }
|
||||
waynest-server = { git = "https://github.com/verdiwm/waynest.git", default-features = false, optional = true }
|
||||
tokio-stream = { version = "0.1.17", optional = true }
|
||||
memmap2 = { version = "0.9.5", optional = true }
|
||||
drm-fourcc = { version = "2.2.0", optional = true }
|
||||
memfd = { version = "0.6.4", optional = true }
|
||||
vulkano = { git = "https://github.com/Schmarni-Dev/vulkano", branch = "0_35_dmabuf_fixes", default-features = false, optional = true }
|
||||
wgpu-hal = { version = "24", optional = true, features = ["vulkan"] }
|
||||
ash = { version = "0.38.0", optional = true, default-features = false }
|
||||
rustix = { version = "1.0.8", features = ["time"] }
|
||||
pin-project-lite = "0.2.16"
|
||||
futures-sink = "0.3.31"
|
||||
|
||||
[dependencies.stardust-xr]
|
||||
git = "https://github.com/StardustXR/core.git"
|
||||
branch = "camera-item"
|
||||
|
||||
# [patch.crates-io.stereokit]
|
||||
# path = "../stereokit-rs"
|
||||
# [patch.crates-io.stereokit-sys]
|
||||
# path = "../stereokit-sys"
|
||||
workspace = true
|
||||
[dependencies.stardust-xr-server-codegen]
|
||||
path = "codegen"
|
||||
|
||||
154
README.md
154
README.md
@@ -1,85 +1,113 @@
|
||||
# Stardust XR Reference Server
|
||||
# Stardust XR Server
|
||||
|
||||
This project is a usable Linux display server that reinvents human-computer interaction for all kinds of XR, from putting 2D/XR apps into various 3D shells for varying uses to SDF-based interaction.
|
||||
Stardust XR is a display server for VR and AR headsets on Linux-based systems. [Stardust provides a 3D environment](https://www.youtube.com/watch?v=v2WblwbaLaA), where anything from 2D windows (including your existing apps!), to 3D apps built from objects, can exist together in physical space.
|
||||
|
||||
## Prerequisites
|
||||
1. Cargo
|
||||
2. CMake
|
||||
3. EGL+GLES 3.2
|
||||
4. GLX+Xlib
|
||||
5. fontconfig
|
||||
6. dlopen
|
||||
7. OpenXR Loader (required even if run in flatscreen mode)
|
||||

|
||||
|
||||
Command line installation of core & dynamic dependencies are provided below:
|
||||
<details>
|
||||
<summary>Ubuntu/Debian</summary>
|
||||
<pre><code class="language-bash">
|
||||
sudo apt update && sudo apt install \
|
||||
build-essential \
|
||||
cargo \
|
||||
cmake \
|
||||
libxkbcommon-dev libudev1 libinput10 libcap2 libmtdev1 libevdev2 libwacom9 libgudev-1.0-0 \
|
||||
libglib2.0-dev libffi8 libpcre2-dev libxkbcommon-x11-dev libxcb-dev libxcb-xkb-dev libxau-dev \
|
||||
libstdc++-dev libx11-dev libxfixes-dev libegl-dev libgbm-dev libfontconfig1-dev libgl-dev \
|
||||
libdrm-dev libexpat1-dev libfreetype6-dev libxml2-dev zlib1g-dev libbz2-dev libpng-dev \
|
||||
libharfbuzz-dev libbrotli-dev liblzma-dev libraphite2-dev
|
||||
</code></pre>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Fedora</summary>
|
||||
<pre><code class="language-bash">
|
||||
sudo apt update && sudo apt install \
|
||||
cargo \
|
||||
cmake \
|
||||
libxkbcommon-devel systemd-devel libinput-devel libcap-devel mtdev-devel libevdev-devel glib2-devel \
|
||||
libffi-devel pcre2-devel libxkbcommon-x11-devel libxcb-devel libXau-devel libstdc++-devel libx11-devel libxfixes-devel \
|
||||
mesa-libEGL-devel mesa-libgbm-devel fontconfig-devel libdrm-devel expat-devel freetype-devel libxml2-devel zlib-devel \
|
||||
bzip2-devel libpng-devel harfbuzz-devel brotli-devel xz-devel graphite2-devel
|
||||
</code></pre>
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Arch Linux</summary>
|
||||
<pre><code class="language-bash">
|
||||
sudo pacman -Syu --needed \
|
||||
cargo \
|
||||
cmake \
|
||||
libxkbcommon systemd libinput libcap mtdev libevdev libwacom glib2 libffi pcre2 libxkbcommon-x11 \
|
||||
libxcb libxau libx11 libxfixes mesa fontconfig libdrm expat freetype2 libxml2 zlib bzip2 \
|
||||
libpng harfbuzz brotli xz graphite
|
||||
</code></pre>
|
||||
</details>
|
||||
|
||||
## Installation
|
||||
|
||||
More detailed instructions and walkthroughs are provided at https://www.stardustxr.org
|
||||
|
||||
The [Terra Repository](https://terra.fyralabs.com/) is required, and comes pre-installed with [Ultramarine Linux](https://ultramarine-linux.org/). Other Fedora Editions and derivatives can directly install terra-release:
|
||||
|
||||
## Build
|
||||
```bash
|
||||
cargo build
|
||||
sudo dnf install --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release
|
||||
```
|
||||
|
||||
For a full installation of the Stardust XR server *and* a selected group of clients, run:
|
||||
|
||||
```bash
|
||||
sudo dnf group install stardust-xr
|
||||
```
|
||||
|
||||
## Manual Build
|
||||
We've provided a manual installation script [here](https://github.com/cyberneticmelon/usefulscripts/blob/main/stardustxr_setup.sh) that clones and builds the Stardust XR server along with a number of other clients from their respective repositories, and provides a startup script for automatically launching some clients.
|
||||
|
||||
After cloning the repository
|
||||
```bash
|
||||
cargo build --release # this is needed to skip validation layers
|
||||
```
|
||||
The latest stable server is automatically built to an appimage at https://github.com/StardustXR/server/releases for easy testing.
|
||||
|
||||
## Usage
|
||||
> [!NOTE]
|
||||
> For help with setting up an XR headset on linux, visit https://stardustxr.org/docs/get-started/setup-openxr
|
||||
|
||||
First, try running `cargo run` in a terminal window. If a headset is plugged in and OpenXR is working no window will show up. However, the headset should show the same things as the window that opens:
|
||||
|
||||

|
||||
The **Stardust XR Server** is a server that runs clients, so without any running, you will see a black screen. If you only have the server installed, we recommend also cloning and building the following clients to start: [Flatland](https://github.com/StardustXR/flatland), which allows normal 2D apps to run in Stardust, [Protostar](https://github.com/StardustXR/protostar), which contains Hexagon Launcher, an app launcher menu, and [Black Hole](https://github.com/StardustXR/black-hole) to quickly tuck away your objects and apps (kind of like desktop peek on Windows).
|
||||
|
||||
Stardust won't do anything interesting without clients! Try some from https://github.com/StardustXR.
|
||||
First, try running `cargo run -- -f` in a terminal window to check out flatscreen mode, (or `stardust-xr-server -f` / `stardust-xr-server_dev -f` if you installed via dnf or the manual installation script, respectively, as they provide symlinks.)
|
||||
|
||||
### Default Sky
|
||||
If there aren't already any clients running, you'll need to manually launch them by either navigating to their repositories and running `cargo run`, or running them via their names if you installed via dnf or the manual installation script, such as `flatland`, `hexagon_launcher`, etc.
|
||||
|
||||
You can set a default skytex/skylight by putting your favorite HDRI equirectangular sky in `~/.config/stardust/skytex.hdr`. Certain clients can override this.
|
||||
> [!IMPORTANT]
|
||||
> [Flatland](https://github.com/StardustXR/flatland) must be running for 2D apps to launch.
|
||||
|
||||
Flatscreen mode when the default skybox is [Zhengyang Gate](https://polyhaven.com/a/zhengyang_gate):
|
||||

|
||||
### Startup Script
|
||||
A startup script can be created at `~/.config/stardust/startup` that will launch specified settings and clients/applications, an example of which is shown [here](https://github.com/cyberneticmelon/usefulscripts/blob/main/startup). If you used the [installation script](https://github.com/cyberneticmelon/usefulscripts/blob/main/stardustxr_setup.sh), one will have already been made for you. This allows wide flexibility of what clients to launch upon startup (and, for example, *where*, using the [Gravity](https://github.com/StardustXR/gravity) client to specify X Y and Z co-ordinates).
|
||||
|
||||
### Windowed Mode
|
||||
### Flatscreen Navigation
|
||||
A video guide showcasing flatscreen controls is available [here](https://www.youtube.com/watch?v=JCYecSlKlDI)
|
||||
|
||||
If the stardust server can't connect to an OpenXR runtime or you force it into flatscreen mode with `-f`, the server will show in a window.
|
||||

|
||||
To move around, hold down `Shift + W A S D`, with `Q` for moving down and `E` for moving up.
|
||||

|
||||
|
||||
You can navigate around by right click + dragging to look around, Shift+W/A/S/D/Q/E to move. If you have a virtual hand, left click pinches, right click points, both make a fist.
|
||||
To look around, hold down `Shift + Right` Click while moving the mouse.
|
||||

|
||||
|
||||
### Flags
|
||||
#### Flatscreen (-f)
|
||||
To drag applications out of the app launcher, hold down `Shift + ~`
|
||||

|
||||
|
||||
The server will show up in windowed mode no matter what with your mouse pointer being turned into a 3D pointer. Keyboard input will be sent to whatever your mouse is hovering over like visionOS simulator.
|
||||
Flatscreen mode upon initial startup:
|
||||

|
||||
### XR Navigation
|
||||
A video guide showcasing XR controls is available [here](https://www.youtube.com/watch?v=RbxFq6JjliA)
|
||||
|
||||
#### Overlay (-o \<PRIORITY>)
|
||||
**Quest 3 Hand tracking**:
|
||||
Pinch to drag and drop, grasp with full hand for grabbing, point and click with pointer finger to click or pinch from a distance
|
||||
|
||||
The server will, if in XR mode, be overlaid using the OpenXR overlay extension with the given priority.
|
||||

|
||||
|
||||
#### Disable controller (--disable-controller)
|
||||
**Quest 3 Controller**:
|
||||
Grab with the grip buttons, click by touching the tip of the cones or by using the trigger from a distance
|
||||
|
||||
Some runtimes such as Monado may emulate a controller using a hand, and this messes with Stardust's input system. Set this flag to ignore the controllers that the OpenXR runtime provides.
|
||||
|
||||
#### Execute (-e </path/to/executable>)
|
||||
|
||||
When wayland and OpenXR and such are initialized, run the given executable (such as a bash script) with all the environment variables needed to connect all clients of any type to the server. If not set, the server will run the executable at `~/.config/stardust/startup` if it exists. This is how stardust desktop environments can be made.
|
||||
|
||||
#### Help (-h, --help)
|
||||
|
||||
help
|
||||
|
||||
## Test
|
||||
|
||||
##### Gnome Graphical Integration Test
|
||||
|
||||
- `nix build .#gnome-graphical-test`
|
||||
|
||||
This test uses Nix to reproducibly execute a QEMU virtual machine which
|
||||
spawns a full Gnome desktop. It runs `monado-service`, `stardust-xr-server`
|
||||
`flatland` underneath of Gnome and then attaches `weston-cliptest` to the
|
||||
`flatland` process running underneath of `stardust-xr-server`, the result is
|
||||
a screenshot in PNG format that should look like expected. If any process in
|
||||
this test produces an exit code above 0, the test will fail, graphical bugs
|
||||
should be visible in the screenshot. An example of the result is below.
|
||||
|
||||
###### Result
|
||||
|
||||

|
||||
|
||||
##### Everything
|
||||
|
||||
`nix flake check` will build every test underneath of the `checks` attribute in the `flake.nix`
|
||||

|
||||
|
||||
14
codegen/Cargo.toml
Normal file
14
codegen/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "stardust-xr-server-codegen"
|
||||
version = "0.1.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
convert_case = "0.6.0"
|
||||
quote = "1.0.33"
|
||||
proc-macro2 = "1.0.71"
|
||||
|
||||
stardust-xr = { workspace = true }
|
||||
752
codegen/src/lib.rs
Normal file
752
codegen/src/lib.rs
Normal file
@@ -0,0 +1,752 @@
|
||||
use convert_case::{Case, Casing};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{quote, ToTokens};
|
||||
use stardust_xr::schemas::protocol::*;
|
||||
|
||||
fn fold_tokens(a: TokenStream, b: TokenStream) -> TokenStream {
|
||||
quote!(#a #b)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn codegen_root_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
codegen_protocol(ROOT_PROTOCOL)
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn codegen_node_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
codegen_protocol(NODE_PROTOCOL)
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn codegen_spatial_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
codegen_protocol(SPATIAL_PROTOCOL)
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn codegen_field_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
codegen_protocol(FIELD_PROTOCOL)
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn codegen_audio_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
codegen_protocol(AUDIO_PROTOCOL)
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn codegen_drawable_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
codegen_protocol(DRAWABLE_PROTOCOL)
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn codegen_input_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
codegen_protocol(INPUT_PROTOCOL)
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn codegen_item_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
codegen_protocol(ITEM_PROTOCOL)
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn codegen_item_camera_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
codegen_protocol(ITEM_CAMERA_PROTOCOL)
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn codegen_item_panel_protocol(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
codegen_protocol(ITEM_PANEL_PROTOCOL)
|
||||
}
|
||||
|
||||
fn codegen_protocol(protocol: &'static str) -> proc_macro::TokenStream {
|
||||
let protocol = Protocol::parse(protocol).unwrap();
|
||||
let interface = protocol
|
||||
.interface
|
||||
.map(|p| {
|
||||
let node_id = p.node_id;
|
||||
let node_id = quote! {
|
||||
const INTERFACE_NODE_ID: u64 = #node_id;
|
||||
};
|
||||
let aspect = generate_aspect(&Aspect {
|
||||
name: "interface".to_string(),
|
||||
id: 0,
|
||||
description: protocol.description.clone(),
|
||||
inherits: vec![],
|
||||
members: p.members,
|
||||
inherited_aspects: vec![],
|
||||
});
|
||||
quote! {
|
||||
#node_id
|
||||
#aspect
|
||||
pub struct Interface;
|
||||
impl crate::nodes::AspectIdentifier for Interface {
|
||||
impl_aspect_for_interface_aspect_id!{}
|
||||
}
|
||||
impl crate::nodes::Aspect for Interface {
|
||||
impl_aspect_for_interface_aspect!{}
|
||||
}
|
||||
pub fn create_interface(client: &std::sync::Arc<crate::core::client::Client>) -> crate::core::error::Result<()>{
|
||||
let node = crate::nodes::Node::from_id(client,INTERFACE_NODE_ID,false);
|
||||
node.add_aspect(Interface);
|
||||
node.add_to_scenegraph()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let custom_enums = protocol
|
||||
.custom_enums
|
||||
.iter()
|
||||
.map(generate_custom_enum)
|
||||
.reduce(fold_tokens)
|
||||
.unwrap_or_default();
|
||||
let custom_unions = protocol
|
||||
.custom_unions
|
||||
.iter()
|
||||
.map(generate_custom_union)
|
||||
.reduce(fold_tokens)
|
||||
.unwrap_or_default();
|
||||
let custom_structs = protocol
|
||||
.custom_structs
|
||||
.iter()
|
||||
.map(generate_custom_struct)
|
||||
.reduce(fold_tokens)
|
||||
.unwrap_or_default();
|
||||
let aspects = protocol
|
||||
.aspects
|
||||
.iter()
|
||||
.map(|a| generate_aspect(&a.blocking_read()))
|
||||
.reduce(fold_tokens)
|
||||
.unwrap_or_default();
|
||||
quote!(#custom_enums #custom_unions #custom_structs #aspects #interface).into()
|
||||
}
|
||||
|
||||
fn generate_custom_enum(custom_enum: &CustomEnum) -> TokenStream {
|
||||
let name = Ident::new(&custom_enum.name.to_case(Case::Pascal), Span::call_site());
|
||||
let description = &custom_enum.description;
|
||||
|
||||
let argument_decls = custom_enum
|
||||
.variants
|
||||
.iter()
|
||||
.map(|a| Ident::new(&a.to_case(Case::Pascal), Span::call_site()).to_token_stream())
|
||||
.reduce(|a, b| quote!(#a, #b))
|
||||
.unwrap_or_default();
|
||||
|
||||
quote! {
|
||||
#[doc = #description]
|
||||
#[derive(Debug, Clone, Copy, serde_repr::Deserialize_repr, serde_repr::Serialize_repr)]
|
||||
#[repr(u32)]
|
||||
pub enum #name {#argument_decls}
|
||||
}
|
||||
}
|
||||
fn generate_custom_union(custom_union: &CustomUnion) -> TokenStream {
|
||||
let name = Ident::new(&custom_union.name.to_case(Case::Pascal), Span::call_site());
|
||||
let description = &custom_union.description;
|
||||
|
||||
let option_decls = custom_union
|
||||
.options
|
||||
.iter()
|
||||
.map(generate_union_option)
|
||||
.reduce(|a, b| quote!(#a, #b))
|
||||
.unwrap_or_default();
|
||||
|
||||
quote! {
|
||||
#[doc = #description]
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum #name {#option_decls}
|
||||
}
|
||||
}
|
||||
fn generate_union_option(union_option: &UnionOption) -> TokenStream {
|
||||
let name = union_option
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|n| n.to_case(Case::Pascal))
|
||||
.unwrap_or_else(|| argument_type_option_name(&union_option._type));
|
||||
let description = union_option
|
||||
.description
|
||||
.as_ref()
|
||||
.map(|d| quote!(#[doc = #d]))
|
||||
.unwrap_or_default();
|
||||
let identifier = Ident::new(&name, Span::call_site());
|
||||
let _type = generate_argument_type(&union_option._type, false, true);
|
||||
quote! (#description #identifier(#_type))
|
||||
}
|
||||
fn generate_custom_struct(custom_struct: &CustomStruct) -> TokenStream {
|
||||
let name = Ident::new(&custom_struct.name.to_case(Case::Pascal), Span::call_site());
|
||||
let description = &custom_struct.description;
|
||||
|
||||
let argument_decls = custom_struct
|
||||
.fields
|
||||
.iter()
|
||||
.map(|a| generate_argument_decl(a, true))
|
||||
.map(|d| quote!(pub #d))
|
||||
.reduce(|a, b| quote!(#a, #b))
|
||||
.unwrap_or_default();
|
||||
|
||||
quote! {
|
||||
#[doc = #description]
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct #name {#argument_decls}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_aspect(aspect: &Aspect) -> TokenStream {
|
||||
let description = &aspect.description;
|
||||
let (client_members, server_members) = aspect
|
||||
.members
|
||||
.iter()
|
||||
.partition::<Vec<_>, _>(|m| m.side == Side::Client);
|
||||
|
||||
let client_mod_name = Ident::new(
|
||||
&format!("{}_client", &aspect.name.to_case(Case::Snake)),
|
||||
Span::call_site(),
|
||||
);
|
||||
let client_side_members = client_members
|
||||
.into_iter()
|
||||
.map(|m| generate_member(aspect.id, &aspect.name.to_case(Case::Pascal), m))
|
||||
.reduce(fold_tokens)
|
||||
.map(|t| {
|
||||
// TODO: properly import all dependencies
|
||||
quote! {
|
||||
#[allow(clippy::all)]
|
||||
pub mod #client_mod_name {
|
||||
use super::*;
|
||||
#t
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let opcodes = aspect
|
||||
.members
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let aspect_name = aspect.name.to_case(Case::ScreamingSnake);
|
||||
let member_name = m.name.to_case(Case::ScreamingSnake);
|
||||
let name_type = if m.side == Side::Client {
|
||||
"CLIENT"
|
||||
} else {
|
||||
"SERVER"
|
||||
};
|
||||
let name = Ident::new(
|
||||
&format!("{aspect_name}_{member_name}_{name_type}_OPCODE"),
|
||||
Span::call_site(),
|
||||
);
|
||||
let opcode = m.opcode;
|
||||
|
||||
quote!(pub(crate) const #name: u64 = #opcode;)
|
||||
})
|
||||
.reduce(fold_tokens)
|
||||
.unwrap_or_default();
|
||||
let alias_info = generate_alias_info(aspect);
|
||||
|
||||
let server_side_members = server_members
|
||||
.into_iter()
|
||||
.map(|m| generate_member(aspect.id, &aspect.name.to_case(Case::Pascal), m))
|
||||
.reduce(fold_tokens)
|
||||
.unwrap_or_default();
|
||||
let aspect_trait_name = Ident::new(
|
||||
&format!("{}Aspect", &aspect.name.to_case(Case::Pascal)),
|
||||
Span::call_site(),
|
||||
);
|
||||
let run_signals = aspect
|
||||
.members
|
||||
.iter()
|
||||
.filter(|m| m.side == Side::Server)
|
||||
.filter(|m| m._type == MemberType::Signal)
|
||||
.map(|m| generate_run_member(&aspect_trait_name, MemberType::Signal, m))
|
||||
.reduce(fold_tokens)
|
||||
.unwrap_or_default();
|
||||
let run_methods = aspect
|
||||
.members
|
||||
.iter()
|
||||
.filter(|m| m.side == Side::Server)
|
||||
.filter(|m| m._type == MemberType::Method)
|
||||
.map(|m| generate_run_member(&aspect_trait_name, MemberType::Method, m))
|
||||
.reduce(fold_tokens)
|
||||
.unwrap_or_default();
|
||||
let server_side_members = quote! {
|
||||
#[allow(clippy::all)]
|
||||
#[doc = #description]
|
||||
pub trait #aspect_trait_name {
|
||||
#server_side_members
|
||||
}
|
||||
};
|
||||
let aspect_id_macro_name = Ident::new(
|
||||
&format!(
|
||||
"impl_aspect_for_{}_aspect_id",
|
||||
aspect.name.to_case(Case::Snake)
|
||||
),
|
||||
Span::call_site(),
|
||||
);
|
||||
let aspect_macro_name = Ident::new(
|
||||
&format!(
|
||||
"impl_aspect_for_{}_aspect",
|
||||
aspect.name.to_case(Case::Snake)
|
||||
),
|
||||
Span::call_site(),
|
||||
);
|
||||
let aspect_id = aspect.id;
|
||||
let aspect_macro = quote! {
|
||||
macro_rules! #aspect_id_macro_name {
|
||||
() => {
|
||||
const ID: u64 = #aspect_id;
|
||||
}
|
||||
}
|
||||
macro_rules! #aspect_macro_name {
|
||||
() => {
|
||||
#[allow(clippy::all)]
|
||||
fn run_signal(
|
||||
&self,
|
||||
_calling_client: std::sync::Arc<crate::core::client::Client>,
|
||||
_node: std::sync::Arc<crate::nodes::Node>,
|
||||
_signal: u64,
|
||||
_message: crate::nodes::Message
|
||||
) -> Result<(), stardust_xr::scenegraph::ScenegraphError> {
|
||||
match _signal {
|
||||
#run_signals
|
||||
_ => Err(stardust_xr::scenegraph::ScenegraphError::MemberNotFound)
|
||||
}
|
||||
}
|
||||
#[allow(clippy::all)]
|
||||
fn run_method(
|
||||
&self,
|
||||
_calling_client: std::sync::Arc<crate::core::client::Client>,
|
||||
_node: std::sync::Arc<crate::nodes::Node>,
|
||||
_method: u64,
|
||||
_message: crate::nodes::Message,
|
||||
_method_response: crate::core::scenegraph::MethodResponseSender,
|
||||
) {
|
||||
match _method {
|
||||
#run_methods
|
||||
_ => {
|
||||
let _ = _method_response.send_err(stardust_xr::scenegraph::ScenegraphError::MemberNotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
quote!(#opcodes #alias_info #client_side_members #server_side_members #aspect_macro)
|
||||
}
|
||||
|
||||
fn generate_alias_opcodes(aspect: &Aspect, side: Side, _type: MemberType) -> TokenStream {
|
||||
aspect
|
||||
.members
|
||||
.iter()
|
||||
.filter(|m| m.side == side)
|
||||
.filter(|m| m._type == _type)
|
||||
.map(|m| m.opcode)
|
||||
.map(|o| quote!(#o))
|
||||
.reduce(|a, b| quote!(#a, #b))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
fn generate_alias_info(aspect: &Aspect) -> TokenStream {
|
||||
let aspect_alias_info_name = Ident::new(
|
||||
&format!(
|
||||
"{}_ASPECT_ALIAS_INFO",
|
||||
aspect.name.to_case(Case::ScreamingSnake)
|
||||
),
|
||||
Span::call_site(),
|
||||
);
|
||||
let local_signals = generate_alias_opcodes(aspect, Side::Server, MemberType::Signal);
|
||||
let local_methods = generate_alias_opcodes(aspect, Side::Server, MemberType::Method);
|
||||
let remote_signals = generate_alias_opcodes(aspect, Side::Client, MemberType::Signal);
|
||||
|
||||
let inherits = aspect
|
||||
.inherits
|
||||
.iter()
|
||||
.map(|a| {
|
||||
Ident::new(
|
||||
&format!("{}_ASPECT_ALIAS_INFO", a.to_case(Case::ScreamingSnake)),
|
||||
Span::call_site(),
|
||||
)
|
||||
})
|
||||
.map(|a| quote!(#a.clone()))
|
||||
.fold(quote!(), |a, b| quote!(#a + #b));
|
||||
|
||||
quote! {
|
||||
lazy_static::lazy_static! {
|
||||
#[allow(clippy::all)]
|
||||
pub static ref #aspect_alias_info_name: crate::nodes::alias::AliasInfo = crate::nodes::alias::AliasInfo {
|
||||
server_signals: vec![#local_signals],
|
||||
server_methods: vec![#local_methods],
|
||||
client_signals: vec![#remote_signals],
|
||||
}
|
||||
#inherits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_member(aspect_id: u64, aspect_name: &str, member: &Member) -> TokenStream {
|
||||
let opcode = member.opcode;
|
||||
let name = Ident::new(&member.name.to_case(Case::Snake), Span::call_site());
|
||||
let description = &member.description;
|
||||
|
||||
let side = member.side;
|
||||
let _type = member._type;
|
||||
|
||||
let first_args = match member.side {
|
||||
Side::Server => {
|
||||
quote!(_node: std::sync::Arc<crate::nodes::Node>, _calling_client: std::sync::Arc<crate::core::client::Client>)
|
||||
}
|
||||
Side::Client => quote!(_node: &crate::nodes::Node),
|
||||
};
|
||||
|
||||
let arguments = member
|
||||
.arguments
|
||||
.iter()
|
||||
.map(|a| Ident::new(&a.name.to_case(Case::Snake), Span::call_site()));
|
||||
let argument_debug = member
|
||||
.arguments
|
||||
.iter()
|
||||
.map(|a| Ident::new(&a.name.to_case(Case::Snake), Span::call_site()))
|
||||
.map(|n| quote!(?#n))
|
||||
.reduce(|a, b| quote!(#a, #b))
|
||||
.map(|args| quote!(#args,));
|
||||
let argument_decls = member
|
||||
.arguments
|
||||
.iter()
|
||||
.map(|a| generate_argument_decl(a, member.side == Side::Server))
|
||||
.fold(first_args, |a, b| quote!(#a, #b));
|
||||
|
||||
let argument_uses = member
|
||||
.arguments
|
||||
.iter()
|
||||
.map(|a| generate_argument_serialize(&a.name, &a._type, a.optional))
|
||||
.reduce(|a, b| quote!(#a, #b))
|
||||
.unwrap_or_default();
|
||||
let return_type = member
|
||||
.return_type
|
||||
.as_ref()
|
||||
.map(|r| generate_argument_type(r, false, true))
|
||||
.unwrap_or_else(|| quote!(()));
|
||||
let name_str = name.to_string();
|
||||
|
||||
match (side, _type) {
|
||||
(Side::Client, MemberType::Signal) => {
|
||||
quote! {
|
||||
#[doc = #description]
|
||||
pub fn #name(#argument_decls) -> crate::core::error::Result<()> {
|
||||
let arguments = (#argument_uses);
|
||||
let (#(#arguments),*) = &arguments;
|
||||
::tracing::trace!(#argument_debug "sent signal to client: {}::{}", #aspect_name, #name_str);
|
||||
let result = stardust_xr::schemas::flex::serialize(&arguments).map_err(|e|e.into()).and_then(|serialized|_node.send_remote_signal(#aspect_id, #opcode, serialized));
|
||||
|
||||
if let Err(err) = result.as_ref() {
|
||||
::tracing::warn!(#argument_debug "failed to send signal to client : {}::{}, error: {}",#aspect_name,#name_str,err);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
(Side::Client, MemberType::Method) => {
|
||||
quote! {
|
||||
#[doc = #description]
|
||||
pub async fn #name(#argument_decls) -> crate::core::error::Result<(#return_type, Vec<std::os::fd::OwnedFd>)> {
|
||||
let arguments = (#argument_uses);
|
||||
let (#(#arguments),*) = &arguments;
|
||||
::tracing::trace!(#argument_debug "called client method: {}::{}",#aspect_name,#name_str);
|
||||
let result = _node.execute_remote_method_typed(#aspect_id, #opcode, &arguments, vec![]).await;
|
||||
|
||||
match result.as_ref() {
|
||||
Ok(value) => {
|
||||
::tracing::trace!(?value, "client method returned value: {}::{}",#aspect_name,#name_str);
|
||||
},
|
||||
Err(err) => {
|
||||
::tracing::warn!(#argument_debug "client method returned error: {}::{}, error: {}",#aspect_name,#name_str,err);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
(Side::Server, MemberType::Signal) => {
|
||||
quote! {
|
||||
#[doc = #description]
|
||||
fn #name(#argument_decls) -> crate::core::error::Result<()>;
|
||||
}
|
||||
}
|
||||
(Side::Server, MemberType::Method) => {
|
||||
quote! {
|
||||
#[doc = #description]
|
||||
fn #name(#argument_decls) -> impl std::future::Future<Output = crate::core::error::Result<#return_type>> + Send + Sync + 'static;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn generate_run_member(aspect_name: &Ident, _type: MemberType, member: &Member) -> TokenStream {
|
||||
let opcode = member.opcode;
|
||||
let member_name_ident = Ident::new(&member.name, Span::call_site());
|
||||
let member_name = member_name_ident.to_string();
|
||||
let aspect_name_str = aspect_name.to_string();
|
||||
|
||||
let argument_names = member
|
||||
.arguments
|
||||
.iter()
|
||||
.map(generate_argument_name)
|
||||
.reduce(|a, b| quote!(#a, #b));
|
||||
let argument_types = member
|
||||
.arguments
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let _type = convert_deserializeable_argument_type(&a._type);
|
||||
generate_argument_type(&_type, a.optional, true)
|
||||
})
|
||||
.reduce(|a, b| quote!(#a, #b));
|
||||
let argument_debug = member
|
||||
.arguments
|
||||
.iter()
|
||||
.map(|a| Ident::new(&a.name.to_case(Case::Snake), Span::call_site()))
|
||||
.map(|n| quote!(?#n))
|
||||
.reduce(|a, b| quote!(#a, #b))
|
||||
.map(|args| quote!(#args,));
|
||||
// dbg!(&argument_types);
|
||||
let deserialize = argument_names
|
||||
.clone()
|
||||
.zip(argument_types)
|
||||
.map(|(argument_names, argument_types)| {
|
||||
quote!{
|
||||
#[allow(unused_parens)]
|
||||
let (#argument_names): (#argument_types) = stardust_xr::schemas::flex::deserialize(_message.as_ref())?;
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let serialize = generate_argument_serialize(
|
||||
"result",
|
||||
&member.return_type.clone().unwrap_or(ArgumentType::Empty),
|
||||
false,
|
||||
);
|
||||
let argument_uses = member
|
||||
.arguments
|
||||
.iter()
|
||||
.map(|a| generate_argument_deserialize(&a.name, &a._type, a.optional))
|
||||
.reduce(|a, b| quote!(#a, #b))
|
||||
.unwrap_or_default();
|
||||
match _type {
|
||||
MemberType::Signal => quote! {
|
||||
#opcode => (move || {
|
||||
#deserialize
|
||||
::tracing::trace!(#argument_debug "received local signal: {}::{}",#aspect_name_str,#member_name);
|
||||
<Self as #aspect_name>::#member_name_ident(_node, _calling_client.clone(), #argument_uses)
|
||||
})().map_err(|e: crate::core::error::ServerError| stardust_xr::scenegraph::ScenegraphError::MemberError { error: e.to_string() }),
|
||||
},
|
||||
MemberType::Method => quote! {
|
||||
#opcode => _method_response.wrap_async(async move {
|
||||
#deserialize
|
||||
::tracing::trace!(#argument_debug "called local method: {}::{}",#aspect_name_str,#member_name);
|
||||
let result = <Self as #aspect_name>::#member_name_ident(_node, _calling_client.clone(), #argument_uses).await;
|
||||
|
||||
match result.as_ref() {
|
||||
Ok(value) => {
|
||||
::tracing::trace!(?value, "client method returned value: {}::{}",#aspect_name_str,#member_name);
|
||||
},
|
||||
Err(err) => {
|
||||
::tracing::warn!("client method returned error: {}::{}, error: {}",#aspect_name_str,#member_name,err);
|
||||
}
|
||||
}
|
||||
let result = result?;
|
||||
Ok((#serialize, Vec::<std::os::fd::OwnedFd>::new()))
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
fn generate_argument_name(argument: &Argument) -> TokenStream {
|
||||
Ident::new(&argument.name.to_case(Case::Snake), Span::call_site()).to_token_stream()
|
||||
}
|
||||
|
||||
fn convert_deserializeable_argument_type(argument_type: &ArgumentType) -> ArgumentType {
|
||||
match argument_type {
|
||||
ArgumentType::Node { .. } => ArgumentType::NodeID,
|
||||
ArgumentType::Vec(v) => {
|
||||
ArgumentType::Vec(Box::new(convert_deserializeable_argument_type(v.as_ref())))
|
||||
}
|
||||
ArgumentType::Map(v) => {
|
||||
ArgumentType::Map(Box::new(convert_deserializeable_argument_type(v.as_ref())))
|
||||
}
|
||||
f => f.clone(),
|
||||
}
|
||||
}
|
||||
fn generate_argument_deserialize(
|
||||
argument_name: &str,
|
||||
argument_type: &ArgumentType,
|
||||
optional: bool,
|
||||
) -> TokenStream {
|
||||
let name = Ident::new(&argument_name.to_case(Case::Snake), Span::call_site());
|
||||
if let ArgumentType::Node { .. } = argument_type {
|
||||
return match optional {
|
||||
true => quote!(#name.map(|n| _calling_client.get_node(#argument_name, n)?)),
|
||||
false => quote!(_calling_client.get_node(#argument_name, #name)?),
|
||||
};
|
||||
}
|
||||
if optional {
|
||||
let mapping = generate_argument_deserialize("o", argument_type, false);
|
||||
return quote!(#name.map(|o| Ok::<_, crate::core::error::ServerError>(#mapping)).transpose()?);
|
||||
}
|
||||
|
||||
match argument_type {
|
||||
ArgumentType::Color => quote!(color::rgba_linear!(#name[0], #name[1], #name[2], #name[3])),
|
||||
ArgumentType::Vec(v) => {
|
||||
let mapping = generate_argument_deserialize("a", v, false);
|
||||
quote!(#name.into_iter().map(|a| Ok(#mapping)).collect::<crate::core::error::Result<Vec<_>>>()?)
|
||||
}
|
||||
ArgumentType::Map(v) => {
|
||||
let mapping = generate_argument_deserialize("a", v, false);
|
||||
quote!(#name.into_iter().map(|(k, a)| Ok((k, #mapping))).collect::<crate::core::error::Result<rustc_hash::FxHashMap<String, _>>>()?)
|
||||
}
|
||||
_ => quote!(#name),
|
||||
}
|
||||
}
|
||||
fn generate_argument_serialize(
|
||||
argument_name: &str,
|
||||
argument_type: &ArgumentType,
|
||||
optional: bool,
|
||||
) -> TokenStream {
|
||||
let name = Ident::new(&argument_name.to_case(Case::Snake), Span::call_site());
|
||||
match argument_type {
|
||||
ArgumentType::Node {
|
||||
_type,
|
||||
return_id_parameter_name: _,
|
||||
} => match optional {
|
||||
true => quote!(#name.map(|n| n.get_id())),
|
||||
false => quote!(#name.get_id()),
|
||||
},
|
||||
ArgumentType::Color => quote!([#name.c.r, #name.c.g, #name.c.b, #name.a]),
|
||||
ArgumentType::Vec(v) => {
|
||||
let mapping = generate_argument_serialize("a", v, false);
|
||||
quote!(#name.into_iter().map(|a| Ok(#mapping)).collect::<crate::core::error::Result<Vec<_>>>()?)
|
||||
}
|
||||
ArgumentType::Map(v) => {
|
||||
let mapping = generate_argument_serialize("a", v, false);
|
||||
quote!(#name.into_iter().map(|(k, a)| Ok((k, #mapping))).collect::<crate::core::error::Result<rustc_hash::FxHashMap<String, _>>>()?)
|
||||
}
|
||||
_ => quote!(#name),
|
||||
}
|
||||
}
|
||||
fn generate_argument_decl(argument: &Argument, owned_values: bool) -> TokenStream {
|
||||
let name = Ident::new(&argument.name.to_case(Case::Snake), Span::call_site());
|
||||
let mut _type = generate_argument_type(&argument._type, argument.optional, owned_values);
|
||||
quote!(#name: #_type)
|
||||
}
|
||||
fn argument_type_option_name(argument_type: &ArgumentType) -> String {
|
||||
match argument_type {
|
||||
ArgumentType::Empty => "Empty".to_string(),
|
||||
ArgumentType::Bool => "Bool".to_string(),
|
||||
ArgumentType::Int => "Int".to_string(),
|
||||
ArgumentType::UInt => "UInt".to_string(),
|
||||
ArgumentType::Float => "Float".to_string(),
|
||||
ArgumentType::Vec2(_) => "Vec2".to_string(),
|
||||
ArgumentType::Vec3(_) => "Vec3".to_string(),
|
||||
ArgumentType::Quat => "Quat".to_string(),
|
||||
ArgumentType::Mat4 => "Mat4".to_string(),
|
||||
ArgumentType::Color => "Color".to_string(),
|
||||
ArgumentType::String => "String".to_string(),
|
||||
ArgumentType::Bytes => "Bytes".to_string(),
|
||||
ArgumentType::Vec(v) => format!("{}Vector", argument_type_option_name(v)),
|
||||
ArgumentType::Map(m) => format!("{}Map", argument_type_option_name(m)),
|
||||
ArgumentType::NodeID => "Node ID".to_string(),
|
||||
ArgumentType::Datamap => "Datamap".to_string(),
|
||||
ArgumentType::ResourceID => "ResourceID".to_string(),
|
||||
ArgumentType::Enum(e) => e.clone(),
|
||||
ArgumentType::Union(u) => u.clone(),
|
||||
ArgumentType::Struct(s) => s.clone(),
|
||||
ArgumentType::Node { _type, .. } => _type.clone(),
|
||||
ArgumentType::Fd => "File Descriptor".to_string(),
|
||||
}
|
||||
}
|
||||
fn generate_argument_type(
|
||||
argument_type: &ArgumentType,
|
||||
optional: bool,
|
||||
owned: bool,
|
||||
) -> TokenStream {
|
||||
let _type = match argument_type {
|
||||
ArgumentType::Empty => quote!(()),
|
||||
ArgumentType::Bool => quote!(bool),
|
||||
ArgumentType::Int => quote!(i32),
|
||||
ArgumentType::UInt => quote!(u32),
|
||||
ArgumentType::Float => quote!(f32),
|
||||
ArgumentType::Vec2(t) => {
|
||||
let t = generate_argument_type(t, false, true);
|
||||
quote!(stardust_xr::values::Vector2<#t>)
|
||||
}
|
||||
ArgumentType::Vec3(t) => {
|
||||
let t = generate_argument_type(t, false, true);
|
||||
quote!(stardust_xr::values::Vector3<#t>)
|
||||
}
|
||||
ArgumentType::Quat => quote!(stardust_xr::values::Quaternion),
|
||||
ArgumentType::Mat4 => quote!(stardust_xr::values::Mat4),
|
||||
ArgumentType::Color => quote!(stardust_xr::values::Color),
|
||||
ArgumentType::Bytes => {
|
||||
if !owned {
|
||||
quote!(&[u8])
|
||||
} else {
|
||||
quote!(Vec<u8>)
|
||||
}
|
||||
}
|
||||
ArgumentType::String => {
|
||||
if !owned {
|
||||
quote!(&str)
|
||||
} else {
|
||||
quote!(String)
|
||||
}
|
||||
}
|
||||
ArgumentType::Vec(t) => {
|
||||
let t = generate_argument_type(t, false, true);
|
||||
if !owned {
|
||||
quote!(&[#t])
|
||||
} else {
|
||||
quote!(Vec<#t>)
|
||||
}
|
||||
}
|
||||
ArgumentType::Map(t) => {
|
||||
let t = generate_argument_type(t, false, true);
|
||||
|
||||
if !owned {
|
||||
quote!(&stardust_xr::values::Map<String, #t>)
|
||||
} else {
|
||||
quote!(stardust_xr::values::Map<String, #t>)
|
||||
}
|
||||
}
|
||||
ArgumentType::NodeID => quote!(u64),
|
||||
ArgumentType::Datamap => {
|
||||
if !owned {
|
||||
quote!(&stardust_xr::values::Datamap)
|
||||
} else {
|
||||
quote!(stardust_xr::values::Datamap)
|
||||
}
|
||||
}
|
||||
ArgumentType::ResourceID => {
|
||||
if !owned {
|
||||
quote!(&stardust_xr::values::ResourceID)
|
||||
} else {
|
||||
quote!(stardust_xr::values::ResourceID)
|
||||
}
|
||||
}
|
||||
ArgumentType::Enum(e) => {
|
||||
let enum_name = Ident::new(&e.to_case(Case::Pascal), Span::call_site());
|
||||
quote!(#enum_name)
|
||||
}
|
||||
ArgumentType::Union(u) => {
|
||||
let union_name = Ident::new(&u.to_case(Case::Pascal), Span::call_site());
|
||||
quote!(#union_name)
|
||||
}
|
||||
ArgumentType::Struct(s) => {
|
||||
let struct_name = Ident::new(&s.to_case(Case::Pascal), Span::call_site());
|
||||
if !owned {
|
||||
quote!(&#struct_name)
|
||||
} else {
|
||||
quote!(#struct_name)
|
||||
}
|
||||
}
|
||||
ArgumentType::Node {
|
||||
_type,
|
||||
return_id_parameter_name: _,
|
||||
} => {
|
||||
if !owned {
|
||||
quote!(&std::sync::Arc<crate::nodes::Node>)
|
||||
} else {
|
||||
quote!(std::sync::Arc<crate::nodes::Node>)
|
||||
}
|
||||
}
|
||||
ArgumentType::Fd => {
|
||||
quote!(&std::os::fd::OwnedFd)
|
||||
}
|
||||
};
|
||||
|
||||
if optional {
|
||||
quote!(Option<#_type>)
|
||||
} else {
|
||||
_type
|
||||
}
|
||||
}
|
||||
199
flake.lock
generated
199
flake.lock
generated
@@ -1,24 +1,23 @@
|
||||
{
|
||||
"nodes": {
|
||||
"fenix": {
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"flatland",
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1678775037,
|
||||
"narHash": "sha256-chx0tWnXKpcayPkPY3Qh+2hNwptvX8XE3o+fYZ+GNzg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "ee59e1c769657b1e27e608f8b981fa8f6b715583",
|
||||
"lastModified": 1712681629,
|
||||
"narHash": "sha256-bMDXn4AkTXLCpoZbII6pDGoSeSe9gI87jxPsHRXgu/E=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "220387ac8e99cbee0ca4c95b621c4bc782b6a235",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
@@ -27,11 +26,11 @@
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1690933134,
|
||||
"narHash": "sha256-ab989mN63fQZBFrkk4Q8bYxQCktuHmBIBqUG1jl6/FQ=",
|
||||
"lastModified": 1754487366,
|
||||
"narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "59cf3f1447cfc75087e7273b04b31e689a8599fb",
|
||||
"rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -42,14 +41,17 @@
|
||||
},
|
||||
"flake-parts_2": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib_2"
|
||||
"nixpkgs-lib": [
|
||||
"hercules-ci-effects",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1688466019,
|
||||
"narHash": "sha256-VeM2akYrBYMsb4W/MmBo1zmaMfgbL4cH3Pu8PGyIwJ0=",
|
||||
"lastModified": 1754487366,
|
||||
"narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "8e8d955c22df93dbe24f19ea04f47a74adbdc5ec",
|
||||
"rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -57,39 +59,17 @@
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"flake-parts_3": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"hercules-ci-effects",
|
||||
"hercules-ci-agent",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1688466019,
|
||||
"narHash": "sha256-VeM2akYrBYMsb4W/MmBo1zmaMfgbL4cH3Pu8PGyIwJ0=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "8e8d955c22df93dbe24f19ea04f47a74adbdc5ec",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flatland": {
|
||||
"inputs": {
|
||||
"fenix": "fenix",
|
||||
"crane": "crane",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694190588,
|
||||
"narHash": "sha256-tcvp09A54AbXkkXS/P6Ed5TUWzMEuQ9h/fnfLz7gnJc=",
|
||||
"lastModified": 1725868201,
|
||||
"narHash": "sha256-rDBQ9tXQCCA7emikSYH59ADJELE2IpzB7eoLrpHYzU4=",
|
||||
"owner": "StardustXR",
|
||||
"repo": "flatland",
|
||||
"rev": "3867067452761959a95497d6589f9336b347270c",
|
||||
"rev": "0914dd3df54a5e6258dfc0a02d65af1c0fc0fc90",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -98,53 +78,17 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"haskell-flake": {
|
||||
"locked": {
|
||||
"lastModified": 1684780604,
|
||||
"narHash": "sha256-2uMZsewmRn7rRtAnnQNw1lj0uZBMh4m6Cs/7dV5YF08=",
|
||||
"owner": "srid",
|
||||
"repo": "haskell-flake",
|
||||
"rev": "74210fa80a49f1b6f67223debdbf1494596ff9f2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "srid",
|
||||
"ref": "0.3.0",
|
||||
"repo": "haskell-flake",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"hercules-ci-agent": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts_3",
|
||||
"haskell-flake": "haskell-flake",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1688568579,
|
||||
"narHash": "sha256-ON0M56wtY/TIIGPkXDlJboAmuYwc73Hi8X9iJGtxOhM=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "hercules-ci-agent",
|
||||
"rev": "367dd8cd649b57009a6502e878005a1e54ad78c5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "hercules-ci-agent",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"hercules-ci-effects": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts_2",
|
||||
"hercules-ci-agent": "hercules-ci-agent",
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1689397210,
|
||||
"narHash": "sha256-fVxZnqxMbsDkB4GzGAs/B41K0wt/e+B/fLxmTFF/S20=",
|
||||
"lastModified": 1755233722,
|
||||
"narHash": "sha256-AavrbMltJKcC2Fx0lfJoZfmy7g87ebXU0ddVenhajLA=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "hercules-ci-effects",
|
||||
"rev": "0a63bfa3f00a3775ea3a6722b247880f1ffe91ce",
|
||||
"rev": "99e03e72e3f7e13506f80ef9ebaedccb929d84d0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -155,63 +99,42 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1678703398,
|
||||
"narHash": "sha256-Y1mW3dBsoWLHpYm+UIHb5VZ7rx024NNHaF16oZBx++o=",
|
||||
"lastModified": 1712791164,
|
||||
"narHash": "sha256-3sbWO1mbpWsLepZGbWaMovSO7ndZeFqDSdX0hZ9nVyw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "67f26c1cfc5d5783628231e776a81c1ade623e0b",
|
||||
"rev": "1042fd8b148a9105f3c0aca3a6177fd1d9360ba5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-22.11",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"dir": "lib",
|
||||
"lastModified": 1690881714,
|
||||
"narHash": "sha256-h/nXluEqdiQHs1oSgkOOWF+j8gcJMWhwnZ9PFabN6q0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9e1960bc196baf6881340d53dccb203a951745a2",
|
||||
"lastModified": 1753579242,
|
||||
"narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "lib",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib_2": {
|
||||
"locked": {
|
||||
"dir": "lib",
|
||||
"lastModified": 1688049487,
|
||||
"narHash": "sha256-100g4iaKC9MalDjUW9iN6Jl/OocTDtXdeAj7pEGIRh4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4bc72cae107788bf3f24f30db2e2f685c9298dc9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "lib",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1688322751,
|
||||
"narHash": "sha256-eW62dC5f33oKZL7VWlomttbUnOTHrAbte9yNUNW8rbk=",
|
||||
"lastModified": 1755027561,
|
||||
"narHash": "sha256-IVft239Bc8p8Dtvf7UAACMG5P3ZV+3/aO28gXpGtMXI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0fbe93c5a7cac99f90b60bdf5f149383daaa615f",
|
||||
"rev": "005433b926e16227259a1843015b5b2b7f7d1fc3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -223,26 +146,11 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1689393711,
|
||||
"narHash": "sha256-l3gyTPy/qWLDk1CY1EgYwlsxcGxN2aVd7MlCzQa69rk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "27fcd46fa18df36d270174246e7bd8f1787129ff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1692734709,
|
||||
"narHash": "sha256-SCFnyHCyYjwEmgUsHDDuU0TsbVMKeU1vwkR+r7uS2Rg=",
|
||||
"lastModified": 1755186698,
|
||||
"narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b85ed9dcbf187b909ef7964774f8847d554fab3b",
|
||||
"rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -257,24 +165,7 @@
|
||||
"flake-parts": "flake-parts",
|
||||
"flatland": "flatland",
|
||||
"hercules-ci-effects": "hercules-ci-effects",
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1678695923,
|
||||
"narHash": "sha256-rDhiiU8P6sf6mgj5IKgCuTRN9uYeqWr6xl4XLkKnMWg=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "95497533524537b1cc7a2870ce94b0b14503be8b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
133
flake.nix
133
flake.nix
@@ -1,7 +1,9 @@
|
||||
{
|
||||
nixConfig = {
|
||||
extra-substituters = [ "https://stardustxr.cachix.org" ];
|
||||
extra-trusted-public-keys = [ "stardustxr.cachix.org-1:mWSn8Ap2RLsIWT/8gsj+VfbJB6xoOkPaZpbjO+r9HBo=" ];
|
||||
extra-trusted-public-keys = [
|
||||
"stardustxr.cachix.org-1:mWSn8Ap2RLsIWT/8gsj+VfbJB6xoOkPaZpbjO+r9HBo="
|
||||
];
|
||||
};
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
@@ -12,16 +14,23 @@
|
||||
# it to create VM Tests
|
||||
flatland.url = "github:StardustXR/flatland";
|
||||
};
|
||||
outputs = inputs@{ self, flake-parts, nixpkgs, hercules-ci-effects, flatland, ... }:
|
||||
outputs =
|
||||
inputs@{
|
||||
self,
|
||||
flake-parts,
|
||||
nixpkgs,
|
||||
hercules-ci-effects,
|
||||
flatland,
|
||||
...
|
||||
}:
|
||||
let
|
||||
name = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.name;
|
||||
src = builtins.path {
|
||||
name = "${name}-source";
|
||||
path = toString ./.;
|
||||
filter = path: type:
|
||||
nixpkgs.lib.all
|
||||
(n: builtins.baseNameOf path != n)
|
||||
[
|
||||
filter =
|
||||
path: type:
|
||||
nixpkgs.lib.all (n: builtins.baseNameOf path != n) [
|
||||
"flake.nix"
|
||||
"flake.lock"
|
||||
"nix"
|
||||
@@ -30,51 +39,79 @@
|
||||
};
|
||||
in
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
imports = [
|
||||
flake-parts.flakeModules.easyOverlay
|
||||
imports = [ flake-parts.flakeModules.easyOverlay ];
|
||||
systems = [
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
"riscv64-linux"
|
||||
];
|
||||
systems = [ "aarch64-linux" "x86_64-linux" "riscv64-linux" ];
|
||||
perSystem = { config, self', inputs', pkgs, system, ... }: {
|
||||
_module.args.pkgs = import inputs.nixpkgs { inherit system; overlays = [ inputs.self.overlays.default ]; };
|
||||
overlayAttrs = config.packages;
|
||||
packages = {
|
||||
default = self'.packages.${name};
|
||||
gnome-graphical-test = self'.checks.gnome-graphical-test;
|
||||
"${name}" = pkgs.callPackage ./nix/stardust-xr-server.nix { inherit name src; };
|
||||
};
|
||||
checks.gnome-graphical-test = pkgs.nixosTest (import ./nix/gnome-graphical-test.nix { inherit pkgs self; });
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = [ self'.packages.default ];
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||
};
|
||||
};
|
||||
flake = {
|
||||
herculesCI.ciSystems = [ "x86_64-linux" ];
|
||||
effects = let
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
hci-effects = hercules-ci-effects.lib.withPkgs pkgs;
|
||||
in { branch, rev, ... }: {
|
||||
gnome-graphical-test = hci-effects.mkEffect {
|
||||
secretsMap."stardustxrDiscord" = "stardustxrDiscord";
|
||||
secretsMap."stardustxrIpfs" = "stardustxrIpfs";
|
||||
effectScript = ''
|
||||
readSecretString stardustxrDiscord .webhook > .webhook
|
||||
readSecretString stardustxrIpfs .basicauth > .basicauth
|
||||
set -x
|
||||
export RESPONSE=$(curl -H @.basicauth -F file=@${self.packages."x86_64-linux".gnome-graphical-test}/screen.png https://ipfs-api.stardustxr.org/api/v0/add)
|
||||
export CID=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r .Hash)
|
||||
set +x
|
||||
export ADDRESS="https://ipfs.stardustxr.org/ipfs/$CID"
|
||||
${pkgs.discord-sh}/bin/discord.sh \
|
||||
--description "\`stardustxr/server\` has been modified, here's how it renders \`weston-cliptest\` on \`flatland\` via \`monado-service\` inside of the \`gnome-graphical-test\`" \
|
||||
--field "Branch;${branch}" \
|
||||
--field "Commit ID;${rev}" \
|
||||
--field "Flatland Revision;${flatland.rev}" \
|
||||
--field "Reproducer;\`nix build github:stardustxr/server/${rev}#gnome-graphical-test\`" \
|
||||
--image "$ADDRESS"
|
||||
'';
|
||||
perSystem =
|
||||
{
|
||||
config,
|
||||
self',
|
||||
inputs',
|
||||
pkgs,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
{
|
||||
_module.args.pkgs = import inputs.nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ inputs.self.overlays.default ];
|
||||
};
|
||||
overlayAttrs = config.packages;
|
||||
packages =
|
||||
{
|
||||
default = self'.packages.${name};
|
||||
gnome-graphical-test = self'.checks.gnome-graphical-test;
|
||||
"${name}" = pkgs.callPackage ./nix/stardust-xr-server.nix {
|
||||
inherit name src;
|
||||
};
|
||||
};
|
||||
apps.default = {
|
||||
type = "app";
|
||||
program = self'.packages.${name} + "/bin/stardust-xr-server";
|
||||
};
|
||||
checks.gnome-graphical-test = pkgs.nixosTest (
|
||||
import ./nix/gnome-graphical-test.nix { inherit pkgs self; }
|
||||
);
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = [ self'.packages.default ];
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||
};
|
||||
};
|
||||
flake = {
|
||||
herculesCI.ciSystems = [ "x86_64-linux" ];
|
||||
effects =
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
hci-effects = hercules-ci-effects.lib.withPkgs pkgs;
|
||||
in
|
||||
{ ref, rev, ... }:
|
||||
{
|
||||
gnome-graphical-test = hci-effects.mkEffect {
|
||||
secretsMap."stardustxrDiscord" = "stardustxrDiscord";
|
||||
secretsMap."stardustxrIpfs" = "stardustxrIpfs";
|
||||
effectScript = ''
|
||||
readSecretString stardustxrDiscord .webhook > .webhook
|
||||
readSecretString stardustxrIpfs .basicauth > .basicauth
|
||||
set -x
|
||||
export RESPONSE=$(curl -H @.basicauth -F file=@${
|
||||
self.packages."x86_64-linux".gnome-graphical-test
|
||||
}/screen.png https://ipfs-api.stardustxr.org/api/v0/add)
|
||||
export CID=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r .Hash)
|
||||
set +x
|
||||
export ADDRESS="https://ipfs.stardustxr.org/ipfs/$CID"
|
||||
${pkgs.discord-sh}/bin/discord.sh \
|
||||
--description "\`stardustxr/server\` has been modified, here's how it renders \`weston-cliptest\` on \`flatland\` via \`monado-service\` inside of the \`gnome-graphical-test\`" \
|
||||
--field "Ref;${ref}" \
|
||||
--field "Commit ID;${rev}" \
|
||||
--field "Flatland Revision;${flatland.rev}" \
|
||||
--field "Reproducer;\`nix build github:stardustxr/server/${rev}#gnome-graphical-test\`" \
|
||||
--image "$ADDRESS"
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 789 KiB |
BIN
img/workflow.png
Normal file
BIN
img/workflow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 872 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
12
justfile
Normal file
12
justfile
Normal file
@@ -0,0 +1,12 @@
|
||||
PREFIX := "usr"
|
||||
BINARY := PREFIX / "bin"
|
||||
DESTDIR := "/"
|
||||
|
||||
build:
|
||||
cargo build --release
|
||||
|
||||
test:
|
||||
cargo test
|
||||
|
||||
install:
|
||||
install -Dm755 target/release/stardust-xr-server {{ DESTDIR }}{{ BINARY }}/stardust-xr-server
|
||||
@@ -20,8 +20,8 @@
|
||||
# Set a nice desktop background that is pleasing to the eyes :3
|
||||
extraGSettingsOverrides = ''
|
||||
[org.gnome.desktop.background]
|
||||
picture-uri='file://${pkgs.gnome.gnome-backgrounds}/share/backgrounds/gnome/blobs-l.svg'
|
||||
picture-uri-dark='file://${pkgs.gnome.gnome-backgrounds}/share/backgrounds/gnome/blobs-l.svg'
|
||||
picture-uri='file://${pkgs.gnome-backgrounds}/share/backgrounds/gnome/blobs-l.svg'
|
||||
picture-uri-dark='file://${pkgs.gnome-backgrounds}/share/backgrounds/gnome/blobs-l.svg'
|
||||
'';
|
||||
};
|
||||
displayManager = {
|
||||
@@ -100,7 +100,7 @@
|
||||
# Eval API is now internal so Shell needs to run in unsafe mode.
|
||||
# TODO: improve test driver so that it supports openqa-like manipulation
|
||||
# that would allow us to drop this mess.
|
||||
"${pkgs.gnome.gnome-shell}/bin/gnome-shell --unsafe-mode"
|
||||
"${pkgs.gnome-shell}/bin/gnome-shell --unsafe-mode"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
27
nix/meshoptimizer.nix
Normal file
27
nix/meshoptimizer.nix
Normal file
@@ -0,0 +1,27 @@
|
||||
{ lib, stdenv, fetchFromGitHub, cmake }:
|
||||
|
||||
stdenv.mkDerivation rec {
|
||||
pname = "meshoptimizer";
|
||||
version = "0.20";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "zeux";
|
||||
repo = "meshoptimizer";
|
||||
rev = "c21d3be6ddf627f8ca852ba4b6db9903b0557858";
|
||||
sha256 = "sha256-QCxpM2g8WtYSZHkBzLTJNQ/oHb5j/n9rjaVmZJcCZIA=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ cmake ];
|
||||
|
||||
cmakeFlags = [
|
||||
"-DCMAKE_POSITION_INDEPENDENT_CODE=ON"
|
||||
];
|
||||
|
||||
meta = with lib; {
|
||||
description = "Mesh optimization library that makes meshes smaller and faster to render";
|
||||
homepage = "https://github.com/zeux/meshoptimizer";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.all;
|
||||
};
|
||||
}
|
||||
|
||||
16
nix/sk_gpu.nix
Normal file
16
nix/sk_gpu.nix
Normal file
@@ -0,0 +1,16 @@
|
||||
{ stdenv, fetchurl, unzip }:
|
||||
|
||||
let
|
||||
sk_gpu_zip = fetchurl {
|
||||
url =
|
||||
"https://github.com/StereoKit/sk_gpu/releases/download/v2024.8.16/sk_gpu.v2024.8.16.zip";
|
||||
sha256 = "sha256-Wk3PZFlWqhrsQ8xG0sQaV2xSasdg2D7TMiPvl/CgtGU=";
|
||||
};
|
||||
in stdenv.mkDerivation rec {
|
||||
name = "sk_gpu";
|
||||
src = sk_gpu_zip;
|
||||
unpackPhase = ''
|
||||
unzip -d $out ${sk_gpu_zip}
|
||||
'';
|
||||
nativeBuildInputs = [ unzip ];
|
||||
}
|
||||
@@ -1,17 +1,27 @@
|
||||
{ rustPlatform
|
||||
, src
|
||||
, name
|
||||
, openxr-loader
|
||||
, libGL
|
||||
, mesa
|
||||
, xorg
|
||||
, fontconfig
|
||||
, libxkbcommon
|
||||
, libclang
|
||||
, cmake
|
||||
, cpm-cmake
|
||||
, pkg-config
|
||||
, llvmPackages
|
||||
{
|
||||
rustPlatform,
|
||||
src,
|
||||
name,
|
||||
vulkan-loader,
|
||||
vulkan-headers,
|
||||
mesa,
|
||||
xorg,
|
||||
fontconfig,
|
||||
libxkbcommon,
|
||||
libclang,
|
||||
|
||||
cmake,
|
||||
pkg-config,
|
||||
llvmPackages,
|
||||
fetchFromGitHub,
|
||||
libXau,
|
||||
|
||||
libXdmcp,
|
||||
stdenv,
|
||||
lib,
|
||||
openxr-loader,
|
||||
wayland,
|
||||
alsa-lib,
|
||||
}:
|
||||
|
||||
rustPlatform.buildRustPackage rec {
|
||||
@@ -20,21 +30,39 @@ rustPlatform.buildRustPackage rec {
|
||||
lockFile = (src + "/Cargo.lock");
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
CPM_SOURCE_CACHE = "./build";
|
||||
postPatch = ''
|
||||
sk=$(echo $cargoDepsCopy/stereokit-sys-*/StereoKit)
|
||||
mkdir -p $sk/build/cpm
|
||||
|
||||
# This is not ideal, the original approach was to fetch the exact cmake
|
||||
# file version that was wanted from GitHub directly, but at least this way it comes from Nixpkgs.. so meh
|
||||
cp ${cpm-cmake}/share/cpm/CPM.cmake $sk/build/cpm/CPM_0.32.2.cmake
|
||||
preBuild = ''
|
||||
substituteInPlace /build/cargo-vendor-dir/bevy_gltf-0.16.1/Cargo.toml \
|
||||
--replace-fail '[lints]' "" \
|
||||
--replace-fail 'workspace = true' ""
|
||||
'';
|
||||
|
||||
postFixup = ''
|
||||
patchelf $out/bin/stardust-xr-server --add-rpath ${vulkan-loader}/lib
|
||||
patchelf $out/bin/stardust-xr-server --add-rpath ${openxr-loader}/lib
|
||||
patchelf $out/bin/stardust-xr-server --add-rpath ${libxkbcommon}/lib
|
||||
'';
|
||||
|
||||
nativeBuildInputs = [
|
||||
cmake pkg-config llvmPackages.libcxxClang
|
||||
cmake
|
||||
pkg-config
|
||||
llvmPackages.libcxxClang
|
||||
];
|
||||
buildInputs = [
|
||||
openxr-loader libGL mesa xorg.libX11 fontconfig libxkbcommon
|
||||
vulkan-loader
|
||||
vulkan-headers
|
||||
mesa
|
||||
xorg.libX11.dev
|
||||
xorg.libXft
|
||||
xorg.libXfixes
|
||||
fontconfig
|
||||
libxkbcommon
|
||||
libXau
|
||||
libXdmcp
|
||||
openxr-loader
|
||||
wayland
|
||||
alsa-lib
|
||||
];
|
||||
|
||||
LIBCLANG_PATH = "${libclang.lib}/lib";
|
||||
}
|
||||
|
||||
|
||||
31
src/core/bevy_channel.rs
Normal file
31
src/core/bevy_channel.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use tokio::sync::mpsc::{self, error::TryRecvError};
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct BevyChannelReader<T: Send + Sync + 'static>(mpsc::UnboundedReceiver<T>);
|
||||
pub struct BevyChannel<T: Send + Sync + 'static>(OnceLock<mpsc::UnboundedSender<T>>);
|
||||
impl<T: Send + Sync + 'static> BevyChannel<T> {
|
||||
pub const fn new() -> Self {
|
||||
Self(OnceLock::new())
|
||||
}
|
||||
pub fn init(&self, app: &mut App) {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
self.0.set(tx).unwrap();
|
||||
app.insert_resource(BevyChannelReader(rx));
|
||||
}
|
||||
pub fn send(&self, msg: T) -> Option<()> {
|
||||
self.0.get()?.send(msg).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> BevyChannelReader<T> {
|
||||
pub fn read(&mut self) -> Option<T> {
|
||||
match self.0.try_recv() {
|
||||
Ok(v) => Some(v),
|
||||
Err(TryRecvError::Disconnected) => panic!("bevy channel should never disconnect"),
|
||||
Err(TryRecvError::Empty) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
use std::ffi::c_void;
|
||||
|
||||
use color_eyre::eyre::{ensure, Result};
|
||||
use smithay::backend::{egl::EGLContext, renderer::gles::GlesRenderer};
|
||||
use stereokit as sk;
|
||||
|
||||
struct EGLRawHandles {
|
||||
display: *const c_void,
|
||||
config: *const c_void,
|
||||
context: *const c_void,
|
||||
}
|
||||
fn get_sk_egl() -> Result<EGLRawHandles> {
|
||||
ensure!(
|
||||
unsafe { sk::sys::backend_graphics_get() }
|
||||
== sk::sys::backend_graphics__backend_graphics_opengles_egl,
|
||||
"StereoKit is not running using EGL!"
|
||||
);
|
||||
|
||||
Ok(unsafe {
|
||||
EGLRawHandles {
|
||||
display: sk::sys::backend_opengl_egl_get_display() as *const c_void,
|
||||
config: sk::sys::backend_opengl_egl_get_config() as *const c_void,
|
||||
context: sk::sys::backend_opengl_egl_get_context() as *const c_void,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub struct BufferManager {
|
||||
pub renderer: GlesRenderer,
|
||||
}
|
||||
impl BufferManager {
|
||||
pub fn new() -> Result<Self> {
|
||||
let egl_raw_handles = get_sk_egl()?;
|
||||
let renderer = unsafe {
|
||||
GlesRenderer::new(EGLContext::from_raw(
|
||||
egl_raw_handles.display,
|
||||
egl_raw_handles.config,
|
||||
egl_raw_handles.context,
|
||||
)?)?
|
||||
};
|
||||
|
||||
Ok(BufferManager { renderer })
|
||||
}
|
||||
|
||||
pub fn make_context_current(&self) {
|
||||
unsafe {
|
||||
let _ = self.renderer.egl_context().make_current();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,57 @@
|
||||
use super::{
|
||||
client_state::{ClientState, CLIENT_STATES},
|
||||
client_state::{CLIENT_STATES, ClientStateParsed},
|
||||
scenegraph::Scenegraph,
|
||||
};
|
||||
use crate::{
|
||||
core::{registry::OwnedRegistry, task},
|
||||
nodes::{audio, data, drawable, fields, hmd, input, items, root::Root, spatial, Node},
|
||||
nodes::{
|
||||
Node, audio, drawable, fields, input, items,
|
||||
root::{ClientState, Root},
|
||||
spatial,
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::{eyre, Result};
|
||||
use color_eyre::eyre::{Result, eyre};
|
||||
use global_counter::primitive::exact::CounterU32;
|
||||
use lazy_static::lazy_static;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use stardust_xr::{
|
||||
messenger::{self, MessageSenderHandle},
|
||||
schemas::flex::serialize,
|
||||
use stardust_xr::messenger::{self, MessageSenderHandle};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
fs,
|
||||
iter::FromIterator,
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock},
|
||||
time::Instant,
|
||||
};
|
||||
use std::{fs, iter::FromIterator, path::PathBuf, sync::Arc};
|
||||
use tokio::{net::UnixStream, task::JoinHandle};
|
||||
use tokio::{net::UnixStream, sync::watch, task::JoinHandle};
|
||||
use tracing::info;
|
||||
|
||||
pub static CLIENTS: OwnedRegistry<Client> = OwnedRegistry::new();
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CLIENTS: OwnedRegistry<Client> = OwnedRegistry::new();
|
||||
static ref INTERNAL_CLIENT_MESSAGE_TIMES: (watch::Sender<Instant>, watch::Receiver<Instant>) = watch::channel(Instant::now());
|
||||
pub static ref INTERNAL_CLIENT: Arc<Client> = CLIENTS.add(Client {
|
||||
pid: None,
|
||||
// env: None,
|
||||
exe: None,
|
||||
|
||||
dispatch_join_handle: OnceCell::new(),
|
||||
flush_join_handle: OnceCell::new(),
|
||||
disconnect_status: OnceCell::new(),
|
||||
dispatch_join_handle: OnceLock::new(),
|
||||
flush_join_handle: OnceLock::new(),
|
||||
disconnect_status: OnceLock::new(),
|
||||
|
||||
id_counter: CounterU32::new(0),
|
||||
message_last_received: INTERNAL_CLIENT_MESSAGE_TIMES.1.clone(),
|
||||
message_sender_handle: None,
|
||||
scenegraph: Default::default(),
|
||||
root: OnceCell::new(),
|
||||
root: OnceLock::new(),
|
||||
base_resource_prefixes: Default::default(),
|
||||
state: Arc::new(ClientState::default()),
|
||||
state: OnceLock::default(),
|
||||
});
|
||||
}
|
||||
pub fn tick_internal_client() {
|
||||
let _ = INTERNAL_CLIENT_MESSAGE_TIMES.0.send(Instant::now());
|
||||
}
|
||||
|
||||
pub fn get_env(pid: i32) -> Result<FxHashMap<String, String>, std::io::Error> {
|
||||
let env = fs::read_to_string(format!("/proc/{pid}/environ"))?;
|
||||
@@ -46,7 +61,7 @@ pub fn get_env(pid: i32) -> Result<FxHashMap<String, String>, std::io::Error> {
|
||||
.map(|(k, v)| (k.to_string(), v.to_string())),
|
||||
))
|
||||
}
|
||||
pub fn state(env: &FxHashMap<String, String>) -> Option<Arc<ClientState>> {
|
||||
pub fn state(env: &FxHashMap<String, String>) -> Option<Arc<ClientStateParsed>> {
|
||||
let token = env.get("STARDUST_STARTUP_TOKEN")?;
|
||||
CLIENT_STATES.lock().get(token).cloned()
|
||||
}
|
||||
@@ -55,21 +70,23 @@ pub struct Client {
|
||||
pub pid: Option<i32>,
|
||||
// env: Option<FxHashMap<String, String>>,
|
||||
exe: Option<PathBuf>,
|
||||
dispatch_join_handle: OnceCell<JoinHandle<Result<()>>>,
|
||||
flush_join_handle: OnceCell<JoinHandle<Result<()>>>,
|
||||
disconnect_status: OnceCell<Result<()>>,
|
||||
dispatch_join_handle: OnceLock<JoinHandle<Result<()>>>,
|
||||
flush_join_handle: OnceLock<JoinHandle<Result<()>>>,
|
||||
disconnect_status: OnceLock<Result<()>>,
|
||||
|
||||
id_counter: CounterU32,
|
||||
message_last_received: watch::Receiver<Instant>,
|
||||
pub message_sender_handle: Option<MessageSenderHandle>,
|
||||
pub scenegraph: Arc<Scenegraph>,
|
||||
pub root: OnceCell<Arc<Root>>,
|
||||
pub root: OnceLock<Arc<Root>>,
|
||||
pub base_resource_prefixes: Mutex<Vec<PathBuf>>,
|
||||
pub state: Arc<ClientState>,
|
||||
pub state: OnceLock<ClientState>,
|
||||
}
|
||||
impl Client {
|
||||
pub fn from_connection(connection: UnixStream) -> Result<Arc<Self>> {
|
||||
let pid = connection.peer_cred().ok().and_then(|c| c.pid());
|
||||
let env = pid.and_then(|pid| get_env(pid).ok());
|
||||
let exe = pid.and_then(|pid| fs::read_link(format!("/proc/{}/exe", pid)).ok());
|
||||
let exe = pid.and_then(|pid| fs::read_link(format!("/proc/{pid}/exe")).ok());
|
||||
info!(
|
||||
pid,
|
||||
exe = exe
|
||||
@@ -83,40 +100,37 @@ impl Client {
|
||||
let state = env
|
||||
.as_ref()
|
||||
.and_then(state)
|
||||
.unwrap_or_else(|| Arc::new(ClientState::default()));
|
||||
.unwrap_or_else(|| Arc::new(ClientStateParsed::default()));
|
||||
|
||||
let (message_time_tx, message_last_received) = watch::channel(Instant::now());
|
||||
let client = CLIENTS.add(Client {
|
||||
pid,
|
||||
// env,
|
||||
exe: exe.clone(),
|
||||
|
||||
dispatch_join_handle: OnceCell::new(),
|
||||
flush_join_handle: OnceCell::new(),
|
||||
disconnect_status: OnceCell::new(),
|
||||
dispatch_join_handle: OnceLock::new(),
|
||||
flush_join_handle: OnceLock::new(),
|
||||
disconnect_status: OnceLock::new(),
|
||||
|
||||
id_counter: CounterU32::new(256),
|
||||
message_last_received,
|
||||
message_sender_handle: Some(messenger_tx.handle()),
|
||||
scenegraph: scenegraph.clone(),
|
||||
root: OnceCell::new(),
|
||||
root: OnceLock::new(),
|
||||
base_resource_prefixes: Default::default(),
|
||||
state,
|
||||
state: OnceLock::default(),
|
||||
});
|
||||
let _ = client.scenegraph.client.set(Arc::downgrade(&client));
|
||||
let _ = client.root.set(Root::create(&client)?);
|
||||
hmd::make_alias(&client)?;
|
||||
let _ = client.root.set(Root::create(&client, state.root)?);
|
||||
spatial::create_interface(&client)?;
|
||||
fields::create_interface(&client)?;
|
||||
drawable::create_interface(&client)?;
|
||||
audio::create_interface(&client)?;
|
||||
data::create_interface(&client)?;
|
||||
items::create_interface(&client)?;
|
||||
input::create_interface(&client)?;
|
||||
items::camera::create_interface(&client)?;
|
||||
items::panel::create_interface(&client)?;
|
||||
|
||||
client
|
||||
.root
|
||||
.get()
|
||||
.unwrap()
|
||||
.node
|
||||
.send_remote_signal("restore_state", serialize(client.state.apply_to(&client))?)?;
|
||||
let _ = client.state.set(state.apply_to(&client));
|
||||
|
||||
let pid_printable = pid
|
||||
.map(|pid| pid.to_string())
|
||||
@@ -128,46 +142,38 @@ impl Client {
|
||||
.map(|exe| exe.to_string())
|
||||
})
|
||||
.unwrap_or_else(|| "??".to_string());
|
||||
let _ = client.dispatch_join_handle.get_or_try_init(|| {
|
||||
let _ = client.dispatch_join_handle.get_or_init(|| {
|
||||
task::new(
|
||||
|| {
|
||||
format!(
|
||||
"client dispatch pid={} exe={}",
|
||||
&pid_printable, &exe_printable,
|
||||
)
|
||||
},
|
||||
|| format!("Stardust client \"{exe_printable}\" dispatch, pid={pid_printable}"),
|
||||
{
|
||||
let client = client.clone();
|
||||
async move {
|
||||
loop {
|
||||
match messenger_rx.dispatch(&*scenegraph).await {
|
||||
Err(e) => {
|
||||
client.disconnect(Err(e.into()));
|
||||
}
|
||||
_ => (),
|
||||
if let Err(e) = messenger_rx.dispatch(&*scenegraph).await {
|
||||
client.disconnect(Err(e.into()));
|
||||
}
|
||||
let _ = message_time_tx.send(Instant::now());
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
let _ = client.flush_join_handle.get_or_try_init(|| {
|
||||
let _ = client.flush_join_handle.get_or_init(|| {
|
||||
task::new(
|
||||
|| format!("client flush pid={} exe={}", &pid_printable, &exe_printable,),
|
||||
|| format!("Stardust client \"{exe_printable}\" flush, pid={pid_printable}"),
|
||||
{
|
||||
let client = client.clone();
|
||||
async move {
|
||||
loop {
|
||||
match messenger_tx.flush().await {
|
||||
Err(e) => {
|
||||
client.disconnect(Err(e.into()));
|
||||
}
|
||||
_ => (),
|
||||
if let Err(e) = messenger_tx.flush().await {
|
||||
client.disconnect(Err(e.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
Ok(client)
|
||||
@@ -189,18 +195,29 @@ impl Client {
|
||||
let cwd_proc_path = format!("/proc/{pid}/cwd");
|
||||
std::fs::read_link(cwd_proc_path).ok()
|
||||
}
|
||||
pub async fn save_state(&self) -> Option<ClientState> {
|
||||
pub async fn save_state(&self) -> Option<ClientStateParsed> {
|
||||
println!("start save state");
|
||||
let internal = self.root.get()?.save_state().await.ok()?;
|
||||
Some(ClientState::from_deserialized(self, internal))
|
||||
println!("finished save state");
|
||||
Some(ClientStateParsed::from_deserialized(self, internal))
|
||||
}
|
||||
|
||||
pub fn generate_id(&self) -> u64 {
|
||||
self.id_counter.inc() as u64
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_node(&self, name: &'static str, path: &str) -> Result<Arc<Node>> {
|
||||
pub fn get_node(&self, name: &'static str, id: u64) -> Result<Arc<Node>> {
|
||||
self.scenegraph
|
||||
.get_node(path)
|
||||
.get_node(id)
|
||||
.ok_or_else(|| eyre!("{} not found", name))
|
||||
}
|
||||
|
||||
pub fn unresponsive(&self) -> bool {
|
||||
let time_since_last_message = self.message_last_received.borrow().elapsed();
|
||||
time_since_last_message.as_millis() > 500
|
||||
}
|
||||
|
||||
pub fn disconnect(&self, reason: Result<()>) {
|
||||
let _ = self.disconnect_status.set(reason);
|
||||
if let Some(dispatch_join_handle) = self.dispatch_join_handle.get() {
|
||||
@@ -212,6 +229,19 @@ impl Client {
|
||||
CLIENTS.remove(self);
|
||||
}
|
||||
}
|
||||
impl Debug for Client {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Client")
|
||||
.field("pid", &self.pid)
|
||||
.field("exe", &self.exe)
|
||||
.field("dispatch_join_handle", &self.dispatch_join_handle)
|
||||
.field("flush_join_handle", &self.flush_join_handle)
|
||||
.field("disconnect_status", &self.disconnect_status)
|
||||
.field("base_resource_prefixes", &self.base_resource_prefixes)
|
||||
.field("state", &self.state)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
impl Drop for Client {
|
||||
fn drop(&mut self) {
|
||||
info!(
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use super::client::{get_env, Client};
|
||||
use crate::nodes::{spatial::Spatial, Node};
|
||||
use super::client::{Client, get_env};
|
||||
use crate::nodes::{Node, root::ClientState, spatial::Spatial};
|
||||
use glam::Mat4;
|
||||
use parking_lot::Mutex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{io::Write, path::PathBuf, sync::Arc};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CLIENT_STATES: Mutex<FxHashMap<String, Arc<ClientState>>> = Default::default();
|
||||
pub static ref CLIENT_STATES: Mutex<FxHashMap<String, Arc<ClientStateParsed>>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -27,29 +31,29 @@ impl LaunchInfo {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ClientState {
|
||||
pub struct ClientStateParsed {
|
||||
pub launch_info: Option<LaunchInfo>,
|
||||
#[serde(skip)]
|
||||
pub data: Option<Vec<u8>>,
|
||||
pub root: Mat4,
|
||||
pub spatial_anchors: FxHashMap<String, Mat4>,
|
||||
}
|
||||
impl ClientState {
|
||||
pub fn from_deserialized(client: &Client, state: ClientStateInternal) -> Self {
|
||||
ClientState {
|
||||
impl ClientStateParsed {
|
||||
pub fn from_deserialized(client: &Client, state: ClientState) -> Self {
|
||||
ClientStateParsed {
|
||||
launch_info: LaunchInfo::from_client(client),
|
||||
data: state.data,
|
||||
root: Self::spatial_transform(client, &state.root.unwrap_or_default())
|
||||
.unwrap_or_default(),
|
||||
root: Self::spatial_transform(client, state.root).unwrap_or_default(),
|
||||
spatial_anchors: state
|
||||
.spatial_anchors
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| Some((k, Self::spatial_transform(client, &v)?)))
|
||||
.filter_map(|(k, v)| Some((k, Self::spatial_transform(client, v)?)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
fn spatial_transform(client: &Client, path: &str) -> Option<Mat4> {
|
||||
let node = client.scenegraph.get_node(path)?;
|
||||
let spatial = node.spatial.get()?;
|
||||
fn spatial_transform(client: &Client, id: u64) -> Option<Mat4> {
|
||||
let node = client.scenegraph.get_node(id)?;
|
||||
let spatial = node.get_aspect::<Spatial>().ok()?;
|
||||
Some(spatial.global_transform())
|
||||
}
|
||||
|
||||
@@ -58,38 +62,58 @@ impl ClientState {
|
||||
CLIENT_STATES.lock().insert(token.clone(), Arc::new(self));
|
||||
token
|
||||
}
|
||||
pub fn to_file(self) {
|
||||
let project_dirs = directories::ProjectDirs::from("", "", "stardust").unwrap();
|
||||
let state_dir = project_dirs.state_dir().unwrap();
|
||||
std::fs::create_dir_all(state_dir).unwrap();
|
||||
let mut file = std::fs::File::create(state_dir.join(nanoid::nanoid!())).unwrap();
|
||||
file.write_all(&stardust_xr::schemas::flex::flexbuffers::to_vec(self).unwrap())
|
||||
.unwrap();
|
||||
pub fn from_file(file: &Path) -> Option<Self> {
|
||||
let file_string = std::fs::read_to_string(file).ok()?;
|
||||
let mut client_state: Self = toml::from_str(&file_string).ok()?;
|
||||
client_state.data = std::fs::read(file.with_extension("bin")).ok();
|
||||
Some(client_state)
|
||||
}
|
||||
pub fn apply_to(&self, client: &Arc<Client>) -> ClientStateInternal {
|
||||
pub fn to_file(&self, directory: &Path) {
|
||||
let app_name = self
|
||||
.launch_info
|
||||
.as_ref()
|
||||
.map(|l| l.cmdline.first().unwrap().split('/').next_back().unwrap())
|
||||
.unwrap_or("unknown");
|
||||
let state_file_prefix = directory.join(format!("{app_name}-{}", nanoid::nanoid!()));
|
||||
let state_metadata_path = state_file_prefix.with_extension("toml");
|
||||
let state_data_path = state_file_prefix.with_extension("bin");
|
||||
|
||||
std::fs::write(state_metadata_path, toml::to_string(&self).unwrap()).unwrap();
|
||||
if let Some(data) = self.data.as_deref() {
|
||||
std::fs::write(state_data_path, data).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, client: &Arc<Client>) -> ClientState {
|
||||
if let Some(root) = client.root.get() {
|
||||
root.set_transform(self.root)
|
||||
}
|
||||
ClientStateInternal {
|
||||
ClientState {
|
||||
data: self.data.clone(),
|
||||
root: Some("/".to_string()),
|
||||
root: 0,
|
||||
spatial_anchors: self
|
||||
.spatial_anchors
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(k.clone(), {
|
||||
let node = Node::create(client, "/spatial/anchor", k, true)
|
||||
.add_to_scenegraph()
|
||||
.unwrap();
|
||||
Spatial::add_to(&node, None, *v, false).unwrap();
|
||||
k.clone()
|
||||
})
|
||||
let node = Node::generate(client, true).add_to_scenegraph().unwrap();
|
||||
Spatial::add_to(&node, None, *v, false);
|
||||
(k.clone(), node.get_id())
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
pub fn launch_command(self) -> Option<Command> {
|
||||
let launch_info = self.launch_info.as_ref()?;
|
||||
let mut cmdline = launch_info.cmdline.iter();
|
||||
let mut command = Command::new(cmdline.next()?);
|
||||
command.args(cmdline);
|
||||
command.current_dir(&launch_info.cwd);
|
||||
command.envs(launch_info.env.iter());
|
||||
command.env("STARDUST_STARTUP_TOKEN", self.token());
|
||||
Some(command)
|
||||
}
|
||||
}
|
||||
impl Default for ClientState {
|
||||
impl Default for ClientStateParsed {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
launch_info: None,
|
||||
@@ -99,10 +123,3 @@ impl Default for ClientState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct ClientStateInternal {
|
||||
data: Option<Vec<u8>>,
|
||||
root: Option<String>,
|
||||
spatial_anchors: FxHashMap<String, String>,
|
||||
}
|
||||
|
||||
10
src/core/color.rs
Normal file
10
src/core/color.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use stardust_xr::values::color::{AlphaColor, Rgb, color_space::LinearRgb};
|
||||
|
||||
pub trait ColorConvert {
|
||||
fn to_bevy(&self) -> bevy::color::Color;
|
||||
}
|
||||
impl ColorConvert for AlphaColor<f32, Rgb<f32, LinearRgb>> {
|
||||
fn to_bevy(&self) -> bevy::color::Color {
|
||||
bevy::color::Color::linear_rgba(self.c.r, self.c.g, self.c.b, self.a)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
use parking_lot::Mutex;
|
||||
use std::any::Any;
|
||||
|
||||
static MAIN_DESTROY_QUEUE: Mutex<Vec<Box<dyn Any + Send + Sync>>> = Mutex::new(Vec::new());
|
||||
|
||||
pub fn add<T: Any + Sync + Send>(thing: T) {
|
||||
MAIN_DESTROY_QUEUE.lock().push(Box::new(thing));
|
||||
}
|
||||
|
||||
pub fn clear() {
|
||||
MAIN_DESTROY_QUEUE.lock().clear();
|
||||
}
|
||||
60
src/core/entity_handle.rs
Normal file
60
src/core/entity_handle.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::nodes::spatial::SpatialNode;
|
||||
|
||||
use super::bevy_channel::{BevyChannel, BevyChannelReader};
|
||||
pub struct EntityHandlePlugin;
|
||||
|
||||
impl Plugin for EntityHandlePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
DESTROY.init(app);
|
||||
app.add_systems(PreUpdate, despawn);
|
||||
}
|
||||
}
|
||||
|
||||
fn despawn(
|
||||
mut cmds: Commands,
|
||||
mut reader: ResMut<BevyChannelReader<Entity>>,
|
||||
child_query: Query<&Children>,
|
||||
has_spatial: Query<Has<SpatialNode>>,
|
||||
) {
|
||||
while let Some(e) = reader.read() {
|
||||
if let Ok(children) = child_query.get(e) {
|
||||
for e in children {
|
||||
if has_spatial.get(*e).unwrap_or_default() {
|
||||
cmds.entity(*e).try_remove::<ChildOf>();
|
||||
}
|
||||
}
|
||||
}
|
||||
cmds.entity(e).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
pub struct EntityHandle(Arc<EntityHandleInner>);
|
||||
impl EntityHandle {
|
||||
pub fn get(&self) -> Entity {
|
||||
self.0.0
|
||||
}
|
||||
pub fn new(entity: Entity) -> Self {
|
||||
Self(EntityHandleInner(entity).into())
|
||||
}
|
||||
}
|
||||
impl Deref for EntityHandle {
|
||||
type Target = Entity;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0.0
|
||||
}
|
||||
}
|
||||
static DESTROY: BevyChannel<Entity> = BevyChannel::new();
|
||||
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct EntityHandleInner(Entity);
|
||||
impl Drop for EntityHandleInner {
|
||||
fn drop(&mut self) {
|
||||
DESTROY.send(self.0);
|
||||
}
|
||||
}
|
||||
72
src/core/error.rs
Normal file
72
src/core/error.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::any::TypeId;
|
||||
|
||||
use color_eyre::eyre::Report;
|
||||
use stardust_xr::{
|
||||
messenger::MessengerError,
|
||||
schemas::flex::{
|
||||
FlexSerializeError,
|
||||
flexbuffers::{DeserializationError, ReaderError},
|
||||
},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T, E = ServerError> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ServerError {
|
||||
#[error("Internal: Unable to get client")]
|
||||
NoClient,
|
||||
#[error("Messenger does not exist for this node")]
|
||||
NoMessenger,
|
||||
#[error("Messenger error: {0}")]
|
||||
MessengerError(#[from] MessengerError),
|
||||
#[error("Remote method error: {0}")]
|
||||
RemoteMethodError(String),
|
||||
#[error("Serialization error: {0}")]
|
||||
SerializationError(#[from] FlexSerializeError),
|
||||
#[error("Deserialization error: {0}")]
|
||||
DeserializationError(#[from] DeserializationError),
|
||||
#[error("Reader error: {0}")]
|
||||
ReaderError(#[from] ReaderError),
|
||||
#[error("Aspect {} does not exist for node", 0.to_string())]
|
||||
NoAspect(TypeId),
|
||||
#[error("{0}")]
|
||||
Report(#[from] Report),
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bail {
|
||||
($msg:literal $(,)?) => {
|
||||
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($msg)));
|
||||
};
|
||||
($err:expr $(,)?) => {
|
||||
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($err)));
|
||||
};
|
||||
($fmt:expr, $($arg:tt)*) => {
|
||||
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($fmt, $($arg)*)));
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ensure {
|
||||
($cond:expr $(,)?) => {
|
||||
if !$cond {
|
||||
$crate::ensure!($cond, concat!("Condition failed: `", stringify!($cond), "`"))
|
||||
}
|
||||
};
|
||||
($cond:expr, $msg:literal $(,)?) => {
|
||||
if !$cond {
|
||||
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($msg)));
|
||||
}
|
||||
};
|
||||
($cond:expr, $err:expr $(,)?) => {
|
||||
if !$cond {
|
||||
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($err)));
|
||||
}
|
||||
};
|
||||
($cond:expr, $fmt:expr, $($arg:tt)*) => {
|
||||
if !$cond {
|
||||
return Err($crate::core::error::ServerError::from(color_eyre::eyre::eyre!($fmt, $($arg)*)));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
use super::client::Client;
|
||||
use super::task;
|
||||
use color_eyre::eyre::Result;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::UnixListener;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::error;
|
||||
|
||||
pub struct EventLoop {
|
||||
join_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl EventLoop {
|
||||
pub fn new(socket_path: PathBuf) -> Result<Arc<Self>> {
|
||||
let socket = UnixListener::bind(socket_path)?;
|
||||
|
||||
let join_handle = task::new(|| "event loop", async move {
|
||||
loop {
|
||||
let Ok((socket, _)) = socket.accept().await else { continue };
|
||||
if let Err(e) = Client::from_connection(socket) {
|
||||
error!(?e, "Unable to create client from connection");
|
||||
}
|
||||
}
|
||||
})?;
|
||||
let event_loop = Arc::new(EventLoop { join_handle });
|
||||
|
||||
Ok(event_loop)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EventLoop {
|
||||
fn drop(&mut self) {
|
||||
self.join_handle.abort();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
pub mod bevy_channel;
|
||||
pub mod client;
|
||||
pub mod client_state;
|
||||
pub mod color;
|
||||
pub mod delta;
|
||||
pub mod destroy_queue;
|
||||
pub mod eventloop;
|
||||
pub mod node_collections;
|
||||
pub mod entity_handle;
|
||||
pub mod error;
|
||||
pub mod registry;
|
||||
pub mod resource;
|
||||
pub mod scenegraph;
|
||||
pub mod task;
|
||||
pub mod typed_datamap;
|
||||
pub mod buffers;
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
use crate::nodes::Node;
|
||||
use parking_lot::Mutex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
hash::Hash,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
// #[derive(Default)]
|
||||
// pub struct LifeLinkedNodeList {
|
||||
// nodes: Mutex<Vec<Weak<Node>>>,
|
||||
// }
|
||||
// impl LifeLinkedNodeList {
|
||||
// pub fn add(&self, node: Weak<Node>) {
|
||||
// self.nodes.lock().push(node);
|
||||
// }
|
||||
// pub fn clear(&self) {
|
||||
// self.nodes
|
||||
// .lock()
|
||||
// .iter()
|
||||
// .filter_map(|node| node.upgrade())
|
||||
// .for_each(|node| {
|
||||
// node.destroy();
|
||||
// });
|
||||
// self.nodes.lock().clear();
|
||||
// }
|
||||
// }
|
||||
// impl Drop for LifeLinkedNodeList {
|
||||
// fn drop(&mut self) {
|
||||
// self.clear();
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct LifeLinkedNodeMap<K: Hash + Eq> {
|
||||
nodes: Mutex<FxHashMap<K, Weak<Node>>>,
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
impl<K: Hash + Eq> LifeLinkedNodeMap<K> {
|
||||
pub fn add(&self, key: K, node: &Arc<Node>) {
|
||||
self.nodes.lock().insert(key, Arc::downgrade(node));
|
||||
}
|
||||
pub fn get<Q>(&self, key: &Q) -> Option<Arc<Node>>
|
||||
where
|
||||
Q: ?Sized,
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq,
|
||||
{
|
||||
self.nodes.lock().get(key).and_then(|n| n.upgrade())
|
||||
}
|
||||
pub fn nodes(&self) -> Vec<Arc<Node>> {
|
||||
self.nodes
|
||||
.lock()
|
||||
.values()
|
||||
.filter_map(|v| v.upgrade())
|
||||
.collect()
|
||||
}
|
||||
pub fn remove<Q>(&self, key: &Q) -> Option<Arc<Node>>
|
||||
where
|
||||
Q: ?Sized,
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq,
|
||||
{
|
||||
self.nodes.lock().remove(key).and_then(|n| n.upgrade())
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
let mut nodes = self.nodes.lock();
|
||||
nodes
|
||||
.values()
|
||||
.filter_map(|node| node.upgrade())
|
||||
.for_each(|node| {
|
||||
node.destroy();
|
||||
});
|
||||
nodes.clear();
|
||||
}
|
||||
}
|
||||
impl<K: Hash + Eq> Drop for LifeLinkedNodeMap<K> {
|
||||
fn drop(&mut self) {
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use parking_lot::{const_mutex, MappedMutexGuard, Mutex, MutexGuard};
|
||||
use dashmap::DashMap;
|
||||
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard, const_mutex};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::ops::Deref;
|
||||
use std::ptr;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::sync::{Arc, LazyLock, Weak};
|
||||
|
||||
pub struct Registry<T: Send + Sync + ?Sized>(Mutex<Option<FxHashMap<usize, Weak<T>>>>);
|
||||
#[derive(Debug)]
|
||||
pub struct Registry<T: Send + Sync + ?Sized>(MaybeLazy<DashMap<usize, Weak<T>>>);
|
||||
|
||||
impl<T: Send + Sync + ?Sized> Registry<T> {
|
||||
pub const fn new() -> Self {
|
||||
Registry(const_mutex(None))
|
||||
}
|
||||
fn lock(&self) -> MappedMutexGuard<FxHashMap<usize, Weak<T>>> {
|
||||
MutexGuard::map(self.0.lock(), |r| {
|
||||
r.get_or_insert_with(|| FxHashMap::default())
|
||||
})
|
||||
Registry(MaybeLazy::Lazy(LazyLock::new(DashMap::default)))
|
||||
}
|
||||
pub fn add(&self, t: T) -> Arc<T>
|
||||
where
|
||||
@@ -25,47 +23,117 @@ impl<T: Send + Sync + ?Sized> Registry<T> {
|
||||
t_arc
|
||||
}
|
||||
pub fn add_raw(&self, t: &Arc<T>) {
|
||||
self.lock()
|
||||
self.0
|
||||
.insert(Arc::as_ptr(t) as *const () as usize, Arc::downgrade(t));
|
||||
}
|
||||
pub fn contains(&self, t: &T) -> bool {
|
||||
self.lock()
|
||||
self.0
|
||||
.contains_key(&(ptr::addr_of!(*t) as *const () as usize))
|
||||
}
|
||||
pub fn get_changes(old: &Registry<T>, new: &Registry<T>) -> (Vec<Arc<T>>, Vec<Arc<T>>) {
|
||||
let mut added = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
|
||||
for pair in new.0.iter() {
|
||||
let (id, entry) = pair.pair();
|
||||
if let Some(entry) = entry.upgrade()
|
||||
&& !old.0.contains_key(id)
|
||||
{
|
||||
added.push(entry);
|
||||
}
|
||||
}
|
||||
for pair in old.0.iter() {
|
||||
let (id, entry) = pair.pair();
|
||||
if let Some(entry) = entry.upgrade()
|
||||
&& !new.0.contains_key(id)
|
||||
{
|
||||
removed.push(entry);
|
||||
}
|
||||
}
|
||||
(added, removed)
|
||||
}
|
||||
pub fn get_valid_contents(&self) -> Vec<Arc<T>> {
|
||||
self.lock()
|
||||
self.0
|
||||
.iter()
|
||||
.filter_map(|pair| pair.1.upgrade())
|
||||
.filter_map(|pair| pair.value().upgrade())
|
||||
.collect()
|
||||
}
|
||||
pub fn set(&self, other: &Registry<T>) {
|
||||
self.clear();
|
||||
for (key, value) in other.0.deref().clone().into_iter() {
|
||||
self.0.insert(key, value);
|
||||
}
|
||||
}
|
||||
pub fn take_valid_contents(&self) -> Vec<Arc<T>> {
|
||||
self.0
|
||||
.lock()
|
||||
.take()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|pair| pair.1.upgrade())
|
||||
.collect()
|
||||
let contents = self.get_valid_contents();
|
||||
self.0.clear();
|
||||
contents
|
||||
}
|
||||
pub fn retain<F: Fn(&Arc<T>) -> bool>(&self, f: F) {
|
||||
self.0.retain(|_, v| {
|
||||
let Some(v) = v.upgrade() else {
|
||||
// why would we want to retain things we can't upgrade?
|
||||
return true;
|
||||
};
|
||||
(f)(&v)
|
||||
})
|
||||
}
|
||||
pub fn remove(&self, t: &T) {
|
||||
self.lock()
|
||||
.remove(&(ptr::addr_of!(*t) as *const () as usize));
|
||||
self.0.remove(&(ptr::addr_of!(*t) as *const () as usize));
|
||||
}
|
||||
pub fn clear(&self) {
|
||||
self.lock().clear();
|
||||
self.0.clear();
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
let registry = self.0.lock();
|
||||
let Some(registry) = &*registry else {return true};
|
||||
if registry.is_empty() {
|
||||
if self.0.is_empty() {
|
||||
return true;
|
||||
}
|
||||
registry.values().all(|v| v.strong_count() == 0)
|
||||
self.0.iter().all(|v| v.value().strong_count() == 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + ?Sized> Clone for Registry<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(Mutex::new(self.0.lock().clone()))
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
impl<T: Send + Sync + ?Sized> Default for Registry<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + Sized> FromIterator<Arc<T>> for Registry<T> {
|
||||
fn from_iter<I: IntoIterator<Item = Arc<T>>>(iter: I) -> Self {
|
||||
Registry(MaybeLazy::NonLazy(
|
||||
iter.into_iter()
|
||||
.map(|i| (Arc::as_ptr(&i) as usize, Arc::downgrade(&i)))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MaybeLazy<T> {
|
||||
Lazy(LazyLock<T>),
|
||||
NonLazy(T),
|
||||
}
|
||||
impl<T: Clone> Clone for MaybeLazy<T> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
MaybeLazy::Lazy(lazy_lock) => Self::NonLazy(lazy_lock.deref().clone()),
|
||||
MaybeLazy::NonLazy(v) => Self::NonLazy(v.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Deref for MaybeLazy<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
MaybeLazy::Lazy(lazy_lock) => lazy_lock,
|
||||
MaybeLazy::NonLazy(v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,10 +143,8 @@ impl<T: Send + Sync + ?Sized> OwnedRegistry<T> {
|
||||
pub const fn new() -> Self {
|
||||
OwnedRegistry(const_mutex(None))
|
||||
}
|
||||
fn lock(&self) -> MappedMutexGuard<FxHashMap<usize, Arc<T>>> {
|
||||
MutexGuard::map(self.0.lock(), |r| {
|
||||
r.get_or_insert_with(|| FxHashMap::default())
|
||||
})
|
||||
fn lock(&self) -> MappedMutexGuard<'_, FxHashMap<usize, Arc<T>>> {
|
||||
MutexGuard::map(self.0.lock(), |r| r.get_or_insert_with(FxHashMap::default))
|
||||
}
|
||||
pub fn add(&self, t: T) -> Arc<T>
|
||||
where
|
||||
@@ -98,9 +164,12 @@ impl<T: Send + Sync + ?Sized> OwnedRegistry<T> {
|
||||
self.lock()
|
||||
.contains_key(&(ptr::addr_of!(*t) as *const () as usize))
|
||||
}
|
||||
pub fn remove(&self, t: &T) {
|
||||
pub fn remove(&self, t: &T) -> Option<Arc<T>>
|
||||
where
|
||||
T: Sized,
|
||||
{
|
||||
self.lock()
|
||||
.remove(&(ptr::addr_of!(*t) as *const () as usize));
|
||||
.remove(&(ptr::addr_of!(*t) as *const () as usize))
|
||||
}
|
||||
pub fn clear(&self) {
|
||||
self.lock().clear();
|
||||
|
||||
@@ -1,83 +1,49 @@
|
||||
use color_eyre::eyre::eyre;
|
||||
use serde::{de::Visitor, Deserialize};
|
||||
use std::{ffi::OsStr, path::PathBuf};
|
||||
use stardust_xr::values::ResourceID;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ResourceID {
|
||||
File(PathBuf),
|
||||
Namespaced { namespace: String, path: PathBuf },
|
||||
use super::client::Client;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref THEMES: Vec<PathBuf> = std::env::var("STARDUST_THEMES").map(|s| s.split(':').map(PathBuf::from).collect()).unwrap_or_default();
|
||||
}
|
||||
impl ResourceID {
|
||||
pub fn get_file(&self, prefixes: &[PathBuf], extensions: &[&OsStr]) -> Option<PathBuf> {
|
||||
match self {
|
||||
ResourceID::File(file) => (file.is_absolute()
|
||||
&& file.exists() && Self::has_extension(file, extensions))
|
||||
.then_some(file.clone()),
|
||||
ResourceID::Namespaced { namespace, path } => {
|
||||
let file_name = path.file_name()?;
|
||||
prefixes
|
||||
.iter()
|
||||
.filter_map(|prefix| {
|
||||
let prefixed_path = prefix.clone().join(namespace).join(path);
|
||||
let parent = prefixed_path.parent()?;
|
||||
std::fs::read_dir(parent).ok()
|
||||
})
|
||||
.flatten()
|
||||
.filter_map(|item| item.ok())
|
||||
.map(|dir_entry| dir_entry.path())
|
||||
.filter(|path| path.file_stem() == Some(file_name))
|
||||
.find(|path| Self::has_extension(path, extensions))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_extension(path: &PathBuf, extensions: &[&OsStr]) -> bool {
|
||||
if let Some(path_extension) = path.extension() {
|
||||
extensions.contains(&path_extension)
|
||||
} else {
|
||||
false
|
||||
fn has_extension(path: &Path, extensions: &[&OsStr]) -> bool {
|
||||
if let Some(path_extension) = path.extension() {
|
||||
extensions.contains(&path_extension)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_resource_file(
|
||||
resource: &ResourceID,
|
||||
client: &Client,
|
||||
extensions: &[&OsStr],
|
||||
) -> Option<PathBuf> {
|
||||
match resource {
|
||||
ResourceID::Direct(file) => {
|
||||
(file.is_absolute() && file.exists() && has_extension(file, extensions))
|
||||
.then_some(file.clone())
|
||||
}
|
||||
ResourceID::Namespaced { namespace, path } => {
|
||||
let file_name = path.file_name()?;
|
||||
let base_prefixes = client.base_resource_prefixes.lock().clone();
|
||||
THEMES
|
||||
.iter()
|
||||
.chain(base_prefixes.iter())
|
||||
.filter_map(|prefix| {
|
||||
let prefixed_path = prefix.clone().join(namespace).join(path);
|
||||
let parent = prefixed_path.parent()?;
|
||||
std::fs::read_dir(parent).ok()
|
||||
})
|
||||
.flatten()
|
||||
.filter_map(|item| item.ok())
|
||||
.map(|dir_entry| dir_entry.path())
|
||||
.filter(|path| path.file_stem() == Some(file_name))
|
||||
.find(|path| has_extension(path, extensions))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for ResourceID {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(ResourceVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct ResourceVisitor;
|
||||
impl<'de> Visitor<'de> for ResourceVisitor {
|
||||
type Value = ResourceID;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("A string containing an absolute path to file or \"[namespace]:[path]\" for a namespaced resource")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(if v.starts_with('/') {
|
||||
let path = PathBuf::from(v);
|
||||
path.metadata().map_err(serde::de::Error::custom)?;
|
||||
ResourceID::File(path)
|
||||
} else if let Some((namespace, path)) = v.split_once(':') {
|
||||
ResourceID::Namespaced {
|
||||
namespace: namespace.to_string(),
|
||||
path: PathBuf::from(path),
|
||||
}
|
||||
} else {
|
||||
return Err(serde::de::Error::custom(eyre!("Invalid format for string")));
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
self.visit_str(&v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,84 @@
|
||||
use crate::nodes::Node;
|
||||
use crate::{core::client::Client, nodes::Message};
|
||||
use color_eyre::eyre::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use crate::{
|
||||
core::{
|
||||
client::Client,
|
||||
error::{Result, ServerError},
|
||||
},
|
||||
nodes::{Message, Node, alias::get_original},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use portable_atomic::Ordering;
|
||||
use rustc_hash::FxHashMap;
|
||||
use stardust_xr::scenegraph;
|
||||
use stardust_xr::scenegraph::ScenegraphError;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::sync::{Arc, Weak};
|
||||
use tokio::sync::oneshot;
|
||||
use serde::Serialize;
|
||||
use stardust_xr::{
|
||||
messenger::MethodResponse,
|
||||
scenegraph::{self, ScenegraphError},
|
||||
schemas::flex::serialize,
|
||||
};
|
||||
use std::{
|
||||
os::fd::OwnedFd,
|
||||
sync::{Arc, OnceLock, Weak},
|
||||
};
|
||||
use tracing::{debug, debug_span};
|
||||
|
||||
pub struct MethodResponseSender(pub(crate) MethodResponse);
|
||||
impl MethodResponseSender {
|
||||
pub fn send_err(self, error: ScenegraphError) {
|
||||
self.0.send(Err(error));
|
||||
}
|
||||
pub fn send<T: Serialize>(self, result: Result<T, ServerError>) {
|
||||
let data = match result {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
self.0.send(Err(ScenegraphError::MemberError {
|
||||
error: e.to_string(),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Ok(serialized) = stardust_xr::schemas::flex::serialize(data) else {
|
||||
self.0.send(Err(ScenegraphError::MemberError {
|
||||
error: "Internal: Failed to serialize".to_string(),
|
||||
}));
|
||||
return;
|
||||
};
|
||||
self.0.send(Ok((&serialized, Vec::<OwnedFd>::new())));
|
||||
}
|
||||
pub fn wrap<T: Serialize, F: FnOnce() -> Result<T>>(self, f: F) {
|
||||
self.send(f())
|
||||
}
|
||||
pub fn wrap_async<T: Serialize>(
|
||||
self,
|
||||
f: impl Future<Output = Result<(T, Vec<OwnedFd>)>> + Send + 'static,
|
||||
) {
|
||||
tokio::task::spawn(async move {
|
||||
let (value, fds) = match f.await {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
self.0.send(Err(ScenegraphError::MemberError {
|
||||
error: e.to_string(),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Ok(serialized) = serialize(value) else {
|
||||
self.0.send(Err(ScenegraphError::MemberError {
|
||||
error: "Internal: Failed to serialize".to_string(),
|
||||
}));
|
||||
return;
|
||||
};
|
||||
self.0.send(Ok((&serialized, fds)));
|
||||
});
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for MethodResponseSender {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("TypedMethodResponse").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Scenegraph {
|
||||
pub(super) client: OnceCell<Weak<Client>>,
|
||||
nodes: Mutex<FxHashMap<String, Arc<Node>>>,
|
||||
pub(super) client: OnceLock<Weak<Client>>,
|
||||
nodes: Mutex<FxHashMap<u64, Arc<Node>>>,
|
||||
}
|
||||
|
||||
impl Scenegraph {
|
||||
@@ -30,55 +93,37 @@ impl Scenegraph {
|
||||
}
|
||||
pub fn add_node_raw(&self, node: Arc<Node>) {
|
||||
debug!(node = ?&*node, "Add node");
|
||||
let path = node.get_path().to_string();
|
||||
self.nodes.lock().insert(path, node);
|
||||
self.nodes.lock().insert(node.get_id(), node);
|
||||
}
|
||||
|
||||
pub fn get_node(&self, path: &str) -> Option<Arc<Node>> {
|
||||
let mut node = self.nodes.lock().get(path)?.clone();
|
||||
while let Some(alias) = node.alias.get() {
|
||||
if alias.enabled.load(Ordering::Relaxed) {
|
||||
node = alias.original.upgrade()?;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(node)
|
||||
pub fn get_node(&self, node: u64) -> Option<Arc<Node>> {
|
||||
let node = self.nodes.lock().get(&node)?.clone();
|
||||
get_original(node, true)
|
||||
}
|
||||
|
||||
pub fn remove_node(&self, path: &str) -> Option<Arc<Node>> {
|
||||
debug!(path, "Remove node");
|
||||
self.nodes.lock().remove(path)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MethodResponseSender(oneshot::Sender<Result<(Vec<u8>, Vec<OwnedFd>), ScenegraphError>>);
|
||||
impl MethodResponseSender {
|
||||
pub fn send(self, t: Result<Message, ScenegraphError>) {
|
||||
let _ = self.0.send(t.map(|m| (m.data, m.fds)));
|
||||
}
|
||||
pub fn wrap_sync<F: FnOnce() -> color_eyre::eyre::Result<Message>>(self, f: F) {
|
||||
self.send(f().map_err(|e| ScenegraphError::MethodError {
|
||||
error: e.to_string(),
|
||||
}))
|
||||
pub fn remove_node(&self, node: u64) -> Option<Arc<Node>> {
|
||||
debug!(node, "Remove node");
|
||||
self.nodes.lock().remove(&node)
|
||||
}
|
||||
}
|
||||
impl scenegraph::Scenegraph for Scenegraph {
|
||||
fn send_signal(
|
||||
&self,
|
||||
path: &str,
|
||||
method: &str,
|
||||
node_id: u64,
|
||||
aspect_id: u64,
|
||||
method: u64,
|
||||
data: &[u8],
|
||||
fds: Vec<OwnedFd>,
|
||||
) -> Result<(), ScenegraphError> {
|
||||
let Some(client) = self.get_client() else {
|
||||
return Err(ScenegraphError::SignalNotFound);
|
||||
return Err(ScenegraphError::NodeNotFound);
|
||||
};
|
||||
debug_span!("Handle signal", path, method).in_scope(|| {
|
||||
self.get_node(path)
|
||||
debug_span!("Handle signal", aspect_id, node_id, method).in_scope(|| {
|
||||
self.get_node(node_id)
|
||||
.ok_or(ScenegraphError::NodeNotFound)?
|
||||
.send_local_signal(
|
||||
client,
|
||||
aspect_id,
|
||||
method,
|
||||
Message {
|
||||
data: data.to_vec(),
|
||||
@@ -89,23 +134,25 @@ impl scenegraph::Scenegraph for Scenegraph {
|
||||
}
|
||||
fn execute_method(
|
||||
&self,
|
||||
path: &str,
|
||||
method: &str,
|
||||
node_id: u64,
|
||||
aspect_id: u64,
|
||||
method: u64,
|
||||
data: &[u8],
|
||||
fds: Vec<OwnedFd>,
|
||||
response: oneshot::Sender<Result<(Vec<u8>, Vec<OwnedFd>), ScenegraphError>>,
|
||||
response: MethodResponse,
|
||||
) {
|
||||
let Some(client) = self.get_client() else {
|
||||
let _ = response.send(Err(ScenegraphError::MethodNotFound));
|
||||
response.send(Err(ScenegraphError::NodeNotFound));
|
||||
return;
|
||||
};
|
||||
debug!(path, method, "Handle method");
|
||||
let Some(node) = self.get_node(path) else {
|
||||
let _ = response.send(Err(ScenegraphError::NodeNotFound));
|
||||
debug!(aspect_id, node_id, method, "Handle method");
|
||||
let Some(node) = self.get_node(node_id) else {
|
||||
response.send(Err(ScenegraphError::NodeNotFound));
|
||||
return;
|
||||
};
|
||||
node.execute_local_method(
|
||||
client,
|
||||
aspect_id,
|
||||
method,
|
||||
Message {
|
||||
data: data.to_vec(),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use color_eyre::eyre::Result;
|
||||
use std::future::Future;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
@@ -11,7 +10,7 @@ pub fn new<
|
||||
>(
|
||||
name_fn: F,
|
||||
async_future: A,
|
||||
) -> Result<JoinHandle<O>> {
|
||||
) -> std::io::Result<JoinHandle<O>> {
|
||||
#[cfg(not(feature = "profile_tokio"))]
|
||||
let result = Ok(tokio::task::spawn(async_future));
|
||||
#[cfg(feature = "profile_tokio")]
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use stardust_xr::schemas::{
|
||||
flat::Datamap,
|
||||
flex::flexbuffers::{FlexbufferSerializer, Reader, ReaderError},
|
||||
};
|
||||
|
||||
use crate::nodes::Message;
|
||||
|
||||
pub struct TypedDatamap<T: DeserializeOwned + Serialize>(T);
|
||||
impl<T: DeserializeOwned + Serialize> TypedDatamap<T> {
|
||||
pub fn new(data: T) -> Self {
|
||||
TypedDatamap(data)
|
||||
}
|
||||
pub fn from_flex(message: Message) -> Result<Self> {
|
||||
let root = Reader::get_root(message.as_ref())?;
|
||||
T::deserialize(root).map(Self::new).map_err(|e| e.into())
|
||||
}
|
||||
pub fn to_datamap(&mut self) -> Result<Datamap> {
|
||||
let mut serializer = FlexbufferSerializer::default();
|
||||
self.0.serialize(&mut serializer)?;
|
||||
Datamap::new(serializer.take_buffer()).map_err(|e| e.into())
|
||||
}
|
||||
pub fn serialize(&mut self) -> Option<Vec<u8>> {
|
||||
let mut serializer = FlexbufferSerializer::default();
|
||||
self.0.serialize(&mut serializer).ok()?;
|
||||
// check if this is actually a map
|
||||
Reader::get_root(serializer.view()).ok()?.get_map().ok()?;
|
||||
Some(serializer.take_buffer())
|
||||
}
|
||||
}
|
||||
impl<T: DeserializeOwned + Serialize> Default for TypedDatamap<T>
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self(T::default())
|
||||
}
|
||||
}
|
||||
impl<T: DeserializeOwned + Serialize> Deref for TypedDatamap<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<T: DeserializeOwned + Serialize> DerefMut for TypedDatamap<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
782
src/main.rs
782
src/main.rs
@@ -1,393 +1,525 @@
|
||||
#![recursion_limit = "256"]
|
||||
#![allow(clippy::empty_docs)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
#![allow(clippy::type_complexity)]
|
||||
mod core;
|
||||
mod nodes;
|
||||
mod objects;
|
||||
mod session;
|
||||
mod spectator_cam;
|
||||
pub mod tracking_offset;
|
||||
#[cfg(feature = "wayland")]
|
||||
mod wayland;
|
||||
|
||||
use crate::core::client::CLIENTS;
|
||||
use crate::core::client_state::ClientState;
|
||||
use crate::core::{destroy_queue, buffers};
|
||||
use crate::nodes::items::camera;
|
||||
use crate::nodes::{audio, drawable, hmd, input};
|
||||
use crate::objects::input::eye_pointer::EyePointer;
|
||||
use crate::objects::input::mouse_pointer::MousePointer;
|
||||
use crate::objects::input::sk_controller::SkController;
|
||||
use crate::objects::input::sk_hand::SkHand;
|
||||
use crate::objects::play_space::PlaySpace;
|
||||
use crate::nodes::drawable::sky::SkyPlugin;
|
||||
use crate::nodes::input;
|
||||
use crate::spectator_cam::SpectatorCameraPlugin;
|
||||
|
||||
use self::core::eventloop::EventLoop;
|
||||
use clap::Parser;
|
||||
use directories::ProjectDirs;
|
||||
use once_cell::sync::OnceCell;
|
||||
use smithay::reexports::nix;
|
||||
use stardust_xr::server;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use stereokit::{
|
||||
named_colors::BLACK, DepthMode, DisplayMode, Handed, LogLevel, StereoKitMultiThread,
|
||||
TextureFormat, TextureType,
|
||||
use bevy::MinimalPlugins;
|
||||
use bevy::a11y::AccessibilityPlugin;
|
||||
use bevy::app::{App, ScheduleRunnerPlugin, TerminalCtrlCHandlerPlugin};
|
||||
use bevy::asset::{AssetMetaCheck, UnapprovedPathMode};
|
||||
use bevy::audio::AudioPlugin;
|
||||
use bevy::core_pipeline::CorePipelinePlugin;
|
||||
use bevy::core_pipeline::oit::OrderIndependentTransparencySettings;
|
||||
use bevy::core_pipeline::tonemapping::Tonemapping;
|
||||
use bevy::diagnostic::DiagnosticsPlugin;
|
||||
use bevy::ecs::schedule::{ExecutorKind, ScheduleLabel};
|
||||
use bevy::gizmos::GizmoPlugin;
|
||||
use bevy::gltf::GltfPlugin;
|
||||
use bevy::input::InputPlugin;
|
||||
use bevy::pbr::PbrPlugin;
|
||||
use bevy::render::pipelined_rendering::{
|
||||
PipelinedRenderThreadOnCreateCallback, PipelinedRenderingPlugin,
|
||||
};
|
||||
use stereokit::{DisplayBlend, Sk};
|
||||
use bevy::render::settings::{Backends, RenderCreation, WgpuSettings};
|
||||
use bevy::render::{RenderDebugFlags, RenderPlugin};
|
||||
use bevy::scene::ScenePlugin;
|
||||
use bevy::window::{CompositeAlphaMode, PresentMode};
|
||||
use bevy::winit::{WakeUp, WinitPlugin};
|
||||
use bevy_dmabuf::import::DmabufImportPlugin;
|
||||
use bevy_mod_openxr::action_set_attaching::OxrActionAttachingPlugin;
|
||||
use bevy_mod_openxr::action_set_syncing::OxrActionSyncingPlugin;
|
||||
use bevy_mod_openxr::add_xr_plugins;
|
||||
use bevy_mod_openxr::exts::OxrExtensions;
|
||||
use bevy_mod_openxr::features::overlay::OxrOverlaySettings;
|
||||
use bevy_mod_openxr::graphics::{GraphicsBackend, OxrManualGraphicsConfig};
|
||||
use bevy_mod_openxr::init::{OxrInitPlugin, should_run_frame_loop};
|
||||
use bevy_mod_openxr::reference_space::OxrReferenceSpacePlugin;
|
||||
use bevy_mod_openxr::render::{OxrRenderPlugin, OxrWaitFrameSystem};
|
||||
use bevy_mod_openxr::resources::{OxrFrameState, OxrFrameWaiter, OxrSessionConfig};
|
||||
use bevy_mod_openxr::types::AppInfo;
|
||||
use bevy_mod_xr::camera::XrProjection;
|
||||
use bevy_mod_xr::session::{XrFirst, XrHandleEvents, XrSessionPlugin};
|
||||
use clap::Parser;
|
||||
use core::client::{Client, tick_internal_client};
|
||||
use core::entity_handle::EntityHandlePlugin;
|
||||
use core::task;
|
||||
use directories::ProjectDirs;
|
||||
use nodes::audio::AudioNodePlugin;
|
||||
use nodes::drawable::lines::LinesNodePlugin;
|
||||
use nodes::drawable::model::ModelNodePlugin;
|
||||
use nodes::drawable::text::TextNodePlugin;
|
||||
use nodes::fields::FieldDebugGizmoPlugin;
|
||||
use nodes::spatial::SpatialNodePlugin;
|
||||
use objects::hmd::HmdPlugin;
|
||||
use objects::input::mouse_pointer::FlatscreenInputPlugin;
|
||||
use objects::input::oxr_controller::ControllerPlugin;
|
||||
use objects::input::oxr_hand::HandPlugin;
|
||||
use objects::play_space::PlaySpacePlugin;
|
||||
use openxr::{EnvironmentBlendMode, ReferenceSpaceType};
|
||||
use session::{launch_start, save_session};
|
||||
use stardust_xr::schemas::dbus::object_registry::ObjectRegistry;
|
||||
use stardust_xr::server::LockedSocket;
|
||||
use std::ops::DerefMut as _;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use tokio::net::UnixListener;
|
||||
use tokio::sync::Notify;
|
||||
use tokio::task::LocalSet;
|
||||
use tokio::{runtime::Handle, sync::oneshot};
|
||||
use tokio::task::JoinError;
|
||||
use tracing::metadata::LevelFilter;
|
||||
use tracing::{debug_span, error, info};
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::filter::Directive;
|
||||
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
||||
use tracking_offset::TrackingOffsetPlugin;
|
||||
#[cfg(feature = "wayland")]
|
||||
use wayland::{Wayland, WaylandPlugin};
|
||||
use zbus::Connection;
|
||||
use zbus::fdo::ObjectManager;
|
||||
|
||||
#[derive(Parser)]
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
struct CliArgs {
|
||||
/// Force flatscreen mode and use the mouse pointer as a 3D pointer
|
||||
#[clap(short, long, action)]
|
||||
flatscreen: bool,
|
||||
force_flatscreen: bool,
|
||||
|
||||
/// Replaces the flatscreen mode with a first person spectator camera
|
||||
#[clap(short, long, action)]
|
||||
spectator: bool,
|
||||
|
||||
/// Creates a transparent window fot the flatscreen mode
|
||||
#[clap(short, long, action)]
|
||||
transparent_flatscreen: bool,
|
||||
|
||||
/// If monado insists on emulating them, set this flag...we want the raw input
|
||||
#[clap(long)]
|
||||
disable_controllers: bool,
|
||||
/// If monado insists on emulating , set this flag...we want the raw input
|
||||
#[clap(long)]
|
||||
disable_hands: bool,
|
||||
|
||||
/// Run Stardust XR as an overlay with given priority
|
||||
#[clap(id = "PRIORITY", short = 'o', long = "overlay", action)]
|
||||
overlay_priority: Option<u32>,
|
||||
|
||||
/// Don't create a tip input for controller because SOME RUNTIMES will lie
|
||||
#[clap(long, action)]
|
||||
disable_controller: bool,
|
||||
/// Debug the clients started by the server
|
||||
#[clap(short = 'd', long = "debug", action)]
|
||||
debug_launched_clients: bool,
|
||||
|
||||
/// Run a script when ready for clients to connect. If this is not set the script at $HOME/.config/stardust/startup will be ran if it exists.
|
||||
#[clap(id = "PATH", short = 'e', long = "execute-startup-script", action)]
|
||||
startup_script: Option<PathBuf>,
|
||||
|
||||
/// Restore the session with the given ID (or `latest`), ignoring the startup script. Sessions are stored in directories at `~/.local/state/stardust/`.
|
||||
#[clap(id = "SESSION_ID", long = "restore", action)]
|
||||
restore: Option<String>,
|
||||
}
|
||||
|
||||
static STARDUST_INSTANCE: OnceCell<String> = OnceCell::new();
|
||||
static SK_MULTITHREAD: OnceCell<Sk> = OnceCell::new();
|
||||
static STOP_NOTIFIER: Notify = Notify::const_new();
|
||||
pub type BevyMaterial = StandardMaterial;
|
||||
|
||||
struct EventLoopInfo {
|
||||
tokio_handle: Handle,
|
||||
socket_path: PathBuf,
|
||||
}
|
||||
static STARDUST_INSTANCE: OnceLock<String> = OnceLock::new();
|
||||
|
||||
fn main() {
|
||||
ctrlc::set_handler(|| {
|
||||
if atty::isnt(atty::Stream::Stdout) {
|
||||
STOP_NOTIFIER.notify_waiters()
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
// #[tokio::main(flavor = "current_thread")]
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<AppExit, JoinError> {
|
||||
color_eyre::install().unwrap();
|
||||
|
||||
let registry = tracing_subscriber::registry();
|
||||
|
||||
#[cfg(feature = "profile_app")]
|
||||
let registry = registry.with(tracing_tracy::TracyLayer::new().with_filter(LevelFilter::DEBUG));
|
||||
let registry = registry.with(
|
||||
tracing_tracy::TracyLayer::new(tracing_tracy::DefaultConfig::default())
|
||||
.with_filter(LevelFilter::DEBUG),
|
||||
);
|
||||
|
||||
#[cfg(feature = "profile_tokio")]
|
||||
let (console_layer, _) = console_subscriber::ConsoleLayer::builder().build();
|
||||
#[cfg(feature = "profile_tokio")]
|
||||
let registry = registry.with(console_layer);
|
||||
let registry = registry.with(console_subscriber::spawn());
|
||||
|
||||
let log_layer = fmt::Layer::new()
|
||||
.with_thread_names(true)
|
||||
.with_ansi(true)
|
||||
.with_line_number(true)
|
||||
.with_filter(EnvFilter::from_default_env());
|
||||
.with_filter(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::WARN.into())
|
||||
.from_env_lossy()
|
||||
.add_directive(Directive::from_str("bevy_mesh_text_3d::text_glyphs=off").unwrap()),
|
||||
);
|
||||
registry.with(log_layer).init();
|
||||
|
||||
let cli_args = CliArgs::parse();
|
||||
|
||||
let locked_socket =
|
||||
LockedSocket::get_free().expect("Unable to find a free stardust socket path");
|
||||
STARDUST_INSTANCE.set(locked_socket.socket_path.file_name().unwrap().to_string_lossy().into_owned()).expect("Someone hasn't done their job, yell at Nova because how is this set multiple times what the hell");
|
||||
info!(
|
||||
socket_path = ?locked_socket.socket_path.display(),
|
||||
"Stardust socket created"
|
||||
);
|
||||
let socket = UnixListener::bind(locked_socket.socket_path)
|
||||
.expect("Couldn't spawn stardust server at {socket_path}");
|
||||
task::new(|| "Stardust socket accept loop", async move {
|
||||
loop {
|
||||
let Ok((stream, _)) = socket.accept().await else {
|
||||
continue;
|
||||
};
|
||||
if let Err(e) = Client::from_connection(stream) {
|
||||
error!(?e, "Unable to create client from connection");
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
info!("Init client join loop");
|
||||
|
||||
let project_dirs = ProjectDirs::from("", "", "stardust");
|
||||
if project_dirs.is_none() {
|
||||
error!("Unable to get Stardust project directories, default skybox and startup script will not work.");
|
||||
}
|
||||
let cli_args = Arc::new(CliArgs::parse());
|
||||
|
||||
let sk = stereokit::Settings {
|
||||
app_name: "Stardust XR".to_string(),
|
||||
display_preference: if cli_args.flatscreen {
|
||||
DisplayMode::Flatscreen
|
||||
} else {
|
||||
DisplayMode::MixedReality
|
||||
},
|
||||
blend_preference: DisplayBlend::AnyTransparent,
|
||||
depth_mode: DepthMode::D32,
|
||||
log_filter: match EnvFilter::from_default_env().max_level_hint() {
|
||||
Some(LevelFilter::ERROR) => LogLevel::Error,
|
||||
Some(LevelFilter::WARN) => LogLevel::Warning,
|
||||
Some(LevelFilter::INFO) => LogLevel::Inform,
|
||||
Some(LevelFilter::DEBUG) => LogLevel::Diagnostic,
|
||||
Some(LevelFilter::TRACE) => LogLevel::Diagnostic,
|
||||
Some(LevelFilter::OFF) => LogLevel::None,
|
||||
None => LogLevel::Warning,
|
||||
},
|
||||
overlay_app: cli_args.overlay_priority.is_some(),
|
||||
overlay_priority: cli_args.overlay_priority.unwrap_or(u32::MAX),
|
||||
disable_desktop_input_window: true,
|
||||
..Default::default()
|
||||
}
|
||||
.init()
|
||||
.expect("StereoKit failed to initialize");
|
||||
let _ = SK_MULTITHREAD.set(sk.multithreaded());
|
||||
info!("Init StereoKit");
|
||||
|
||||
sk.material_set_shader(
|
||||
sk.material_find("default/material_pbr").unwrap(),
|
||||
sk.shader_find("default/shader_pbr_clip").unwrap(),
|
||||
);
|
||||
|
||||
// Skytex/light stuff
|
||||
{
|
||||
if let Some((light, tex)) = project_dirs
|
||||
.as_ref()
|
||||
.and_then(|dirs| {
|
||||
let skytex_path = dirs.config_dir().join("skytex.hdr");
|
||||
skytex_path
|
||||
.exists()
|
||||
.then(|| sk.tex_create_cubemap_file(&skytex_path, true, 100).ok())
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
sk.render_set_skytex(&tex);
|
||||
sk.render_set_skylight(light);
|
||||
} else {
|
||||
sk.render_set_skytex(sk.tex_gen_color(
|
||||
BLACK,
|
||||
1,
|
||||
1,
|
||||
TextureType::CUBEMAP,
|
||||
TextureFormat::RGBA32,
|
||||
));
|
||||
}
|
||||
error!(
|
||||
"Unable to get Stardust project directories, default skybox and startup script will not work."
|
||||
);
|
||||
}
|
||||
|
||||
let mut mouse_pointer = cli_args
|
||||
.flatscreen
|
||||
.then(MousePointer::new)
|
||||
.transpose()
|
||||
.unwrap();
|
||||
let mut hands = (!cli_args.flatscreen)
|
||||
.then(|| {
|
||||
let left = SkHand::new(Handed::Left).ok();
|
||||
let right = SkHand::new(Handed::Right).ok();
|
||||
left.zip(right)
|
||||
})
|
||||
.flatten();
|
||||
let mut controllers = (!cli_args.flatscreen && !cli_args.disable_controller)
|
||||
.then(|| {
|
||||
let left = SkController::new(&sk, Handed::Left).ok();
|
||||
let right = SkController::new(&sk, Handed::Right).ok();
|
||||
left.zip(right)
|
||||
})
|
||||
.flatten();
|
||||
let eye_pointer = (sk.active_display_mode() == DisplayMode::MixedReality
|
||||
&& sk.device_has_eye_gaze())
|
||||
.then(EyePointer::new)
|
||||
.transpose()
|
||||
.unwrap();
|
||||
let dbus_connection = Connection::session()
|
||||
.await
|
||||
.expect("Could not open dbus session");
|
||||
// why is this requested here? should there be a specific server bus name that we check
|
||||
// instead?
|
||||
dbus_connection
|
||||
.request_name("org.stardustxr.HMD")
|
||||
.await
|
||||
.expect(
|
||||
"Another instance of the server is running. This is not supported currently (but is planned).",
|
||||
);
|
||||
|
||||
if hands.is_none() {
|
||||
sk.input_hand_visible(Handed::Left, false);
|
||||
sk.input_hand_visible(Handed::Right, false);
|
||||
}
|
||||
dbus_connection
|
||||
.object_server()
|
||||
.at("/", ObjectManager)
|
||||
.await
|
||||
.expect("Couldn't add the object manager");
|
||||
|
||||
let play_space = sk
|
||||
.world_has_bounds()
|
||||
.then(|| PlaySpace::new().ok())
|
||||
.flatten();
|
||||
|
||||
let (info_sender, info_receiver) = oneshot::channel::<EventLoopInfo>();
|
||||
let event_thread = std::thread::Builder::new()
|
||||
.name("event_loop".to_owned())
|
||||
.spawn(move || event_loop(info_sender))
|
||||
.unwrap();
|
||||
let event_loop_info = info_receiver.blocking_recv().unwrap();
|
||||
let _tokio_handle = event_loop_info.tokio_handle.enter();
|
||||
|
||||
let mut buffer_manager = buffers::BufferManager::new().expect("Could not initialize buffer manager");
|
||||
let object_registry = ObjectRegistry::new(&dbus_connection).await;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
let mut wayland = wayland::Wayland::new(&buffer_manager).expect("Could not initialize wayland");
|
||||
info!("Stardust ready!");
|
||||
let _wayland = Wayland::new().expect("Couldn't create Wayland instance");
|
||||
|
||||
let mut startup_child = (|| {
|
||||
let project_dirs = project_dirs.as_ref()?;
|
||||
let startup_script_path = cli_args
|
||||
.startup_script
|
||||
.clone()
|
||||
.and_then(|p| p.canonicalize().ok())
|
||||
.unwrap_or_else(|| project_dirs.config_dir().join("startup"));
|
||||
let mut startup_command = Command::new("bash");
|
||||
startup_command.arg(startup_script_path);
|
||||
startup_command.arg("&");
|
||||
|
||||
startup_command.stdin(Stdio::null());
|
||||
startup_command.stdout(Stdio::null());
|
||||
startup_command.stderr(Stdio::null());
|
||||
startup_command.env(
|
||||
"FLAT_WAYLAND_DISPLAY",
|
||||
std::env::var_os("WAYLAND_DISPLAY").unwrap_or_default(),
|
||||
);
|
||||
startup_command.env(
|
||||
"STARDUST_INSTANCE",
|
||||
event_loop_info
|
||||
.socket_path
|
||||
.file_name()
|
||||
.expect("Stardust socket path not found"),
|
||||
);
|
||||
#[cfg(feature = "wayland")]
|
||||
{
|
||||
if let Some(wayland_socket) = wayland.socket_name.as_ref() {
|
||||
startup_command.env("WAYLAND_DISPLAY", &wayland_socket);
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
startup_command.env("DISPLAY", format!(":{}", wayland.xwayland_state.display));
|
||||
startup_command.env("GDK_BACKEND", "wayland");
|
||||
startup_command.env("QT_QPA_PLATFORM", "wayland");
|
||||
startup_command.env("MOZ_ENABLE_WAYLAND", "1");
|
||||
startup_command.env("CLUTTER_BACKEND", "wayland");
|
||||
startup_command.env("SDL_VIDEODRIVER", "wayland");
|
||||
let ready_notifier = Arc::new(Notify::new());
|
||||
let io_loop = tokio::task::spawn_blocking({
|
||||
let ready_notifier = ready_notifier.clone();
|
||||
let project_dirs = project_dirs.clone();
|
||||
let cli_args = cli_args.clone();
|
||||
let dbus_connection = dbus_connection.clone();
|
||||
move || {
|
||||
bevy_loop(
|
||||
ready_notifier,
|
||||
project_dirs,
|
||||
cli_args,
|
||||
dbus_connection,
|
||||
object_registry,
|
||||
)
|
||||
}
|
||||
unsafe {
|
||||
startup_command.pre_exec(|| {
|
||||
nix::unistd::setsid()
|
||||
.map(|_| ())
|
||||
.map_err(|_| std::io::ErrorKind::Other.into())
|
||||
})
|
||||
};
|
||||
let child = startup_command.spawn().ok()?;
|
||||
Some(child)
|
||||
})();
|
||||
|
||||
let mut last_frame_delta = Duration::ZERO;
|
||||
let mut sleep_duration = Duration::ZERO;
|
||||
debug_span!("StereoKit").in_scope(|| {
|
||||
sk.run(
|
||||
|sk| {
|
||||
let _span = debug_span!("StereoKit step");
|
||||
let _span = _span.enter();
|
||||
|
||||
camera::send_rendered();
|
||||
hmd::frame(sk);
|
||||
#[cfg(feature = "wayland")]
|
||||
wayland.frame_event(sk);
|
||||
destroy_queue::clear();
|
||||
|
||||
if let Some(mouse_pointer) = &mut mouse_pointer {
|
||||
mouse_pointer.update(sk);
|
||||
}
|
||||
if let Some((left_hand, right_hand)) = &mut hands {
|
||||
left_hand.update(!cli_args.disable_controller, sk);
|
||||
right_hand.update(!cli_args.disable_controller, sk);
|
||||
}
|
||||
if let Some((left_controller, right_controller)) = &mut controllers {
|
||||
left_controller.update(sk);
|
||||
right_controller.update(sk);
|
||||
}
|
||||
if let Some(eye_pointer) = &eye_pointer {
|
||||
eye_pointer.update(sk);
|
||||
}
|
||||
if let Some(play_space) = &play_space {
|
||||
play_space.update(sk);
|
||||
}
|
||||
input::process_input();
|
||||
nodes::root::Root::send_frame_events(sk.time_elapsed_unscaled());
|
||||
adaptive_sleep(
|
||||
sk,
|
||||
&mut last_frame_delta,
|
||||
&mut sleep_duration,
|
||||
Duration::from_micros(250),
|
||||
);
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
wayland.update(sk, &mut buffer_manager);
|
||||
drawable::draw(sk);
|
||||
audio::update(sk);
|
||||
|
||||
buffer_manager.make_context_current();
|
||||
|
||||
camera::update(sk, &mut buffer_manager);
|
||||
},
|
||||
|_sk| {
|
||||
info!("Cleanly shut down StereoKit");
|
||||
|
||||
if let Some(mut startup_child) = startup_child.take() {
|
||||
let _ = startup_child.kill();
|
||||
}
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
drop(wayland);
|
||||
|
||||
STOP_NOTIFIER.notify_waiters();
|
||||
event_thread
|
||||
.join()
|
||||
.expect("Failed to cleanly shut down event loop")
|
||||
.unwrap();
|
||||
ready_notifier.notified().await;
|
||||
let mut startup_children = project_dirs
|
||||
.as_ref()
|
||||
.map(|project_dirs| launch_start(&cli_args, project_dirs))
|
||||
.unwrap_or_default();
|
||||
let return_value = io_loop.await;
|
||||
info!("Stopping...");
|
||||
if let Some(project_dirs) = project_dirs {
|
||||
save_session(&project_dirs).await;
|
||||
}
|
||||
for mut startup_child in startup_children.drain(..) {
|
||||
let _ = startup_child.kill();
|
||||
}
|
||||
|
||||
info!("Cleanly shut down Stardust");
|
||||
return_value
|
||||
}
|
||||
|
||||
fn adaptive_sleep(
|
||||
sk: &impl StereoKitMultiThread,
|
||||
last_frame_delta: &mut Duration,
|
||||
sleep_duration: &mut Duration,
|
||||
sleep_duration_increase: Duration,
|
||||
// static DEFAULT_SKYTEX: OnceLock<Tex> = OnceLock::new();
|
||||
// static DEFAULT_SKYLIGHT: OnceLock<SphericalHarmonics> = OnceLock::new();
|
||||
|
||||
#[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct PreFrameWait;
|
||||
#[derive(Resource, Deref)]
|
||||
pub struct ObjectRegistryRes(Arc<ObjectRegistry>);
|
||||
#[derive(Resource, Deref)]
|
||||
pub struct DbusConnection(Connection);
|
||||
|
||||
fn bevy_loop(
|
||||
ready_notifier: Arc<Notify>,
|
||||
_project_dirs: Option<ProjectDirs>,
|
||||
args: CliArgs,
|
||||
dbus_connection: Connection,
|
||||
object_registry: Arc<ObjectRegistry>,
|
||||
) -> AppExit {
|
||||
let mut app = App::new();
|
||||
app.insert_resource(DbusConnection(dbus_connection));
|
||||
app.insert_resource(OxrManualGraphicsConfig {
|
||||
fallback_backend: GraphicsBackend::Vulkan(()),
|
||||
vk_instance_exts: Vec::new(),
|
||||
vk_device_exts: bevy_dmabuf::required_device_extensions(),
|
||||
});
|
||||
app.add_plugins(AssetPlugin {
|
||||
meta_check: AssetMetaCheck::Never,
|
||||
unapproved_path_mode: UnapprovedPathMode::Allow,
|
||||
..default()
|
||||
});
|
||||
let mut plugins = MinimalPlugins
|
||||
.build()
|
||||
.add(DiagnosticsPlugin)
|
||||
.add(TransformPlugin)
|
||||
.add(InputPlugin)
|
||||
.add(AccessibilityPlugin);
|
||||
plugins = plugins
|
||||
.add(TerminalCtrlCHandlerPlugin)
|
||||
// bevy_mod_openxr will replace this, TODO: figure out how to mix this with
|
||||
// bevy-dmabuf
|
||||
.add(RenderPlugin {
|
||||
render_creation: RenderCreation::Automatic(WgpuSettings {
|
||||
backends: Some(Backends::VULKAN),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
.add(ImagePlugin::default())
|
||||
.add(CorePipelinePlugin)
|
||||
// theoretically we shouldn't need this because of bevy_sk, but everything is tangled in
|
||||
// there and idk what we actually need to run
|
||||
.add(PbrPlugin {
|
||||
// this seems to only apply to StandardMaterial, we don't use that
|
||||
prepass_enabled: true,
|
||||
add_default_deferred_lighting_plugin: false,
|
||||
use_gpu_instance_buffer_builder: true,
|
||||
debug_flags: RenderDebugFlags::default(),
|
||||
})
|
||||
// required for gltf
|
||||
.add(ScenePlugin)
|
||||
.add(GltfPlugin::default())
|
||||
// .add(AnimationPlugin)
|
||||
.add(AudioPlugin::default())
|
||||
.add(GizmoPlugin)
|
||||
.add(WindowPlugin::default());
|
||||
#[cfg(feature = "wayland")]
|
||||
{
|
||||
plugins = plugins.add(DmabufImportPlugin);
|
||||
}
|
||||
let mut task_pool_plugin = TaskPoolPlugin::default();
|
||||
// make tokio work
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
let enter_runtime_context = Arc::new(move || {
|
||||
// TODO: this might be a memory leak
|
||||
std::mem::forget(handle.enter());
|
||||
});
|
||||
task_pool_plugin.task_pool_options.io.on_thread_spawn = Some(enter_runtime_context.clone());
|
||||
task_pool_plugin.task_pool_options.compute.on_thread_spawn =
|
||||
Some(enter_runtime_context.clone());
|
||||
task_pool_plugin
|
||||
.task_pool_options
|
||||
.async_compute
|
||||
.on_thread_spawn = Some(enter_runtime_context.clone());
|
||||
plugins = plugins.set(task_pool_plugin);
|
||||
if std::env::var("DISPLAY").is_ok_and(|s| !s.is_empty())
|
||||
|| std::env::var("WAYLAND_DISPLAY").is_ok_and(|s| !s.is_empty())
|
||||
{
|
||||
let mut plugin = WinitPlugin::<WakeUp>::default();
|
||||
plugin.run_on_any_thread = true;
|
||||
plugins = plugins.add(plugin).disable::<ScheduleRunnerPlugin>();
|
||||
plugins = match args.spectator {
|
||||
true => plugins.add(SpectatorCameraPlugin),
|
||||
false => plugins.add(FlatscreenInputPlugin),
|
||||
};
|
||||
}
|
||||
app.insert_resource(PipelinedRenderThreadOnCreateCallback(
|
||||
enter_runtime_context.clone(),
|
||||
));
|
||||
app.add_plugins(
|
||||
if !args.force_flatscreen {
|
||||
add_xr_plugins(plugins)
|
||||
.set(OxrInitPlugin {
|
||||
app_info: AppInfo {
|
||||
name: "Stardust XR".into(),
|
||||
version: bevy_mod_openxr::types::Version(0, 44, 1),
|
||||
},
|
||||
exts: {
|
||||
// all OpenXR extensions can be requested here
|
||||
let mut exts = OxrExtensions::default();
|
||||
exts.enable_hand_tracking();
|
||||
if args.overlay_priority.is_some() {
|
||||
exts.enable_extx_overlay();
|
||||
}
|
||||
exts.khr_convert_timespec_time = true;
|
||||
exts
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.set(OxrRenderPlugin {
|
||||
default_wait_frame: false,
|
||||
..default()
|
||||
})
|
||||
.set(OxrReferenceSpacePlugin {
|
||||
default_primary_ref_space: ReferenceSpaceType::LOCAL,
|
||||
})
|
||||
// Disable a bunch of unneeded plugins
|
||||
// we don't do any action stuff that needs to integrate with the ecosystem
|
||||
.disable::<OxrActionAttachingPlugin>()
|
||||
.disable::<OxrActionSyncingPlugin>()
|
||||
} else {
|
||||
// enable a event
|
||||
plugins = plugins.add(XrSessionPlugin { auto_handle: false });
|
||||
bevy_dmabuf::wgpu_init::add_dmabuf_init_plugin(plugins)
|
||||
}
|
||||
.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
transparent: args.transparent_flatscreen,
|
||||
present_mode: PresentMode::AutoNoVsync,
|
||||
composite_alpha_mode: if args.transparent_flatscreen {
|
||||
CompositeAlphaMode::PreMultiplied
|
||||
} else {
|
||||
CompositeAlphaMode::Auto
|
||||
},
|
||||
title: "StardustXR server flatscreen mode".to_string(),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}),
|
||||
);
|
||||
app.add_plugins(PipelinedRenderingPlugin);
|
||||
|
||||
app.add_plugins(bevy_sk::hand::HandPlugin);
|
||||
app.add_plugins(bevy_equirect::EquirectangularPlugin);
|
||||
// app.add_plugins(HandGizmosPlugin);
|
||||
app.world_mut().resource_mut::<AmbientLight>().brightness = 1000.0;
|
||||
if let Some(priority) = args.overlay_priority {
|
||||
app.insert_resource(OxrOverlaySettings {
|
||||
session_layer_placement: priority,
|
||||
..default()
|
||||
});
|
||||
}
|
||||
app.insert_resource(OxrSessionConfig {
|
||||
blend_mode_preference: vec![
|
||||
EnvironmentBlendMode::ALPHA_BLEND,
|
||||
EnvironmentBlendMode::ADDITIVE,
|
||||
EnvironmentBlendMode::OPAQUE,
|
||||
],
|
||||
..default()
|
||||
});
|
||||
let mut pre_frame_wait = Schedule::new(PreFrameWait);
|
||||
pre_frame_wait.set_executor_kind(ExecutorKind::MultiThreaded);
|
||||
app.add_schedule(pre_frame_wait);
|
||||
app.insert_resource(ClearColor(Color::BLACK.with_alpha(0.0)));
|
||||
app.insert_resource(ObjectRegistryRes(object_registry));
|
||||
#[cfg(feature = "bevy_debugging")]
|
||||
{
|
||||
use bevy::remote::{RemotePlugin, http::RemoteHttpPlugin};
|
||||
app.add_plugins((RemotePlugin::default(), RemoteHttpPlugin::default()));
|
||||
}
|
||||
// the Stardust server plugins
|
||||
// infra plugins
|
||||
app.add_plugins(EntityHandlePlugin);
|
||||
// node plugins
|
||||
app.add_plugins((
|
||||
SpatialNodePlugin,
|
||||
ModelNodePlugin,
|
||||
TextNodePlugin,
|
||||
LinesNodePlugin,
|
||||
AudioNodePlugin,
|
||||
// not really a node ig? at least for now
|
||||
SkyPlugin,
|
||||
));
|
||||
// object plugins
|
||||
app.add_plugins((PlaySpacePlugin, HandPlugin, ControllerPlugin, HmdPlugin));
|
||||
// feature plugins
|
||||
#[cfg(feature = "wayland")]
|
||||
app.add_plugins(WaylandPlugin);
|
||||
app.add_plugins((TrackingOffsetPlugin, FieldDebugGizmoPlugin));
|
||||
app.add_systems(PostStartup, move || {
|
||||
ready_notifier.notify_waiters();
|
||||
});
|
||||
app.add_observer(cam_settings);
|
||||
app.add_systems(
|
||||
XrFirst,
|
||||
xr_step
|
||||
.in_set(OxrWaitFrameSystem)
|
||||
.in_set(XrHandleEvents::FrameLoop),
|
||||
);
|
||||
|
||||
app.run()
|
||||
}
|
||||
|
||||
fn cam_settings(
|
||||
trigger: Trigger<OnAdd, Camera3d>,
|
||||
mut query: Query<(Entity, &mut Projection, &mut Msaa, &mut Tonemapping), With<Camera3d>>,
|
||||
mut cmds: Commands,
|
||||
) {
|
||||
let frame_delta = Duration::from_secs_f64(sk.time_elapsed_unscaled());
|
||||
if *last_frame_delta < frame_delta {
|
||||
if let Some(frame_delta_delta) = frame_delta.checked_sub(*last_frame_delta) {
|
||||
if let Some(new_sleep_duration) = sleep_duration.checked_sub(frame_delta_delta) {
|
||||
*sleep_duration = new_sleep_duration;
|
||||
let Ok((entity, mut projection, mut msaa, mut tonemapping)) = query.get_mut(trigger.target())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
info!("modifying cam");
|
||||
match projection.deref_mut() {
|
||||
Projection::Perspective(perspective_projection) => perspective_projection.near = 0.003,
|
||||
Projection::Orthographic(orthographic_projection) => orthographic_projection.near = 0.003,
|
||||
Projection::Custom(custom_projection) => {
|
||||
if let Some(xr) = custom_projection.get_mut::<XrProjection>() {
|
||||
xr.near = 0.003
|
||||
} else {
|
||||
error_once!("unknown custom camera projection");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
*sleep_duration += sleep_duration_increase;
|
||||
}
|
||||
|
||||
debug_span!("Sleep", ?sleep_duration, ?frame_delta, ?last_frame_delta).in_scope(|| {
|
||||
*last_frame_delta = frame_delta;
|
||||
std::thread::sleep(*sleep_duration); // to give clients a chance to even update anything before drawing
|
||||
});
|
||||
*msaa = Msaa::Off;
|
||||
*tonemapping = Tonemapping::None;
|
||||
cmds.entity(entity)
|
||||
.insert(OrderIndependentTransparencySettings::default());
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
// #[tokio::main(flavor = "current_thread")]
|
||||
async fn event_loop(info_sender: oneshot::Sender<EventLoopInfo>) -> color_eyre::eyre::Result<()> {
|
||||
let socket_path =
|
||||
server::get_free_socket_path().expect("Unable to find a free stardust socket path");
|
||||
STARDUST_INSTANCE.set(socket_path.file_name().unwrap().to_string_lossy().into_owned()).expect("Someone hasn't done their job, yell at Nova because how is this set multiple times what the hell");
|
||||
let _event_loop = EventLoop::new(socket_path.clone()).expect("Couldn't create server socket");
|
||||
info!("Init event loop");
|
||||
info!(
|
||||
socket_path = ?socket_path.display(),
|
||||
"Stardust socket created"
|
||||
);
|
||||
let _ = info_sender.send(EventLoopInfo {
|
||||
tokio_handle: Handle::current(),
|
||||
socket_path,
|
||||
});
|
||||
fn xr_step(world: &mut World) {
|
||||
// update things like the Xr input methods
|
||||
world.run_schedule(PreFrameWait);
|
||||
input::process_input();
|
||||
let time = world.resource::<bevy::prelude::Time>().delta_secs_f64();
|
||||
nodes::root::Root::send_frame_events(time);
|
||||
|
||||
STOP_NOTIFIER.notified().await;
|
||||
println!("Stopping...");
|
||||
save_clients().await;
|
||||
let should_wait = world
|
||||
.run_system_cached(should_run_frame_loop)
|
||||
.unwrap_or(false);
|
||||
// we might want to do an adaptive sleep when not OpenXR waiting
|
||||
if should_wait {
|
||||
world.resource_scope::<OxrFrameWaiter, _>(|world, mut waiter| {
|
||||
let state = waiter
|
||||
.wait()
|
||||
.inspect_err(|err| error!("failed to wait OpenXR frame: {err}"))
|
||||
.ok();
|
||||
|
||||
info!("Cleanly shut down event loop");
|
||||
|
||||
unsafe {
|
||||
stereokit::sys::sk_quit();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_clients() {
|
||||
let local_set = LocalSet::new();
|
||||
for client in CLIENTS.get_vec() {
|
||||
local_set.spawn_local(async move {
|
||||
tokio::select! {
|
||||
biased;
|
||||
s = client.save_state() => {s.map(ClientState::to_file);},
|
||||
_ = tokio::time::sleep(Duration::from_millis(100)) => (),
|
||||
if let Some(state) = state {
|
||||
world.insert_resource(OxrFrameState(state));
|
||||
}
|
||||
});
|
||||
}
|
||||
local_set.await;
|
||||
|
||||
tick_internal_client();
|
||||
}
|
||||
|
||||
pub fn get_time(pipelined: bool, state: &OxrFrameState) -> openxr::Time {
|
||||
if pipelined {
|
||||
openxr::Time::from_nanos(
|
||||
state.predicted_display_time.as_nanos() + state.predicted_display_period.as_nanos(),
|
||||
)
|
||||
} else {
|
||||
state.predicted_display_time
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,163 @@
|
||||
use super::Node;
|
||||
use crate::core::client::Client;
|
||||
use color_eyre::eyre::{ensure, Result};
|
||||
use portable_atomic::AtomicBool;
|
||||
use std::sync::{Arc, Weak};
|
||||
use super::{Aspect, AspectIdentifier, Node};
|
||||
use crate::core::{
|
||||
client::Client, error::Result, registry::Registry, scenegraph::MethodResponseSender,
|
||||
};
|
||||
use std::{
|
||||
ops::Add,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct AliasInfo {
|
||||
pub(super) server_signals: Vec<&'static str>,
|
||||
pub(super) server_methods: Vec<&'static str>,
|
||||
pub(super) client_signals: Vec<&'static str>,
|
||||
pub(super) server_signals: Vec<u64>,
|
||||
pub(super) server_methods: Vec<u64>,
|
||||
pub(super) client_signals: Vec<u64>,
|
||||
}
|
||||
impl Add for AliasInfo {
|
||||
type Output = AliasInfo;
|
||||
fn add(mut self, mut rhs: Self) -> Self::Output {
|
||||
self.server_signals.append(&mut rhs.server_signals);
|
||||
self.server_methods.append(&mut rhs.server_methods);
|
||||
self.client_signals.append(&mut rhs.client_signals);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct Alias {
|
||||
pub enabled: Arc<AtomicBool>,
|
||||
pub(super) node: Weak<Node>,
|
||||
pub original: Weak<Node>,
|
||||
|
||||
pub info: AliasInfo,
|
||||
pub(super) original: Weak<Node>,
|
||||
pub(super) info: AliasInfo,
|
||||
}
|
||||
impl Alias {
|
||||
pub fn create(
|
||||
client: &Arc<Client>,
|
||||
parent: &str,
|
||||
name: &str,
|
||||
original: &Arc<Node>,
|
||||
client: &Arc<Client>,
|
||||
info: AliasInfo,
|
||||
list: Option<&AliasList>,
|
||||
) -> Result<Arc<Node>> {
|
||||
ensure!(
|
||||
client
|
||||
.scenegraph
|
||||
.get_node(&(parent.to_string() + "/" + name))
|
||||
.is_none(),
|
||||
"Node already exists"
|
||||
);
|
||||
let node = Node::generate(client, true).add_to_scenegraph()?;
|
||||
Self::add_to(&node, original, info)?;
|
||||
if let Some(list) = list {
|
||||
list.add(&node);
|
||||
}
|
||||
Ok(node)
|
||||
}
|
||||
pub fn create_with_id(
|
||||
original: &Arc<Node>,
|
||||
client: &Arc<Client>,
|
||||
new_id: u64,
|
||||
info: AliasInfo,
|
||||
list: Option<&AliasList>,
|
||||
) -> Result<Arc<Node>> {
|
||||
let node = Node::from_id(client, new_id, true).add_to_scenegraph()?;
|
||||
Self::add_to(&node, original, info)?;
|
||||
if let Some(list) = list {
|
||||
list.add(&node);
|
||||
}
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
let node = Node::create(client, parent, name, true).add_to_scenegraph()?;
|
||||
fn add_to(new_node: &Arc<Node>, original: &Arc<Node>, info: AliasInfo) -> Result<()> {
|
||||
let alias = Alias {
|
||||
enabled: Arc::new(AtomicBool::new(true)),
|
||||
node: Arc::downgrade(&node),
|
||||
node: Arc::downgrade(new_node),
|
||||
original: Arc::downgrade(original),
|
||||
info,
|
||||
};
|
||||
let alias = original.aliases.add(alias);
|
||||
let _ = node.alias.set(alias);
|
||||
Ok(node)
|
||||
new_node.add_aspect_raw(alias);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl AspectIdentifier for Alias {
|
||||
const ID: u64 = 0;
|
||||
}
|
||||
impl Aspect for Alias {
|
||||
fn run_signal(
|
||||
&self,
|
||||
_calling_client: Arc<Client>,
|
||||
_node: Arc<Node>,
|
||||
_signal: u64,
|
||||
_message: super::Message,
|
||||
) -> Result<(), stardust_xr::scenegraph::ScenegraphError> {
|
||||
Ok(())
|
||||
}
|
||||
fn run_method(
|
||||
&self,
|
||||
_calling_client: Arc<Client>,
|
||||
_node: Arc<Node>,
|
||||
_method: u64,
|
||||
_message: super::Message,
|
||||
_response: MethodResponseSender,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_original(node: Arc<Node>, stop_on_disabled: bool) -> Option<Arc<Node>> {
|
||||
let Ok(alias) = node.get_aspect::<Alias>() else {
|
||||
return Some(node);
|
||||
};
|
||||
if stop_on_disabled && !node.enabled() {
|
||||
return None;
|
||||
}
|
||||
get_original(alias.original.upgrade()?, stop_on_disabled)
|
||||
}
|
||||
pub fn links_to(alias: Arc<Node>, original: Weak<Node>) -> bool {
|
||||
let Ok(alias) = alias.get_aspect::<Alias>() else {
|
||||
return false;
|
||||
};
|
||||
if alias.original.ptr_eq(&original) {
|
||||
return true;
|
||||
}
|
||||
let Some(original_strong) = alias.original.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
links_to(original_strong, original)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct AliasList(Registry<Node>);
|
||||
impl AliasList {
|
||||
fn add(&self, node: &Arc<Node>) {
|
||||
self.0.add_raw(node);
|
||||
}
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub fn get_from_original_node(&self, original: Weak<Node>) -> Option<Arc<Node>> {
|
||||
self.0
|
||||
.get_valid_contents()
|
||||
.into_iter()
|
||||
.find(move |node| links_to(node.clone(), original.clone()))
|
||||
}
|
||||
pub fn get_from_aspect<A: AspectIdentifier>(&self, aspect: &A) -> Option<Arc<Node>> {
|
||||
self.0.get_valid_contents().into_iter().find(|node| {
|
||||
let Some(node) = get_original(node.clone(), false) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(aspect2) = node.get_aspect::<A>() else {
|
||||
return false;
|
||||
};
|
||||
std::ptr::eq(Arc::as_ptr(&aspect2), aspect)
|
||||
})
|
||||
}
|
||||
pub fn get_aliases(&self) -> Vec<Arc<Node>> {
|
||||
self.0.get_valid_contents()
|
||||
}
|
||||
pub fn remove_aspect<A: AspectIdentifier>(&self, aspect: &A) {
|
||||
self.0.retain(|node| {
|
||||
let Some(original) = get_original(node.clone(), false) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(aspect2) = original.get_aspect::<A>() else {
|
||||
return false;
|
||||
};
|
||||
!std::ptr::eq(Arc::as_ptr(&aspect2), aspect)
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Drop for AliasList {
|
||||
fn drop(&mut self) {
|
||||
for node in self.0.take_valid_contents() {
|
||||
node.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +1,166 @@
|
||||
use super::{Message, Node};
|
||||
use super::spatial::SpatialNode;
|
||||
use super::{Aspect, AspectIdentifier, Node};
|
||||
use crate::core::client::Client;
|
||||
use crate::core::destroy_queue;
|
||||
use crate::core::entity_handle::EntityHandle;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::registry::Registry;
|
||||
use crate::core::resource::ResourceID;
|
||||
use crate::nodes::spatial::{find_spatial_parent, parse_transform, Spatial};
|
||||
use color_eyre::eyre::{ensure, eyre, Result};
|
||||
use glam::{vec3, Vec4Swizzles};
|
||||
use once_cell::sync::OnceCell;
|
||||
use crate::core::resource::get_resource_file;
|
||||
use crate::nodes::spatial::{SPATIAL_ASPECT_ALIAS_INFO, Spatial, Transform};
|
||||
use bevy::audio::{PlaybackMode, Volume};
|
||||
use bevy_mod_openxr::session::OxrSession;
|
||||
use bevy_mod_xr::session::{XrPreDestroySession, XrSessionCreated};
|
||||
use bevy_mod_xr::spaces::XrSpace;
|
||||
use color_eyre::eyre::eyre;
|
||||
use parking_lot::Mutex;
|
||||
use send_wrapper::SendWrapper;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flex::deserialize;
|
||||
use stardust_xr::values::Transform;
|
||||
use std::ops::DerefMut;
|
||||
use std::{ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
use stereokit::{Sound as SkSound, SoundInstance, StereoKitDraw};
|
||||
use stardust_xr::values::ResourceID;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::transform::components::Transform as BevyTransform;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::{ffi::OsStr, path::PathBuf};
|
||||
|
||||
pub struct AudioNodePlugin;
|
||||
impl Plugin for AudioNodePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Update, update_sound_event);
|
||||
app.add_systems(XrSessionCreated, spawn_hmd_audio_listener);
|
||||
app.add_systems(XrPreDestroySession, despawn_hmd_audio_listener);
|
||||
}
|
||||
}
|
||||
|
||||
fn despawn_hmd_audio_listener(mut cmds: Commands, session: Res<OxrSession>, res: Res<HmdListener>) {
|
||||
cmds.remove_resource::<HmdListener>();
|
||||
cmds.entity(res.0).despawn();
|
||||
_ = session.destroy_space(res.1);
|
||||
}
|
||||
|
||||
fn spawn_hmd_audio_listener(mut cmds: Commands, session: Res<OxrSession>) {
|
||||
let space = session
|
||||
.create_reference_space(openxr::ReferenceSpaceType::VIEW, BevyTransform::IDENTITY)
|
||||
.unwrap();
|
||||
let listener = cmds
|
||||
.spawn((
|
||||
Name::new("HMD audio listener"),
|
||||
space.0,
|
||||
SpatialListener::new(0.2),
|
||||
))
|
||||
.id();
|
||||
cmds.insert_resource(HmdListener(listener, space.0));
|
||||
}
|
||||
#[derive(Resource)]
|
||||
struct HmdListener(Entity, XrSpace);
|
||||
fn update_sound_event(
|
||||
mut cmds: Commands,
|
||||
sinks: Query<&SpatialAudioSink>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
for sound in SOUND_REGISTRY.get_valid_contents() {
|
||||
if sound.entity.get().is_none() {
|
||||
let handle = asset_server.load(sound.pending_audio_path.as_path());
|
||||
let entity = cmds
|
||||
.spawn((
|
||||
Name::new("Audio Node"),
|
||||
SpatialNode(Arc::downgrade(&sound.spatial)),
|
||||
AudioPlayer::new(handle),
|
||||
PlaybackSettings {
|
||||
mode: PlaybackMode::Once,
|
||||
volume: Volume::Linear(sound.volume),
|
||||
speed: 1.0,
|
||||
paused: true,
|
||||
muted: false,
|
||||
spatial: true,
|
||||
spatial_scale: None,
|
||||
},
|
||||
))
|
||||
.id();
|
||||
let entity = EntityHandle::new(entity);
|
||||
sound.spatial.set_entity(entity.clone());
|
||||
sound.entity.set(entity).unwrap();
|
||||
}
|
||||
if let Some(sink) = sound.entity.get().and_then(|e| sinks.get(e.get()).ok()) {
|
||||
if sound.play.lock().take().is_some() {
|
||||
sink.play();
|
||||
}
|
||||
if sound.stop.lock().take().is_some() {
|
||||
sink.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static SOUND_REGISTRY: Registry<Sound> = Registry::new();
|
||||
|
||||
stardust_xr_server_codegen::codegen_audio_protocol!();
|
||||
pub struct Sound {
|
||||
space: Arc<Spatial>,
|
||||
spatial: Arc<Spatial>,
|
||||
|
||||
volume: f32,
|
||||
pending_audio_path: PathBuf,
|
||||
sk_sound: OnceCell<SendWrapper<SkSound>>,
|
||||
instance: Mutex<Option<SoundInstance>>,
|
||||
entity: OnceLock<EntityHandle>,
|
||||
stop: Mutex<Option<()>>,
|
||||
play: Mutex<Option<()>>,
|
||||
}
|
||||
|
||||
impl Sound {
|
||||
pub fn add_to(node: &Arc<Node>, resource_id: ResourceID) -> Result<Arc<Sound>> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
let pending_audio_path = resource_id
|
||||
.get_file(
|
||||
&node
|
||||
.get_client()
|
||||
.ok_or_else(|| eyre!("Client not found"))?
|
||||
.base_resource_prefixes
|
||||
.lock()
|
||||
.clone(),
|
||||
&[OsStr::new("wav"), OsStr::new("mp3")],
|
||||
)
|
||||
.ok_or_else(|| eyre!("Resource not found"))?;
|
||||
let pending_audio_path = get_resource_file(
|
||||
&resource_id,
|
||||
&*node.get_client().ok_or_else(|| eyre!("Client not found"))?,
|
||||
&[OsStr::new("wav"), OsStr::new("mp3")],
|
||||
)
|
||||
.ok_or_else(|| eyre!("Resource not found"))?;
|
||||
let sound = Sound {
|
||||
space: node.spatial.get().unwrap().clone(),
|
||||
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
|
||||
volume: 1.0,
|
||||
pending_audio_path,
|
||||
sk_sound: OnceCell::new(),
|
||||
instance: Mutex::new(None),
|
||||
entity: OnceLock::new(),
|
||||
stop: Mutex::new(None),
|
||||
play: Mutex::new(None),
|
||||
};
|
||||
let sound_arc = SOUND_REGISTRY.add(sound);
|
||||
node.add_local_signal("play", Sound::play_flex);
|
||||
node.add_local_signal("stop", Sound::stop_flex);
|
||||
let _ = node.sound.set(sound_arc.clone());
|
||||
node.add_aspect_raw(sound_arc.clone());
|
||||
Ok(sound_arc)
|
||||
}
|
||||
|
||||
fn update(&self, sk: &impl StereoKitDraw) {
|
||||
let sound = self.sk_sound.get_or_init(|| {
|
||||
SendWrapper::new(sk.sound_create(self.pending_audio_path.clone()).unwrap())
|
||||
});
|
||||
if self.stop.lock().take().is_some() {
|
||||
if let Some(instance) = self.instance.lock().take() {
|
||||
sk.sound_inst_stop(instance);
|
||||
}
|
||||
}
|
||||
if self.play.lock().is_some() && self.instance.lock().is_none() {
|
||||
self.instance.lock().replace(sk.sound_play(
|
||||
sound.as_ref(),
|
||||
vec3(0.0, 0.0, 0.0),
|
||||
self.volume,
|
||||
));
|
||||
}
|
||||
if let Some(instance) = self.instance.lock().deref_mut() {
|
||||
sk.sound_inst_set_pos(*instance, self.space.global_transform().w_axis.xyz());
|
||||
}
|
||||
}
|
||||
|
||||
fn play_flex(node: &Node, _calling_client: Arc<Client>, _message: Message) -> Result<()> {
|
||||
let sound = node.sound.get().unwrap();
|
||||
}
|
||||
impl AspectIdentifier for Sound {
|
||||
impl_aspect_for_sound_aspect_id! {}
|
||||
}
|
||||
impl Aspect for Sound {
|
||||
impl_aspect_for_sound_aspect! {}
|
||||
}
|
||||
impl SoundAspect for Sound {
|
||||
fn play(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
||||
let sound = node.get_aspect::<Sound>().unwrap();
|
||||
sound.play.lock().replace(());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_flex(node: &Node, _calling_client: Arc<Client>, _message: Message) -> Result<()> {
|
||||
let sound = node.sound.get().unwrap();
|
||||
fn stop(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
||||
let sound = node.get_aspect::<Sound>().unwrap();
|
||||
sound.stop.lock().replace(());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(sk: &impl StereoKitDraw) {
|
||||
for sound in SOUND_REGISTRY.get_valid_contents() {
|
||||
sound.update(sk)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_interface(client: &Arc<Client>) -> Result<()> {
|
||||
let node = Node::create(client, "", "audio", false);
|
||||
node.add_local_signal("create_sound", create_flex);
|
||||
node.add_to_scenegraph().map(|_| ())
|
||||
}
|
||||
|
||||
pub fn create_flex(_node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateSoundInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
resource: ResourceID,
|
||||
}
|
||||
let info: CreateSoundInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/audio/sound", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, true);
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
Sound::add_to(&node, info.resource)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Drop for Sound {
|
||||
fn drop(&mut self) {
|
||||
if let Some(instance) = self.instance.lock().take() {
|
||||
destroy_queue::add(instance);
|
||||
}
|
||||
SOUND_REGISTRY.remove(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl InterfaceAspect for Interface {
|
||||
#[doc = "Create a sound node. WAV and MP3 are supported."]
|
||||
fn create_sound(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
resource: ResourceID,
|
||||
) -> Result<()> {
|
||||
let node = Node::from_id(&calling_client, id, true);
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
let transform = transform.to_mat4(true, true, true);
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent.clone()), transform, false);
|
||||
Sound::add_to(&node, resource)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,355 +0,0 @@
|
||||
use super::alias::AliasInfo;
|
||||
use super::fields::Field;
|
||||
use super::spatial::{parse_transform, Spatial};
|
||||
use super::{Alias, Message, Node};
|
||||
use crate::core::client::Client;
|
||||
use crate::core::node_collections::LifeLinkedNodeMap;
|
||||
use crate::core::registry::Registry;
|
||||
use crate::core::scenegraph::MethodResponseSender;
|
||||
use crate::nodes::fields::{find_field, FIELD_ALIAS_INFO};
|
||||
use crate::nodes::spatial::find_spatial_parent;
|
||||
use color_eyre::eyre::{bail, ensure, eyre, Result};
|
||||
use glam::vec3a;
|
||||
use lazy_static::lazy_static;
|
||||
use mint::{Quaternion, Vector3};
|
||||
use nanoid::nanoid;
|
||||
use parking_lot::Mutex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stardust_xr::schemas::flex::{deserialize, flexbuffers, serialize};
|
||||
use stardust_xr::values::Transform;
|
||||
use std::fmt::Display;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref KEYMAPS: Mutex<FxHashMap<String, String>> = Mutex::new(FxHashMap::default());
|
||||
}
|
||||
|
||||
static PULSE_SENDER_REGISTRY: Registry<PulseSender> = Registry::new();
|
||||
pub static PULSE_RECEIVER_REGISTRY: Registry<PulseReceiver> = Registry::new();
|
||||
|
||||
pub fn mask_matches(mask_map_lesser: &Mask, mask_map_greater: &Mask) -> bool {
|
||||
(|| -> Result<_> {
|
||||
for key in mask_map_lesser.get_mask()?.iter_keys() {
|
||||
let lesser_key = mask_map_lesser.get_mask()?.index(key)?;
|
||||
let greater_key = mask_map_greater.get_mask()?.index(key)?;
|
||||
// otherwise zero-length vectors don't count the same as a single type vector
|
||||
if lesser_key.flexbuffer_type().is_heterogenous_vector()
|
||||
&& lesser_key.as_vector().len() == 0
|
||||
&& greater_key.flexbuffer_type().is_vector()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !lesser_key.flexbuffer_type().is_null()
|
||||
&& lesser_key.flexbuffer_type() != greater_key.flexbuffer_type()
|
||||
{
|
||||
return Err(flexbuffers::ReaderError::InvalidPackedType {}.into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})()
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub struct Mask(pub Vec<u8>);
|
||||
impl Mask {
|
||||
pub fn from_struct<T: Default + Serialize>() -> Self {
|
||||
let mut serializer = flexbuffers::FlexbufferSerializer::new();
|
||||
T::default().serialize(&mut serializer).unwrap();
|
||||
Mask(serializer.take_buffer())
|
||||
}
|
||||
pub fn get_mask(&self) -> Result<flexbuffers::MapReader<&[u8]>> {
|
||||
flexbuffers::Reader::get_root(self.0.as_slice())
|
||||
.map_err(|_| eyre!("Mask is not a valid flexbuffer"))?
|
||||
.get_map()
|
||||
.map_err(|_| eyre!("Mask is not a valid map"))
|
||||
}
|
||||
}
|
||||
impl Display for Mask {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
flexbuffers::Reader::get_root(self.0.as_slice())
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SendDataInfo<'a> {
|
||||
uid: &'a str,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct PulseSender {
|
||||
uid: String,
|
||||
node: Weak<Node>,
|
||||
pub mask: Mask,
|
||||
aliases: LifeLinkedNodeMap<String>,
|
||||
}
|
||||
impl PulseSender {
|
||||
pub fn add_to(node: &Arc<Node>, mask: Mask) -> Result<Arc<PulseSender>> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
|
||||
let sender = PulseSender {
|
||||
uid: nanoid!(),
|
||||
node: Arc::downgrade(node),
|
||||
mask,
|
||||
aliases: LifeLinkedNodeMap::default(),
|
||||
};
|
||||
let sender = PULSE_SENDER_REGISTRY.add(sender);
|
||||
let _ = node.pulse_sender.set(sender.clone());
|
||||
node.add_local_signal("send_data", PulseSender::send_data_flex);
|
||||
for receiver in PULSE_RECEIVER_REGISTRY.get_valid_contents() {
|
||||
sender.handle_new_receiver(&receiver);
|
||||
}
|
||||
Ok(sender.clone())
|
||||
}
|
||||
fn handle_new_receiver(&self, receiver: &PulseReceiver) {
|
||||
if !mask_matches(&self.mask, &receiver.mask) {
|
||||
return;
|
||||
}
|
||||
let Some(tx_node) = self.node.upgrade() else {return};
|
||||
let Some(tx_client) = tx_node.get_client() else {return};
|
||||
let Some(rx_node) = receiver.node.upgrade() else {return};
|
||||
// Receiver itself
|
||||
let rx_alias = Alias::create(
|
||||
&tx_client,
|
||||
tx_node.get_path(),
|
||||
receiver.uid.as_str(),
|
||||
&rx_node,
|
||||
AliasInfo {
|
||||
server_methods: vec!["sendData", "getTransform"],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
if let Ok(rx_alias) = rx_alias {
|
||||
self.aliases.add(receiver.uid.clone(), &rx_alias);
|
||||
|
||||
if let Some(rx_field_node) = receiver.field.spatial_ref().node.upgrade() {
|
||||
// Receiver's field
|
||||
let rx_field_alias = Alias::create(
|
||||
&tx_client,
|
||||
rx_alias.get_path(),
|
||||
"field",
|
||||
&rx_field_node,
|
||||
FIELD_ALIAS_INFO.clone(),
|
||||
);
|
||||
if let Ok(rx_field_alias) = rx_field_alias {
|
||||
self.aliases
|
||||
.add(receiver.uid.clone() + "-field", &rx_field_alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NewReceiverInfo<'a> {
|
||||
uid: &'a str,
|
||||
distance: f32,
|
||||
position: Vector3<f32>,
|
||||
rotation: Quaternion<f32>,
|
||||
}
|
||||
|
||||
let (_, rotation, position) = Spatial::space_to_space_matrix(
|
||||
rx_node.spatial.get().map(|s| s.as_ref()),
|
||||
tx_node.spatial.get().map(|s| s.as_ref()),
|
||||
)
|
||||
.to_scale_rotation_translation();
|
||||
|
||||
let info = NewReceiverInfo {
|
||||
uid: &receiver.uid,
|
||||
distance: receiver
|
||||
.field
|
||||
.distance(tx_node.spatial.get().unwrap(), vec3a(0.0, 0.0, 0.0)),
|
||||
position: position.into(),
|
||||
rotation: rotation.into(),
|
||||
};
|
||||
|
||||
let Ok(data) = serialize(info) else {return};
|
||||
let _ = tx_node.send_remote_signal("new_receiver", data);
|
||||
}
|
||||
|
||||
fn handle_drop_receiver(&self, receiver: &PulseReceiver) {
|
||||
let uid = receiver.uid.as_str();
|
||||
self.aliases.remove(uid);
|
||||
self.aliases.remove(&(uid.to_string() + "-field"));
|
||||
let Some(tx_node) = self.node.upgrade() else {return};
|
||||
let Ok(data) = serialize(&uid) else {return};
|
||||
let _ = tx_node.send_remote_signal("drop_receiver", data);
|
||||
}
|
||||
|
||||
fn send_data_flex(node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
let info: SendDataInfo = deserialize(message.as_ref())?;
|
||||
let receiver_node = calling_client.get_node("Pulse receiver", info.uid)?;
|
||||
let receiver =
|
||||
receiver_node.get_aspect("Pulse Receiver", "pulse receiver", |n| &n.pulse_receiver)?;
|
||||
let receiver_mask = &receiver_node
|
||||
.get_aspect("Pulse receiver", "pulse receiver", |node| {
|
||||
&node.pulse_receiver
|
||||
})?
|
||||
.mask;
|
||||
|
||||
let data_mask = Mask(info.data);
|
||||
data_mask.get_mask()?;
|
||||
ensure!(
|
||||
mask_matches(receiver_mask, &data_mask),
|
||||
"Message ({}) does not contain the same keys as the receiver's mask ({})",
|
||||
data_mask,
|
||||
receiver_mask
|
||||
);
|
||||
receiver.send_data(&node.pulse_sender.get().unwrap().uid, data_mask.0)
|
||||
}
|
||||
}
|
||||
impl Drop for PulseSender {
|
||||
fn drop(&mut self) {
|
||||
PULSE_SENDER_REGISTRY.remove(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PulseReceiver {
|
||||
uid: String,
|
||||
pub node: Weak<Node>,
|
||||
pub field: Arc<Field>,
|
||||
pub mask: Mask,
|
||||
}
|
||||
impl PulseReceiver {
|
||||
pub fn add_to(node: &Arc<Node>, field: Arc<Field>, mask: Mask) -> Result<Arc<PulseReceiver>> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
|
||||
let receiver = PulseReceiver {
|
||||
uid: nanoid!(),
|
||||
node: Arc::downgrade(node),
|
||||
field,
|
||||
mask,
|
||||
};
|
||||
let receiver = PULSE_RECEIVER_REGISTRY.add(receiver);
|
||||
|
||||
for sender in PULSE_SENDER_REGISTRY.get_valid_contents() {
|
||||
sender.handle_new_receiver(&receiver);
|
||||
}
|
||||
let _ = node.pulse_receiver.set(receiver.clone());
|
||||
Ok(receiver)
|
||||
}
|
||||
|
||||
pub fn send_data(&self, uid: &str, data: Vec<u8>) -> Result<()> {
|
||||
if let Some(node) = self.node.upgrade() {
|
||||
node.send_remote_signal("data", serialize(SendDataInfo { uid, data })?)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PulseReceiver {
|
||||
fn drop(&mut self) {
|
||||
PULSE_RECEIVER_REGISTRY.remove(self);
|
||||
for sender in PULSE_SENDER_REGISTRY.get_valid_contents() {
|
||||
sender.handle_drop_receiver(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_interface(client: &Arc<Client>) -> Result<()> {
|
||||
let node = Node::create(client, "", "data", false);
|
||||
node.add_local_signal("create_pulse_sender", create_pulse_sender_flex);
|
||||
node.add_local_signal("create_pulse_receiver", create_pulse_receiver_flex);
|
||||
node.add_local_method("register_keymap", register_keymap_flex);
|
||||
node.add_local_method("get_keymap", get_keymap_flex);
|
||||
node.add_to_scenegraph().map(|_| ())
|
||||
}
|
||||
|
||||
pub fn create_pulse_sender_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreatePulseSenderInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
mask: Vec<u8>,
|
||||
}
|
||||
let info: CreatePulseSenderInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/data/sender", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
|
||||
let mask = Mask(info.mask);
|
||||
mask.get_mask()?;
|
||||
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
PulseSender::add_to(&node, mask)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_pulse_receiver_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreatePulseReceiverInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
field_path: &'a str,
|
||||
mask: Vec<u8>,
|
||||
}
|
||||
let info: CreatePulseReceiverInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/data/receiver", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
let field = find_field(&calling_client, info.field_path)?;
|
||||
let mask = Mask(info.mask);
|
||||
mask.get_mask()?;
|
||||
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
PulseReceiver::add_to(&node, field, mask)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register_keymap_flex(
|
||||
_node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
let keymap: String = deserialize(message.as_ref())?;
|
||||
let mut keymaps = KEYMAPS.lock();
|
||||
if let Some(found_keymap_id) = keymaps
|
||||
.iter()
|
||||
.filter(|(_k, v)| *v == &keymap)
|
||||
.map(|(k, _v)| k)
|
||||
.last()
|
||||
{
|
||||
return Ok(serialize(found_keymap_id)?.into());
|
||||
}
|
||||
|
||||
let generated_id = nanoid!();
|
||||
keymaps.insert(generated_id.clone(), keymap);
|
||||
|
||||
Ok(serialize(generated_id)?.into())
|
||||
});
|
||||
}
|
||||
pub fn get_keymap_flex(
|
||||
_node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
let keymap_id: &str = deserialize(message.as_ref())?;
|
||||
let keymaps = KEYMAPS.lock();
|
||||
let Some(keymap) = keymaps.get(keymap_id) else {bail!("Could not find keymap. Try registering it")};
|
||||
|
||||
Ok(serialize(keymap)?.into())
|
||||
});
|
||||
}
|
||||
5
src/nodes/drawable/holdout.wgsl
Normal file
5
src/nodes/drawable/holdout.wgsl
Normal file
@@ -0,0 +1,5 @@
|
||||
fn fragment(
|
||||
in: VertexOutput
|
||||
) -> FragmentOutput {
|
||||
return vec4(0.0);
|
||||
}
|
||||
56
src/nodes/drawable/line.wgsl
Normal file
56
src/nodes/drawable/line.wgsl
Normal file
@@ -0,0 +1,56 @@
|
||||
#import bevy_pbr::{
|
||||
pbr_fragment::pbr_input_from_standard_material,
|
||||
pbr_functions::alpha_discard,
|
||||
}
|
||||
|
||||
#ifdef PREPASS_PIPELINE
|
||||
#import bevy_pbr::{
|
||||
prepass_io::{VertexOutput, FragmentOutput},
|
||||
pbr_deferred_functions::deferred_output,
|
||||
}
|
||||
#else
|
||||
#import bevy_pbr::{
|
||||
forward_io::{VertexOutput, FragmentOutput},
|
||||
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
|
||||
}
|
||||
#endif
|
||||
#ifdef OIT_ENABLED
|
||||
#import bevy_core_pipeline::oit::oit_draw
|
||||
#endif
|
||||
|
||||
@fragment
|
||||
fn fragment(
|
||||
in: VertexOutput,
|
||||
@builtin(front_facing) is_front: bool,
|
||||
) -> FragmentOutput {
|
||||
// generate a PbrInput struct from the StandardMaterial bindings
|
||||
var pbr_input = pbr_input_from_standard_material(in, is_front);
|
||||
|
||||
#ifdef VERTEX_COLORS
|
||||
// Multiply emissive color by vertex color
|
||||
pbr_input.material.emissive *= vec4(in.color.rgb, 1.0);
|
||||
#endif
|
||||
|
||||
// alpha discard
|
||||
// pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
|
||||
|
||||
#ifdef PREPASS_PIPELINE
|
||||
// in deferred mode we can't modify anything after that, as lighting is run in a separate fullscreen shader.
|
||||
let out = deferred_output(in, pbr_input);
|
||||
#else
|
||||
var out: FragmentOutput;
|
||||
// apply lighting
|
||||
out.color = apply_pbr_lighting(pbr_input);
|
||||
|
||||
// apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr)
|
||||
// note this does not include fullscreen postprocessing effects like bloom.
|
||||
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
|
||||
#endif
|
||||
|
||||
#ifdef OIT_ENABLED
|
||||
oit_draw(in.position, out.color, false);
|
||||
discard;
|
||||
#else
|
||||
return out;
|
||||
#endif
|
||||
}
|
||||
@@ -1,126 +1,370 @@
|
||||
use super::{Line, LinesAspect};
|
||||
use crate::{
|
||||
core::{client::Client, registry::Registry},
|
||||
nodes::{
|
||||
spatial::{find_spatial_parent, parse_transform, Spatial},
|
||||
Message, Node,
|
||||
BevyMaterial,
|
||||
core::{
|
||||
client::Client, color::ColorConvert, entity_handle::EntityHandle, error::Result,
|
||||
registry::Registry,
|
||||
},
|
||||
nodes::{Node, drawable::LinePoint, spatial::Spatial},
|
||||
};
|
||||
use bevy::{
|
||||
asset::{AssetEvents, RenderAssetUsages, weak_handle},
|
||||
pbr::{ExtendedMaterial, MaterialExtension},
|
||||
prelude::*,
|
||||
render::{
|
||||
mesh::{Indices, PrimitiveTopology, VertexAttributeValues},
|
||||
primitives::Aabb,
|
||||
render_resource::{AsBindGroup, ShaderRef},
|
||||
view::VisibilitySystems,
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::{bail, ensure, Result};
|
||||
use glam::Vec3A;
|
||||
use mint::Vector3;
|
||||
use glam::Vec3;
|
||||
use parking_lot::Mutex;
|
||||
use portable_atomic::{AtomicBool, Ordering};
|
||||
use prisma::{Flatten, Lerp, Rgba};
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::{schemas::flex::deserialize, values::Transform};
|
||||
use std::{collections::VecDeque, sync::Arc};
|
||||
use stereokit::{bounds_grow_to_fit_pt, Bounds, Color128, LinePoint as SkLinePoint, StereoKitDraw};
|
||||
use std::sync::{
|
||||
Arc, OnceLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use super::Drawable;
|
||||
type LineMaterial = ExtendedMaterial<BevyMaterial, LineExtension>;
|
||||
const LINE_SHADER_HANDLE: Handle<Shader> = weak_handle!("7d28aa5a-3abd-43bb-b0e9-0de8b81b650d");
|
||||
// No extra data needed for a simple holdout
|
||||
#[derive(Default, Asset, AsBindGroup, TypePath, Debug, Clone)]
|
||||
#[data(50, u32, binding_array(101))]
|
||||
#[bindless(index_table(range(50..51), binding(100)))]
|
||||
pub struct LineExtension {}
|
||||
impl From<&LineExtension> for u32 {
|
||||
fn from(_: &LineExtension) -> Self {
|
||||
0
|
||||
}
|
||||
}
|
||||
impl MaterialExtension for LineExtension {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
LINE_SHADER_HANDLE.into()
|
||||
}
|
||||
|
||||
fn prepass_fragment_shader() -> ShaderRef {
|
||||
LINE_SHADER_HANDLE.into()
|
||||
}
|
||||
|
||||
fn deferred_fragment_shader() -> ShaderRef {
|
||||
LINE_SHADER_HANDLE.into()
|
||||
}
|
||||
|
||||
fn alpha_mode() -> Option<AlphaMode> {
|
||||
Some(AlphaMode::Blend)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LinesNodePlugin;
|
||||
impl Plugin for LinesNodePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
build_line_mesh
|
||||
.after(TransformSystem::TransformPropagate)
|
||||
.before(AssetEvents)
|
||||
.after(VisibilitySystems::VisibilityPropagate)
|
||||
.before(VisibilitySystems::CheckVisibility),
|
||||
);
|
||||
app.world_mut().resource_mut::<Assets<Shader>>().insert(
|
||||
LINE_SHADER_HANDLE.id(),
|
||||
Shader::from_wgsl(
|
||||
include_str!("line.wgsl"),
|
||||
std::path::Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("line.wgsl")
|
||||
.to_string_lossy(),
|
||||
),
|
||||
);
|
||||
app.add_plugins(MaterialPlugin::<LineMaterial>::default());
|
||||
}
|
||||
}
|
||||
|
||||
fn build_line_mesh(
|
||||
mut cmds: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<LineMaterial>>,
|
||||
query: Query<(Ref<GlobalTransform>, &InheritedVisibility)>,
|
||||
) {
|
||||
for lines in LINES_REGISTRY.get_valid_contents().into_iter()
|
||||
{
|
||||
let Some((transform, visibil)) = lines.spatial.get_entity().and_then(|e| query.get(e).ok())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if !(lines.gen_mesh.load(Ordering::Relaxed) || transform.is_changed()) {
|
||||
continue;
|
||||
}
|
||||
lines.gen_mesh.store(false, Ordering::Relaxed);
|
||||
let mut vertex_positions = Vec::<Vec3>::new();
|
||||
let mut vertex_normals = Vec::<Vec3>::new();
|
||||
let mut vertex_colors = Vec::<[f32; 4]>::new();
|
||||
let mut vertex_indices = Vec::<u32>::new();
|
||||
let lines_data = lines.data.lock();
|
||||
if lines_data.is_empty() {
|
||||
*lines.bounds.lock() = Some(Aabb::default());
|
||||
lines.setup_complete.notify_waiters();
|
||||
match lines.entity.get() {
|
||||
Some(e) => cmds.entity(**e),
|
||||
None => {
|
||||
// if we couldn't get the lines entity then we need to gen the mesh later
|
||||
lines.gen_mesh.store(true, Ordering::Relaxed);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
.remove::<Mesh3d>();
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut indices_set = 0;
|
||||
for line in lines_data.iter() {
|
||||
// yes this alloc is suboptimal, but good enough for now
|
||||
let line_points = line
|
||||
.points
|
||||
.iter()
|
||||
.map(|p: &LinePoint| LinePoint {
|
||||
// point: transform.transform_point(p.point.into()).into(),
|
||||
point: transform.transform_point(p.point.into()).into(),
|
||||
thickness: p.thickness,
|
||||
color: p.color,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let start_set = indices_set;
|
||||
// Create a sliding window of points to process each segment of the line
|
||||
// For cyclic lines: wraps around by connecting last point back to first
|
||||
// For non-cyclic lines: handles endpoints with None values
|
||||
let point_windows = {
|
||||
let mut out = Vec::new();
|
||||
let mut last = line.cyclic.then(|| line_points.last()).flatten();
|
||||
let mut peekable = line_points.iter().peekable();
|
||||
while let Some(curr) = peekable.next() {
|
||||
// Skip this point if it has the same position as the previous point
|
||||
if let Some(prev) = last
|
||||
&& Vec3::from(prev.point) == Vec3::from(curr.point)
|
||||
{
|
||||
last = Some(curr);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut end = false;
|
||||
// Determine the next point - either the next in sequence or
|
||||
// for cyclic lines, wrap back to first point at the end
|
||||
let next = match peekable.peek() {
|
||||
Some(v) => Some(*v),
|
||||
None => {
|
||||
end = true;
|
||||
line.cyclic.then(|| line_points.first()).flatten()
|
||||
}
|
||||
};
|
||||
|
||||
out.push((last, curr, next, end));
|
||||
last = Some(curr);
|
||||
}
|
||||
out
|
||||
};
|
||||
// if we can't make a full line, don't bother trying
|
||||
if point_windows.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
for (last, curr, next, last_point) in point_windows {
|
||||
let last_quat = last.map(|v| {
|
||||
Quat::from_rotation_arc(
|
||||
Vec3::Y,
|
||||
(Vec3::from(curr.point) - Vec3::from(v.point)).normalize(),
|
||||
)
|
||||
});
|
||||
let next_quat = next.map(|v| {
|
||||
Quat::from_rotation_arc(
|
||||
Vec3::Y,
|
||||
(Vec3::from(v.point) - Vec3::from(curr.point)).normalize(),
|
||||
)
|
||||
});
|
||||
let quat = match (last_quat, next_quat) {
|
||||
(None, None) => {
|
||||
error!("no previous or next point in line");
|
||||
break;
|
||||
}
|
||||
(None, Some(next)) => next,
|
||||
(Some(last), None) => last,
|
||||
(Some(last), Some(next)) => last.lerp(next, 0.5),
|
||||
};
|
||||
if !quat.is_finite() {
|
||||
error!("non finite quat: next: {next:?}, last: {last:?}, curr: {curr:?},");
|
||||
break;
|
||||
}
|
||||
let normals = [
|
||||
Vec3::X,
|
||||
Vec3::new(1., 0., 1.).normalize(),
|
||||
Vec3::Z,
|
||||
Vec3::new(-1., 0., 1.).normalize(),
|
||||
Vec3::NEG_X,
|
||||
Vec3::new(-1., 0., -1.).normalize(),
|
||||
Vec3::NEG_Z,
|
||||
Vec3::new(1., 0., -1.).normalize(),
|
||||
]
|
||||
.map(Vec3::normalize)
|
||||
.map(|v| quat * v);
|
||||
let points = normals.map(|v| (v * curr.thickness) + Vec3::from(curr.point));
|
||||
vertex_normals.extend(normals);
|
||||
vertex_positions.extend(points);
|
||||
vertex_colors.extend([curr.color.to_bevy().to_linear().to_f32_array(); 8]);
|
||||
// Only connect vertices between segments if this isn't the end point
|
||||
if !last_point {
|
||||
vertex_indices.extend(indices(indices_set));
|
||||
}
|
||||
indices_set += 1;
|
||||
}
|
||||
if indices_set > 0 {
|
||||
// Handle the connection between start and end points:
|
||||
// - For cyclic lines: connect last segment back to first
|
||||
// - For non-cyclic lines: add caps at both ends
|
||||
if line.cyclic {
|
||||
vertex_indices.extend(cyclic_indices(start_set, indices_set - 1));
|
||||
} else {
|
||||
vertex_indices.extend(cap_indices(start_set, false));
|
||||
vertex_indices.extend(cap_indices(indices_set - 1, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut mesh = Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetUsages::RENDER_WORLD,
|
||||
);
|
||||
mesh.insert_indices(Indices::U32(vertex_indices));
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertex_positions);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_normals);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, vertex_colors);
|
||||
|
||||
let mut entity = match lines.entity.get() {
|
||||
Some(e) => cmds.entity(**e),
|
||||
None => {
|
||||
let e = cmds.spawn((
|
||||
Name::new("LinesNode"),
|
||||
MeshMaterial3d(materials.add(ExtendedMaterial {
|
||||
base: BevyMaterial {
|
||||
base_color: Color::WHITE,
|
||||
perceptual_roughness: 1.0,
|
||||
alpha_mode: AlphaMode::Premultiplied,
|
||||
emissive: Color::linear_rgba(0.25, 0.25, 0.25, 1.0).into(),
|
||||
..default()
|
||||
},
|
||||
extension: LineExtension {},
|
||||
})),
|
||||
));
|
||||
_ = lines.entity.set(EntityHandle::new(e.id()));
|
||||
e
|
||||
}
|
||||
};
|
||||
if let Some(VertexAttributeValues::Float32x3(values)) =
|
||||
mesh.attribute(Mesh::ATTRIBUTE_POSITION)
|
||||
{
|
||||
let global_to_local = transform.affine().inverse();
|
||||
let local_aabb = Aabb::enclosing(
|
||||
values
|
||||
.iter()
|
||||
.map(|p| global_to_local.transform_point3(Vec3::from_slice(p))),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
let global_aabb =
|
||||
Aabb::enclosing(values.iter().map(|p| Vec3::from_slice(p))).unwrap_or_default();
|
||||
*lines.bounds.lock() = Some(local_aabb);
|
||||
lines.setup_complete.notify_waiters();
|
||||
entity.insert(global_aabb);
|
||||
}
|
||||
entity
|
||||
.insert(Mesh3d(meshes.add(mesh)))
|
||||
.insert(*visibil)
|
||||
.insert(match visibil.get() {
|
||||
true => Visibility::Visible,
|
||||
false => Visibility::Hidden,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const END_CAP_INDICES: [u32; 18] = [0, 1, 7, 7, 1, 2, 7, 2, 6, 6, 2, 3, 6, 3, 5, 5, 3, 4];
|
||||
fn cap_indices(set: u32, flip: bool) -> [u32; END_CAP_INDICES.len()] {
|
||||
let mut out = END_CAP_INDICES.map(|v| v + (set * 8));
|
||||
if flip {
|
||||
out.reverse();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// const BASE: [u16; 6] = [0, 8, 1, 8, 9, 1];
|
||||
|
||||
// Defines how vertices are connected between consecutive cross-sections to form the tube
|
||||
const INDICES: [u32; 48] = [
|
||||
0, 8, 1, 8, 9, 1, 1, 9, 2, 9, 10, 2, 2, 10, 3, 10, 11, 3, 3, 11, 4, 11, 12, 4, 4, 12, 5, 12,
|
||||
13, 5, 5, 13, 6, 13, 14, 6, 6, 14, 7, 14, 15, 7, 7, 15, 0, 15, 8, 0,
|
||||
];
|
||||
fn indices(set: u32) -> [u32; INDICES.len()] {
|
||||
INDICES.map(|v| v + (set * 8))
|
||||
}
|
||||
fn cyclic_indices(start_set: u32, end_set: u32) -> [u32; INDICES.len()] {
|
||||
let mut out = INDICES.map(|v| {
|
||||
if v < 8 {
|
||||
v + ((start_set) * 8)
|
||||
} else {
|
||||
v + ((end_set - 1) * 8)
|
||||
}
|
||||
});
|
||||
out.reverse();
|
||||
out
|
||||
}
|
||||
|
||||
static LINES_REGISTRY: Registry<Lines> = Registry::new();
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct LinePointRaw {
|
||||
point: Vector3<f32>,
|
||||
thickness: f32,
|
||||
color: [f32; 4],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LineData {
|
||||
points: Vec<LinePointRaw>,
|
||||
cyclic: bool,
|
||||
}
|
||||
|
||||
pub struct Lines {
|
||||
enabled: Arc<AtomicBool>,
|
||||
space: Arc<Spatial>,
|
||||
data: Mutex<LineData>,
|
||||
spatial: Arc<Spatial>,
|
||||
data: Mutex<Vec<Line>>,
|
||||
gen_mesh: AtomicBool,
|
||||
entity: OnceLock<EntityHandle>,
|
||||
bounds: Mutex<Option<Aabb>>,
|
||||
setup_complete: Notify,
|
||||
}
|
||||
impl Lines {
|
||||
fn add_to(node: &Arc<Node>, points: Vec<LinePointRaw>, cyclic: bool) -> Result<Arc<Lines>> {
|
||||
ensure!(
|
||||
node.drawable.get().is_none(),
|
||||
"Internal: Node already has a drawable attached!"
|
||||
);
|
||||
|
||||
let _ = node.spatial.get().unwrap().bounding_box_calc.set(|node| {
|
||||
let mut bounds = Bounds::default();
|
||||
let Some(Drawable::Lines(lines)) = node.drawable.get() else {return bounds};
|
||||
for point in &lines.data.lock().points {
|
||||
bounds = bounds_grow_to_fit_pt(bounds, point.point);
|
||||
}
|
||||
|
||||
bounds
|
||||
});
|
||||
pub fn add_to(node: &Arc<Node>, lines: Vec<Line>) -> Result<Arc<Lines>> {
|
||||
let _ = node
|
||||
.get_aspect::<Spatial>()
|
||||
.unwrap()
|
||||
.bounding_box_calc
|
||||
.set(|node| {
|
||||
Box::pin(async {
|
||||
let Ok(lines) = node.get_aspect::<Lines>() else {
|
||||
return Default::default();
|
||||
};
|
||||
let bounds = *lines.bounds.lock();
|
||||
match bounds {
|
||||
Some(aabb) => aabb,
|
||||
None => {
|
||||
lines.setup_complete.notified().await;
|
||||
lines.bounds.lock().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let lines = LINES_REGISTRY.add(Lines {
|
||||
enabled: node.enabled.clone(),
|
||||
space: node.get_aspect("Lines", "spatial", |n| &n.spatial)?.clone(),
|
||||
data: Mutex::new(LineData { points, cyclic }),
|
||||
spatial: node.get_aspect::<Spatial>()?.clone(),
|
||||
data: Mutex::new(lines),
|
||||
gen_mesh: AtomicBool::new(true),
|
||||
entity: OnceLock::new(),
|
||||
bounds: Mutex::new(Some(Aabb::default())),
|
||||
setup_complete: Notify::new(),
|
||||
});
|
||||
node.add_local_signal("set_points", Lines::set_points_flex);
|
||||
node.add_local_signal("set_cyclic", Lines::set_cyclic_flex);
|
||||
let _ = node.drawable.set(Drawable::Lines(lines.clone()));
|
||||
node.add_aspect_raw(lines.clone());
|
||||
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
fn draw(&self, draw_ctx: &impl StereoKitDraw) {
|
||||
let transform_mat = self.space.global_transform();
|
||||
let data = self.data.lock().clone();
|
||||
let mut points: VecDeque<SkLinePoint> = data
|
||||
.points
|
||||
.iter()
|
||||
.map(|p| SkLinePoint {
|
||||
pt: transform_mat.transform_point3a(Vec3A::from(p.point)).into(),
|
||||
thickness: p.thickness,
|
||||
color: p.color.map(|c| (c * 255.0) as u8).into(),
|
||||
})
|
||||
.collect();
|
||||
if data.cyclic && !points.is_empty() {
|
||||
let first = data.points.first().unwrap();
|
||||
let last = data.points.last().unwrap();
|
||||
let color = Rgba::from_slice(&first.color).lerp(&Rgba::from_slice(&last.color), 0.5);
|
||||
let connect_point = SkLinePoint {
|
||||
pt: transform_mat
|
||||
.transform_point3a(Vec3A::from(first.point).lerp(Vec3A::from(last.point), 0.5))
|
||||
.into(),
|
||||
thickness: (first.thickness + last.thickness) * 0.5,
|
||||
color: Color128::from([color.red(), color.green(), color.blue(), color.alpha()])
|
||||
.into(),
|
||||
};
|
||||
points.push_front(connect_point.clone());
|
||||
points.push_back(connect_point);
|
||||
}
|
||||
draw_ctx.line_add_listv(points.make_contiguous());
|
||||
}
|
||||
|
||||
pub fn set_points_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let Some(Drawable::Lines(lines)) = node.drawable.get() else {bail!("Not a drawable??")};
|
||||
|
||||
let mut points: Vec<LinePointRaw> = deserialize(message.as_ref())?;
|
||||
for p in &mut points {
|
||||
p.color[0] = p.color[0].powf(2.2);
|
||||
p.color[1] = p.color[1].powf(2.2);
|
||||
p.color[2] = p.color[2].powf(2.2);
|
||||
}
|
||||
lines.data.lock().points = points;
|
||||
Ok(())
|
||||
}
|
||||
pub fn set_cyclic_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let Some(Drawable::Lines(lines)) = node.drawable.get() else {bail!("Not a drawable??")};
|
||||
|
||||
lines.data.lock().cyclic = deserialize(message.as_ref())?;
|
||||
}
|
||||
impl LinesAspect for Lines {
|
||||
fn set_lines(node: Arc<Node>, _calling_client: Arc<Client>, lines: Vec<Line>) -> Result<()> {
|
||||
let lines_aspect = node.get_aspect::<Lines>()?;
|
||||
*lines_aspect.data.lock() = lines;
|
||||
lines_aspect.gen_mesh.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -129,37 +373,3 @@ impl Drop for Lines {
|
||||
LINES_REGISTRY.remove(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_all(draw_ctx: &impl StereoKitDraw) {
|
||||
for lines in LINES_REGISTRY.get_valid_contents() {
|
||||
if lines.enabled.load(Ordering::Relaxed) {
|
||||
lines.draw(draw_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_flex(_node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateTextInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
points: Vec<LinePointRaw>,
|
||||
cyclic: bool,
|
||||
}
|
||||
let mut info: CreateTextInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/drawable/lines", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, true);
|
||||
|
||||
for p in &mut info.points {
|
||||
p.color[0] = p.color[0].powf(2.2);
|
||||
p.color[1] = p.color[1].powf(2.2);
|
||||
p.color[2] = p.color[2].powf(2.2);
|
||||
}
|
||||
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
Lines::add_to(&node, info.points, info.cyclic)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,76 +1,133 @@
|
||||
pub mod lines;
|
||||
pub mod model;
|
||||
pub mod shaders;
|
||||
pub mod sky;
|
||||
pub mod text;
|
||||
|
||||
use self::{
|
||||
lines::Lines,
|
||||
model::{Model, ModelPart},
|
||||
text::Text,
|
||||
use self::{lines::Lines, model::Model, text::Text};
|
||||
use super::{
|
||||
Aspect, AspectIdentifier, Node,
|
||||
spatial::{Spatial, Transform},
|
||||
};
|
||||
|
||||
use super::{Message, Node};
|
||||
use crate::core::client::Client;
|
||||
use color_eyre::eyre::Result;
|
||||
use crate::core::{client::Client, error::Result, resource::get_resource_file};
|
||||
use crate::nodes::spatial::SPATIAL_ASPECT_ALIAS_INFO;
|
||||
use color_eyre::eyre::eyre;
|
||||
use model::ModelPart;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flex::deserialize;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use stereokit::StereoKitDraw;
|
||||
use tracing::instrument;
|
||||
use stardust_xr::values::ResourceID;
|
||||
use std::{ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
|
||||
pub fn create_interface(client: &Arc<Client>) -> Result<()> {
|
||||
let node = Node::create(client, "", "drawable", false);
|
||||
node.add_local_signal("create_lines", lines::create_flex);
|
||||
node.add_local_signal("create_model", model::create_flex);
|
||||
node.add_local_signal("create_text", text::create_flex);
|
||||
node.add_local_signal("set_sky_file", set_sky_file_flex);
|
||||
node.add_to_scenegraph().map(|_| ())
|
||||
static QUEUED_SKYLIGHT: Mutex<Option<Option<PathBuf>>> = Mutex::new(None);
|
||||
static QUEUED_SKYTEX: Mutex<Option<Option<PathBuf>>> = Mutex::new(None);
|
||||
|
||||
stardust_xr_server_codegen::codegen_drawable_protocol!();
|
||||
|
||||
impl AspectIdentifier for Lines {
|
||||
impl_aspect_for_lines_aspect_id! {}
|
||||
}
|
||||
impl Aspect for Lines {
|
||||
impl_aspect_for_lines_aspect! {}
|
||||
}
|
||||
impl AspectIdentifier for Model {
|
||||
impl_aspect_for_model_aspect_id! {}
|
||||
}
|
||||
impl Aspect for Model {
|
||||
impl_aspect_for_model_aspect! {}
|
||||
}
|
||||
impl AspectIdentifier for ModelPart {
|
||||
impl_aspect_for_model_part_aspect_id! {}
|
||||
}
|
||||
impl Aspect for ModelPart {
|
||||
impl_aspect_for_model_part_aspect! {}
|
||||
}
|
||||
impl AspectIdentifier for Text {
|
||||
impl_aspect_for_text_aspect_id! {}
|
||||
}
|
||||
impl Aspect for Text {
|
||||
impl_aspect_for_text_aspect! {}
|
||||
}
|
||||
|
||||
pub enum Drawable {
|
||||
Lines(Arc<Lines>),
|
||||
Model(Arc<Model>),
|
||||
ModelPart(Arc<ModelPart>),
|
||||
Text(Arc<Text>),
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(sk))]
|
||||
pub fn draw(sk: &impl StereoKitDraw) {
|
||||
lines::draw_all(sk);
|
||||
model::draw_all(sk);
|
||||
text::draw_all(sk);
|
||||
|
||||
if let Some(skytex) = QUEUED_SKYTEX.lock().take() {
|
||||
if let Ok((_skylight, skytex)) = sk.tex_create_cubemap_file(&skytex, true, i32::MAX) {
|
||||
sk.render_set_skytex(&skytex);
|
||||
}
|
||||
impl InterfaceAspect for Interface {
|
||||
fn set_sky_tex(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
tex: Option<ResourceID>,
|
||||
) -> Result<()> {
|
||||
let resource_path = tex
|
||||
.map(|tex| {
|
||||
get_resource_file(&tex, &calling_client, &[OsStr::new("hdr")])
|
||||
.ok_or(eyre!("Could not find resource"))
|
||||
})
|
||||
.transpose()?;
|
||||
QUEUED_SKYTEX.lock().replace(resource_path);
|
||||
Ok(())
|
||||
}
|
||||
if let Some(skylight) = QUEUED_SKYLIGHT.lock().take() {
|
||||
if let Ok((skylight, _)) = sk.tex_create_cubemap_file(&skylight, true, i32::MAX) {
|
||||
sk.render_set_skylight(skylight);
|
||||
}
|
||||
|
||||
fn set_sky_light(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
light: Option<ResourceID>,
|
||||
) -> Result<()> {
|
||||
let resource_path = light
|
||||
.map(|light| {
|
||||
get_resource_file(&light, &calling_client, &[OsStr::new("hdr")])
|
||||
.ok_or(eyre!("Could not find resource"))
|
||||
})
|
||||
.transpose()?;
|
||||
QUEUED_SKYLIGHT.lock().replace(resource_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_lines(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
lines: Vec<Line>,
|
||||
) -> Result<()> {
|
||||
let node = Node::from_id(&calling_client, id, true);
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
let transform = transform.to_mat4(true, true, true);
|
||||
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent.clone()), transform, false);
|
||||
Lines::add_to(&node, lines)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_model(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
model: ResourceID,
|
||||
) -> Result<()> {
|
||||
let node = Node::from_id(&calling_client, id, true);
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
let transform = transform.to_mat4(true, true, true);
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent.clone()), transform, false);
|
||||
Model::add_to(&node, model)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_text(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
text: String,
|
||||
style: TextStyle,
|
||||
) -> Result<()> {
|
||||
let node = Node::from_id(&calling_client, id, true);
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
let transform = transform.to_mat4(true, true, true);
|
||||
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent.clone()), transform, false);
|
||||
Text::add_to(&node, text, style)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
static QUEUED_SKYLIGHT: Mutex<Option<PathBuf>> = Mutex::new(None);
|
||||
static QUEUED_SKYTEX: Mutex<Option<PathBuf>> = Mutex::new(None);
|
||||
|
||||
fn set_sky_file_flex(_node: &Node, _calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct SkyFileInfo {
|
||||
path: PathBuf,
|
||||
skytex: Option<bool>,
|
||||
skylight: Option<bool>,
|
||||
}
|
||||
let info: SkyFileInfo = deserialize(message.as_ref())?;
|
||||
info.path.metadata()?;
|
||||
if info.skytex.unwrap_or_default() {
|
||||
QUEUED_SKYTEX.lock().replace(info.path.clone());
|
||||
}
|
||||
if info.skylight.unwrap_or_default() {
|
||||
QUEUED_SKYLIGHT.lock().replace(info.path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,361 +1,661 @@
|
||||
use super::Node;
|
||||
use super::{MODEL_PART_ASPECT_ALIAS_INFO, MaterialParameter, ModelAspect, ModelPartAspect};
|
||||
use crate::core::bevy_channel::{BevyChannel, BevyChannelReader};
|
||||
use crate::core::client::Client;
|
||||
use crate::core::node_collections::LifeLinkedNodeMap;
|
||||
use crate::core::color::ColorConvert as _;
|
||||
use crate::core::entity_handle::EntityHandle;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::registry::Registry;
|
||||
use crate::core::resource::ResourceID;
|
||||
use crate::nodes::drawable::Drawable;
|
||||
use crate::nodes::spatial::{find_spatial_parent, parse_transform, Spatial};
|
||||
use crate::nodes::Message;
|
||||
use crate::SK_MULTITHREAD;
|
||||
use color_eyre::eyre::{bail, ensure, eyre, Result};
|
||||
use glam::Mat4;
|
||||
use mint::{ColumnMatrix4, Vector2, Vector3, Vector4};
|
||||
use once_cell::sync::OnceCell;
|
||||
use crate::core::resource::get_resource_file;
|
||||
use crate::nodes::Node;
|
||||
use crate::nodes::alias::{Alias, AliasList};
|
||||
use crate::nodes::spatial::{Spatial, SpatialNode};
|
||||
use crate::{BevyMaterial, bail};
|
||||
use bevy::asset::{load_internal_asset, weak_handle};
|
||||
use bevy::gltf::GltfLoaderSettings;
|
||||
use bevy::pbr::{ExtendedMaterial, MaterialExtension};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::primitives::Aabb;
|
||||
use bevy::render::render_resource::{AsBindGroup, ShaderRef};
|
||||
use color_eyre::eyre::eyre;
|
||||
use parking_lot::Mutex;
|
||||
use portable_atomic::{AtomicBool, Ordering};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flex::deserialize;
|
||||
use stardust_xr::values::Transform;
|
||||
use rustc_hash::{FxHashMap, FxHasher};
|
||||
use stardust_xr::values::ResourceID;
|
||||
use std::ffi::OsStr;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Weak};
|
||||
use stereokit::named_colors::WHITE;
|
||||
use stereokit::{
|
||||
Bounds, Color128, Material, Model as SKModel, RenderLayer, Shader, StereoKitDraw,
|
||||
StereoKitMultiThread,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, OnceLock, Weak};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
static LOAD_MODEL: BevyChannel<(Arc<Model>, PathBuf)> = BevyChannel::new();
|
||||
|
||||
type HoldoutMaterial = ExtendedMaterial<BevyMaterial, HoldoutExtension>;
|
||||
const HOLDOUT_SHADER_HANDLE: Handle<Shader> = weak_handle!("92b481b7-d3da-4188-b252-2335ec814ee2");
|
||||
const HOLDOUT_MATERIAL_HANDLE: Handle<HoldoutMaterial> =
|
||||
weak_handle!("d56f1d62-9121-434b-a34f-9f0bbd6b3390");
|
||||
|
||||
pub struct ModelNodePlugin;
|
||||
#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct ModelNodeSystemSet;
|
||||
impl Plugin for ModelNodePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
LOAD_MODEL.init(app);
|
||||
|
||||
load_internal_asset!(
|
||||
app,
|
||||
HOLDOUT_SHADER_HANDLE,
|
||||
"holdout.wgsl",
|
||||
Shader::from_wgsl
|
||||
);
|
||||
app.add_plugins(MaterialPlugin::<HoldoutMaterial>::default());
|
||||
app.world_mut()
|
||||
.resource_mut::<Assets<HoldoutMaterial>>()
|
||||
.insert(&HOLDOUT_MATERIAL_HANDLE, HoldoutMaterial::default());
|
||||
|
||||
app.init_resource::<MaterialRegistry>();
|
||||
app.add_systems(
|
||||
Update,
|
||||
(load_models, gen_model_parts, apply_materials)
|
||||
.chain()
|
||||
.in_set(ModelNodeSystemSet),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// No extra data needed for a simple holdout
|
||||
#[derive(Default, Asset, AsBindGroup, TypePath, Debug, Clone)]
|
||||
#[data(50, u32, binding_array(101))]
|
||||
#[bindless(index_table(range(50..51), binding(100)))]
|
||||
pub struct HoldoutExtension {}
|
||||
impl From<&HoldoutExtension> for u32 {
|
||||
fn from(_: &HoldoutExtension) -> Self {
|
||||
0
|
||||
}
|
||||
}
|
||||
impl MaterialExtension for HoldoutExtension {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
HOLDOUT_SHADER_HANDLE.into()
|
||||
}
|
||||
|
||||
fn alpha_mode() -> Option<AlphaMode> {
|
||||
Some(AlphaMode::Opaque)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct ModelNode(Weak<Model>);
|
||||
|
||||
fn load_models(
|
||||
asset_server: Res<AssetServer>,
|
||||
mut cmds: Commands,
|
||||
mut mpsc_receiver: ResMut<BevyChannelReader<(Arc<Model>, PathBuf)>>,
|
||||
) {
|
||||
while let Some((model, path)) = mpsc_receiver.read() {
|
||||
// idk of the asset label is the correct approach here
|
||||
let handle = asset_server.load_with_settings(
|
||||
GltfAssetLabel::Scene(0).from_asset(path),
|
||||
|settings: &mut GltfLoaderSettings| {
|
||||
settings.load_cameras = false;
|
||||
settings.load_lights = false;
|
||||
},
|
||||
);
|
||||
let entity = cmds
|
||||
.spawn((
|
||||
Name::new("ModelNode"),
|
||||
SceneRoot(handle),
|
||||
ModelNode(Arc::downgrade(&model)),
|
||||
SpatialNode(Arc::downgrade(&model.spatial)),
|
||||
Visibility::Hidden,
|
||||
))
|
||||
.id();
|
||||
model
|
||||
.bevy_scene_entity
|
||||
.set(EntityHandle::new(entity))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_materials(
|
||||
mut commands: Commands,
|
||||
mut query: Query<&mut MeshMaterial3d<BevyMaterial>>,
|
||||
mut material_registry: ResMut<MaterialRegistry>,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut materials: ResMut<Assets<BevyMaterial>>,
|
||||
) -> bevy::prelude::Result {
|
||||
for model_part in MODEL_REGISTRY
|
||||
.get_valid_contents()
|
||||
.iter()
|
||||
.filter_map(|p| p.parts.get())
|
||||
.flatten()
|
||||
{
|
||||
let entity = **model_part.mesh_entity.get().unwrap();
|
||||
let Ok(mut mesh_mat) = query.get_mut(entity) else {
|
||||
continue;
|
||||
};
|
||||
if model_part.holdout.load(Ordering::Relaxed) {
|
||||
commands
|
||||
.entity(entity)
|
||||
.remove::<MeshMaterial3d<BevyMaterial>>()
|
||||
.insert(MeshMaterial3d(HOLDOUT_MATERIAL_HANDLE));
|
||||
continue;
|
||||
}
|
||||
if let Some(material) = model_part.pending_material_replacement.lock().take()
|
||||
&& let Some(material) = materials.get(&material)
|
||||
{
|
||||
let handle = material_registry.get_handle(material.clone(), &mut materials);
|
||||
mesh_mat.0 = handle;
|
||||
}
|
||||
for (param_name, param) in model_part.pending_material_parameters.lock().drain() {
|
||||
let mut new_mat = materials.get(&mesh_mat.0).unwrap().clone();
|
||||
param.apply_to_material(
|
||||
&model_part.spatial.node().unwrap().get_client().unwrap(),
|
||||
&mut new_mat,
|
||||
¶m_name,
|
||||
&asset_server,
|
||||
);
|
||||
let handle = material_registry.get_handle(new_mat, &mut materials);
|
||||
mesh_mat.0 = handle;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn gen_model_parts(
|
||||
scenes: Res<Assets<Scene>>,
|
||||
query: Query<(&SceneRoot, &ModelNode, &Children)>,
|
||||
children_query: Query<&Children>,
|
||||
part_query: Query<(&Name, Option<&Children>, &Transform), Without<Mesh3d>>,
|
||||
part_mesh_query: Query<(&Transform, &Aabb), With<Mesh3d>>,
|
||||
has_mesh: Query<Has<Mesh3d>>,
|
||||
mut cmds: Commands,
|
||||
) {
|
||||
for (scene_root, model_node, model_children) in query.iter() {
|
||||
let Some(model) = model_node.0.upgrade() else {
|
||||
continue;
|
||||
};
|
||||
if model.parts.get().is_some() {
|
||||
continue;
|
||||
}
|
||||
if scenes.get(scene_root.0.id()).is_none() {
|
||||
continue;
|
||||
}
|
||||
let mut parts = Vec::new();
|
||||
for entity in model_children
|
||||
.iter()
|
||||
.filter_map(|e| children_query.get(e).ok())
|
||||
.flat_map(|c| c.iter())
|
||||
{
|
||||
gen_path(
|
||||
entity,
|
||||
&part_query,
|
||||
None,
|
||||
&mut |entity, name, transform, parent, children| {
|
||||
let path = parent
|
||||
.as_ref()
|
||||
.map(|p| format!("{}/{}", &p.path, name.as_str()))
|
||||
.unwrap_or_else(|| name.to_string());
|
||||
let parent_spatial = parent
|
||||
.as_ref()
|
||||
.map(|p| p.spatial.clone())
|
||||
.unwrap_or_else(|| model.spatial.clone());
|
||||
let client = model.spatial.node()?.get_client()?;
|
||||
let (spatial, model_part) =
|
||||
match model.pre_bound_parts.lock().iter().find(|v| v.path == path) {
|
||||
None => {
|
||||
let node =
|
||||
client.scenegraph.add_node(Node::generate(&client, false));
|
||||
let spatial = Spatial::add_to(
|
||||
&node,
|
||||
Some(parent_spatial.clone()),
|
||||
transform.compute_matrix(),
|
||||
false,
|
||||
);
|
||||
let model_part = node.add_aspect(ModelPart {
|
||||
entity: OnceLock::new(),
|
||||
mesh_entity: OnceLock::new(),
|
||||
path,
|
||||
spatial: spatial.clone(),
|
||||
pending_material_parameters: Mutex::default(),
|
||||
pending_material_replacement: Mutex::default(),
|
||||
holdout: AtomicBool::new(false),
|
||||
aliases: AliasList::default(),
|
||||
bounds: OnceLock::new(),
|
||||
});
|
||||
(spatial, model_part)
|
||||
}
|
||||
Some(part) => {
|
||||
part.spatial.set_spatial_parent(&parent_spatial).unwrap();
|
||||
(part.spatial.clone(), part.clone())
|
||||
}
|
||||
};
|
||||
let aabb = Aabb::enclosing(
|
||||
children
|
||||
.iter()
|
||||
.flat_map(|v| v.iter())
|
||||
.filter_map(|e| part_mesh_query.get(e).ok())
|
||||
.flat_map(|(transform, aabb)| {
|
||||
[
|
||||
transform.transform_point(aabb.min().into()),
|
||||
transform.transform_point(aabb.max().into()),
|
||||
]
|
||||
}),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
_ = spatial.bounding_box_calc.set(move |n| {
|
||||
Box::pin(async {
|
||||
n.get_aspect::<ModelPart>()
|
||||
.ok()
|
||||
.and_then(|v| v.bounds.get().copied())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
});
|
||||
let _ = spatial.set_spatial_parent(&parent_spatial);
|
||||
spatial.set_local_transform(transform.compute_matrix());
|
||||
let entity_handle = EntityHandle::new(entity);
|
||||
spatial.set_entity(entity_handle.clone());
|
||||
cmds.entity(entity)
|
||||
.insert(SpatialNode(Arc::downgrade(&spatial)));
|
||||
let mesh_entity = children_query
|
||||
.get(entity)
|
||||
.iter()
|
||||
.flat_map(|v| v.iter())
|
||||
.find(|e| has_mesh.get(*e).unwrap_or(false))?;
|
||||
_ = model_part.bounds.set(aabb);
|
||||
_ = model_part.entity.set(entity_handle);
|
||||
_ = model_part.mesh_entity.set(EntityHandle::new(mesh_entity));
|
||||
parts.push(model_part.clone());
|
||||
Some(model_part)
|
||||
},
|
||||
);
|
||||
}
|
||||
_ = model.parts.set(parts);
|
||||
model.pre_bound_parts.lock().clear();
|
||||
model
|
||||
.spatial
|
||||
.set_entity(model.bevy_scene_entity.get().unwrap().clone());
|
||||
model.setup_complete.store(true, Ordering::Relaxed);
|
||||
model.setup_complete_notify.notify_waiters();
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_path(
|
||||
current_entity: Entity,
|
||||
part_query: &Query<(&Name, Option<&Children>, &Transform), Without<Mesh3d>>,
|
||||
parent: Option<Arc<ModelPart>>,
|
||||
func: &mut dyn FnMut(
|
||||
Entity,
|
||||
&Name,
|
||||
&Transform,
|
||||
Option<Arc<ModelPart>>,
|
||||
Option<&Children>,
|
||||
) -> Option<Arc<ModelPart>>,
|
||||
) {
|
||||
let Ok((name, children, transform)) = part_query.get(current_entity) else {
|
||||
return;
|
||||
};
|
||||
let Some(parent) = func(current_entity, name, transform, parent, children) else {
|
||||
return;
|
||||
};
|
||||
for e in children.iter().flat_map(|c| c.iter()) {
|
||||
gen_path(e, part_query, Some(parent.clone()), func);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Deref, DerefMut, Clone, Copy, Eq, PartialOrd, Ord, Hash)]
|
||||
struct HashedPbrMaterial(u64);
|
||||
impl HashedPbrMaterial {
|
||||
fn new(material: &BevyMaterial) -> Self {
|
||||
let mut hasher = FxHasher::default();
|
||||
Self::hash_pbr_mat(material, &mut hasher);
|
||||
Self(hasher.finish())
|
||||
}
|
||||
fn hash_pbr_mat<H: Hasher>(mat: &BevyMaterial, state: &mut H) {
|
||||
hash_color(mat.base_color, state);
|
||||
hash_color(mat.emissive.into(), state);
|
||||
state.write_u32(mat.metallic.to_bits());
|
||||
state.write_u32(mat.perceptual_roughness.to_bits());
|
||||
match mat.alpha_mode {
|
||||
AlphaMode::Opaque => state.write_u8(0),
|
||||
AlphaMode::Mask(v) => {
|
||||
state.write_u8(1);
|
||||
state.write_u32(v.to_bits());
|
||||
}
|
||||
AlphaMode::Blend => state.write_u8(2),
|
||||
AlphaMode::Premultiplied => state.write_u8(3),
|
||||
AlphaMode::AlphaToCoverage => state.write_u8(4),
|
||||
AlphaMode::Add => state.write_u8(5),
|
||||
AlphaMode::Multiply => state.write_u8(6),
|
||||
}
|
||||
state.write_u8(mat.double_sided as u8);
|
||||
mat.base_color_texture.hash(state);
|
||||
mat.emissive_texture.hash(state);
|
||||
mat.metallic_roughness_texture.hash(state);
|
||||
mat.occlusion_texture.hash(state);
|
||||
}
|
||||
}
|
||||
fn hash_color<H: Hasher>(color: Color, state: &mut H) {
|
||||
match color {
|
||||
Color::Srgba(srgba) => {
|
||||
state.write_u8(0);
|
||||
state.write(&srgba.to_u8_array());
|
||||
}
|
||||
Color::LinearRgba(linear_rgba) => {
|
||||
state.write_u8(1);
|
||||
state.write(&linear_rgba.to_u8_array());
|
||||
}
|
||||
Color::Hsla(hsla) => {
|
||||
state.write_u8(2);
|
||||
hsla.to_f32_array()
|
||||
.iter()
|
||||
.for_each(|v| state.write_u32(v.to_bits()));
|
||||
}
|
||||
Color::Hsva(hsva) => {
|
||||
state.write_u8(3);
|
||||
hsva.to_f32_array()
|
||||
.iter()
|
||||
.for_each(|v| state.write_u32(v.to_bits()));
|
||||
}
|
||||
Color::Hwba(hwba) => {
|
||||
state.write_u8(4);
|
||||
hwba.to_f32_array()
|
||||
.iter()
|
||||
.for_each(|v| state.write_u32(v.to_bits()));
|
||||
}
|
||||
Color::Laba(laba) => {
|
||||
state.write_u8(5);
|
||||
laba.to_f32_array()
|
||||
.iter()
|
||||
.for_each(|v| state.write_u32(v.to_bits()));
|
||||
}
|
||||
Color::Lcha(lcha) => {
|
||||
state.write_u8(6);
|
||||
lcha.to_f32_array()
|
||||
.iter()
|
||||
.for_each(|v| state.write_u32(v.to_bits()));
|
||||
}
|
||||
Color::Oklaba(oklaba) => {
|
||||
state.write_u8(7);
|
||||
oklaba
|
||||
.to_f32_array()
|
||||
.iter()
|
||||
.for_each(|v| state.write_u32(v.to_bits()));
|
||||
}
|
||||
Color::Oklcha(oklcha) => {
|
||||
state.write_u8(8);
|
||||
oklcha
|
||||
.to_f32_array()
|
||||
.iter()
|
||||
.for_each(|v| state.write_u32(v.to_bits()));
|
||||
}
|
||||
Color::Xyza(xyza) => {
|
||||
state.write_u8(9);
|
||||
xyza.to_f32_array()
|
||||
.iter()
|
||||
.for_each(|v| state.write_u32(v.to_bits()));
|
||||
}
|
||||
}
|
||||
}
|
||||
static MODEL_REGISTRY: Registry<Model> = Registry::new();
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum MaterialParameter {
|
||||
Float(f32),
|
||||
Vector2(Vector2<f32>),
|
||||
Vector3(Vector3<f32>),
|
||||
Vector4(Vector4<f32>),
|
||||
Color([f32; 4]),
|
||||
Int(i32),
|
||||
Int2(Vector2<i32>),
|
||||
Int3(Vector3<i32>),
|
||||
Int4(Vector4<i32>),
|
||||
Bool(bool),
|
||||
UInt(u32),
|
||||
UInt2(Vector2<u32>),
|
||||
UInt3(Vector3<u32>),
|
||||
UInt4(Vector4<u32>),
|
||||
Matrix(ColumnMatrix4<f32>),
|
||||
Texture(ResourceID),
|
||||
}
|
||||
impl MaterialParameter {
|
||||
fn apply_to_material(
|
||||
&self,
|
||||
client: &Client,
|
||||
sk: &impl StereoKitMultiThread,
|
||||
material: &Material,
|
||||
mat: &mut BevyMaterial,
|
||||
parameter_name: &str,
|
||||
asset_server: &AssetServer,
|
||||
) {
|
||||
match self {
|
||||
MaterialParameter::Float(val) => {
|
||||
sk.material_set_float(material, parameter_name, *val);
|
||||
}
|
||||
MaterialParameter::Vector2(val) => {
|
||||
sk.material_set_vector2(material, parameter_name, *val);
|
||||
}
|
||||
MaterialParameter::Vector3(val) => {
|
||||
sk.material_set_vector3(material, parameter_name, *val);
|
||||
}
|
||||
MaterialParameter::Vector4(val) => {
|
||||
sk.material_set_vector4(material, parameter_name, *val);
|
||||
}
|
||||
MaterialParameter::Color(val) => {
|
||||
sk.material_set_color(material, parameter_name, Color128::from(val.clone()));
|
||||
}
|
||||
MaterialParameter::Int(val) => {
|
||||
sk.material_set_int(material, parameter_name, *val);
|
||||
}
|
||||
MaterialParameter::Int2(val) => {
|
||||
sk.material_set_int2(material, parameter_name, val.x, val.y);
|
||||
}
|
||||
MaterialParameter::Int3(val) => {
|
||||
sk.material_set_int3(material, parameter_name, val.x, val.y, val.z);
|
||||
}
|
||||
MaterialParameter::Int4(val) => {
|
||||
sk.material_set_int4(material, parameter_name, val.w, val.x, val.y, val.z);
|
||||
}
|
||||
MaterialParameter::Bool(val) => {
|
||||
sk.material_set_bool(material, parameter_name, *val);
|
||||
}
|
||||
MaterialParameter::UInt(val) => {
|
||||
sk.material_set_uint(material, parameter_name, *val);
|
||||
}
|
||||
MaterialParameter::UInt2(val) => {
|
||||
sk.material_set_uint2(material, parameter_name, val.x, val.y);
|
||||
}
|
||||
MaterialParameter::UInt3(val) => {
|
||||
sk.material_set_uint3(material, parameter_name, val.x, val.y, val.z);
|
||||
}
|
||||
MaterialParameter::UInt4(val) => {
|
||||
sk.material_set_uint4(material, parameter_name, val.w, val.x, val.y, val.z);
|
||||
}
|
||||
MaterialParameter::Matrix(val) => {
|
||||
sk.material_set_matrix(material, parameter_name, Mat4::from(*val));
|
||||
}
|
||||
MaterialParameter::Texture(resource) => {
|
||||
let Some(texture_path) = resource.get_file(
|
||||
&client.base_resource_prefixes.lock().clone(),
|
||||
&[OsStr::new("png"), OsStr::new("jpg")],
|
||||
) else {return};
|
||||
if let Ok(tex) = sk.tex_create_file(texture_path, true, 0) {
|
||||
sk.material_set_texture(material, parameter_name, &tex);
|
||||
MaterialParameter::Bool(val) => match parameter_name {
|
||||
"double_sided" => mat.double_sided = *val,
|
||||
v => {
|
||||
error!("unknown param_name ({v}) for color")
|
||||
}
|
||||
},
|
||||
MaterialParameter::Int(_val) => {
|
||||
// nothing uses an int
|
||||
}
|
||||
MaterialParameter::UInt(_val) => {
|
||||
// nothing uses an uint
|
||||
}
|
||||
MaterialParameter::Float(val) => {
|
||||
match parameter_name {
|
||||
"metallic" => mat.metallic = *val,
|
||||
"roughness" => mat.perceptual_roughness = *val,
|
||||
// we probably don't want to expose tex_scale
|
||||
// "tex_scale" => mat.tex_scale = *val,
|
||||
v => {
|
||||
error!("unknown param_name ({v}) for float")
|
||||
}
|
||||
}
|
||||
}
|
||||
MaterialParameter::Vec2(_val) => {
|
||||
// nothing uses a Vec2
|
||||
}
|
||||
MaterialParameter::Vec3(_val) => {
|
||||
// nothing uses a Vec3
|
||||
}
|
||||
MaterialParameter::Color(color) => match parameter_name {
|
||||
"color" => mat.base_color = color.to_bevy(),
|
||||
"emission_factor" => mat.emissive = color.to_bevy().to_linear(),
|
||||
v => {
|
||||
error!("unknown param_name ({v}) for color")
|
||||
}
|
||||
},
|
||||
MaterialParameter::Texture(resource) => {
|
||||
let Some(texture_path) =
|
||||
get_resource_file(resource, client, &[OsStr::new("png"), OsStr::new("jpg")])
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let handle = asset_server.load(texture_path);
|
||||
match parameter_name {
|
||||
"diffuse" => mat.base_color_texture = Some(handle),
|
||||
"emission" => mat.emissive_texture = Some(handle),
|
||||
"metal" => mat.metallic_roughness_texture = Some(handle),
|
||||
"occlusion" => mat.occlusion_texture = Some(handle),
|
||||
v => {
|
||||
error!("unknown param_name ({v}) for texture");
|
||||
}
|
||||
}
|
||||
// mat.alpha_mode = AlphaMode::Blend;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModelPart {
|
||||
id: i32,
|
||||
path: PathBuf,
|
||||
space: Arc<Spatial>,
|
||||
model: Weak<Model>,
|
||||
entity: OnceLock<EntityHandle>,
|
||||
mesh_entity: OnceLock<EntityHandle>,
|
||||
path: String,
|
||||
spatial: Arc<Spatial>,
|
||||
pending_material_parameters: Mutex<FxHashMap<String, MaterialParameter>>,
|
||||
pending_material_replacement: Mutex<Option<Arc<Material>>>,
|
||||
pending_material_replacement: Mutex<Option<Handle<BevyMaterial>>>,
|
||||
holdout: AtomicBool,
|
||||
aliases: AliasList,
|
||||
bounds: OnceLock<Aabb>,
|
||||
}
|
||||
impl ModelPart {
|
||||
fn create_for_model(sk: &impl StereoKitMultiThread, model: &Arc<Model>, sk_model: &SKModel) {
|
||||
let first_root_part = sk.model_node_get_root(sk_model);
|
||||
let mut current_option_part = Some(first_root_part);
|
||||
|
||||
while let Some(current_part) = &mut current_option_part {
|
||||
ModelPart::create(sk, model, sk_model, *current_part);
|
||||
|
||||
if let Some(child) = sk.model_node_child(sk_model, *current_part) {
|
||||
*current_part = child;
|
||||
} else if let Some(sibling) = sk.model_node_sibling(sk_model, *current_part) {
|
||||
*current_part = sibling;
|
||||
} else {
|
||||
while let Some(current_part) = &mut current_option_part {
|
||||
if let Some(sibling) = sk.model_node_sibling(sk_model, *current_part) {
|
||||
*current_part = sibling;
|
||||
break;
|
||||
}
|
||||
current_option_part = sk.model_node_parent(sk_model, *current_part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create(
|
||||
sk: &impl StereoKitMultiThread,
|
||||
model: &Arc<Model>,
|
||||
sk_model: &SKModel,
|
||||
id: i32,
|
||||
) -> Option<Arc<Self>> {
|
||||
let parent_node = sk
|
||||
.model_node_parent(sk_model, id)
|
||||
.and_then(|id| model.parts.get(&id));
|
||||
let parent_part = parent_node
|
||||
.as_ref()
|
||||
.and_then(|node| match node.drawable.get() {
|
||||
Some(Drawable::ModelPart(model_part)) => Some(model_part),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let stardust_model_part = model.space.node()?;
|
||||
let client = stardust_model_part.get_client()?;
|
||||
let mut part_path = parent_part.map(|n| n.path.clone()).unwrap_or_default();
|
||||
part_path.push(sk.model_node_get_name(sk_model, id)?);
|
||||
let node = client.scenegraph.add_node(Node::create(
|
||||
&client,
|
||||
stardust_model_part.get_path(),
|
||||
part_path.to_str()?,
|
||||
false,
|
||||
));
|
||||
let spatial_parent = parent_node
|
||||
.and_then(|n| n.spatial.get().cloned())
|
||||
.unwrap_or_else(|| model.space.clone());
|
||||
let space = Spatial::add_to(
|
||||
&node,
|
||||
Some(spatial_parent),
|
||||
sk.model_node_get_transform_local(sk_model, id),
|
||||
false,
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
let _ = node.spatial.get().unwrap().bounding_box_calc.set(|node| {
|
||||
let Some(Drawable::ModelPart(model_part)) = node.drawable.get() else {return Bounds::default()};
|
||||
let Some(sk) = SK_MULTITHREAD.get() else {return Bounds::default()};
|
||||
let Some(model) = model_part.model.upgrade() else {return Bounds::default()};
|
||||
let Some(sk_model) = model.sk_model.get() else {return Bounds::default()};
|
||||
let Some(sk_mesh) = sk.model_node_get_mesh(sk_model, model_part.id) else {return Bounds::default()};
|
||||
sk.mesh_get_bounds(sk_mesh)
|
||||
});
|
||||
|
||||
let model_part = Arc::new(ModelPart {
|
||||
id,
|
||||
path: part_path,
|
||||
space,
|
||||
model: Arc::downgrade(model),
|
||||
pending_material_parameters: Mutex::new(FxHashMap::default()),
|
||||
pending_material_replacement: Mutex::new(None),
|
||||
});
|
||||
node.add_local_signal(
|
||||
"set_material_parameter",
|
||||
ModelPart::set_material_parameter_flex,
|
||||
);
|
||||
let _ = node.drawable.set(Drawable::ModelPart(model_part.clone()));
|
||||
model.parts.add(id, &node);
|
||||
Some(model_part)
|
||||
}
|
||||
|
||||
fn set_material_parameter_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let Some(Drawable::ModelPart(model_part)) = node.drawable.get() else {bail!("Not a drawable??")};
|
||||
|
||||
let (name, value): (String, MaterialParameter) = deserialize(message.as_ref())?;
|
||||
|
||||
model_part
|
||||
.pending_material_parameters
|
||||
.lock()
|
||||
.insert(name, value);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn replace_material(&self, replacement: Arc<Material>) {
|
||||
pub fn replace_material(&self, replacement: Handle<BevyMaterial>) {
|
||||
self.pending_material_replacement
|
||||
.lock()
|
||||
.replace(replacement);
|
||||
}
|
||||
pub fn set_material_parameter(&self, parameter_name: String, value: MaterialParameter) {
|
||||
self.pending_material_parameters
|
||||
.lock()
|
||||
.insert(parameter_name, value);
|
||||
}
|
||||
}
|
||||
impl ModelPartAspect for ModelPart {
|
||||
#[doc = "Set this model part's material to one that cuts a hole in the world. Often used for overlays/passthrough where you want to show the background through an object."]
|
||||
fn apply_holdout_material(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
||||
let model_part = node.get_aspect::<ModelPart>()?;
|
||||
model_part.holdout.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&self, sk: &impl StereoKitDraw) {
|
||||
let Some(model) = self.model.upgrade() else {return};
|
||||
let Some(sk_model) = model.sk_model.get() else {return};
|
||||
let Some(node) = model.space.node() else {return};
|
||||
let Some(client) = node.get_client() else {return};
|
||||
if let Some(material_replacement) = self.pending_material_replacement.lock().take() {
|
||||
sk.model_node_set_material(sk_model, self.id, material_replacement.as_ref().as_ref());
|
||||
#[doc = "Set the material parameter with `parameter_name` to `value`"]
|
||||
fn set_material_parameter(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
parameter_name: String,
|
||||
value: MaterialParameter,
|
||||
) -> Result<()> {
|
||||
let model_part = node.get_aspect::<ModelPart>()?;
|
||||
model_part.set_material_parameter(parameter_name, value);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[derive(Default, Resource)]
|
||||
pub struct MaterialRegistry(FxHashMap<HashedPbrMaterial, Handle<BevyMaterial>>);
|
||||
impl MaterialRegistry {
|
||||
/// returns strong handle for PbrMaterial elminitating duplications
|
||||
pub fn get_handle(
|
||||
&mut self,
|
||||
material: BevyMaterial,
|
||||
materials: &mut ResMut<Assets<BevyMaterial>>,
|
||||
) -> Handle<BevyMaterial> {
|
||||
let hash = HashedPbrMaterial::new(&material);
|
||||
match self
|
||||
.0
|
||||
.get(&hash)
|
||||
.and_then(|v| materials.get_strong_handle(v.id()))
|
||||
{
|
||||
Some(v) => v,
|
||||
None => {
|
||||
let handle = materials.add(material);
|
||||
self.0.insert(hash, handle.clone_weak());
|
||||
handle
|
||||
}
|
||||
}
|
||||
|
||||
let mut material_parameters = self.pending_material_parameters.lock();
|
||||
for (parameter_name, parameter_value) in material_parameters.drain() {
|
||||
let Some(material) = sk.model_node_get_material(sk_model, self.id) else {continue};
|
||||
let new_material = sk.material_copy(material);
|
||||
parameter_value.apply_to_material(&client, sk, &new_material, parameter_name.as_str());
|
||||
sk.model_node_set_material(sk_model, self.id, &new_material);
|
||||
}
|
||||
|
||||
sk.model_node_set_transform_model(
|
||||
sk_model,
|
||||
self.id,
|
||||
Spatial::space_to_space_matrix(Some(&self.space), Some(&model.space)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Model {
|
||||
self_ref: Weak<Model>,
|
||||
enabled: Arc<AtomicBool>,
|
||||
space: Arc<Spatial>,
|
||||
spatial: Arc<Spatial>,
|
||||
_resource_id: ResourceID,
|
||||
sk_model: OnceCell<SKModel>,
|
||||
parts: LifeLinkedNodeMap<i32>,
|
||||
bevy_scene_entity: OnceLock<EntityHandle>,
|
||||
parts: OnceLock<Vec<Arc<ModelPart>>>,
|
||||
pre_bound_parts: Mutex<Vec<Arc<ModelPart>>>,
|
||||
setup_complete: AtomicBool,
|
||||
setup_complete_notify: Notify,
|
||||
}
|
||||
unsafe impl Send for Model {}
|
||||
unsafe impl Sync for Model {}
|
||||
|
||||
impl Model {
|
||||
pub fn add_to(node: &Arc<Node>, resource_id: ResourceID) -> Result<Arc<Model>> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
ensure!(
|
||||
node.drawable.get().is_none(),
|
||||
"Internal: Node already has a drawable attached!"
|
||||
);
|
||||
let pending_model_path = get_resource_file(
|
||||
&resource_id,
|
||||
&*node.get_client().ok_or_else(|| eyre!("Client not found"))?,
|
||||
&[OsStr::new("glb"), OsStr::new("gltf")],
|
||||
)
|
||||
.ok_or_else(|| eyre!("Resource not found"))?;
|
||||
|
||||
let pending_model_path = resource_id
|
||||
.get_file(
|
||||
&node
|
||||
.get_client()
|
||||
.ok_or_else(|| eyre!("Client not found"))?
|
||||
.base_resource_prefixes
|
||||
.lock()
|
||||
.clone(),
|
||||
&[OsStr::new("glb"), OsStr::new("gltf")],
|
||||
)
|
||||
.ok_or_else(|| eyre!("Resource not found"))?;
|
||||
|
||||
let model = Arc::new_cyclic(|self_ref| Model {
|
||||
self_ref: self_ref.clone(),
|
||||
enabled: node.enabled.clone(),
|
||||
space: node.spatial.get().unwrap().clone(),
|
||||
let model = Arc::new(Model {
|
||||
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
|
||||
_resource_id: resource_id,
|
||||
sk_model: OnceCell::new(),
|
||||
parts: LifeLinkedNodeMap::default(),
|
||||
bevy_scene_entity: OnceLock::new(),
|
||||
pre_bound_parts: Mutex::default(),
|
||||
parts: OnceLock::new(),
|
||||
setup_complete_notify: Notify::new(),
|
||||
setup_complete: AtomicBool::new(false),
|
||||
});
|
||||
_ = model.spatial.bounding_box_calc.set(|n| {
|
||||
Box::pin(async {
|
||||
if let Ok(model) = n.get_aspect::<Model>()
|
||||
&& !model.setup_complete.load(Ordering::Relaxed)
|
||||
{
|
||||
model.setup_complete_notify.notified().await;
|
||||
}
|
||||
Aabb::default()
|
||||
})
|
||||
});
|
||||
LOAD_MODEL
|
||||
.send((model.clone(), pending_model_path))
|
||||
.unwrap();
|
||||
MODEL_REGISTRY.add_raw(&model);
|
||||
|
||||
let sk = SK_MULTITHREAD.get().unwrap();
|
||||
let sk_model = sk.model_copy(
|
||||
sk.model_create_file(pending_model_path.to_str().unwrap(), None::<Shader>)?,
|
||||
);
|
||||
ModelPart::create_for_model(sk, &model.self_ref.upgrade().unwrap(), &sk_model);
|
||||
let _ = model.sk_model.set(sk_model);
|
||||
let _ = node.drawable.set(Drawable::Model(model.clone()));
|
||||
node.add_aspect_raw(model.clone());
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
fn draw(&self, sk: &impl StereoKitDraw) {
|
||||
let Some(sk_model) = self.sk_model.get() else {return};
|
||||
for model_node_node in self.parts.nodes() {
|
||||
let Some(Drawable::ModelPart(model_node)) = model_node_node.drawable.get() else {continue};
|
||||
model_node.update(sk);
|
||||
}
|
||||
|
||||
sk.model_draw(
|
||||
sk_model,
|
||||
self.space.global_transform(),
|
||||
WHITE,
|
||||
RenderLayer::LAYER0,
|
||||
);
|
||||
pub fn get_model_part(self: &Arc<Self>, part_path: String) -> Result<Arc<ModelPart>> {
|
||||
let part = match self
|
||||
.parts
|
||||
.get()
|
||||
.map(|v| v.iter().find(|p| p.path == part_path))
|
||||
{
|
||||
Some(Some(part)) => part.clone(),
|
||||
Some(None) => {
|
||||
let paths = self
|
||||
.parts
|
||||
.get()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|p| &p.path)
|
||||
.collect::<Vec<_>>();
|
||||
bail!(
|
||||
"Couldn't find model part at path {part_path}, all available paths: {paths:?}",
|
||||
);
|
||||
}
|
||||
None => {
|
||||
let client = self.spatial.node().unwrap().get_client().unwrap();
|
||||
let part_node = client.scenegraph.add_node(Node::generate(&client, false));
|
||||
let spatial = Spatial::add_to(
|
||||
&part_node,
|
||||
Some(self.spatial.clone()),
|
||||
Mat4::IDENTITY,
|
||||
false,
|
||||
);
|
||||
let part = part_node.add_aspect(ModelPart {
|
||||
entity: OnceLock::new(),
|
||||
mesh_entity: OnceLock::new(),
|
||||
path: part_path,
|
||||
spatial,
|
||||
pending_material_parameters: Mutex::default(),
|
||||
pending_material_replacement: Mutex::default(),
|
||||
holdout: AtomicBool::new(false),
|
||||
aliases: AliasList::default(),
|
||||
bounds: OnceLock::new(),
|
||||
});
|
||||
self.pre_bound_parts.lock().push(part.clone());
|
||||
part
|
||||
}
|
||||
};
|
||||
Ok(part)
|
||||
}
|
||||
}
|
||||
impl ModelAspect for Model {
|
||||
#[doc = "Bind a model part to the node with the ID input."]
|
||||
fn bind_model_part(
|
||||
node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
part_path: String,
|
||||
) -> Result<()> {
|
||||
let model = node.get_aspect::<Model>()?;
|
||||
let part = model.get_model_part(part_path)?;
|
||||
Alias::create_with_id(
|
||||
&part.spatial.node().unwrap(),
|
||||
&calling_client,
|
||||
id,
|
||||
MODEL_PART_ASPECT_ALIAS_INFO.clone(),
|
||||
Some(&part.aliases),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Model {
|
||||
fn drop(&mut self) {
|
||||
for p in self.parts.get().iter().flat_map(|v| v.iter()) {
|
||||
if let Some(node) = p.spatial.node() {
|
||||
node.destroy();
|
||||
}
|
||||
}
|
||||
for p in self.pre_bound_parts.lock().iter() {
|
||||
if let Some(node) = p.spatial.node() {
|
||||
node.destroy();
|
||||
}
|
||||
}
|
||||
MODEL_REGISTRY.remove(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_all(sk: &impl StereoKitDraw) {
|
||||
for model in MODEL_REGISTRY.get_valid_contents() {
|
||||
if model.enabled.load(Ordering::Relaxed) {
|
||||
model.draw(sk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_flex(_node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateModelInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
resource: ResourceID,
|
||||
}
|
||||
let info: CreateModelInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/drawable/model", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, true);
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
Model::add_to(&node, info.resource)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
use smithay::backend::renderer::gles::{
|
||||
ffi::{self, Gles2, FRAGMENT_SHADER, VERTEX_SHADER},
|
||||
GlesError,
|
||||
};
|
||||
use std::mem::transmute;
|
||||
use stereokit::Shader;
|
||||
use tracing::error;
|
||||
|
||||
// Simula shader with fancy lanzcos sampling
|
||||
pub const UNLIT_SHADER_BYTES: &[u8] = include_bytes!("shader_unlit_gamma.sks");
|
||||
|
||||
// Simula shader with fancy lanzcos sampling
|
||||
pub const PANEL_SHADER_BYTES: &[u8] = include_bytes!("shader_unlit_simula.sks");
|
||||
|
||||
struct FfiAssetHeader {
|
||||
asset_type: i32,
|
||||
asset_state: i32,
|
||||
id: u64,
|
||||
index: u64,
|
||||
refs: i32,
|
||||
debug: *mut u8,
|
||||
}
|
||||
|
||||
struct FfiSkgShader {
|
||||
meta: *mut u8,
|
||||
vertex: u32,
|
||||
pixel: u32,
|
||||
program: u32,
|
||||
compute: u32,
|
||||
}
|
||||
|
||||
struct FfiShader {
|
||||
header: FfiAssetHeader,
|
||||
shader: FfiSkgShader,
|
||||
}
|
||||
|
||||
unsafe fn compile_shader(
|
||||
gl: &ffi::Gles2,
|
||||
variant: ffi::types::GLuint,
|
||||
src: &str,
|
||||
) -> Result<ffi::types::GLuint, GlesError> {
|
||||
let shader = gl.CreateShader(variant);
|
||||
if shader == 0 {
|
||||
return Err(GlesError::CreateShaderObject);
|
||||
}
|
||||
|
||||
gl.ShaderSource(
|
||||
shader,
|
||||
1,
|
||||
&src.as_ptr() as *const *const u8 as *const *const ffi::types::GLchar,
|
||||
&(src.len() as i32) as *const _,
|
||||
);
|
||||
gl.CompileShader(shader);
|
||||
|
||||
let mut status = ffi::FALSE as i32;
|
||||
gl.GetShaderiv(shader, ffi::COMPILE_STATUS, &mut status as *mut _);
|
||||
if status == ffi::FALSE as i32 {
|
||||
let mut max_len = 0;
|
||||
gl.GetShaderiv(shader, ffi::INFO_LOG_LENGTH, &mut max_len as *mut _);
|
||||
|
||||
let mut error = Vec::with_capacity(max_len as usize);
|
||||
let mut len = 0;
|
||||
gl.GetShaderInfoLog(
|
||||
shader,
|
||||
max_len as _,
|
||||
&mut len as *mut _,
|
||||
error.as_mut_ptr() as *mut _,
|
||||
);
|
||||
error.set_len(len as usize);
|
||||
|
||||
error!(
|
||||
"[GL] {}",
|
||||
std::str::from_utf8(&error).unwrap_or("<Error Message no utf8>")
|
||||
);
|
||||
|
||||
gl.DeleteShader(shader);
|
||||
return Err(GlesError::ShaderCompileError);
|
||||
}
|
||||
|
||||
Ok(shader)
|
||||
}
|
||||
|
||||
unsafe fn link_program(
|
||||
gl: &ffi::Gles2,
|
||||
vert: ffi::types::GLuint,
|
||||
frag: ffi::types::GLuint,
|
||||
) -> Result<ffi::types::GLuint, GlesError> {
|
||||
let program = gl.CreateProgram();
|
||||
gl.AttachShader(program, vert);
|
||||
gl.AttachShader(program, frag);
|
||||
gl.LinkProgram(program);
|
||||
// gl.DetachShader(program, vert);
|
||||
// gl.DetachShader(program, frag);
|
||||
// gl.DeleteShader(vert);
|
||||
// gl.DeleteShader(frag);
|
||||
|
||||
let mut status = ffi::FALSE as i32;
|
||||
gl.GetProgramiv(program, ffi::LINK_STATUS, &mut status as *mut _);
|
||||
if status == ffi::FALSE as i32 {
|
||||
let mut max_len = 0;
|
||||
gl.GetProgramiv(program, ffi::INFO_LOG_LENGTH, &mut max_len as *mut _);
|
||||
|
||||
let mut error = Vec::with_capacity(max_len as usize);
|
||||
let mut len = 0;
|
||||
gl.GetProgramInfoLog(
|
||||
program,
|
||||
max_len as _,
|
||||
&mut len as *mut _,
|
||||
error.as_mut_ptr() as *mut _,
|
||||
);
|
||||
error.set_len(len as usize);
|
||||
|
||||
error!(
|
||||
"[GL] {}",
|
||||
std::str::from_utf8(&error).unwrap_or("<Error Message no utf8>")
|
||||
);
|
||||
|
||||
gl.DeleteProgram(program);
|
||||
return Err(GlesError::ProgramLinkError);
|
||||
}
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
pub unsafe fn shader_inject(
|
||||
c: &Gles2,
|
||||
sk_shader: &mut Shader,
|
||||
vert_str: &str,
|
||||
frag_str: &str,
|
||||
) -> Result<(), GlesError> {
|
||||
let gl_vert = compile_shader(c, VERTEX_SHADER, vert_str)?;
|
||||
let gl_frag = compile_shader(c, FRAGMENT_SHADER, frag_str)?;
|
||||
let gl_prog = link_program(c, gl_vert, gl_frag)?;
|
||||
|
||||
let shader: *mut FfiShader = transmute(sk_shader.0.as_mut());
|
||||
if let Some(shader) = shader.as_mut() {
|
||||
shader.shader.vertex = gl_vert;
|
||||
shader.shader.pixel = gl_frag;
|
||||
shader.shader.program = gl_prog;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#include "stereokit.hlsli"
|
||||
|
||||
//--name = sk/unlit
|
||||
//--diffuse = white
|
||||
//--uv_offset = 0.0, 0.0
|
||||
//--uv_scale = 1.0, 1.0
|
||||
Texture2D diffuse : register(t0);
|
||||
SamplerState diffuse_s : register(s0);
|
||||
float2 uv_scale;
|
||||
float2 uv_offset;
|
||||
|
||||
struct vsIn {
|
||||
float4 pos : SV_Position;
|
||||
float3 norm : NORMAL0;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
struct psIn {
|
||||
float4 pos : SV_POSITION;
|
||||
float2 uv : TEXCOORD0;
|
||||
uint view_id : SV_RenderTargetArrayIndex;
|
||||
};
|
||||
|
||||
psIn vs(vsIn input, uint id : SV_InstanceID) {
|
||||
psIn o;
|
||||
o.view_id = id % sk_view_count;
|
||||
id = id / sk_view_count;
|
||||
|
||||
float3 world = mul(float4(input.pos.xyz, 1), sk_inst[id].world).xyz;
|
||||
o.pos = mul(float4(world, 1), sk_viewproj[o.view_id]);
|
||||
|
||||
o.uv = (input.uv + uv_offset) * uv_scale;
|
||||
return o;
|
||||
}
|
||||
float4 ps(psIn input) : SV_TARGET {
|
||||
float4 col = diffuse.Sample(diffuse_s, input.uv);
|
||||
col.rgb = pow(col.rgb, float3(2.2));
|
||||
|
||||
return col;
|
||||
}
|
||||
Binary file not shown.
@@ -1,119 +0,0 @@
|
||||
#include "stereokit.hlsli"
|
||||
|
||||
// Port of https://github.com/SimulaVR/Simula/blob/master/addons/godot-haskell-plugin/TextShader.tres to StereoKit and HLSL.
|
||||
|
||||
//--name = stardust/text_shader
|
||||
//--diffuse = white
|
||||
//--uv_offset = 0.0, 0.0
|
||||
//--uv_scale = 1.0, 1.0
|
||||
//--fcFactor = 1.0
|
||||
//--ripple = 4.0
|
||||
//--alpha_min = 0.0
|
||||
//--alpha_max = 1.0
|
||||
Texture2D diffuse : register(t0);
|
||||
SamplerState diffuse_s : register(s0);
|
||||
float4 diffuse_i;
|
||||
float2 uv_scale;
|
||||
float2 uv_offset;
|
||||
float fcFactor;
|
||||
float ripple;
|
||||
float alpha_min;
|
||||
float alpha_max;
|
||||
|
||||
struct vsIn {
|
||||
float4 pos : SV_Position;
|
||||
float3 norm : NORMAL0;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
struct psIn {
|
||||
float4 pos : SV_POSITION;
|
||||
float2 uv : TEXCOORD0;
|
||||
uint view_id : SV_RenderTargetArrayIndex;
|
||||
};
|
||||
|
||||
psIn vs(vsIn input, uint id : SV_InstanceID) {
|
||||
psIn o;
|
||||
o.view_id = id % sk_view_count;
|
||||
id = id / sk_view_count;
|
||||
|
||||
float3 world = mul(float4(input.pos.xyz, 1), sk_inst[id].world).xyz;
|
||||
o.pos = mul(float4(world, 1), sk_viewproj[o.view_id]);
|
||||
|
||||
o.uv = (input.uv + uv_offset) * uv_scale;
|
||||
return o;
|
||||
}
|
||||
|
||||
float map(float value, float min1, float max1, float min2, float max2) {
|
||||
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
|
||||
}
|
||||
|
||||
// float gaussian(float x, float t) {
|
||||
// float PI = 3.14159265358;
|
||||
// return exp(-x*x/(2.0 * t*t))/(sqrt(2.0*PI)*t);
|
||||
// }
|
||||
|
||||
float besselI0(float x) {
|
||||
return 1.0 + pow(x, 2.0) * (0.25 + pow(x, 2.0) * (0.015625 + pow(x, 2.0) * (0.000434028 + pow(x, 2.0) * (6.78168e-6 + pow(x, 2.0) * (6.78168e-8 + pow(x, 2.0) * (4.7095e-10 + pow(x, 2.0) * (2.40281e-12 + pow(x, 2.0) * (9.38597e-15 + pow(x, 2.0) * (2.8969e-17 + 7.24226e-20 * pow(x, 2.0))))))))));
|
||||
}
|
||||
|
||||
float kaiser(float x, float alpha) {
|
||||
if (x > 1.0) {
|
||||
return 0.0;
|
||||
}
|
||||
return besselI0(alpha * sqrt(1.0-x*x));
|
||||
}
|
||||
|
||||
float4 lowpassFilter(Texture2D tex, sampler2D texSampler, float2 uv, float alpha) {
|
||||
float PI = 3.14159265358;
|
||||
|
||||
float4 q = float4(0.0);
|
||||
|
||||
float2 dx_uv = ddx(uv);
|
||||
float2 dy_uv = ddy(uv);
|
||||
//float width = sqrt(max(dot(dx_uv, dx_uv), dot(dy_uv, dy_uv)));
|
||||
float2 width = abs(float2(dx_uv.x, dy_uv.y));
|
||||
|
||||
float2 pixelWidth = floor(width * diffuse_i.xy);
|
||||
float2 aspectRatio = normalize(pixelWidth);
|
||||
|
||||
float2 xyf = uv * diffuse_i.xy;
|
||||
int2 xy = int2(xyf);
|
||||
|
||||
pixelWidth = clamp(pixelWidth, float2(1.0), float2(2.0));
|
||||
|
||||
int2 start = xy - int2(pixelWidth);
|
||||
int2 end = xy + int2(pixelWidth);
|
||||
|
||||
float4 outColor = float4(0.0);
|
||||
|
||||
float qSum = 0.0;
|
||||
|
||||
for (int v = start.y; v <= end.y; v++) {
|
||||
for (int u = start.x; u <= end.x; u++) {
|
||||
float kx = fcFactor * (xyf.x - float(u))/pixelWidth.x;
|
||||
float ky = fcFactor * (xyf.y - float(v))/pixelWidth.y;
|
||||
|
||||
//float lanczosValue = gaussian(kx, fcx);
|
||||
float lanczosValue = kaiser(sqrt(kx*kx + ky*ky), alpha);
|
||||
|
||||
q += tex.Sample(texSampler, (float2(u, v)+float2(0.5))/diffuse_i.xy) * lanczosValue;
|
||||
// q += tex.Load(int3(u, v, 0)) * lanczosValue;
|
||||
qSum += lanczosValue;
|
||||
}
|
||||
}
|
||||
|
||||
return q/qSum;
|
||||
}
|
||||
|
||||
float4 ps(psIn input) : SV_TARGET {
|
||||
float gamma = 2.2;
|
||||
// float4 col = diffuse.Sample(diffuse_s, input.uv);
|
||||
|
||||
// float4 col = lowpassFilter(diffuse, diffuse_s, diffuse_i.xy, float2(1.0 - input.uv.x, input.uv.y), ripple);
|
||||
float4 col = lowpassFilter(diffuse, diffuse_s, input.uv, ripple);
|
||||
// float4 col = diffuse.Sample(diffuse_s, input.uv);
|
||||
col.rgb = pow(col.rgb, float3(gamma));
|
||||
col.a = map(col.a, 0, 1, alpha_min, alpha_max);
|
||||
|
||||
return col;
|
||||
}
|
||||
Binary file not shown.
72
src/nodes/drawable/sky.rs
Normal file
72
src/nodes/drawable/sky.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use bevy::{
|
||||
app::{Plugin, Update},
|
||||
color::Color,
|
||||
core_pipeline::{Skybox, core_3d::Camera3d},
|
||||
ecs::{
|
||||
entity::Entity,
|
||||
query::With,
|
||||
system::{Commands, Query, ResMut},
|
||||
},
|
||||
pbr::{AmbientLight, environment_map::EnvironmentMapLight},
|
||||
};
|
||||
use bevy_equirect::EquirectManager;
|
||||
use glam::Quat;
|
||||
|
||||
pub struct SkyPlugin;
|
||||
|
||||
impl Plugin for SkyPlugin {
|
||||
fn build(&self, app: &mut bevy::app::App) {
|
||||
app.add_systems(Update, apply_sky);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make this work with cameras spawned after setting the sky texture
|
||||
fn apply_sky(
|
||||
mut equirect: ResMut<EquirectManager>,
|
||||
cameras: Query<Entity, With<Camera3d>>,
|
||||
mut cmds: Commands,
|
||||
) {
|
||||
if let Some(tex) = super::QUEUED_SKYTEX.lock().take() {
|
||||
if let Some(path) = tex {
|
||||
let image_handle = equirect.load_equirect_as_cubemap(path, 1024);
|
||||
for cam in cameras {
|
||||
cmds.entity(cam).insert(Skybox {
|
||||
image: image_handle.clone(),
|
||||
brightness: 1000.0,
|
||||
rotation: Quat::IDENTITY,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for cam in cameras {
|
||||
cmds.entity(cam).remove::<Skybox>();
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(light) = super::QUEUED_SKYLIGHT.lock().take() {
|
||||
if let Some(path) = light {
|
||||
let image_handle = equirect.load_equirect_as_cubemap(path, 1024);
|
||||
for cam in cameras {
|
||||
cmds.entity(cam)
|
||||
.insert(EnvironmentMapLight {
|
||||
diffuse_map: image_handle.clone(),
|
||||
// we might want to use the SkyTex for this?
|
||||
specular_map: image_handle.clone(),
|
||||
intensity: 1000.0,
|
||||
rotation: Quat::IDENTITY,
|
||||
affects_lightmapped_mesh_diffuse: false,
|
||||
})
|
||||
.remove::<AmbientLight>();
|
||||
}
|
||||
} else {
|
||||
for cam in cameras {
|
||||
cmds.entity(cam)
|
||||
.insert(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 1000.0,
|
||||
affects_lightmapped_meshes: true,
|
||||
})
|
||||
.remove::<EnvironmentMapLight>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,222 +1,227 @@
|
||||
use crate::{
|
||||
core::{client::Client, destroy_queue, registry::Registry, resource::ResourceID},
|
||||
BevyMaterial,
|
||||
core::{
|
||||
bevy_channel::{BevyChannel, BevyChannelReader},
|
||||
client::Client,
|
||||
color::ColorConvert,
|
||||
entity_handle::EntityHandle,
|
||||
error::Result,
|
||||
registry::Registry,
|
||||
resource::get_resource_file,
|
||||
},
|
||||
nodes::{
|
||||
drawable::Drawable,
|
||||
spatial::{find_spatial_parent, parse_transform, Spatial},
|
||||
Message, Node,
|
||||
Node,
|
||||
drawable::{TextFit, XAlign},
|
||||
spatial::{Spatial, SpatialNode},
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::{bail, ensure, eyre, Result};
|
||||
use glam::{vec3, Mat4, Vec2};
|
||||
use mint::Vector2;
|
||||
use once_cell::sync::OnceCell;
|
||||
use bevy::{platform::collections::HashMap, prelude::*};
|
||||
use bevy_mesh_text_3d::{
|
||||
Align, Attrs, HorizontalAnchorPoint, MeshTextPlugin, Settings as FontSettings, VerticalAlign,
|
||||
VerticalAnchorPoint, generate_meshes,
|
||||
};
|
||||
use color_eyre::eyre::eyre;
|
||||
use core::f32;
|
||||
use parking_lot::Mutex;
|
||||
use portable_atomic::{AtomicBool, Ordering};
|
||||
use prisma::{Flatten, Rgba};
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::{schemas::flex::deserialize, values::Transform};
|
||||
use std::{ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
use stereokit::{named_colors::WHITE, Color128, StereoKitDraw, TextAlign, TextFit, TextStyle};
|
||||
use std::{ffi::OsStr, mem, path::PathBuf, sync::Arc};
|
||||
|
||||
static SPAWN_TEXT: BevyChannel<Arc<Text>> = BevyChannel::new();
|
||||
|
||||
pub struct TextNodePlugin;
|
||||
|
||||
impl Plugin for TextNodePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// Text init stuff
|
||||
app.add_plugins(MeshTextPlugin);
|
||||
app.world_mut()
|
||||
.resource_mut::<FontSettings>()
|
||||
.font_system
|
||||
.db_mut()
|
||||
.load_system_fonts();
|
||||
|
||||
SPAWN_TEXT.init(app);
|
||||
app.init_resource::<MaterialRegistry>();
|
||||
app.add_systems(Update, spawn_text);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_text(
|
||||
mut mpsc: ResMut<BevyChannelReader<Arc<Text>>>,
|
||||
mut cmds: Commands,
|
||||
mut font_settings: ResMut<FontSettings>,
|
||||
mut material_registry: ResMut<MaterialRegistry>,
|
||||
mut materials: ResMut<Assets<BevyMaterial>>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut font_registry: Local<FontDatabaseRegistry>,
|
||||
) {
|
||||
while let Some(text) = mpsc.read() {
|
||||
if let Some(entity) = text.entity.lock().take() {
|
||||
cmds.entity(*entity).despawn();
|
||||
}
|
||||
let style = text.data.lock();
|
||||
let old_db = text.font_path.clone().map(|p| {
|
||||
let db = font_registry.get(p);
|
||||
mem::swap(font_settings.font_system.db_mut(), db);
|
||||
db
|
||||
});
|
||||
let attrs = Attrs::new().weight(cosmic_text::Weight::BOLD);
|
||||
let alignment = Some(match style.text_align_x {
|
||||
super::XAlign::Left => Align::Right,
|
||||
super::XAlign::Center => Align::Center,
|
||||
super::XAlign::Right => Align::Left,
|
||||
});
|
||||
let vertical_alignment = Some(match style.text_align_y {
|
||||
super::YAlign::Top => VerticalAlign::Top,
|
||||
super::YAlign::Center => VerticalAlign::Middle,
|
||||
super::YAlign::Bottom => VerticalAlign::Bottom,
|
||||
});
|
||||
let text_string = text.text.lock().clone();
|
||||
let max_width = style.bounds.as_ref().map(|v| v.bounds.x);
|
||||
let max_height = style.bounds.as_ref().map(|v| v.bounds.y);
|
||||
let horizontal_anchor_point = style
|
||||
.bounds
|
||||
.as_ref()
|
||||
.map(|v| match v.anchor_align_x {
|
||||
XAlign::Left => HorizontalAnchorPoint::Left,
|
||||
XAlign::Center => HorizontalAnchorPoint::Middle,
|
||||
XAlign::Right => HorizontalAnchorPoint::Right,
|
||||
})
|
||||
.unwrap_or(HorizontalAnchorPoint::Middle);
|
||||
let vertical_anchor_point = style
|
||||
.bounds
|
||||
.as_ref()
|
||||
.map(|v| match v.anchor_align_y {
|
||||
YAlign::Top => VerticalAnchorPoint::Top,
|
||||
YAlign::Center => VerticalAnchorPoint::Middle,
|
||||
YAlign::Bottom => VerticalAnchorPoint::Bottom,
|
||||
})
|
||||
.unwrap_or(VerticalAnchorPoint::Middle);
|
||||
let wrap = matches!(style.bounds.as_ref().map(|v| v.fit), Some(TextFit::Wrap));
|
||||
let char_meshes = generate_meshes(
|
||||
bevy_mesh_text_3d::InputText::Simple {
|
||||
text: text_string,
|
||||
material: material_registry.get_handle(
|
||||
BevyMaterial {
|
||||
base_color: style.color.to_bevy(),
|
||||
emissive: Color::WHITE.to_linear(),
|
||||
metallic: 0.0,
|
||||
perceptual_roughness: 1.0,
|
||||
alpha_mode: AlphaMode::Premultiplied,
|
||||
double_sided: false,
|
||||
..default()
|
||||
},
|
||||
&mut materials,
|
||||
),
|
||||
attrs,
|
||||
},
|
||||
&mut font_settings,
|
||||
bevy_mesh_text_3d::Parameters {
|
||||
extrusion_depth: 0.0,
|
||||
font_size: style.character_height,
|
||||
line_height: style.character_height * 1.1,
|
||||
alignment,
|
||||
max_width: wrap.then_some(0).and(max_width),
|
||||
max_height: wrap.then_some(0).and(max_height),
|
||||
vertical_alignment,
|
||||
horizontal_anchor_point,
|
||||
vertical_anchor_point,
|
||||
},
|
||||
&mut meshes,
|
||||
);
|
||||
if let Some(db) = old_db {
|
||||
mem::swap(font_settings.font_system.db_mut(), db);
|
||||
}
|
||||
let Ok((char_meshes, _text_size)) =
|
||||
char_meshes.inspect_err(|err| error!("unable to create text meshes: {err}"))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let letters = char_meshes
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
cmds.spawn((Mesh3d(v.mesh), MeshMaterial3d(v.material), v.transform))
|
||||
.id()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let entity = cmds
|
||||
.spawn((
|
||||
Name::new("TextNode"),
|
||||
SpatialNode(Arc::downgrade(&text.spatial)),
|
||||
))
|
||||
.add_children(&letters)
|
||||
.id();
|
||||
let entity = EntityHandle::new(entity);
|
||||
text.entity.lock().replace(entity.clone());
|
||||
text.spatial.set_entity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FontDatabaseRegistry(HashMap<PathBuf, cosmic_text::fontdb::Database>);
|
||||
impl FontDatabaseRegistry {
|
||||
fn get(&mut self, path: PathBuf) -> &mut cosmic_text::fontdb::Database {
|
||||
self.0.entry(path).or_insert_with_key(|path| {
|
||||
let mut db = cosmic_text::fontdb::Database::new();
|
||||
if let Err(err) = db.load_font_file(path) {
|
||||
error!("unable to load font file {} {err}", path.to_string_lossy());
|
||||
};
|
||||
db
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
use super::{TextAspect, TextStyle, YAlign, model::MaterialRegistry};
|
||||
|
||||
static TEXT_REGISTRY: Registry<Text> = Registry::new();
|
||||
|
||||
struct TextData {
|
||||
text: String,
|
||||
character_height: f32,
|
||||
text_align: TextAlign,
|
||||
bounds: Option<Vec2>,
|
||||
fit: TextFit,
|
||||
bounds_align: TextAlign,
|
||||
color: Rgba<f32>,
|
||||
}
|
||||
|
||||
pub struct Text {
|
||||
enabled: Arc<AtomicBool>,
|
||||
space: Arc<Spatial>,
|
||||
spatial: Arc<Spatial>,
|
||||
font_path: Option<PathBuf>,
|
||||
style: OnceCell<TextStyle>,
|
||||
|
||||
data: Mutex<TextData>,
|
||||
entity: Mutex<Option<EntityHandle>>,
|
||||
text: Mutex<String>,
|
||||
data: Mutex<TextStyle>,
|
||||
}
|
||||
impl Text {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn add_to(
|
||||
node: &Arc<Node>,
|
||||
font_resource_id: Option<ResourceID>,
|
||||
text: String,
|
||||
character_height: f32,
|
||||
text_align: TextAlign,
|
||||
bounds: Option<Vector2<f32>>,
|
||||
fit: TextFit,
|
||||
bounds_align: TextAlign,
|
||||
color: Rgba<f32>,
|
||||
) -> Result<Arc<Text>> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
ensure!(
|
||||
node.drawable.get().is_none(),
|
||||
"Internal: Node already has a drawable attached!"
|
||||
);
|
||||
|
||||
pub fn add_to(node: &Arc<Node>, text: String, style: TextStyle) -> Result<Arc<Text>> {
|
||||
let client = node.get_client().ok_or_else(|| eyre!("Client not found"))?;
|
||||
let text = TEXT_REGISTRY.add(Text {
|
||||
enabled: node.enabled.clone(),
|
||||
space: node.spatial.get().unwrap().clone(),
|
||||
font_path: font_resource_id.and_then(|res| {
|
||||
res.get_file(
|
||||
&client.base_resource_prefixes.lock().clone(),
|
||||
&[OsStr::new("ttf"), OsStr::new("otf")],
|
||||
)
|
||||
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
|
||||
font_path: style.font.as_ref().and_then(|res| {
|
||||
get_resource_file(res, &client, &[OsStr::new("ttf"), OsStr::new("otf")])
|
||||
}),
|
||||
style: OnceCell::new(),
|
||||
|
||||
data: Mutex::new(TextData {
|
||||
text,
|
||||
character_height,
|
||||
text_align,
|
||||
bounds: bounds.map(|b| b.into()),
|
||||
fit,
|
||||
bounds_align,
|
||||
color,
|
||||
}),
|
||||
entity: Mutex::new(None),
|
||||
text: Mutex::new(text),
|
||||
data: Mutex::new(style),
|
||||
});
|
||||
node.add_local_signal("set_character_height", Text::set_character_height_flex);
|
||||
node.add_local_signal("set_text", Text::set_text_flex);
|
||||
let _ = node.drawable.set(Drawable::Text(text.clone()));
|
||||
node.add_aspect_raw(text.clone());
|
||||
_ = SPAWN_TEXT.send(text.clone());
|
||||
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
fn draw(&self, sk: &impl StereoKitDraw) {
|
||||
let style = self
|
||||
.style
|
||||
.get_or_try_init(|| -> Result<TextStyle, color_eyre::eyre::Error> {
|
||||
let font = self
|
||||
.font_path
|
||||
.as_deref()
|
||||
.and_then(|path| sk.font_create(path).ok())
|
||||
.unwrap_or_else(|| sk.font_find("default/font").unwrap());
|
||||
Ok(unsafe { sk.text_make_style(font, 1.0, WHITE) })
|
||||
});
|
||||
|
||||
if let Ok(style) = style {
|
||||
let data = self.data.lock();
|
||||
let transform = self.space.global_transform()
|
||||
* Mat4::from_scale(vec3(
|
||||
data.character_height,
|
||||
data.character_height,
|
||||
data.character_height,
|
||||
));
|
||||
if let Some(bounds) = data.bounds {
|
||||
sk.text_add_in(
|
||||
&data.text,
|
||||
transform,
|
||||
bounds / data.character_height,
|
||||
data.fit,
|
||||
*style,
|
||||
data.bounds_align,
|
||||
data.text_align,
|
||||
vec3(0.0, 0.0, 0.0),
|
||||
Color128::from([
|
||||
data.color.red(),
|
||||
data.color.green(),
|
||||
data.color.blue(),
|
||||
data.color.alpha(),
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
sk.text_add_at(
|
||||
&data.text,
|
||||
transform,
|
||||
*style,
|
||||
data.bounds_align,
|
||||
data.text_align,
|
||||
vec3(0.0, 0.0, 0.0),
|
||||
Color128::from([
|
||||
data.color.red(),
|
||||
data.color.green(),
|
||||
data.color.blue(),
|
||||
data.color.alpha(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_character_height_flex(
|
||||
node: &Node,
|
||||
}
|
||||
impl TextAspect for Text {
|
||||
fn set_character_height(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
height: f32,
|
||||
) -> Result<()> {
|
||||
let Some(Drawable::Text(text)) = node.drawable.get() else {bail!("Not a drawable??")};
|
||||
|
||||
text.data.lock().character_height = deserialize(message.as_ref())?;
|
||||
let this_text = node.get_aspect::<Text>()?;
|
||||
this_text.data.lock().character_height = height;
|
||||
_ = SPAWN_TEXT.send(this_text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_text_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let Some(Drawable::Text(text)) = node.drawable.get() else {bail!("Not a drawable??")};
|
||||
|
||||
text.data.lock().text = deserialize(message.as_ref())?;
|
||||
fn set_text(node: Arc<Node>, _calling_client: Arc<Client>, text: String) -> Result<()> {
|
||||
let this_text = node.get_aspect::<Text>()?;
|
||||
*this_text.text.lock() = text;
|
||||
_ = SPAWN_TEXT.send(this_text);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Drop for Text {
|
||||
fn drop(&mut self) {
|
||||
if let Some(style) = self.style.take() {
|
||||
destroy_queue::add(style);
|
||||
}
|
||||
TEXT_REGISTRY.remove(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_all(sk: &impl StereoKitDraw) {
|
||||
for text in TEXT_REGISTRY.get_valid_contents() {
|
||||
if text.enabled.load(Ordering::Relaxed) {
|
||||
text.draw(sk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_flex(_node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateTextInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
text: String,
|
||||
font_resource: Option<ResourceID>,
|
||||
character_height: f32,
|
||||
text_align: TextAlign,
|
||||
bounds: Option<Vector2<f32>>,
|
||||
fit: TextFit,
|
||||
bounds_align: TextAlign,
|
||||
color: [f32; 4],
|
||||
}
|
||||
let info: CreateTextInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/drawable/text", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, true);
|
||||
let color = Rgba::from_slice(&info.color);
|
||||
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
Text::add_to(
|
||||
&node,
|
||||
info.font_resource,
|
||||
info.text,
|
||||
info.character_height,
|
||||
info.text_align,
|
||||
info.bounds,
|
||||
info.fit,
|
||||
info.bounds_align,
|
||||
color,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
402
src/nodes/fields.rs
Normal file
402
src/nodes/fields.rs
Normal file
@@ -0,0 +1,402 @@
|
||||
use super::alias::{Alias, AliasInfo};
|
||||
use super::spatial::{
|
||||
SPATIAL_REF_GET_LOCAL_BOUNDING_BOX_SERVER_OPCODE,
|
||||
SPATIAL_REF_GET_RELATIVE_BOUNDING_BOX_SERVER_OPCODE, SPATIAL_REF_GET_TRANSFORM_SERVER_OPCODE,
|
||||
Spatial,
|
||||
};
|
||||
use super::{Aspect, AspectIdentifier, Node};
|
||||
use crate::DbusConnection;
|
||||
use crate::core::client::Client;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::registry::Registry;
|
||||
use crate::nodes::spatial::SPATIAL_ASPECT_ALIAS_INFO;
|
||||
use crate::nodes::spatial::SPATIAL_REF_ASPECT_ALIAS_INFO;
|
||||
use crate::nodes::spatial::Transform;
|
||||
use bevy::app::{Plugin, Update};
|
||||
use bevy::color::Color;
|
||||
use bevy::ecs::resource::Resource;
|
||||
use bevy::ecs::schedule::IntoScheduleConfigs;
|
||||
use bevy::ecs::system::Res;
|
||||
use bevy::gizmos::gizmos::Gizmos;
|
||||
use bevy::gizmos::primitives::dim3::GizmoPrimitive3d;
|
||||
use bevy::math::primitives::{Cylinder, Torus};
|
||||
use color_eyre::eyre::OptionExt;
|
||||
use glam::{Vec3, Vec3A, Vec3Swizzles, vec2, vec3, vec3a};
|
||||
use parking_lot::Mutex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use stardust_xr::values::Vector3;
|
||||
use std::sync::{Arc, LazyLock, Weak};
|
||||
use zbus::interface;
|
||||
|
||||
// TODO: get SDFs working properly with non-uniform scale and so on, output distance relative to the spatial it's compared against
|
||||
|
||||
pub static FIELD_ALIAS_INFO: LazyLock<AliasInfo> = LazyLock::new(|| AliasInfo {
|
||||
server_methods: vec![
|
||||
SPATIAL_REF_GET_TRANSFORM_SERVER_OPCODE,
|
||||
SPATIAL_REF_GET_LOCAL_BOUNDING_BOX_SERVER_OPCODE,
|
||||
SPATIAL_REF_GET_RELATIVE_BOUNDING_BOX_SERVER_OPCODE,
|
||||
FIELD_REF_DISTANCE_SERVER_OPCODE,
|
||||
FIELD_REF_NORMAL_SERVER_OPCODE,
|
||||
FIELD_REF_CLOSEST_POINT_SERVER_OPCODE,
|
||||
FIELD_REF_RAY_MARCH_SERVER_OPCODE,
|
||||
],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
pub struct FieldDebugGizmoPlugin;
|
||||
impl Plugin for FieldDebugGizmoPlugin {
|
||||
fn build(&self, app: &mut bevy::app::App) {
|
||||
let (tx, rx) = tokio::sync::watch::channel(false);
|
||||
let conn = app.world().resource::<DbusConnection>().0.clone();
|
||||
tokio::spawn(async move {
|
||||
_ = conn
|
||||
.object_server()
|
||||
.at("/org/stardustxr/Server", FieldDebugGizmos { state: tx })
|
||||
.await;
|
||||
});
|
||||
app.insert_resource(FieldDebugGizmosEnabled(rx));
|
||||
app.add_systems(
|
||||
Update,
|
||||
draw_field_gizmos.run_if(|res: Res<FieldDebugGizmosEnabled>| *res.0.borrow()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct FieldDebugGizmosEnabled(tokio::sync::watch::Receiver<bool>);
|
||||
|
||||
fn draw_field_gizmos(mut gizmos: Gizmos) {
|
||||
FIELD_REGISTRY_DEBUG_GIZMOS
|
||||
.get_valid_contents()
|
||||
.iter()
|
||||
.for_each(|f| {
|
||||
let transform =
|
||||
bevy::transform::components::Transform::from_matrix(f.spatial.global_transform());
|
||||
let color = Color::srgb_u8(0x04, 0xFD, 0x4C);
|
||||
match f.shape.lock().clone() {
|
||||
Shape::Box(size) => gizmos.cuboid(transform.with_scale(size.into()), color),
|
||||
Shape::Cylinder(CylinderShape { length, radius }) => {
|
||||
gizmos
|
||||
.primitive_3d(
|
||||
&Cylinder {
|
||||
radius,
|
||||
half_height: length * 0.5,
|
||||
},
|
||||
transform.to_isometry(),
|
||||
color,
|
||||
)
|
||||
.resolution(32);
|
||||
}
|
||||
Shape::Sphere(radius) => {
|
||||
gizmos.sphere(transform.to_isometry(), radius, color);
|
||||
}
|
||||
Shape::Torus(TorusShape { radius_a, radius_b }) => {
|
||||
let minor_radius;
|
||||
let major_radius;
|
||||
if radius_a >= radius_b {
|
||||
major_radius = radius_a;
|
||||
minor_radius = radius_b;
|
||||
} else {
|
||||
major_radius = radius_b;
|
||||
minor_radius = radius_a;
|
||||
}
|
||||
gizmos
|
||||
.primitive_3d(
|
||||
&Torus {
|
||||
minor_radius,
|
||||
major_radius,
|
||||
},
|
||||
transform.to_isometry(),
|
||||
color,
|
||||
)
|
||||
.minor_resolution(32)
|
||||
.major_resolution(32);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
struct FieldDebugGizmos {
|
||||
state: tokio::sync::watch::Sender<bool>,
|
||||
}
|
||||
|
||||
#[interface(name = "org.stardustxr.debug.FieldDebugGizmos")]
|
||||
impl FieldDebugGizmos {
|
||||
fn enable(&mut self) {
|
||||
_ = self.state.send(true);
|
||||
}
|
||||
fn disable(&mut self) {
|
||||
_ = self.state.send(false);
|
||||
}
|
||||
}
|
||||
|
||||
static FIELD_REGISTRY_DEBUG_GIZMOS: Registry<Field> = Registry::new();
|
||||
|
||||
stardust_xr_server_codegen::codegen_field_protocol!();
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref EXPORTED_FIELDS: Mutex<FxHashMap<u64, Weak<Node>>> = Mutex::new(FxHashMap::default());
|
||||
}
|
||||
|
||||
pub trait FieldTrait: Send + Sync + 'static {
|
||||
fn spatial_ref(&self) -> &Spatial;
|
||||
|
||||
fn local_distance(&self, p: Vec3A) -> f32;
|
||||
fn local_normal(&self, p: Vec3A, r: f32) -> Vec3A {
|
||||
let d = self.local_distance(p);
|
||||
let e = vec2(r, 0_f32);
|
||||
|
||||
let n = vec3a(d, d, d)
|
||||
- vec3a(
|
||||
self.local_distance(vec3a(e.x, e.y, e.y)),
|
||||
self.local_distance(vec3a(e.y, e.x, e.y)),
|
||||
self.local_distance(vec3a(e.y, e.y, e.x)),
|
||||
);
|
||||
|
||||
n.normalize()
|
||||
}
|
||||
fn local_closest_point(&self, p: Vec3A, r: f32) -> Vec3A {
|
||||
p - (self.local_normal(p, r) * self.local_distance(p))
|
||||
}
|
||||
|
||||
fn distance(&self, reference_space: &Spatial, p: Vec3A) -> f32 {
|
||||
let reference_to_local_space =
|
||||
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
|
||||
let local_p = reference_to_local_space.transform_point3a(p);
|
||||
self.local_distance(local_p)
|
||||
}
|
||||
fn normal(&self, reference_space: &Spatial, p: Vec3A, r: f32) -> Vec3A {
|
||||
let reference_to_local_space =
|
||||
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
|
||||
let local_p = reference_to_local_space.transform_point3a(p);
|
||||
reference_to_local_space
|
||||
.inverse()
|
||||
.transform_vector3a(self.local_normal(local_p, r))
|
||||
}
|
||||
fn closest_point(&self, reference_space: &Spatial, p: Vec3A, r: f32) -> Vec3A {
|
||||
let reference_to_local_space =
|
||||
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
|
||||
let local_p = reference_to_local_space.transform_point3a(p);
|
||||
reference_to_local_space
|
||||
.inverse()
|
||||
.transform_point3a(self.local_closest_point(local_p, r))
|
||||
}
|
||||
|
||||
fn ray_march(&self, ray: Ray) -> RayMarchResult {
|
||||
let mut result = RayMarchResult {
|
||||
ray_origin: ray.origin.into(),
|
||||
ray_direction: ray.direction.into(),
|
||||
min_distance: f32::MAX,
|
||||
deepest_point_distance: 0_f32,
|
||||
ray_length: 0_f32,
|
||||
ray_steps: 0,
|
||||
};
|
||||
|
||||
let ray_to_field_matrix =
|
||||
Spatial::space_to_space_matrix(Some(&ray.space), Some(self.spatial_ref()));
|
||||
let mut ray_point = ray_to_field_matrix.transform_point3a(ray.origin.into());
|
||||
let ray_direction = ray_to_field_matrix
|
||||
.transform_vector3a(ray.direction.into())
|
||||
.normalize();
|
||||
|
||||
while result.ray_steps < MAX_RAY_STEPS && result.ray_length < MAX_RAY_LENGTH {
|
||||
let distance = self.local_distance(ray_point);
|
||||
let march_distance = distance.clamp(MIN_RAY_MARCH, MAX_RAY_MARCH);
|
||||
|
||||
result.ray_length += march_distance;
|
||||
ray_point += ray_direction * march_distance;
|
||||
|
||||
if result.min_distance > distance {
|
||||
result.deepest_point_distance = result.ray_length;
|
||||
result.min_distance = distance;
|
||||
}
|
||||
|
||||
result.ray_steps += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ray {
|
||||
pub origin: Vec3,
|
||||
pub direction: Vec3,
|
||||
pub space: Arc<Spatial>,
|
||||
}
|
||||
|
||||
// const MIN_RAY_STEPS: u32 = 0;
|
||||
const MAX_RAY_STEPS: u32 = 1000;
|
||||
|
||||
const MIN_RAY_MARCH: f32 = 0.001_f32;
|
||||
const MAX_RAY_MARCH: f32 = f32::MAX;
|
||||
|
||||
// const MIN_RAY_LENGTH: f32 = 0_f32;
|
||||
const MAX_RAY_LENGTH: f32 = 1000_f32;
|
||||
|
||||
pub struct Field {
|
||||
pub spatial: Arc<Spatial>,
|
||||
pub shape: Mutex<Shape>,
|
||||
}
|
||||
impl Field {
|
||||
pub fn add_to(node: &Arc<Node>, shape: Shape) -> Result<Arc<Field>> {
|
||||
let spatial = node.get_aspect::<Spatial>()?;
|
||||
let field = Field {
|
||||
spatial,
|
||||
shape: Mutex::new(shape),
|
||||
};
|
||||
let field = node.add_aspect(field);
|
||||
FIELD_REGISTRY_DEBUG_GIZMOS.add_raw(&field);
|
||||
node.add_aspect(FieldRef);
|
||||
Ok(field)
|
||||
}
|
||||
}
|
||||
impl Drop for Field {
|
||||
fn drop(&mut self) {
|
||||
FIELD_REGISTRY_DEBUG_GIZMOS.remove(self);
|
||||
}
|
||||
}
|
||||
impl AspectIdentifier for Field {
|
||||
impl_aspect_for_field_aspect_id! {}
|
||||
}
|
||||
impl Aspect for Field {
|
||||
impl_aspect_for_field_aspect! {}
|
||||
}
|
||||
impl FieldAspect for Field {
|
||||
fn set_shape(node: Arc<Node>, _calling_client: Arc<Client>, shape: Shape) -> Result<()> {
|
||||
let field = node.get_aspect::<Field>()?;
|
||||
*field.shape.lock() = shape;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn export_field(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<u64> {
|
||||
let id = rand::random();
|
||||
EXPORTED_FIELDS.lock().insert(id, Arc::downgrade(&node));
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
impl FieldTrait for Field {
|
||||
fn spatial_ref(&self) -> &Spatial {
|
||||
&self.spatial
|
||||
}
|
||||
fn local_distance(&self, p: Vec3A) -> f32 {
|
||||
match self.shape.lock().clone() {
|
||||
Shape::Box(size) => {
|
||||
let q = vec3(
|
||||
p.x.abs() - (size.x * 0.5_f32),
|
||||
p.y.abs() - (size.y * 0.5_f32),
|
||||
p.z.abs() - (size.z * 0.5_f32),
|
||||
);
|
||||
let v = vec3a(q.x.max(0_f32), q.y.max(0_f32), q.z.max(0_f32));
|
||||
v.length() + q.x.max(q.y.max(q.z)).min(0_f32)
|
||||
}
|
||||
Shape::Cylinder(CylinderShape { length, radius }) => {
|
||||
let d = vec2(p.xz().length().abs() - radius, p.y.abs() - (length * 0.5));
|
||||
d.x.max(d.y).min(0.0) + d.max(vec2(0.0, 0.0)).length()
|
||||
}
|
||||
Shape::Sphere(radius) => p.length() - radius,
|
||||
Shape::Torus(TorusShape { radius_a, radius_b }) => {
|
||||
let q = vec2(p.xz().length() - radius_a, p.y);
|
||||
q.length() - radius_b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FieldRef;
|
||||
impl AspectIdentifier for FieldRef {
|
||||
impl_aspect_for_field_ref_aspect_id! {}
|
||||
}
|
||||
impl Aspect for FieldRef {
|
||||
impl_aspect_for_field_ref_aspect! {}
|
||||
}
|
||||
impl FieldRefAspect for FieldRef {
|
||||
async fn distance(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
space: Arc<Node>,
|
||||
point: Vector3<f32>,
|
||||
) -> Result<f32> {
|
||||
let reference_space = space.get_aspect::<Spatial>()?;
|
||||
let field = node.get_aspect::<Field>()?;
|
||||
Ok(field.distance(&reference_space, point.into()))
|
||||
}
|
||||
|
||||
async fn normal(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
space: Arc<Node>,
|
||||
point: Vector3<f32>,
|
||||
) -> Result<Vector3<f32>> {
|
||||
let reference_space = space.get_aspect::<Spatial>()?;
|
||||
let field = node.get_aspect::<Field>()?;
|
||||
Ok(field.normal(&reference_space, point.into(), 0.0001).into())
|
||||
}
|
||||
|
||||
async fn closest_point(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
space: Arc<Node>,
|
||||
point: Vector3<f32>,
|
||||
) -> Result<Vector3<f32>> {
|
||||
let reference_space = space.get_aspect::<Spatial>()?;
|
||||
let field = node.get_aspect::<Field>()?;
|
||||
Ok(field
|
||||
.closest_point(&reference_space, point.into(), 0.0001)
|
||||
.into())
|
||||
}
|
||||
|
||||
async fn ray_march(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
space: Arc<Node>,
|
||||
ray_origin: Vector3<f32>,
|
||||
ray_direction: Vector3<f32>,
|
||||
) -> Result<RayMarchResult> {
|
||||
let space = space.get_aspect::<Spatial>()?;
|
||||
let field = node.get_aspect::<Field>()?;
|
||||
Ok(field.ray_march(Ray {
|
||||
origin: ray_origin.into(),
|
||||
direction: ray_direction.into(),
|
||||
space,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl InterfaceAspect for Interface {
|
||||
async fn import_field_ref(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
uid: u64,
|
||||
) -> Result<Arc<Node>> {
|
||||
Ok(EXPORTED_FIELDS
|
||||
.lock()
|
||||
.get(&uid)
|
||||
.and_then(|s| s.upgrade())
|
||||
.map(|s| {
|
||||
Alias::create(
|
||||
&s,
|
||||
&calling_client,
|
||||
FIELD_REF_ASPECT_ALIAS_INFO.clone(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.ok_or_eyre("Couldn't import field with that ID")?)
|
||||
}
|
||||
|
||||
fn create_field(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
shape: Shape,
|
||||
) -> Result<()> {
|
||||
let transform = transform.to_mat4(true, true, false);
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent.clone()), transform, false);
|
||||
Field::add_to(&node, shape)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
use super::{Field, FieldTrait, Node};
|
||||
use crate::core::client::Client;
|
||||
use crate::nodes::spatial::{find_spatial_parent, parse_transform, Spatial};
|
||||
use crate::nodes::Message;
|
||||
use color_eyre::eyre::{ensure, Result};
|
||||
use glam::{vec3, vec3a, Vec3, Vec3A};
|
||||
use mint::Vector3;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flex::deserialize;
|
||||
use stardust_xr::values::Transform;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct BoxField {
|
||||
space: Arc<Spatial>,
|
||||
size: Mutex<Vec3>,
|
||||
}
|
||||
|
||||
impl BoxField {
|
||||
pub fn add_to(node: &Arc<Node>, size: Vector3<f32>) -> Result<Arc<Field>> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
ensure!(
|
||||
node.field.get().is_none(),
|
||||
"Internal: Node already has a field attached!"
|
||||
);
|
||||
let box_field = BoxField {
|
||||
space: node.spatial.get().unwrap().clone(),
|
||||
size: Mutex::new(size.into()),
|
||||
};
|
||||
box_field.add_field_methods(node);
|
||||
node.add_local_signal("set_size", BoxField::set_size_flex);
|
||||
let field = Arc::new(Field::Box(box_field));
|
||||
let _ = node.field.set(field.clone());
|
||||
Ok(field)
|
||||
}
|
||||
|
||||
pub fn set_size(&self, size: Vector3<f32>) {
|
||||
*self.size.lock() = size.into();
|
||||
}
|
||||
|
||||
pub fn set_size_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let Field::Box(box_field) = node.field.get().unwrap().as_ref() else { return Ok(()) };
|
||||
box_field.set_size(deserialize(message.as_ref())?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FieldTrait for BoxField {
|
||||
fn local_distance(&self, p: Vec3A) -> f32 {
|
||||
let size = self.size.lock();
|
||||
let q = vec3(
|
||||
p.x.abs() - (size.x * 0.5_f32),
|
||||
p.y.abs() - (size.y * 0.5_f32),
|
||||
p.z.abs() - (size.z * 0.5_f32),
|
||||
);
|
||||
let v = vec3a(q.x.max(0_f32), q.y.max(0_f32), q.z.max(0_f32));
|
||||
v.length() + q.x.max(q.y.max(q.z)).min(0_f32)
|
||||
}
|
||||
fn spatial_ref(&self) -> &Spatial {
|
||||
self.space.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_box_field_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateFieldInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
size: Vector3<f32>,
|
||||
}
|
||||
let info: CreateFieldInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/field", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
BoxField::add_to(&node, info.size)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
use super::{Field, FieldTrait, Node};
|
||||
use crate::core::client::Client;
|
||||
use crate::nodes::spatial::{find_spatial_parent, parse_transform, Spatial};
|
||||
use crate::nodes::Message;
|
||||
use color_eyre::eyre::{ensure, Result};
|
||||
use glam::{swizzles::*, vec2, Vec3A};
|
||||
use portable_atomic::AtomicF32;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flex::deserialize;
|
||||
use stardust_xr::values::Transform;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct CylinderField {
|
||||
space: Arc<Spatial>,
|
||||
length: AtomicF32,
|
||||
radius: AtomicF32,
|
||||
}
|
||||
|
||||
impl CylinderField {
|
||||
pub fn add_to(node: &Arc<Node>, length: f32, radius: f32) -> Result<()> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
ensure!(
|
||||
node.field.get().is_none(),
|
||||
"Internal: Node already has a field attached!"
|
||||
);
|
||||
let cylinder_field = CylinderField {
|
||||
space: node.spatial.get().unwrap().clone(),
|
||||
length: AtomicF32::new(length.abs()),
|
||||
radius: AtomicF32::new(radius.abs()),
|
||||
};
|
||||
cylinder_field.add_field_methods(node);
|
||||
node.add_local_signal("set_size", CylinderField::set_size_flex);
|
||||
let _ = node.field.set(Arc::new(Field::Cylinder(cylinder_field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_size(&self, length: f32, radius: f32) {
|
||||
self.length.store(length.abs(), Ordering::Relaxed);
|
||||
self.radius.store(radius.abs(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn set_size_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let Field::Cylinder(cylinder_field) = node.field.get().unwrap().as_ref() else { return Ok(()) };
|
||||
let (length, radius) = deserialize(message.as_ref())?;
|
||||
cylinder_field.set_size(length, radius);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FieldTrait for CylinderField {
|
||||
fn local_distance(&self, p: Vec3A) -> f32 {
|
||||
let radius = self.radius.load(Ordering::Relaxed);
|
||||
let length = self.length.load(Ordering::Relaxed);
|
||||
let d = vec2(p.xy().length().abs() - radius, p.z.abs() - (length * 0.5));
|
||||
|
||||
d.x.max(d.y).min(0.0) + d.max(vec2(0.0, 0.0)).length()
|
||||
}
|
||||
fn spatial_ref(&self) -> &Spatial {
|
||||
self.space.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_cylinder_field_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateFieldInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
length: f32,
|
||||
radius: f32,
|
||||
}
|
||||
let info: CreateFieldInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/field", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
CylinderField::add_to(&node, info.length, info.radius)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
pub mod r#box;
|
||||
mod cylinder;
|
||||
mod sphere;
|
||||
mod torus;
|
||||
|
||||
use self::cylinder::{create_cylinder_field_flex, CylinderField};
|
||||
use self::r#box::{create_box_field_flex, BoxField};
|
||||
use self::sphere::{create_sphere_field_flex, SphereField};
|
||||
use self::torus::{create_torus_field_flex, TorusField};
|
||||
|
||||
use super::alias::AliasInfo;
|
||||
use super::spatial::Spatial;
|
||||
use super::{Message, Node};
|
||||
use crate::core::client::Client;
|
||||
use crate::core::scenegraph::MethodResponseSender;
|
||||
use crate::nodes::spatial::find_reference_space;
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::{vec2, vec3a, Vec3, Vec3A};
|
||||
use mint::Vector3;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stardust_xr::schemas::flex::{deserialize, serialize};
|
||||
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub static FIELD_ALIAS_INFO: Lazy<AliasInfo> = Lazy::new(|| AliasInfo {
|
||||
server_methods: vec!["distance", "normal", "closest_point", "ray_march"],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
pub trait FieldTrait {
|
||||
fn add_field_methods(&self, node: &Arc<Node>) {
|
||||
node.add_local_method("distance", field_distance_flex);
|
||||
node.add_local_method("normal", field_normal_flex);
|
||||
node.add_local_method("closest_point", field_closest_point_flex);
|
||||
node.add_local_method("ray_march", field_ray_march_flex);
|
||||
}
|
||||
fn spatial_ref(&self) -> &Spatial;
|
||||
|
||||
fn local_distance(&self, p: Vec3A) -> f32;
|
||||
fn local_normal(&self, p: Vec3A, r: f32) -> Vec3A {
|
||||
let d = self.local_distance(p);
|
||||
let e = vec2(r, 0_f32);
|
||||
|
||||
let n = vec3a(d, d, d)
|
||||
- vec3a(
|
||||
self.local_distance(vec3a(e.x, e.y, e.y)),
|
||||
self.local_distance(vec3a(e.y, e.x, e.y)),
|
||||
self.local_distance(vec3a(e.y, e.y, e.x)),
|
||||
);
|
||||
|
||||
n.normalize()
|
||||
}
|
||||
fn local_closest_point(&self, p: Vec3A, r: f32) -> Vec3A {
|
||||
p - (self.local_normal(p, r) * self.local_distance(p))
|
||||
}
|
||||
|
||||
fn distance(&self, reference_space: &Spatial, p: Vec3A) -> f32 {
|
||||
let reference_to_local_space =
|
||||
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
|
||||
let local_p = reference_to_local_space.transform_point3a(p);
|
||||
self.local_distance(local_p)
|
||||
}
|
||||
fn normal(&self, reference_space: &Spatial, p: Vec3A, r: f32) -> Vec3A {
|
||||
let reference_to_local_space =
|
||||
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
|
||||
let local_p = reference_to_local_space.transform_point3a(p);
|
||||
reference_to_local_space
|
||||
.inverse()
|
||||
.transform_vector3a(self.local_normal(local_p, r))
|
||||
}
|
||||
fn closest_point(&self, reference_space: &Spatial, p: Vec3A, r: f32) -> Vec3A {
|
||||
let reference_to_local_space =
|
||||
Spatial::space_to_space_matrix(Some(reference_space), Some(self.spatial_ref()));
|
||||
let local_p = reference_to_local_space.transform_point3a(p);
|
||||
reference_to_local_space
|
||||
.inverse()
|
||||
.transform_point3a(self.local_closest_point(local_p, r))
|
||||
}
|
||||
|
||||
fn ray_march(&self, ray: Ray) -> RayMarchResult {
|
||||
let mut result = RayMarchResult {
|
||||
min_distance: f32::MAX,
|
||||
deepest_point_distance: 0_f32,
|
||||
ray_length: 0_f32,
|
||||
ray_steps: 0,
|
||||
};
|
||||
|
||||
let ray_to_field_matrix =
|
||||
Spatial::space_to_space_matrix(Some(&ray.space), Some(self.spatial_ref()));
|
||||
let mut ray_point = ray_to_field_matrix.transform_point3a(ray.origin.into());
|
||||
let ray_direction = ray_to_field_matrix.transform_vector3a(ray.direction.into());
|
||||
|
||||
while result.ray_steps < MAX_RAY_STEPS && result.ray_length < MAX_RAY_LENGTH {
|
||||
let distance = self.local_distance(ray_point);
|
||||
let march_distance = distance.clamp(MIN_RAY_MARCH, MAX_RAY_MARCH);
|
||||
|
||||
result.ray_length += march_distance;
|
||||
ray_point += ray_direction * march_distance;
|
||||
|
||||
if result.min_distance > distance {
|
||||
result.deepest_point_distance = result.ray_length;
|
||||
result.min_distance = distance;
|
||||
}
|
||||
|
||||
result.ray_steps += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Ray {
|
||||
pub origin: Vec3,
|
||||
pub direction: Vec3,
|
||||
pub space: Arc<Spatial>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RayMarchResult {
|
||||
pub min_distance: f32,
|
||||
pub deepest_point_distance: f32,
|
||||
pub ray_length: f32,
|
||||
pub ray_steps: u32,
|
||||
}
|
||||
|
||||
// const MIN_RAY_STEPS: u32 = 0;
|
||||
const MAX_RAY_STEPS: u32 = 1000;
|
||||
|
||||
const MIN_RAY_MARCH: f32 = 0.001_f32;
|
||||
const MAX_RAY_MARCH: f32 = f32::MAX;
|
||||
|
||||
// const MIN_RAY_LENGTH: f32 = 0_f32;
|
||||
const MAX_RAY_LENGTH: f32 = 1000_f32;
|
||||
|
||||
fn field_distance_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
#[derive(Deserialize)]
|
||||
struct FieldInfoArgs<'a> {
|
||||
reference_space_path: &'a str,
|
||||
point: Vector3<f32>,
|
||||
}
|
||||
let args: FieldInfoArgs = deserialize(message.as_ref())?;
|
||||
let reference_space = find_reference_space(&calling_client, args.reference_space_path)?;
|
||||
|
||||
let distance = node
|
||||
.field
|
||||
.get()
|
||||
.unwrap()
|
||||
.distance(reference_space.as_ref(), args.point.into());
|
||||
Ok(serialize(distance)?.into())
|
||||
});
|
||||
}
|
||||
fn field_normal_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
#[derive(Deserialize)]
|
||||
struct FieldInfoArgs<'a> {
|
||||
reference_space_path: &'a str,
|
||||
point: Vector3<f32>,
|
||||
}
|
||||
let args: FieldInfoArgs = deserialize(message.as_ref())?;
|
||||
let reference_space = find_reference_space(&calling_client, args.reference_space_path)?;
|
||||
|
||||
let normal = node.field.get().as_ref().unwrap().normal(
|
||||
reference_space.as_ref(),
|
||||
args.point.into(),
|
||||
0.001,
|
||||
);
|
||||
Ok(serialize(mint::Vector3::from(normal))?.into())
|
||||
});
|
||||
}
|
||||
fn field_closest_point_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
#[derive(Deserialize)]
|
||||
struct FieldInfoArgs<'a> {
|
||||
reference_space_path: &'a str,
|
||||
point: Vector3<f32>,
|
||||
}
|
||||
let args: FieldInfoArgs = deserialize(message.as_ref())?;
|
||||
let reference_space = find_reference_space(&calling_client, args.reference_space_path)?;
|
||||
|
||||
let closest_point = node.field.get().as_ref().unwrap().closest_point(
|
||||
reference_space.as_ref(),
|
||||
args.point.into(),
|
||||
0.001,
|
||||
);
|
||||
Ok(serialize(mint::Vector3::from(closest_point))?.into())
|
||||
});
|
||||
}
|
||||
fn field_ray_march_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
#[derive(Deserialize)]
|
||||
struct FieldInfoArgs<'a> {
|
||||
reference_space_path: &'a str,
|
||||
ray_origin: Vector3<f32>,
|
||||
ray_direction: Vector3<f32>,
|
||||
}
|
||||
let args: FieldInfoArgs = deserialize(message.as_ref())?;
|
||||
let reference_space = find_reference_space(&calling_client, args.reference_space_path)?;
|
||||
|
||||
let ray_march_result = node.field.get().unwrap().ray_march(Ray {
|
||||
origin: args.ray_origin.into(),
|
||||
direction: args.ray_direction.into(),
|
||||
space: reference_space,
|
||||
});
|
||||
Ok(serialize(ray_march_result)?.into())
|
||||
});
|
||||
}
|
||||
|
||||
pub enum Field {
|
||||
Box(BoxField),
|
||||
Cylinder(CylinderField),
|
||||
Sphere(SphereField),
|
||||
Torus(TorusField),
|
||||
}
|
||||
|
||||
impl Deref for Field {
|
||||
type Target = dyn FieldTrait;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Field::Box(field) => field,
|
||||
Field::Cylinder(field) => field,
|
||||
Field::Sphere(field) => field,
|
||||
Field::Torus(field) => field,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_interface(client: &Arc<Client>) -> Result<()> {
|
||||
let node = Node::create(client, "", "field", false);
|
||||
node.add_local_signal("create_box_field", create_box_field_flex);
|
||||
node.add_local_signal("create_cylinder_field", create_cylinder_field_flex);
|
||||
node.add_local_signal("create_sphere_field", create_sphere_field_flex);
|
||||
node.add_local_signal("create_torus_field", create_torus_field_flex);
|
||||
node.add_to_scenegraph().map(|_| ())
|
||||
}
|
||||
|
||||
pub fn find_field(client: &Client, path: &str) -> Result<Arc<Field>> {
|
||||
client
|
||||
.get_node("Field", path)?
|
||||
.get_aspect("Field", "info", |n| &n.field)
|
||||
.cloned()
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
use super::{Field, FieldTrait, Node};
|
||||
use crate::core::client::Client;
|
||||
use crate::nodes::spatial::{find_spatial_parent, Spatial};
|
||||
use crate::nodes::Message;
|
||||
use color_eyre::eyre::{ensure, Result};
|
||||
use glam::{Mat4, Vec3A};
|
||||
use mint::Vector3;
|
||||
use portable_atomic::AtomicF32;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flex::deserialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct SphereField {
|
||||
space: Arc<Spatial>,
|
||||
radius: AtomicF32,
|
||||
}
|
||||
|
||||
impl SphereField {
|
||||
pub fn add_to(node: &Arc<Node>, radius: f32) -> Result<()> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
ensure!(
|
||||
node.field.get().is_none(),
|
||||
"Internal: Node already has a field attached!"
|
||||
);
|
||||
let sphere_field = SphereField {
|
||||
space: node.spatial.get().unwrap().clone(),
|
||||
radius: AtomicF32::new(radius),
|
||||
};
|
||||
sphere_field.add_field_methods(node);
|
||||
node.add_local_signal("set_radius", SphereField::set_radius_flex);
|
||||
let _ = node.field.set(Arc::new(Field::Sphere(sphere_field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_radius(&self, radius: f32) {
|
||||
self.radius.store(radius, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn set_radius_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let Field::Sphere(sphere_field) = node.field.get().unwrap().as_ref() else { return Ok(()) };
|
||||
sphere_field.set_radius(deserialize(message.as_ref())?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FieldTrait for SphereField {
|
||||
fn local_distance(&self, p: Vec3A) -> f32 {
|
||||
p.length() - self.radius.load(Ordering::Relaxed)
|
||||
}
|
||||
fn local_normal(&self, p: Vec3A, _r: f32) -> Vec3A {
|
||||
-p.normalize()
|
||||
}
|
||||
fn local_closest_point(&self, p: Vec3A, _r: f32) -> Vec3A {
|
||||
p.normalize() * self.radius.load(Ordering::Relaxed)
|
||||
}
|
||||
fn spatial_ref(&self) -> &Spatial {
|
||||
self.space.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_sphere_field_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateFieldInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
origin: Option<Vector3<f32>>,
|
||||
radius: f32,
|
||||
}
|
||||
let info: CreateFieldInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/field", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = Mat4::from_translation(
|
||||
info.origin
|
||||
.unwrap_or_else(|| Vector3::from([0.0; 3]))
|
||||
.into(),
|
||||
);
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
SphereField::add_to(&node, info.radius)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
use super::{Field, FieldTrait, Node};
|
||||
use crate::core::client::Client;
|
||||
use crate::nodes::spatial::{find_spatial_parent, parse_transform, Spatial};
|
||||
use crate::nodes::Message;
|
||||
use color_eyre::eyre::{ensure, Result};
|
||||
use glam::{swizzles::*, vec2, Vec3A};
|
||||
use portable_atomic::AtomicF32;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flex::deserialize;
|
||||
use stardust_xr::values::Transform;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct TorusField {
|
||||
space: Arc<Spatial>,
|
||||
radius_a: AtomicF32,
|
||||
radius_b: AtomicF32,
|
||||
}
|
||||
|
||||
impl TorusField {
|
||||
pub fn add_to(node: &Arc<Node>, radius_a: f32, radius_b: f32) -> Result<()> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
ensure!(
|
||||
node.field.get().is_none(),
|
||||
"Internal: Node already has a field attached!"
|
||||
);
|
||||
let torus_field = TorusField {
|
||||
space: node.spatial.get().unwrap().clone(),
|
||||
radius_a: AtomicF32::new(radius_a.abs()),
|
||||
radius_b: AtomicF32::new(radius_b.abs()),
|
||||
};
|
||||
torus_field.add_field_methods(node);
|
||||
node.add_local_signal("set_size", TorusField::set_size_flex);
|
||||
let _ = node.field.set(Arc::new(Field::Torus(torus_field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_size(&self, radius_a: f32, radius_b: f32) {
|
||||
self.radius_a.store(radius_a.abs(), Ordering::Relaxed);
|
||||
self.radius_b.store(radius_b.abs(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn set_size_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let Field::Torus(torus_field) = node.field.get().unwrap().as_ref() else { return Ok(()) };
|
||||
let (radius_a, radius_b) = deserialize(message.as_ref())?;
|
||||
torus_field.set_size(radius_a, radius_b);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FieldTrait for TorusField {
|
||||
fn local_distance(&self, p: Vec3A) -> f32 {
|
||||
let radius_a = self.radius_a.load(Ordering::Relaxed);
|
||||
let radius_b = self.radius_b.load(Ordering::Relaxed);
|
||||
let q = vec2(p.xz().length() - radius_a, p.y);
|
||||
q.length() - radius_b
|
||||
}
|
||||
fn spatial_ref(&self) -> &Spatial {
|
||||
self.space.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_torus_field_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateFieldInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
radius_a: f32,
|
||||
radius_b: f32,
|
||||
}
|
||||
let info: CreateFieldInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/field", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
TorusField::add_to(&node, info.radius_a, info.radius_b)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use super::{alias::Alias, spatial::Spatial, Node};
|
||||
use crate::{
|
||||
core::client::{Client, INTERNAL_CLIENT},
|
||||
nodes::alias::AliasInfo,
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::{vec3, Mat4};
|
||||
use std::sync::Arc;
|
||||
use stereokit::StereoKitMultiThread;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HMD: Arc<Node> = create();
|
||||
}
|
||||
|
||||
fn create() -> Arc<Node> {
|
||||
let node = Arc::new(Node::create(&INTERNAL_CLIENT, "", "hmd", false));
|
||||
Spatial::add_to(&node, None, Mat4::IDENTITY, false).expect("Unable to make spatial for HMD");
|
||||
|
||||
node
|
||||
}
|
||||
|
||||
pub fn frame(sk: &impl StereoKitMultiThread) {
|
||||
let spatial = HMD
|
||||
.spatial
|
||||
.get()
|
||||
.expect("Unable to get spatial to update HMD");
|
||||
let hmd_pose = sk.input_head();
|
||||
*spatial.transform.lock() = Mat4::from_scale_rotation_translation(
|
||||
vec3(1.0, 1.0, 1.0),
|
||||
hmd_pose.orientation.into(),
|
||||
hmd_pose.position.into(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn make_alias(client: &Arc<Client>) -> Result<Arc<Node>> {
|
||||
Alias::create(
|
||||
client,
|
||||
"",
|
||||
"hmd",
|
||||
&HMD,
|
||||
AliasInfo {
|
||||
server_signals: vec!["get_bounds", "get_transform"],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,51 +1,89 @@
|
||||
use crate::nodes::fields::Field;
|
||||
use super::{Finger, Hand, InputDataTrait, InputHandler, InputMethod, Joint, Thumb};
|
||||
use crate::nodes::fields::{Field, FieldTrait};
|
||||
use crate::nodes::spatial::Spatial;
|
||||
use glam::{vec3a, Mat4};
|
||||
use stardust_xr::schemas::flat::{Hand as FlatHand, InputDataType, Joint};
|
||||
use glam::{Mat4, Quat, vec3a};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{DistanceLink, InputSpecialization};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Hand {
|
||||
pub base: FlatHand,
|
||||
}
|
||||
impl InputSpecialization for Hand {
|
||||
fn compare_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
|
||||
self.true_distance(space, field).abs()
|
||||
impl Default for Joint {
|
||||
fn default() -> Self {
|
||||
Joint {
|
||||
position: [0.0; 3].into(),
|
||||
rotation: Quat::IDENTITY.into(),
|
||||
radius: 0.0,
|
||||
distance: 0.0,
|
||||
}
|
||||
}
|
||||
fn true_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
|
||||
}
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Finger {
|
||||
fn default() -> Self {
|
||||
Finger {
|
||||
tip: Default::default(),
|
||||
distal: Default::default(),
|
||||
intermediate: Default::default(),
|
||||
proximal: Default::default(),
|
||||
metacarpal: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Thumb {
|
||||
fn default() -> Self {
|
||||
Thumb {
|
||||
tip: Default::default(),
|
||||
distal: Default::default(),
|
||||
proximal: Default::default(),
|
||||
metacarpal: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Hand {
|
||||
fn default() -> Self {
|
||||
Hand {
|
||||
right: Default::default(),
|
||||
thumb: Default::default(),
|
||||
index: Default::default(),
|
||||
middle: Default::default(),
|
||||
ring: Default::default(),
|
||||
little: Default::default(),
|
||||
palm: Default::default(),
|
||||
wrist: Default::default(),
|
||||
elbow: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputDataTrait for Hand {
|
||||
fn distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
|
||||
let mut min_distance = f32::MAX;
|
||||
|
||||
for tip in [
|
||||
&self.base.thumb.tip.position,
|
||||
&self.base.index.tip.position,
|
||||
&self.base.middle.tip.position,
|
||||
&self.base.ring.tip.position,
|
||||
&self.base.little.tip.position,
|
||||
&self.thumb.tip.position,
|
||||
&self.index.tip.position,
|
||||
&self.middle.tip.position,
|
||||
&self.ring.tip.position,
|
||||
&self.little.tip.position,
|
||||
] {
|
||||
min_distance = min_distance.min(field.distance(space, vec3a(tip.x, tip.y, tip.z)));
|
||||
}
|
||||
|
||||
min_distance
|
||||
}
|
||||
fn serialize(
|
||||
&self,
|
||||
distance_link: &DistanceLink,
|
||||
local_to_handler_matrix: Mat4,
|
||||
) -> InputDataType {
|
||||
let mut hand = self.base;
|
||||
fn transform(&mut self, method: &InputMethod, handler: &InputHandler) {
|
||||
let local_to_handler_matrix =
|
||||
Spatial::space_to_space_matrix(Some(&method.spatial), Some(&handler.spatial));
|
||||
let mut joints: Vec<&mut Joint> = Vec::new();
|
||||
|
||||
joints.extend([&mut hand.palm, &mut hand.wrist]);
|
||||
if let Some(elbow) = &mut hand.elbow {
|
||||
joints.extend([&mut self.palm, &mut self.wrist]);
|
||||
if let Some(elbow) = &mut self.elbow {
|
||||
joints.push(elbow);
|
||||
}
|
||||
for finger in [
|
||||
&mut hand.index,
|
||||
&mut hand.middle,
|
||||
&mut hand.ring,
|
||||
&mut hand.little,
|
||||
&mut self.index,
|
||||
&mut self.middle,
|
||||
&mut self.ring,
|
||||
&mut self.little,
|
||||
] {
|
||||
joints.extend([
|
||||
&mut finger.tip,
|
||||
@@ -56,10 +94,10 @@ impl InputSpecialization for Hand {
|
||||
]);
|
||||
}
|
||||
joints.extend([
|
||||
&mut hand.thumb.tip,
|
||||
&mut hand.thumb.distal,
|
||||
&mut hand.thumb.proximal,
|
||||
&mut hand.thumb.metacarpal,
|
||||
&mut self.thumb.tip,
|
||||
&mut self.thumb.distal,
|
||||
&mut self.thumb.proximal,
|
||||
&mut self.thumb.metacarpal,
|
||||
]);
|
||||
|
||||
for joint in joints {
|
||||
@@ -68,12 +106,7 @@ impl InputSpecialization for Hand {
|
||||
let (_, rotation, position) = joint_matrix.to_scale_rotation_translation();
|
||||
joint.position = position.into();
|
||||
joint.rotation = rotation.into();
|
||||
joint.distance = distance_link
|
||||
.handler
|
||||
.field
|
||||
.distance(&distance_link.handler.spatial, position.into());
|
||||
joint.distance = handler.field.distance(&handler.spatial, position.into());
|
||||
}
|
||||
|
||||
InputDataType::Hand(Box::new(hand))
|
||||
}
|
||||
}
|
||||
|
||||
39
src/nodes/input/handler.rs
Normal file
39
src/nodes/input/handler.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::{INPUT_HANDLER_REGISTRY, INPUT_METHOD_REGISTRY, InputHandlerAspect};
|
||||
use crate::nodes::{Node, alias::AliasList, fields::Field, spatial::Spatial};
|
||||
use color_eyre::eyre::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct InputHandler {
|
||||
pub spatial: Arc<Spatial>,
|
||||
pub field: Arc<Field>,
|
||||
pub(super) method_aliases: AliasList,
|
||||
}
|
||||
impl InputHandler {
|
||||
pub fn add_to(node: &Arc<Node>, field: &Arc<Field>) -> Result<()> {
|
||||
let handler = InputHandler {
|
||||
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
|
||||
field: field.clone(),
|
||||
method_aliases: AliasList::default(),
|
||||
};
|
||||
for method in INPUT_METHOD_REGISTRY.get_valid_contents() {
|
||||
method.handle_new_handler(&handler);
|
||||
}
|
||||
let handler = INPUT_HANDLER_REGISTRY.add(handler);
|
||||
node.add_aspect_raw(handler);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl InputHandlerAspect for InputHandler {}
|
||||
impl PartialEq for InputHandler {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.spatial == other.spatial
|
||||
}
|
||||
}
|
||||
impl Drop for InputHandler {
|
||||
fn drop(&mut self) {
|
||||
INPUT_HANDLER_REGISTRY.remove(self);
|
||||
for method in INPUT_METHOD_REGISTRY.get_valid_contents() {
|
||||
method.handle_drop_handler(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
280
src/nodes/input/method.rs
Normal file
280
src/nodes/input/method.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use super::{
|
||||
INPUT_HANDLER_REGISTRY, INPUT_METHOD_REF_ASPECT_ALIAS_INFO, INPUT_METHOD_REGISTRY, InputData,
|
||||
InputDataTrait, InputDataType, InputHandler, InputMethodAspect, InputMethodRefAspect,
|
||||
input_method_client,
|
||||
};
|
||||
use crate::{
|
||||
core::{
|
||||
client::Client,
|
||||
error::{Result, ServerError},
|
||||
registry::Registry,
|
||||
},
|
||||
nodes::{
|
||||
Node,
|
||||
alias::{Alias, AliasList},
|
||||
fields::{FIELD_ALIAS_INFO, Field},
|
||||
spatial::Spatial,
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::eyre;
|
||||
use parking_lot::Mutex;
|
||||
use stardust_xr::values::Datamap;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
pub struct InputMethod {
|
||||
pub spatial: Arc<Spatial>,
|
||||
pub data: Mutex<InputDataType>,
|
||||
pub datamap: Mutex<Datamap>,
|
||||
|
||||
handler_aliases: AliasList,
|
||||
handler_field_aliases: AliasList,
|
||||
pub(super) handler_order: Mutex<Vec<Weak<InputHandler>>>,
|
||||
pub capture_attempts: Registry<InputHandler>,
|
||||
pub captures: Registry<InputHandler>,
|
||||
}
|
||||
impl InputMethod {
|
||||
pub fn add_to(
|
||||
node: &Arc<Node>,
|
||||
data: InputDataType,
|
||||
datamap: Datamap,
|
||||
) -> Result<Arc<InputMethod>> {
|
||||
let method = InputMethod {
|
||||
spatial: node.get_aspect::<Spatial>().unwrap().clone(),
|
||||
data: Mutex::new(data),
|
||||
datamap: Mutex::new(datamap),
|
||||
|
||||
handler_aliases: AliasList::default(),
|
||||
handler_field_aliases: AliasList::default(),
|
||||
handler_order: Mutex::new(Vec::new()),
|
||||
capture_attempts: Registry::new(),
|
||||
captures: Registry::new(),
|
||||
};
|
||||
for handler in INPUT_HANDLER_REGISTRY.get_valid_contents() {
|
||||
method.handle_new_handler(&handler);
|
||||
}
|
||||
let method = INPUT_METHOD_REGISTRY.add(method);
|
||||
node.add_aspect_raw(method.clone());
|
||||
node.add_aspect(InputMethodRef);
|
||||
Ok(method)
|
||||
}
|
||||
|
||||
pub fn distance(&self, to: &Field) -> f32 {
|
||||
self.data.lock().distance(&self.spatial, to)
|
||||
}
|
||||
|
||||
pub fn set_handler_order<'a>(&self, handlers: impl Iterator<Item = &'a Arc<InputHandler>>) {
|
||||
*self.handler_order.lock() = handlers.map(Arc::downgrade).collect();
|
||||
}
|
||||
|
||||
pub(super) fn make_alias(&self, handler: &InputHandler) {
|
||||
let Some(method_node) = self.spatial.node() else {
|
||||
return;
|
||||
};
|
||||
let Some(handler_node) = handler.spatial.node() else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = handler_node.get_client() else {
|
||||
return;
|
||||
};
|
||||
let Ok(method_alias) = Alias::create(
|
||||
&method_node,
|
||||
&client,
|
||||
INPUT_METHOD_REF_ASPECT_ALIAS_INFO.clone(),
|
||||
Some(&handler.method_aliases),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
method_alias.set_enabled(false);
|
||||
}
|
||||
pub(super) fn handle_new_handler(&self, handler: &InputHandler) {
|
||||
self.make_alias(handler);
|
||||
|
||||
let Some(method_node) = self.spatial.node() else {
|
||||
return;
|
||||
};
|
||||
let Some(method_client) = method_node.get_client() else {
|
||||
return;
|
||||
};
|
||||
let Some(handler_node) = handler.spatial.node() else {
|
||||
return;
|
||||
};
|
||||
// Receiver itself
|
||||
let Ok(handler_alias) = Alias::create(
|
||||
&handler_node,
|
||||
&method_client,
|
||||
INPUT_METHOD_REF_ASPECT_ALIAS_INFO.clone(),
|
||||
Some(&self.handler_aliases),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(handler_field_node) = handler.field.spatial.node() else {
|
||||
return;
|
||||
};
|
||||
// Handler's field
|
||||
let Ok(rx_field_alias) = Alias::create(
|
||||
&handler_field_node,
|
||||
&method_client,
|
||||
FIELD_ALIAS_INFO.clone(),
|
||||
Some(&self.handler_field_aliases),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = input_method_client::create_handler(&method_node, &handler_alias, &rx_field_alias);
|
||||
}
|
||||
pub(super) fn handle_drop_handler(&self, handler: &InputHandler) {
|
||||
let Some(tx_node) = self.spatial.node() else {
|
||||
return;
|
||||
};
|
||||
let Some(handler_alias) = self.handler_aliases.get_from_aspect(handler) else {
|
||||
return;
|
||||
};
|
||||
let _ = input_method_client::destroy_handler(&tx_node, handler_alias.id);
|
||||
self.handler_aliases.remove_aspect(handler);
|
||||
self.handler_field_aliases
|
||||
.remove_aspect(handler.field.as_ref());
|
||||
self.capture_attempts.remove(handler);
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, handler))]
|
||||
pub(super) fn serialize(&self, alias_id: u64, handler: &Arc<InputHandler>) -> InputData {
|
||||
let mut input = self.data.lock().clone();
|
||||
input.transform(self, handler);
|
||||
|
||||
InputData {
|
||||
id: alias_id,
|
||||
input,
|
||||
distance: self.distance(&handler.field),
|
||||
datamap: self.datamap.lock().clone(),
|
||||
order: self
|
||||
.handler_order
|
||||
.lock()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, h)| h.ptr_eq(&Arc::downgrade(handler)))
|
||||
.unwrap()
|
||||
.0 as u32,
|
||||
captured: self.captures.get_valid_contents().contains(handler),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn cull_capture_attempts(&self) {
|
||||
let sent = self
|
||||
.handler_order
|
||||
.lock()
|
||||
.iter()
|
||||
.filter_map(Weak::upgrade)
|
||||
.collect::<Registry<InputHandler>>();
|
||||
self.capture_attempts.retain(|handler| {
|
||||
!handler
|
||||
.spatial
|
||||
.node()
|
||||
.and_then(|n| n.get_client())
|
||||
.map(|c| c.unresponsive())
|
||||
.unwrap_or(false)
|
||||
&& sent.contains(handler)
|
||||
});
|
||||
}
|
||||
}
|
||||
impl InputMethodAspect for InputMethod {
|
||||
#[doc = "Set the spatial input component of this input method. You must keep the same input data type throughout the entire thing."]
|
||||
fn set_input(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
input: InputDataType,
|
||||
) -> Result<()> {
|
||||
let input_method = node.get_aspect::<InputMethod>()?;
|
||||
*input_method.data.lock() = input;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc = "Set the datmap of this input method"]
|
||||
fn set_datamap(node: Arc<Node>, _calling_client: Arc<Client>, datamap: Datamap) -> Result<()> {
|
||||
let input_method = node.get_aspect::<InputMethod>()?;
|
||||
*input_method.datamap.lock() = datamap;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc = "Manually set the order of handlers to propagate input to, or else let the server decide."]
|
||||
fn set_handler_order(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
handlers: Vec<Arc<Node>>,
|
||||
) -> Result<()> {
|
||||
let input_method = node.get_aspect::<InputMethod>()?;
|
||||
let handlers = handlers
|
||||
.into_iter()
|
||||
.filter_map(|p| p.get_aspect::<InputHandler>().ok())
|
||||
.map(|i| Arc::downgrade(&i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
*input_method.handler_order.lock() = handlers;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc = "Set which handlers are captured."]
|
||||
fn set_captures(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
handlers: Vec<Arc<Node>>,
|
||||
) -> Result<()> {
|
||||
let input_method = node.get_aspect::<InputMethod>()?;
|
||||
input_method.captures.clear();
|
||||
for handler in handlers {
|
||||
let Ok(handler) = handler.get_aspect::<InputHandler>() else {
|
||||
continue;
|
||||
};
|
||||
input_method.captures.add_raw(&handler);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Drop for InputMethod {
|
||||
fn drop(&mut self) {
|
||||
INPUT_METHOD_REGISTRY.remove(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputMethodRef;
|
||||
impl InputMethodRefAspect for InputMethodRef {
|
||||
#[doc = "Try to capture the input method with the given handler. When the handler does not get input from the method, it will be released."]
|
||||
fn try_capture(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
handler: Arc<Node>,
|
||||
) -> Result<()> {
|
||||
let input_method = node.get_aspect::<InputMethod>()?;
|
||||
let input_handler = handler.get_aspect::<InputHandler>()?;
|
||||
|
||||
input_method.capture_attempts.add_raw(&input_handler);
|
||||
|
||||
let Some(handler_alias) = input_method
|
||||
.handler_aliases
|
||||
.get_from_aspect(&*input_handler)
|
||||
else {
|
||||
return Err(ServerError::Report(eyre!(
|
||||
"Internal: Couldn't get handler alias somehow?"
|
||||
)));
|
||||
};
|
||||
input_method_client::request_capture_handler(&node, handler_alias.get_id())
|
||||
}
|
||||
|
||||
#[doc = "If captured by this handler, release it (e.g. the object is let go of after grabbing)."]
|
||||
fn release(node: Arc<Node>, _calling_client: Arc<Client>, handler: Arc<Node>) -> Result<()> {
|
||||
let input_method = node.get_aspect::<InputMethod>()?;
|
||||
let input_handler = handler.get_aspect::<InputHandler>()?;
|
||||
|
||||
input_method.capture_attempts.remove(&input_handler);
|
||||
|
||||
let Some(handler_alias) = input_method
|
||||
.handler_aliases
|
||||
.get_from_aspect(&*input_handler)
|
||||
else {
|
||||
return Err(ServerError::Report(eyre!(
|
||||
"Internal: Couldn't get handler alias somehow?"
|
||||
)));
|
||||
};
|
||||
input_method_client::release_handler(&node, handler_alias.get_id())
|
||||
}
|
||||
}
|
||||
@@ -1,421 +1,182 @@
|
||||
pub mod hand;
|
||||
pub mod pointer;
|
||||
pub mod tip;
|
||||
#![allow(clippy::needless_question_mark)]
|
||||
|
||||
use self::hand::Hand;
|
||||
use self::pointer::Pointer;
|
||||
use self::tip::Tip;
|
||||
mod hand;
|
||||
mod handler;
|
||||
mod method;
|
||||
mod pointer;
|
||||
mod tip;
|
||||
|
||||
use super::{
|
||||
alias::{Alias, AliasInfo},
|
||||
fields::{find_field, Field, FIELD_ALIAS_INFO},
|
||||
spatial::{find_spatial_parent, parse_transform, Spatial},
|
||||
Message, Node,
|
||||
};
|
||||
use crate::core::registry::Registry;
|
||||
use crate::core::{client::Client, node_collections::LifeLinkedNodeMap};
|
||||
use color_eyre::eyre::{ensure, Result};
|
||||
use glam::Mat4;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use portable_atomic::AtomicBool;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::{flat::InputData, flex::deserialize};
|
||||
use stardust_xr::schemas::{
|
||||
flat::{Datamap, InputDataType},
|
||||
flex::serialize,
|
||||
};
|
||||
use stardust_xr::values::Transform;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{Arc, Weak};
|
||||
use tracing::{debug_span, instrument};
|
||||
use bevy::tasks::ComputeTaskPool;
|
||||
use bevy::tasks::ParallelSlice;
|
||||
pub use handler::*;
|
||||
pub use method::*;
|
||||
use tracing::debug_span;
|
||||
|
||||
use super::Aspect;
|
||||
use super::AspectIdentifier;
|
||||
use super::fields::Field;
|
||||
use super::spatial::Spatial;
|
||||
use crate::core::error::Result;
|
||||
use crate::nodes::spatial::SPATIAL_ASPECT_ALIAS_INFO;
|
||||
use crate::nodes::spatial::SPATIAL_REF_ASPECT_ALIAS_INFO;
|
||||
use crate::{core::client::Client, nodes::Node};
|
||||
use crate::{core::registry::Registry, nodes::spatial::Transform};
|
||||
use stardust_xr::values::Datamap;
|
||||
use std::sync::Arc;
|
||||
|
||||
static INPUT_METHOD_REGISTRY: Registry<InputMethod> = Registry::new();
|
||||
static INPUT_HANDLER_REGISTRY: Registry<InputHandler> = Registry::new();
|
||||
pub static INPUT_HANDLER_REGISTRY: Registry<InputHandler> = Registry::new();
|
||||
|
||||
pub trait InputSpecialization: Send + Sync {
|
||||
fn compare_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32;
|
||||
fn true_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32;
|
||||
fn serialize(
|
||||
&self,
|
||||
distance_link: &DistanceLink,
|
||||
local_to_handler_matrix: Mat4,
|
||||
) -> InputDataType;
|
||||
stardust_xr_server_codegen::codegen_input_protocol!();
|
||||
|
||||
impl AspectIdentifier for InputHandler {
|
||||
impl_aspect_for_input_handler_aspect_id! {}
|
||||
}
|
||||
pub enum InputType {
|
||||
Pointer(Pointer),
|
||||
Hand(Box<Hand>),
|
||||
Tip(Tip),
|
||||
impl Aspect for InputHandler {
|
||||
impl_aspect_for_input_handler_aspect! {}
|
||||
}
|
||||
impl Deref for InputType {
|
||||
type Target = dyn InputSpecialization;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
impl AspectIdentifier for InputMethod {
|
||||
impl_aspect_for_input_method_aspect_id! {}
|
||||
}
|
||||
impl Aspect for InputMethod {
|
||||
impl_aspect_for_input_method_aspect! {}
|
||||
}
|
||||
impl AspectIdentifier for InputMethodRef {
|
||||
impl_aspect_for_input_method_ref_aspect_id! {}
|
||||
}
|
||||
impl Aspect for InputMethodRef {
|
||||
impl_aspect_for_input_method_ref_aspect! {}
|
||||
}
|
||||
|
||||
pub trait InputDataTrait {
|
||||
fn transform(&mut self, method: &InputMethod, handler: &InputHandler);
|
||||
fn distance(&self, space: &Arc<Spatial>, field: &Field) -> f32;
|
||||
}
|
||||
impl InputDataTrait for InputDataType {
|
||||
fn transform(&mut self, method: &InputMethod, handler: &InputHandler) {
|
||||
match self {
|
||||
InputType::Pointer(p) => p,
|
||||
InputType::Hand(h) => h.as_ref(),
|
||||
InputType::Tip(t) => t,
|
||||
InputDataType::Pointer(i) => i.transform(method, handler),
|
||||
InputDataType::Hand(i) => i.transform(method, handler),
|
||||
InputDataType::Tip(i) => i.transform(method, handler),
|
||||
}
|
||||
}
|
||||
|
||||
fn distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
|
||||
match self {
|
||||
InputDataType::Pointer(i) => i.distance(space, field),
|
||||
InputDataType::Hand(i) => i.distance(space, field),
|
||||
InputDataType::Tip(i) => i.distance(space, field),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputMethod {
|
||||
node: Weak<Node>,
|
||||
uid: String,
|
||||
pub enabled: Mutex<bool>,
|
||||
pub spatial: Arc<Spatial>,
|
||||
pub specialization: Mutex<InputType>,
|
||||
captures: Registry<InputHandler>,
|
||||
pub datamap: Mutex<Option<Datamap>>,
|
||||
handler_aliases: LifeLinkedNodeMap<String>,
|
||||
handler_order: OnceCell<Mutex<Vec<Weak<InputHandler>>>>,
|
||||
}
|
||||
impl InputMethod {
|
||||
#[allow(dead_code)]
|
||||
pub fn add_to(
|
||||
node: &Arc<Node>,
|
||||
specialization: InputType,
|
||||
datamap: Option<Datamap>,
|
||||
) -> Result<Arc<InputMethod>> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
|
||||
node.add_local_signal("capture", InputMethod::capture_flex);
|
||||
node.add_local_signal("set_datamap", InputMethod::set_datamap_flex);
|
||||
node.add_local_signal("set_handlers", InputMethod::set_handlers_flex);
|
||||
|
||||
let method = InputMethod {
|
||||
node: Arc::downgrade(node),
|
||||
uid: node.uid.clone(),
|
||||
enabled: Mutex::new(true),
|
||||
spatial: node.spatial.get().unwrap().clone(),
|
||||
specialization: Mutex::new(specialization),
|
||||
captures: Registry::new(),
|
||||
datamap: Mutex::new(datamap),
|
||||
handler_aliases: LifeLinkedNodeMap::default(),
|
||||
handler_order: OnceCell::new(),
|
||||
};
|
||||
for handler in INPUT_HANDLER_REGISTRY.get_valid_contents() {
|
||||
method.handle_new_handler(&handler);
|
||||
method.make_alias(&handler);
|
||||
}
|
||||
let method = INPUT_METHOD_REGISTRY.add(method);
|
||||
let _ = node.input_method.set(method.clone());
|
||||
Ok(method)
|
||||
}
|
||||
fn get(node: &Node) -> Result<Arc<Self>> {
|
||||
node.get_aspect("Input Method", "input method", |n| &n.input_method)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn capture_flex(node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
let method = InputMethod::get(node)?;
|
||||
let handler = InputHandler::find(&calling_client, deserialize(message.as_ref())?)?;
|
||||
|
||||
method.captures.add_raw(&handler);
|
||||
node.send_remote_signal("capture", message)
|
||||
}
|
||||
fn set_datamap_flex(node: &Node, _calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
let method = InputMethod::get(node)?;
|
||||
method.datamap.lock().replace(Datamap::new(message.data)?);
|
||||
Ok(())
|
||||
}
|
||||
fn set_handlers_flex(node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
let method = InputMethod::get(node)?;
|
||||
let handler_paths: Vec<&str> = deserialize(message.as_ref())?;
|
||||
let handlers: Vec<Weak<InputHandler>> = handler_paths
|
||||
.into_iter()
|
||||
.filter_map(|p| InputHandler::find(&calling_client, p).ok())
|
||||
.map(|h| Arc::downgrade(&h))
|
||||
.collect();
|
||||
|
||||
*method
|
||||
.handler_order
|
||||
.get_or_init(|| Mutex::new(Vec::new()))
|
||||
.lock() = handlers;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_alias(&self, handler: &InputHandler) {
|
||||
let Some(method_node) = self.node.upgrade() else {return};
|
||||
let Some(handler_node) = handler.node.upgrade() else {return};
|
||||
let Some(client) = handler_node.get_client() else {return};
|
||||
let Ok(method_alias) = Alias::create(
|
||||
&client,
|
||||
handler_node.get_path(),
|
||||
&self.uid,
|
||||
&method_node,
|
||||
AliasInfo {
|
||||
server_signals: vec!["capture"],
|
||||
..Default::default()
|
||||
},
|
||||
) else {return};
|
||||
method_alias.enabled.store(false, Ordering::Relaxed);
|
||||
handler
|
||||
.method_aliases
|
||||
.add(self as *const InputMethod as usize, &method_alias);
|
||||
}
|
||||
|
||||
fn compare_distance(&self, to: &Field) -> f32 {
|
||||
self.specialization
|
||||
.lock()
|
||||
.compare_distance(&self.spatial, to)
|
||||
}
|
||||
fn true_distance(&self, to: &Field) -> f32 {
|
||||
self.specialization.lock().true_distance(&self.spatial, to)
|
||||
}
|
||||
|
||||
fn handle_new_handler(&self, handler: &InputHandler) {
|
||||
let Some(method_node) = self.node.upgrade() else {return};
|
||||
let Some(method_client) = method_node.get_client() else {return};
|
||||
let Some(handler_node) = handler.node.upgrade() else {return};
|
||||
// Receiver itself
|
||||
let Ok(handler_alias) = Alias::create(
|
||||
&method_client,
|
||||
method_node.get_path(),
|
||||
handler.uid.as_str(),
|
||||
&handler_node,
|
||||
AliasInfo {
|
||||
server_methods: vec!["getTransform"],
|
||||
..Default::default()
|
||||
},
|
||||
) else {return};
|
||||
self.handler_aliases
|
||||
.add(handler.uid.clone(), &handler_alias);
|
||||
|
||||
if let Some(handler_field_node) = handler.field.spatial_ref().node.upgrade() {
|
||||
// Handler's field
|
||||
let Ok(rx_field_alias) = Alias::create(
|
||||
&method_client,
|
||||
handler_alias.get_path(),
|
||||
"field",
|
||||
&handler_field_node,
|
||||
FIELD_ALIAS_INFO.clone(),
|
||||
) else {return};
|
||||
self.handler_aliases
|
||||
.add(handler.uid.clone() + "-field", &rx_field_alias);
|
||||
}
|
||||
|
||||
let Ok(data) = serialize(&handler.uid) else {return};
|
||||
let _ = method_node.send_remote_signal("handler_created", data);
|
||||
}
|
||||
fn handle_drop_handler(&self, handler: &InputHandler) {
|
||||
let uid = handler.uid.as_str();
|
||||
self.handler_aliases.remove(uid);
|
||||
self.handler_aliases.remove(&(uid.to_string() + "-field"));
|
||||
let Some(tx_node) = self.node.upgrade() else {return};
|
||||
let Ok(data) = serialize(&uid) else {return};
|
||||
let _ = tx_node.send_remote_signal("handler_destroyed", data);
|
||||
}
|
||||
}
|
||||
impl Drop for InputMethod {
|
||||
fn drop(&mut self) {
|
||||
INPUT_METHOD_REGISTRY.remove(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DistanceLink {
|
||||
distance: f32,
|
||||
method: Arc<InputMethod>,
|
||||
handler: Arc<InputHandler>,
|
||||
}
|
||||
impl DistanceLink {
|
||||
fn from(method: Arc<InputMethod>, handler: Arc<InputHandler>) -> Self {
|
||||
DistanceLink {
|
||||
distance: method.compare_distance(&handler.field),
|
||||
method,
|
||||
handler,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_input(&self, order: u32, datamap: Datamap) {
|
||||
self.handler.send_input(order, self, datamap);
|
||||
}
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
fn serialize(&self, order: u32, datamap: Datamap) -> Vec<u8> {
|
||||
let input = self.method.specialization.lock().serialize(
|
||||
self,
|
||||
Spatial::space_to_space_matrix(Some(&self.method.spatial), Some(&self.handler.spatial)),
|
||||
);
|
||||
|
||||
let root = InputData {
|
||||
uid: self.method.uid.clone(),
|
||||
input,
|
||||
distance: self.method.true_distance(&self.handler.field),
|
||||
datamap,
|
||||
order,
|
||||
};
|
||||
root.serialize()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputHandler {
|
||||
enabled: Arc<AtomicBool>,
|
||||
uid: String,
|
||||
node: Weak<Node>,
|
||||
spatial: Arc<Spatial>,
|
||||
field: Arc<Field>,
|
||||
method_aliases: LifeLinkedNodeMap<usize>,
|
||||
}
|
||||
impl InputHandler {
|
||||
pub fn add_to(node: &Arc<Node>, field: &Arc<Field>) -> Result<()> {
|
||||
ensure!(
|
||||
node.spatial.get().is_some(),
|
||||
"Internal: Node does not have a spatial attached!"
|
||||
);
|
||||
|
||||
let handler = InputHandler {
|
||||
enabled: node.enabled.clone(),
|
||||
uid: node.uid.clone(),
|
||||
node: Arc::downgrade(node),
|
||||
spatial: node.spatial.get().unwrap().clone(),
|
||||
field: field.clone(),
|
||||
method_aliases: LifeLinkedNodeMap::default(),
|
||||
};
|
||||
for method in INPUT_METHOD_REGISTRY.get_valid_contents() {
|
||||
method.make_alias(&handler);
|
||||
method.handle_new_handler(&handler);
|
||||
}
|
||||
let handler = INPUT_HANDLER_REGISTRY.add(handler);
|
||||
let _ = node.input_handler.set(handler);
|
||||
Ok(())
|
||||
}
|
||||
fn find(client: &Client, path: &str) -> Result<Arc<Self>> {
|
||||
InputHandler::get(&*client.get_node("Input Handler", path)?)
|
||||
}
|
||||
fn get(node: &Node) -> Result<Arc<Self>> {
|
||||
node.get_aspect("Input Handler", "input handler", |n| &n.input_handler)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self, distance_link))]
|
||||
fn send_input(&self, order: u32, distance_link: &DistanceLink, datamap: Datamap) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("input", distance_link.serialize(order, datamap));
|
||||
}
|
||||
}
|
||||
impl PartialEq for InputHandler {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.spatial == other.spatial
|
||||
}
|
||||
}
|
||||
impl Drop for InputHandler {
|
||||
fn drop(&mut self) {
|
||||
INPUT_HANDLER_REGISTRY.remove(self);
|
||||
for method in INPUT_METHOD_REGISTRY.get_valid_contents() {
|
||||
method.handle_drop_handler(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_interface(client: &Arc<Client>) -> Result<()> {
|
||||
let node = Node::create(client, "", "input", false);
|
||||
node.add_local_signal("create_input_handler", create_input_handler_flex);
|
||||
node.add_local_signal("create_input_method_pointer", pointer::create_pointer_flex);
|
||||
node.add_local_signal("create_input_method_tip", tip::create_tip_flex);
|
||||
node.add_to_scenegraph().map(|_| ())
|
||||
}
|
||||
|
||||
pub fn create_input_handler_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateInputHandlerInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
impl InterfaceAspect for Interface {
|
||||
#[doc = "Create an input method node"]
|
||||
fn create_input_method(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
field_path: &'a str,
|
||||
}
|
||||
let info: CreateInputHandlerInfo = deserialize(message.as_ref())?;
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, true);
|
||||
let field = find_field(&calling_client, info.field_path)?;
|
||||
initial_data: InputDataType,
|
||||
datamap: Datamap,
|
||||
) -> Result<()> {
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
let transform = transform.to_mat4(true, true, true);
|
||||
|
||||
let node =
|
||||
Node::create(&calling_client, "/input/handler", info.name, true).add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
InputHandler::add_to(&node, &field)?;
|
||||
Ok(())
|
||||
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent.clone()), transform, false);
|
||||
InputMethod::add_to(&node, initial_data, datamap)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc = "Create an input handler node"]
|
||||
fn create_input_handler(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
field: Arc<Node>,
|
||||
) -> Result<()> {
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
let transform = transform.to_mat4(true, true, true);
|
||||
let field = field.get_aspect::<Field>()?;
|
||||
|
||||
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent.clone()), transform, false);
|
||||
InputHandler::add_to(&node, &field)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug")]
|
||||
pub fn process_input() {
|
||||
// Iterate over all valid input methods
|
||||
let methods = debug_span!("Get valid methods").in_scope(|| {
|
||||
INPUT_METHOD_REGISTRY
|
||||
.get_valid_contents()
|
||||
.into_iter()
|
||||
.filter(|method| *method.enabled.lock())
|
||||
.filter(|method| method.datamap.lock().is_some())
|
||||
});
|
||||
let handlers = INPUT_HANDLER_REGISTRY.get_valid_contents();
|
||||
const LIMIT: usize = 50;
|
||||
for method in methods {
|
||||
for alias in method.node.upgrade().unwrap().aliases.get_valid_contents() {
|
||||
alias.enabled.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
debug_span!("Process input method").in_scope(|| {
|
||||
// Get all valid input handlers and convert them to DistanceLink objects
|
||||
let distance_links: Vec<DistanceLink> = debug_span!("Generate distance links")
|
||||
.in_scope(|| {
|
||||
if let Some(handler_order) = method.handler_order.get() {
|
||||
let handler_order = handler_order.lock();
|
||||
handler_order
|
||||
.iter()
|
||||
.filter_map(|h| h.upgrade())
|
||||
.filter(|handler| handler.enabled.load(Ordering::Relaxed))
|
||||
.map(|handler| DistanceLink::from(method.clone(), handler))
|
||||
.collect()
|
||||
} else {
|
||||
let mut distance_links: Vec<_> = handlers
|
||||
.iter()
|
||||
.filter(|handler| handler.enabled.load(Ordering::Relaxed))
|
||||
.map(|handler| {
|
||||
debug_span!("Create distance link").in_scope(|| {
|
||||
DistanceLink::from(method.clone(), handler.clone())
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort the distance links by their distance in ascending order
|
||||
debug_span!("Sort distance links").in_scope(|| {
|
||||
distance_links.sort_unstable_by(|a, b| {
|
||||
a.distance.abs().partial_cmp(&b.distance.abs()).unwrap()
|
||||
});
|
||||
});
|
||||
|
||||
distance_links.truncate(LIMIT);
|
||||
distance_links
|
||||
}
|
||||
});
|
||||
|
||||
let captures = method.captures.take_valid_contents();
|
||||
// Iterate over the distance links and send input to them
|
||||
for (i, distance_link) in distance_links.into_iter().enumerate() {
|
||||
if i > LIMIT {
|
||||
break;
|
||||
let methods = INPUT_METHOD_REGISTRY
|
||||
.get_valid_contents()
|
||||
.into_iter()
|
||||
.filter(|method| {
|
||||
let Some(node) = method.spatial.node() else {
|
||||
return false;
|
||||
};
|
||||
node.enabled()
|
||||
});
|
||||
INPUT_HANDLER_REGISTRY
|
||||
.get_valid_contents()
|
||||
.into_iter()
|
||||
.par_splat_map(ComputeTaskPool::get(), None, |_, handlers| {
|
||||
for handler in handlers {
|
||||
let _span = debug_span!("handle input handler").entered();
|
||||
for method_alias in handler.method_aliases.get_aliases() {
|
||||
method_alias.set_enabled(false);
|
||||
}
|
||||
|
||||
if let Some(method_alias) = distance_link
|
||||
.handler
|
||||
.method_aliases
|
||||
.get(&(Arc::as_ptr(&distance_link.method) as usize))
|
||||
.and_then(|a| a.alias.get().cloned())
|
||||
let Some(handler_node) = handler.spatial.node() else {
|
||||
continue;
|
||||
};
|
||||
if !handler_node.enabled() {
|
||||
continue;
|
||||
}
|
||||
if let Some(handler_field_node) = handler.field.spatial.node()
|
||||
&& !handler_field_node.enabled()
|
||||
{
|
||||
method_alias.enabled.store(true, Ordering::Relaxed);
|
||||
}
|
||||
distance_link.send_input(i as u32, method.datamap.lock().clone().unwrap());
|
||||
continue;
|
||||
};
|
||||
|
||||
// If the current distance link is in the list of captured input handlers,
|
||||
// break out of the loop to avoid sending input to the remaining distance links
|
||||
if captures.contains(&distance_link.handler) {
|
||||
break;
|
||||
}
|
||||
let ser_span = debug_span!("serializing input").entered();
|
||||
let (methods, datas) = methods
|
||||
.clone()
|
||||
// filter out methods without the handler in their handler order
|
||||
.filter(|a| {
|
||||
a.handler_order
|
||||
.lock()
|
||||
.iter()
|
||||
.any(|h| h.ptr_eq(&Arc::downgrade(handler)))
|
||||
})
|
||||
// filter out methods without the proper alias
|
||||
.filter_map(|m| {
|
||||
Some((
|
||||
handler
|
||||
.method_aliases
|
||||
.get_from_original_node(m.spatial.node.clone())?,
|
||||
m,
|
||||
))
|
||||
})
|
||||
// make sure the input method alias is enabled
|
||||
.inspect(|(a, _)| {
|
||||
a.set_enabled(true);
|
||||
})
|
||||
// serialize the data
|
||||
.map(|(a, m)| (a.clone(), m.serialize(a.get_id(), handler)))
|
||||
.unzip::<_, _, Vec<_>, Vec<_>>();
|
||||
drop(ser_span);
|
||||
|
||||
let _span = debug_span!("client input").entered();
|
||||
let _ = input_handler_client::input(&handler_node, &methods, &datas);
|
||||
}
|
||||
});
|
||||
for method in methods {
|
||||
method.cull_capture_attempts();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +1,52 @@
|
||||
use super::{DistanceLink, InputSpecialization};
|
||||
use crate::core::client::Client;
|
||||
use crate::nodes::fields::{Field, Ray, RayMarchResult};
|
||||
use crate::nodes::input::{InputMethod, InputType};
|
||||
use crate::nodes::spatial::{find_spatial_parent, parse_transform, Spatial};
|
||||
use crate::nodes::{Message, Node};
|
||||
use glam::{vec3, Mat4};
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flat::{Datamap, InputDataType, Pointer as FlatPointer};
|
||||
use stardust_xr::schemas::flex::deserialize;
|
||||
use stardust_xr::values::Transform;
|
||||
use std::sync::Arc;
|
||||
use super::{InputDataTrait, InputHandler, InputMethod, Pointer};
|
||||
use crate::nodes::{
|
||||
fields::{Field, FieldTrait, Ray, RayMarchResult},
|
||||
spatial::Spatial,
|
||||
};
|
||||
use glam::{Mat4, Quat, vec3};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Pointer;
|
||||
// impl Default for Pointer {
|
||||
// fn default() -> Self {
|
||||
// Pointer {
|
||||
// grab: Default::default(),
|
||||
// select: Default::default(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
impl Default for Pointer {
|
||||
fn default() -> Self {
|
||||
Pointer {
|
||||
origin: [0.0; 3].into(),
|
||||
orientation: Quat::IDENTITY.into(),
|
||||
deepest_point: [0.0; 3].into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Pointer {
|
||||
fn ray_march(&self, space: &Arc<Spatial>, field: &Field) -> RayMarchResult {
|
||||
fn ray_march(&self, method_space: &Arc<Spatial>, field: &Field) -> RayMarchResult {
|
||||
field.ray_march(Ray {
|
||||
origin: vec3(0.0, 0.0, 0.0),
|
||||
direction: vec3(0.0, 0.0, -1.0),
|
||||
space: space.clone(),
|
||||
space: Spatial::new(
|
||||
Weak::new(),
|
||||
Some(method_space.clone()),
|
||||
Mat4::from_rotation_translation(self.orientation.into(), self.origin.into()),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl InputSpecialization for Pointer {
|
||||
fn compare_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
|
||||
let ray_info = self.ray_march(space, field);
|
||||
if ray_info.min_distance > 0.0 {
|
||||
ray_info.deepest_point_distance + 1000.0
|
||||
} else {
|
||||
ray_info
|
||||
.deepest_point_distance
|
||||
.hypot(0.001 / ray_info.min_distance)
|
||||
}
|
||||
}
|
||||
fn true_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
|
||||
impl InputDataTrait for Pointer {
|
||||
fn distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
|
||||
let ray_info = self.ray_march(space, field);
|
||||
ray_info.min_distance
|
||||
}
|
||||
fn serialize(
|
||||
&self,
|
||||
distance_link: &DistanceLink,
|
||||
local_to_handler_matrix: Mat4,
|
||||
) -> InputDataType {
|
||||
fn transform(&mut self, method: &InputMethod, handler: &InputHandler) {
|
||||
let local_to_handler_matrix =
|
||||
Mat4::from_rotation_translation(self.orientation.into(), self.origin.into())
|
||||
* Spatial::space_to_space_matrix(Some(&method.spatial), Some(&handler.spatial));
|
||||
let (_, orientation, origin) = local_to_handler_matrix.to_scale_rotation_translation();
|
||||
let direction = local_to_handler_matrix.transform_vector3(vec3(0.0, 0.0, -1.0));
|
||||
let ray_march = self.ray_march(&distance_link.method.spatial, &distance_link.handler.field);
|
||||
|
||||
let ray_march = self.ray_march(&method.spatial, &handler.field);
|
||||
let direction = local_to_handler_matrix
|
||||
.transform_vector3(vec3(0.0, 0.0, -1.0))
|
||||
.normalize();
|
||||
let deepest_point = (direction * ray_march.deepest_point_distance) + origin;
|
||||
|
||||
InputDataType::Pointer(FlatPointer {
|
||||
origin: origin.into(),
|
||||
orientation: orientation.into(),
|
||||
deepest_point: deepest_point.into(),
|
||||
})
|
||||
self.origin = origin.into();
|
||||
self.orientation = orientation.into();
|
||||
self.deepest_point = deepest_point.into();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_pointer_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> color_eyre::eyre::Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreatePointerInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
datamap: Option<Vec<u8>>,
|
||||
}
|
||||
let info: CreatePointerInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/input/method/pointer", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
InputMethod::add_to(
|
||||
&node,
|
||||
InputType::Pointer(Pointer),
|
||||
info.datamap.and_then(|datamap| Datamap::new(datamap).ok()),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,73 +1,29 @@
|
||||
use super::{DistanceLink, InputSpecialization};
|
||||
use crate::core::client::Client;
|
||||
use crate::nodes::fields::Field;
|
||||
use crate::nodes::input::{InputMethod, InputType};
|
||||
use crate::nodes::spatial::{find_spatial_parent, parse_transform, Spatial};
|
||||
use crate::nodes::{Message, Node};
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::{vec3a, Mat4};
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flat::{Datamap, InputDataType, Tip as FlatTip};
|
||||
use stardust_xr::schemas::flex::deserialize;
|
||||
use stardust_xr::values::Transform;
|
||||
use super::{InputDataTrait, InputHandler, InputMethod, Tip};
|
||||
use crate::nodes::{
|
||||
fields::{Field, FieldTrait},
|
||||
spatial::Spatial,
|
||||
};
|
||||
use glam::{Mat4, Quat};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Tip {
|
||||
pub radius: f32,
|
||||
}
|
||||
impl Tip {
|
||||
fn set_radius(node: &Node, _calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
if let InputType::Tip(tip) = &mut *node.input_method.get().unwrap().specialization.lock() {
|
||||
tip.radius = deserialize(message.as_ref())?;
|
||||
impl Default for Tip {
|
||||
fn default() -> Self {
|
||||
Tip {
|
||||
origin: [0.0; 3].into(),
|
||||
orientation: Quat::IDENTITY.into(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl InputSpecialization for Tip {
|
||||
fn compare_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
|
||||
field.distance(space, vec3a(0.0, 0.0, 0.0)).abs()
|
||||
impl InputDataTrait for Tip {
|
||||
fn distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
|
||||
field.distance(space, self.origin.into())
|
||||
}
|
||||
fn true_distance(&self, space: &Arc<Spatial>, field: &Field) -> f32 {
|
||||
field.distance(space, vec3a(0.0, 0.0, 0.0))
|
||||
}
|
||||
fn serialize(
|
||||
&self,
|
||||
_distance_link: &DistanceLink,
|
||||
local_to_handler_matrix: Mat4,
|
||||
) -> InputDataType {
|
||||
fn transform(&mut self, method: &InputMethod, handler: &InputHandler) {
|
||||
let local_to_handler_matrix =
|
||||
Spatial::space_to_space_matrix(Some(&method.spatial), Some(&handler.spatial))
|
||||
* Mat4::from_rotation_translation(self.orientation.into(), self.origin.into());
|
||||
let (_, orientation, origin) = local_to_handler_matrix.to_scale_rotation_translation();
|
||||
InputDataType::Tip(FlatTip {
|
||||
origin: origin.into(),
|
||||
orientation: orientation.into(),
|
||||
radius: self.radius,
|
||||
})
|
||||
self.origin = origin.into();
|
||||
self.orientation = orientation.into();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_tip_flex(_node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateTipInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
radius: f32,
|
||||
datamap: Option<Vec<u8>>,
|
||||
}
|
||||
let info: CreateTipInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/input/method/tip", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
InputMethod::add_to(
|
||||
&node,
|
||||
InputType::Tip(Tip {
|
||||
radius: info.radius,
|
||||
}),
|
||||
info.datamap.and_then(|datamap| Datamap::new(datamap).ok()),
|
||||
)?;
|
||||
node.add_local_signal("set_radius", Tip::set_radius);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,380 +1,187 @@
|
||||
use super::{Item, ItemType};
|
||||
#![allow(dead_code)]
|
||||
use super::{Item, ItemType, create_item_acceptor_flex, register_item_ui_flex};
|
||||
use crate::bail;
|
||||
use crate::core::error::Result;
|
||||
use crate::nodes::Aspect;
|
||||
use crate::nodes::AspectIdentifier;
|
||||
use crate::nodes::items::ITEM_ACCEPTOR_ASPECT_ALIAS_INFO;
|
||||
use crate::nodes::items::ITEM_ASPECT_ALIAS_INFO;
|
||||
use crate::{
|
||||
core::{
|
||||
buffers::BufferManager,
|
||||
client::{Client, INTERNAL_CLIENT},
|
||||
registry::Registry,
|
||||
scenegraph::MethodResponseSender,
|
||||
},
|
||||
core::{client::Client, registry::Registry, scenegraph::MethodResponseSender},
|
||||
nodes::{
|
||||
drawable::{model::ModelPart, shaders::UNLIT_SHADER_BYTES, Drawable},
|
||||
items::TypeInfo,
|
||||
spatial::{find_spatial_parent, parse_transform, Spatial},
|
||||
Message, Node,
|
||||
drawable::model::ModelPart,
|
||||
items::TypeInfo,
|
||||
spatial::{Spatial, Transform},
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::{bail, Result};
|
||||
use glam::Mat4;
|
||||
use lazy_static::lazy_static;
|
||||
use mint::{RowMatrix4, Vector2};
|
||||
use nanoid::nanoid;
|
||||
use once_cell::sync::OnceCell;
|
||||
use mint::{ColumnMatrix4, Vector2};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use smithay::backend::{
|
||||
allocator::{
|
||||
dmabuf::{Dmabuf, DmabufFlags},
|
||||
Buffer,
|
||||
},
|
||||
renderer::{gles, ImportDma},
|
||||
};
|
||||
use stardust_xr::{
|
||||
scenegraph::ScenegraphError,
|
||||
schemas::flex::{deserialize, serialize},
|
||||
values::{BufferColorspace, BufferInfo, Transform},
|
||||
};
|
||||
use std::{ffi::c_void, sync::Arc};
|
||||
use stereokit::{
|
||||
Color128, Material, Rect, RenderLayer, StereoKitDraw, Tex, TextureFormat, TextureType,
|
||||
Transparency,
|
||||
};
|
||||
use tokio::sync::mpsc::{self, error::SendError};
|
||||
|
||||
use stardust_xr::schemas::flex::{deserialize, serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
stardust_xr_server_codegen::codegen_item_camera_protocol!();
|
||||
lazy_static! {
|
||||
pub(super) static ref ITEM_TYPE_INFO_CAMERA: TypeInfo = TypeInfo {
|
||||
type_name: "camera",
|
||||
aliased_local_signals: vec!["apply_preview_material"],
|
||||
aliased_local_methods: vec!["render"],
|
||||
aliased_remote_signals: vec![],
|
||||
alias_info: CAMERA_ITEM_ASPECT_ALIAS_INFO.clone(),
|
||||
ui_node_id: INTERFACE_NODE_ID,
|
||||
ui: Default::default(),
|
||||
items: Registry::new(),
|
||||
acceptors: Registry::new(),
|
||||
add_acceptor_aspect: |node| {
|
||||
node.add_aspect(CameraItemAcceptor);
|
||||
},
|
||||
add_ui_aspect: |node| {
|
||||
node.add_aspect(CameraItemUi);
|
||||
},
|
||||
new_acceptor_fn: |node, acceptor, acceptor_field| {
|
||||
let _ = camera_item_ui_client::create_acceptor(node, acceptor, acceptor_field);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct FrameInfo {
|
||||
proj_matrix: Mat4,
|
||||
preview_size: Vector2<u32>,
|
||||
px_size: Vector2<u32>,
|
||||
}
|
||||
|
||||
pub struct CameraItem {
|
||||
space: Arc<Spatial>,
|
||||
frame_info: Mutex<FrameInfo>,
|
||||
sk_tex: OnceCell<Tex>,
|
||||
sk_mat: OnceCell<Arc<Material>>,
|
||||
render_requests_tx: mpsc::UnboundedSender<(Dmabuf, BufferColorspace, MethodResponseSender)>,
|
||||
render_requests_rx:
|
||||
Mutex<mpsc::UnboundedReceiver<(Dmabuf, BufferColorspace, MethodResponseSender)>>,
|
||||
rendered_notifiers: Mutex<Vec<MethodResponseSender>>,
|
||||
applied_preview_to: Registry<ModelPart>,
|
||||
apply_preview_to: Registry<ModelPart>,
|
||||
applied_to: Registry<ModelPart>,
|
||||
apply_to: Registry<ModelPart>,
|
||||
}
|
||||
#[allow(unused)]
|
||||
impl CameraItem {
|
||||
pub fn add_to(node: &Arc<Node>, proj_matrix: Mat4, preview_size: Vector2<u32>) {
|
||||
let (render_requests_tx, render_requests_rx) = mpsc::unbounded_channel();
|
||||
let camera_specialization = CameraItem {
|
||||
space: node.spatial.get().unwrap().clone(),
|
||||
pub fn add_to(node: &Arc<Node>, proj_matrix: Mat4, px_size: Vector2<u32>) {
|
||||
let item = Arc::new(CameraItem {
|
||||
space: node.get_aspect::<Spatial>().unwrap().clone(),
|
||||
frame_info: Mutex::new(FrameInfo {
|
||||
proj_matrix,
|
||||
preview_size,
|
||||
px_size,
|
||||
}),
|
||||
sk_tex: OnceCell::new(),
|
||||
sk_mat: OnceCell::new(),
|
||||
render_requests_tx,
|
||||
render_requests_rx: Mutex::new(render_requests_rx),
|
||||
rendered_notifiers: Mutex::new(Vec::new()),
|
||||
applied_preview_to: Registry::new(),
|
||||
apply_preview_to: Registry::new(),
|
||||
};
|
||||
Item::add_to(
|
||||
node,
|
||||
nanoid!(),
|
||||
&ITEM_TYPE_INFO_CAMERA,
|
||||
ItemType::Camera(camera_specialization),
|
||||
);
|
||||
node.add_local_method("render", CameraItem::render_flex);
|
||||
node.add_local_signal(
|
||||
"apply_preview_material",
|
||||
CameraItem::apply_preview_material_flex,
|
||||
);
|
||||
applied_to: Registry::new(),
|
||||
apply_to: Registry::new(),
|
||||
});
|
||||
Item::add_to(node, &ITEM_TYPE_INFO_CAMERA, ItemType::Camera(item.clone()));
|
||||
node.add_aspect_raw(item);
|
||||
}
|
||||
|
||||
fn render_flex(
|
||||
node: &Node,
|
||||
fn frame_flex(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
_message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
let Some(item) = node.item.get() else {
|
||||
let _ = response.send(Err(ScenegraphError::MethodError {
|
||||
error: "Item not found".to_string(),
|
||||
}));
|
||||
return;
|
||||
};
|
||||
let ItemType::Camera(camera) = &item.specialization else {
|
||||
let _ = response.send(Err(ScenegraphError::MethodError {
|
||||
error: "Wrong item type?".to_string(),
|
||||
}));
|
||||
return;
|
||||
};
|
||||
|
||||
let buffer_info: BufferInfo = match deserialize(&message.data) {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
let _ = response.send(Err(ScenegraphError::MethodError {
|
||||
error: e.to_string(),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut builder = Dmabuf::builder(
|
||||
(buffer_info.width as i32, buffer_info.height as i32),
|
||||
buffer_info.fourcc.try_into().unwrap(),
|
||||
DmabufFlags::from_bits_truncate(buffer_info.flags),
|
||||
);
|
||||
for (fd, plane) in message.fds.into_iter().zip(buffer_info.planes) {
|
||||
builder.add_plane(
|
||||
fd,
|
||||
plane.idx,
|
||||
plane.offset,
|
||||
plane.stride,
|
||||
plane.modifier.try_into().unwrap(),
|
||||
);
|
||||
}
|
||||
let buffer_to_render = builder.build().unwrap();
|
||||
|
||||
if let Err(SendError((_dmabuf, _colorspace, sender))) =
|
||||
camera
|
||||
.render_requests_tx
|
||||
.send((buffer_to_render, buffer_info.colorspace, response))
|
||||
{
|
||||
sender.send(Err(ScenegraphError::MethodError {
|
||||
error: "Internal: sender broke????".to_string(),
|
||||
}));
|
||||
}
|
||||
response.wrap(move || {
|
||||
let ItemType::Camera(_camera) = &node.get_aspect::<Item>().unwrap().specialization
|
||||
else {
|
||||
bail!("Wrong item type?");
|
||||
};
|
||||
Ok(serialize(())?)
|
||||
});
|
||||
}
|
||||
|
||||
fn apply_preview_material_flex(
|
||||
node: &Node,
|
||||
node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let Some(item) = node.item.get() else {
|
||||
bail!("Item not found?")
|
||||
let ItemType::Camera(camera) = &node.get_aspect::<Item>().unwrap().specialization else {
|
||||
bail!("Wrong item type?");
|
||||
};
|
||||
let ItemType::Camera(camera) = &item.specialization else {
|
||||
bail!("Wrong item type?")
|
||||
};
|
||||
|
||||
let model_part_path = deserialize(&message.data)?;
|
||||
let model_part_node = calling_client.get_node("Model part", model_part_path)?;
|
||||
let Drawable::ModelPart(model_part) =
|
||||
model_part_node.get_aspect("Model part", "model part", |n| &n.drawable)?
|
||||
else {
|
||||
bail!("Drawable is not a model node")
|
||||
};
|
||||
|
||||
camera.applied_preview_to.add_raw(model_part);
|
||||
camera.apply_preview_to.add_raw(model_part);
|
||||
|
||||
let model_part_node =
|
||||
calling_client.get_node("Model part", deserialize(&message.data).unwrap())?;
|
||||
let model_part = model_part_node.get_aspect::<ModelPart>()?;
|
||||
camera.applied_to.add_raw(&model_part);
|
||||
camera.apply_to.add_raw(&model_part);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize_start_data(&self, id: &str) -> Result<Message> {
|
||||
Ok(serialize(id)?.into())
|
||||
pub fn send_ui_item_created(&self, node: &Node, item: &Arc<Node>) {
|
||||
let _ = camera_item_ui_client::create_item(node, item);
|
||||
}
|
||||
|
||||
pub fn update(&self, sk: &impl StereoKitDraw, buffer_manager: &mut BufferManager) {
|
||||
let frame_info = self.frame_info.lock();
|
||||
self.render_preview(sk, &*frame_info);
|
||||
self.render_dmabuf(sk, buffer_manager, &*frame_info);
|
||||
pub fn send_acceptor_item_created(&self, node: &Node, item: &Arc<Node>) {
|
||||
let _ = camera_item_acceptor_client::capture_item(node, item);
|
||||
}
|
||||
}
|
||||
impl AspectIdentifier for CameraItem {
|
||||
impl_aspect_for_camera_item_aspect_id! {}
|
||||
}
|
||||
impl Aspect for CameraItem {
|
||||
impl_aspect_for_camera_item_aspect! {}
|
||||
}
|
||||
impl CameraItemAspect for CameraItem {}
|
||||
|
||||
fn render_preview(&self, sk: &impl StereoKitDraw, frame_info: &FrameInfo) {
|
||||
if !self.apply_preview_to.is_empty() {
|
||||
let sk_tex = self.sk_tex.get_or_init(|| {
|
||||
sk.tex_gen_color(
|
||||
Color128::default(),
|
||||
frame_info.preview_size.x as i32,
|
||||
frame_info.preview_size.y as i32,
|
||||
TextureType::RENDER_TARGET,
|
||||
TextureFormat::RGBA32Linear,
|
||||
)
|
||||
});
|
||||
let sk_mat = self.sk_mat.get_or_init(|| {
|
||||
let shader = sk.shader_create_mem(&UNLIT_SHADER_BYTES).unwrap();
|
||||
let mat = sk.material_create(&shader);
|
||||
sk.material_set_texture(&mat, "diffuse", sk_tex.as_ref());
|
||||
sk.material_set_transparency(&mat, Transparency::Blend);
|
||||
Arc::new(mat)
|
||||
});
|
||||
for model_part in self.apply_preview_to.take_valid_contents() {
|
||||
model_part.replace_material(sk_mat.clone())
|
||||
}
|
||||
}
|
||||
pub struct CameraItemUi;
|
||||
impl AspectIdentifier for CameraItemUi {
|
||||
impl_aspect_for_camera_item_ui_aspect_id! {}
|
||||
}
|
||||
impl Aspect for CameraItemUi {
|
||||
impl_aspect_for_camera_item_ui_aspect! {}
|
||||
}
|
||||
impl CameraItemUiAspect for CameraItemUi {}
|
||||
|
||||
if !self.applied_preview_to.is_empty() {
|
||||
sk.render_to(
|
||||
self.sk_tex.get().unwrap(),
|
||||
self.space.global_transform(),
|
||||
frame_info.proj_matrix,
|
||||
RenderLayer::all(),
|
||||
stereokit::RenderClear::All,
|
||||
Rect {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: 0.0,
|
||||
h: 0.0,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_dmabuf(
|
||||
&self,
|
||||
sk: &impl StereoKitDraw,
|
||||
buffer_manager: &mut BufferManager,
|
||||
frame_info: &FrameInfo,
|
||||
) {
|
||||
let mut render_notifiers = self.rendered_notifiers.lock();
|
||||
let mut render_requests_rx = self.render_requests_rx.lock();
|
||||
while let Ok((buffer_to_render, colorspace, render_response_sender)) =
|
||||
render_requests_rx.try_recv()
|
||||
{
|
||||
let imported_dmabuf = buffer_manager
|
||||
.renderer
|
||||
.import_dmabuf(&buffer_to_render, None);
|
||||
let smithay_tex = match imported_dmabuf {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
let _ = render_response_sender.send(Err(ScenegraphError::MethodError {
|
||||
error: e.to_string(),
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let texture_format;
|
||||
let native_format;
|
||||
|
||||
match colorspace {
|
||||
BufferColorspace::SRGB => match buffer_to_render.format().code {
|
||||
drm_fourcc::DrmFourcc::Argb8888 => {
|
||||
texture_format = TextureFormat::RGBA32;
|
||||
native_format = gles::ffi::SRGB8_ALPHA8;
|
||||
}
|
||||
drm_fourcc::DrmFourcc::Abgr8888 => {
|
||||
texture_format = TextureFormat::BGRA32;
|
||||
native_format = gles::ffi::SRGB8_ALPHA8;
|
||||
}
|
||||
_ => {
|
||||
let _ = render_response_sender.send(Err(ScenegraphError::MethodError {
|
||||
error: "Unsupported pixel format".to_string(),
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
},
|
||||
BufferColorspace::Linear => match buffer_to_render.format().code {
|
||||
drm_fourcc::DrmFourcc::Argb8888 => {
|
||||
texture_format = TextureFormat::RGBA32Linear;
|
||||
native_format = gles::ffi::RGBA8;
|
||||
}
|
||||
drm_fourcc::DrmFourcc::Abgr8888 => {
|
||||
texture_format = TextureFormat::BGRA32Linear;
|
||||
native_format = gles::ffi::RGBA8;
|
||||
}
|
||||
_ => {
|
||||
let _ = render_response_sender.send(Err(ScenegraphError::MethodError {
|
||||
error: "Unsupported pixel format".to_string(),
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: This doesn't seem to care about the texture format, it seems to always output with sRGB.
|
||||
let sk_tex = sk.tex_create(TextureType::IMAGE_NO_MIPS, texture_format);
|
||||
unsafe {
|
||||
sk.tex_set_surface(
|
||||
&sk_tex,
|
||||
smithay_tex.tex_id() as usize as *mut c_void,
|
||||
TextureType::RENDER_TARGET,
|
||||
native_format.into(),
|
||||
buffer_to_render.size().w,
|
||||
buffer_to_render.size().h,
|
||||
1,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
sk.render_to(
|
||||
sk_tex,
|
||||
self.space.global_transform(),
|
||||
frame_info.proj_matrix,
|
||||
RenderLayer::all(),
|
||||
stereokit::RenderClear::All,
|
||||
Rect {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: 0.0,
|
||||
h: 0.0,
|
||||
},
|
||||
);
|
||||
render_notifiers.push(render_response_sender);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_rendered(&self) {
|
||||
for notifier in self.rendered_notifiers.lock().drain(..) {
|
||||
let _ = notifier.send(Ok(Vec::new().into()));
|
||||
}
|
||||
pub struct CameraItemAcceptor;
|
||||
impl AspectIdentifier for CameraItemAcceptor {
|
||||
impl_aspect_for_camera_item_acceptor_aspect_id! {}
|
||||
}
|
||||
impl Aspect for CameraItemAcceptor {
|
||||
impl_aspect_for_camera_item_acceptor_aspect! {}
|
||||
}
|
||||
impl CameraItemAcceptorAspect for CameraItemAcceptor {
|
||||
fn capture_item(node: Arc<Node>, _calling_client: Arc<Client>, item: Arc<Node>) -> Result<()> {
|
||||
super::acceptor_capture_item_flex(node, item)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(sk: &impl StereoKitDraw, buffer_manager: &mut BufferManager) {
|
||||
for camera in ITEM_TYPE_INFO_CAMERA.items.get_valid_contents() {
|
||||
let ItemType::Camera(camera) = &camera.specialization else {
|
||||
continue;
|
||||
};
|
||||
camera.update(sk, buffer_manager);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_rendered() {
|
||||
for camera in ITEM_TYPE_INFO_CAMERA.items.get_valid_contents() {
|
||||
let ItemType::Camera(camera) = &camera.specialization else {
|
||||
continue;
|
||||
};
|
||||
camera.send_rendered();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn create_camera_item_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateCameraItemInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
impl InterfaceAspect for Interface {
|
||||
#[doc = "Create a camera item at a specific location"]
|
||||
fn create_camera_item(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
proj_matrix: RowMatrix4<f32>,
|
||||
preview_size: Vector2<u32>,
|
||||
}
|
||||
let info: CreateCameraItemInfo = deserialize(message.as_ref())?;
|
||||
let parent_name = format!("/item/{}/item", ITEM_TYPE_INFO_CAMERA.type_name);
|
||||
let space = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
proj_matrix: ColumnMatrix4<f32>,
|
||||
px_size: Vector2<u32>,
|
||||
) -> Result<()> {
|
||||
let space = parent.get_aspect::<Spatial>()?;
|
||||
let transform = transform.to_mat4(true, true, false);
|
||||
|
||||
let node =
|
||||
Node::create(&INTERNAL_CLIENT, &parent_name, info.name, false).add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, None, transform * space.global_transform(), false)?;
|
||||
CameraItem::add_to(&node, info.proj_matrix.into(), info.preview_size);
|
||||
// TODO: this means ANY client can render anywhere with no limits, this needs to be changed!!
|
||||
node.item
|
||||
.get()
|
||||
.unwrap()
|
||||
.make_alias_named(&calling_client, &parent_name, info.name)?;
|
||||
Ok(())
|
||||
let node = Node::from_id(&calling_client, id, false).add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, None, transform * space.global_transform(), false);
|
||||
CameraItem::add_to(&node, proj_matrix.into(), px_size);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc = "Register this client to manage camera items and create default 3D UI for them."]
|
||||
fn register_camera_item_ui(node: Arc<Node>, calling_client: Arc<Client>) -> Result<()> {
|
||||
node.add_aspect(CameraItemUi);
|
||||
register_item_ui_flex(calling_client, &ITEM_TYPE_INFO_CAMERA)
|
||||
}
|
||||
|
||||
#[doc = "Create an item acceptor to allow temporary ownership of a given type of item. Creates a node at `/item/camera/acceptor/<name>`."]
|
||||
fn create_camera_item_acceptor(
|
||||
node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
field: Arc<Node>,
|
||||
) -> Result<()> {
|
||||
node.add_aspect(CameraItemAcceptor);
|
||||
create_item_acceptor_flex(
|
||||
calling_client,
|
||||
id,
|
||||
parent,
|
||||
transform,
|
||||
&ITEM_TYPE_INFO_CAMERA,
|
||||
field,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
use super::{Item, ItemType};
|
||||
use crate::{
|
||||
core::{
|
||||
client::{Client, INTERNAL_CLIENT},
|
||||
registry::Registry,
|
||||
scenegraph::MethodResponseSender,
|
||||
},
|
||||
nodes::{
|
||||
items::TypeInfo,
|
||||
spatial::{find_spatial_parent, parse_transform, Spatial},
|
||||
Message, Node,
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::{eyre, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use nanoid::nanoid;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::{
|
||||
schemas::flex::{deserialize, serialize},
|
||||
values::Transform,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
lazy_static! {
|
||||
pub(super) static ref ITEM_TYPE_INFO_ENVIRONMENT: TypeInfo = TypeInfo {
|
||||
type_name: "environment",
|
||||
aliased_local_signals: vec!["apply_sky_tex", "apply_sky_light"],
|
||||
aliased_local_methods: vec![],
|
||||
aliased_remote_signals: vec![],
|
||||
ui: Default::default(),
|
||||
items: Registry::new(),
|
||||
acceptors: Registry::new(),
|
||||
};
|
||||
}
|
||||
|
||||
pub struct EnvironmentItem {
|
||||
path: String,
|
||||
}
|
||||
impl EnvironmentItem {
|
||||
pub fn add_to(node: &Arc<Node>, path: String) {
|
||||
Item::add_to(
|
||||
node,
|
||||
nanoid!(),
|
||||
&ITEM_TYPE_INFO_ENVIRONMENT,
|
||||
ItemType::Environment(EnvironmentItem { path }),
|
||||
);
|
||||
node.add_local_method("get_path", EnvironmentItem::get_path_flex);
|
||||
}
|
||||
|
||||
fn get_path_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
_message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
let ItemType::Environment(environment_item) = &node.item.get().unwrap().specialization else {
|
||||
return Err(eyre!("Wrong item type?"))
|
||||
};
|
||||
Ok(serialize(environment_item.path.as_str())?.into())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn serialize_start_data(&self, id: &str) -> Result<Message> {
|
||||
Ok(serialize((id, self.path.as_str()))?.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn create_environment_item_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateEnvironmentItemInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
item_data: String,
|
||||
}
|
||||
let info: CreateEnvironmentItemInfo = deserialize(message.as_ref())?;
|
||||
let parent_name = format!("/item/{}/item", ITEM_TYPE_INFO_ENVIRONMENT.type_name);
|
||||
let space = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
|
||||
let node =
|
||||
Node::create(&INTERNAL_CLIENT, &parent_name, info.name, false).add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, None, transform * space.global_transform(), false)?;
|
||||
EnvironmentItem::add_to(&node, info.item_data);
|
||||
node.item
|
||||
.get()
|
||||
.unwrap()
|
||||
.make_alias_named(&calling_client, &parent_name, info.name)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,44 +1,26 @@
|
||||
pub mod camera;
|
||||
mod environment;
|
||||
pub mod panel;
|
||||
|
||||
use self::camera::CameraItem;
|
||||
use self::environment::{EnvironmentItem, ITEM_TYPE_INFO_ENVIRONMENT};
|
||||
use self::panel::{PanelItemTrait, ITEM_TYPE_INFO_PANEL};
|
||||
use super::fields::Field;
|
||||
use super::spatial::{find_spatial_parent, parse_transform, Spatial};
|
||||
use super::{Alias, Message, Node};
|
||||
use self::panel::PanelItemTrait;
|
||||
use super::alias::AliasList;
|
||||
use super::fields::{FIELD_ALIAS_INFO, Field};
|
||||
use super::spatial::Spatial;
|
||||
use super::{Alias, Aspect, AspectIdentifier, Node};
|
||||
use crate::core::client::Client;
|
||||
use crate::core::node_collections::LifeLinkedNodeMap;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::registry::Registry;
|
||||
use crate::ensure;
|
||||
use crate::nodes::alias::AliasInfo;
|
||||
use crate::nodes::fields::find_field;
|
||||
use color_eyre::eyre::{ensure, eyre, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use nanoid::nanoid;
|
||||
use crate::nodes::spatial::SPATIAL_ASPECT_ALIAS_INFO;
|
||||
use crate::nodes::spatial::Transform;
|
||||
use parking_lot::Mutex;
|
||||
use portable_atomic::Ordering;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flex::{deserialize, serialize};
|
||||
use stardust_xr::values::Transform;
|
||||
use std::hash::Hash;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
lazy_static! {
|
||||
static ref ITEM_ALIAS_LOCAL_SIGNALS: Vec<&'static str> = vec![
|
||||
"get_bounds",
|
||||
"get_transform",
|
||||
"set_transform",
|
||||
"set_spatial_parent",
|
||||
"set_spatial_parent_in_place",
|
||||
"set_zoneable",
|
||||
"release",
|
||||
];
|
||||
static ref ITEM_ALIAS_LOCAL_METHODS: Vec<&'static str> = vec![];
|
||||
static ref ITEM_ALIAS_REMOTE_SIGNALS: Vec<&'static str> = vec![];
|
||||
}
|
||||
stardust_xr_server_codegen::codegen_item_protocol!();
|
||||
|
||||
pub fn capture(item: &Arc<Item>, acceptor: &Arc<ItemAcceptor>) {
|
||||
fn capture(item: &Arc<Item>, acceptor: &Arc<ItemAcceptor>) {
|
||||
if item.captured_acceptor.lock().strong_count() > 0 {
|
||||
release(item);
|
||||
}
|
||||
@@ -54,19 +36,21 @@ fn release(item: &Item) {
|
||||
*captured_acceptor = Weak::default();
|
||||
acceptor.handle_release(item);
|
||||
if let Some(ui) = item.type_info.ui.lock().upgrade() {
|
||||
ui.handle_release_item(item, &acceptor);
|
||||
ui.handle_release_item(item, acceptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TypeInfo {
|
||||
pub type_name: &'static str,
|
||||
pub aliased_local_signals: Vec<&'static str>,
|
||||
pub aliased_local_methods: Vec<&'static str>,
|
||||
pub aliased_remote_signals: Vec<&'static str>,
|
||||
pub alias_info: AliasInfo,
|
||||
pub ui_node_id: u64,
|
||||
pub ui: Mutex<Weak<ItemUI>>,
|
||||
pub items: Registry<Item>,
|
||||
pub acceptors: Registry<ItemAcceptor>,
|
||||
pub add_ui_aspect: fn(node: &Node),
|
||||
pub add_acceptor_aspect: fn(node: &Node),
|
||||
pub new_acceptor_fn: fn(node: &Node, acceptor: &Arc<Node>, acceptor_field: &Arc<Node>),
|
||||
}
|
||||
impl Hash for TypeInfo {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
@@ -81,8 +65,7 @@ impl PartialEq for TypeInfo {
|
||||
impl Eq for TypeInfo {}
|
||||
|
||||
pub struct Item {
|
||||
node: Weak<Node>,
|
||||
uid: String,
|
||||
spatial: Arc<Spatial>,
|
||||
type_info: &'static TypeInfo,
|
||||
captured_acceptor: Mutex<Weak<ItemAcceptor>>,
|
||||
pub specialization: ItemType,
|
||||
@@ -90,24 +73,21 @@ pub struct Item {
|
||||
impl Item {
|
||||
pub fn add_to(
|
||||
node: &Arc<Node>,
|
||||
uid: String,
|
||||
type_info: &'static TypeInfo,
|
||||
specialization: ItemType,
|
||||
) -> Arc<Self> {
|
||||
let item = Item {
|
||||
node: Arc::downgrade(node),
|
||||
uid,
|
||||
spatial: node.aspects.get::<Spatial>().unwrap(),
|
||||
type_info,
|
||||
captured_acceptor: Default::default(),
|
||||
specialization,
|
||||
};
|
||||
let item = type_info.items.add(item);
|
||||
|
||||
node.add_local_signal("release", Item::release_flex);
|
||||
if let Some(ui) = type_info.ui.lock().upgrade() {
|
||||
ui.handle_create_item(&item);
|
||||
}
|
||||
let _ = node.item.set(item.clone());
|
||||
node.add_aspect_raw(item.clone());
|
||||
|
||||
// if let Some(auto_acceptor) = node.get_client().and_then(|client| {
|
||||
// client
|
||||
@@ -121,44 +101,25 @@ impl Item {
|
||||
|
||||
item
|
||||
}
|
||||
fn make_alias_named(
|
||||
&self,
|
||||
client: &Arc<Client>,
|
||||
parent: &str,
|
||||
name: &str,
|
||||
) -> Result<Arc<Node>> {
|
||||
fn make_alias(&self, client: &Arc<Client>, alias_list: &AliasList) -> Result<Arc<Node>> {
|
||||
Alias::create(
|
||||
&self.spatial.node().unwrap(),
|
||||
client,
|
||||
parent,
|
||||
name,
|
||||
&self.node.upgrade().unwrap(),
|
||||
AliasInfo {
|
||||
server_signals: [
|
||||
&self.type_info.aliased_local_signals,
|
||||
ITEM_ALIAS_LOCAL_SIGNALS.as_slice(),
|
||||
]
|
||||
.concat(),
|
||||
server_methods: [
|
||||
&self.type_info.aliased_local_methods,
|
||||
ITEM_ALIAS_LOCAL_METHODS.as_slice(),
|
||||
]
|
||||
.concat(),
|
||||
client_signals: [
|
||||
&self.type_info.aliased_remote_signals,
|
||||
ITEM_ALIAS_REMOTE_SIGNALS.as_slice(),
|
||||
]
|
||||
.concat(),
|
||||
},
|
||||
self.type_info.alias_info.clone() + ITEM_ASPECT_ALIAS_INFO.clone(),
|
||||
Some(alias_list),
|
||||
)
|
||||
}
|
||||
fn make_alias(&self, client: &Arc<Client>, parent: &str) -> Result<Arc<Node>> {
|
||||
self.make_alias_named(client, parent, &self.uid)
|
||||
}
|
||||
|
||||
fn release_flex(node: &Node, _calling_client: Arc<Client>, _message: Message) -> Result<()> {
|
||||
let item = node.get_aspect("Item", "item", |n| &n.item)?;
|
||||
release(item);
|
||||
|
||||
}
|
||||
impl AspectIdentifier for Item {
|
||||
impl_aspect_for_item_aspect_id! {}
|
||||
}
|
||||
impl Aspect for Item {
|
||||
impl_aspect_for_item_aspect! {}
|
||||
}
|
||||
impl ItemAspect for Item {
|
||||
fn release(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
||||
let item = node.get_aspect::<Item>()?;
|
||||
release(&item);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -172,17 +133,22 @@ impl Drop for Item {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "wayland"), allow(dead_code))]
|
||||
pub enum ItemType {
|
||||
Camera(CameraItem),
|
||||
Environment(EnvironmentItem),
|
||||
Camera(Arc<CameraItem>),
|
||||
Panel(Arc<dyn PanelItemTrait>),
|
||||
}
|
||||
impl ItemType {
|
||||
fn serialize_start_data(&self, id: &str) -> Result<Message> {
|
||||
fn send_ui_item_created(&self, node: &Node, item: &Arc<Node>) {
|
||||
match self {
|
||||
ItemType::Camera(c) => c.serialize_start_data(id),
|
||||
ItemType::Environment(e) => e.serialize_start_data(id),
|
||||
ItemType::Panel(p) => p.serialize_start_data(id),
|
||||
ItemType::Camera(c) => c.send_ui_item_created(node, item),
|
||||
ItemType::Panel(p) => p.send_ui_item_created(node, item),
|
||||
}
|
||||
}
|
||||
fn send_acceptor_item_created(&self, node: &Node, item: &Arc<Node>) {
|
||||
match self {
|
||||
ItemType::Camera(c) => c.send_acceptor_item_created(node, item),
|
||||
ItemType::Panel(p) => p.send_acceptor_item_created(node, item),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,9 +166,9 @@ impl ItemType {
|
||||
pub struct ItemUI {
|
||||
node: Weak<Node>,
|
||||
type_info: &'static TypeInfo,
|
||||
item_aliases: LifeLinkedNodeMap<String>,
|
||||
acceptor_aliases: LifeLinkedNodeMap<String>,
|
||||
acceptor_field_aliases: LifeLinkedNodeMap<String>,
|
||||
item_aliases: AliasList,
|
||||
acceptor_aliases: AliasList,
|
||||
acceptor_field_aliases: AliasList,
|
||||
}
|
||||
impl ItemUI {
|
||||
fn add_to(node: &Arc<Node>, type_info: &'static TypeInfo) -> Result<()> {
|
||||
@@ -214,12 +180,12 @@ impl ItemUI {
|
||||
let ui = Arc::new(ItemUI {
|
||||
node: Arc::downgrade(node),
|
||||
type_info,
|
||||
item_aliases: Default::default(),
|
||||
acceptor_aliases: Default::default(),
|
||||
acceptor_field_aliases: Default::default(),
|
||||
item_aliases: AliasList::default(),
|
||||
acceptor_aliases: AliasList::default(),
|
||||
acceptor_field_aliases: AliasList::default(),
|
||||
});
|
||||
*type_info.ui.lock() = Arc::downgrade(&ui);
|
||||
let _ = node.item_ui.set(ui.clone());
|
||||
node.add_aspect_raw(ui.clone());
|
||||
|
||||
for item in type_info.items.get_valid_contents() {
|
||||
ui.handle_create_item(&item);
|
||||
@@ -229,62 +195,107 @@ impl ItemUI {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn send_state(&self, state: &str, name: &str) {
|
||||
let Ok(serialized_data) = serialize(name) else {return};
|
||||
let _ = self
|
||||
.node
|
||||
.upgrade()
|
||||
.unwrap()
|
||||
.send_remote_signal(state, serialized_data);
|
||||
}
|
||||
|
||||
fn handle_create_item(&self, item: &Item) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let Some(client) = node.get_client() else {return};
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = node.get_client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Ok(alias_node) = item.make_alias(&client, &(node.get_path().to_string() + "/item")) {
|
||||
self.item_aliases.add(item.uid.clone(), &alias_node);
|
||||
}
|
||||
let Ok(item_alias) = item.make_alias(&client, &self.item_aliases) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(serialized_data) = item.specialization.serialize_start_data(&item.uid) else {return};
|
||||
let _ = node.send_remote_signal("create_item", serialized_data);
|
||||
}
|
||||
fn handle_destroy_item(&self, item: &Item) {
|
||||
self.item_aliases.remove(&item.uid);
|
||||
self.send_state("destroy_item", item.uid.as_str());
|
||||
item.specialization.send_ui_item_created(&node, &item_alias);
|
||||
}
|
||||
fn handle_capture_item(&self, item: &Item, acceptor: &ItemAcceptor) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
|
||||
let Ok(message) = serialize((item.uid.as_str(), acceptor.uid.as_str())) else {return};
|
||||
let _ = node.send_remote_signal("capture_item", message);
|
||||
let Some(item_alias) = self.item_aliases.get_from_aspect(item) else {
|
||||
return;
|
||||
};
|
||||
let Some(acceptor_alias) = self.acceptor_aliases.get_from_aspect(acceptor) else {
|
||||
return;
|
||||
};
|
||||
let _ = item_ui_client::capture_item(
|
||||
&self.node.upgrade().unwrap(),
|
||||
item_alias.id,
|
||||
acceptor_alias.id,
|
||||
);
|
||||
}
|
||||
fn handle_release_item(&self, item: &Item, acceptor: &ItemAcceptor) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
|
||||
let Ok(message) = serialize((item.uid.as_str(), acceptor.uid.as_str())) else {return};
|
||||
let _ = node.send_remote_signal("release_item", message);
|
||||
let Some(item_alias) = self.item_aliases.get_from_aspect(item) else {
|
||||
return;
|
||||
};
|
||||
let Some(acceptor_alias) = self.acceptor_aliases.get_from_aspect(acceptor) else {
|
||||
return;
|
||||
};
|
||||
let _ = item_ui_client::release_item(
|
||||
&self.node.upgrade().unwrap(),
|
||||
item_alias.id,
|
||||
acceptor_alias.id,
|
||||
);
|
||||
}
|
||||
fn handle_destroy_item(&self, item: &Item) {
|
||||
let Some(item_alias) = self
|
||||
.item_aliases
|
||||
.get_from_original_node(item.spatial.node.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let _ = item_ui_client::destroy_item(&self.node.upgrade().unwrap(), item_alias.id);
|
||||
self.item_aliases.remove_aspect(item);
|
||||
}
|
||||
fn handle_create_acceptor(&self, acceptor: &ItemAcceptor) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let Some(client) = node.get_client() else {return};
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = node.get_client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok((alias, field_alias)) = acceptor.make_aliases(
|
||||
let Some(acceptor_node) = acceptor.spatial.node() else {
|
||||
return;
|
||||
};
|
||||
let Ok(acceptor_alias) = Alias::create(
|
||||
&acceptor_node,
|
||||
&client,
|
||||
&format!("/item/{}/acceptor", self.type_info.type_name),
|
||||
) else {return};
|
||||
self.acceptor_aliases.add(acceptor.uid.clone(), &alias);
|
||||
self.acceptor_field_aliases
|
||||
.add(acceptor.uid.clone(), &field_alias);
|
||||
let Ok(message) = serialize(&acceptor.uid) else {return};
|
||||
let _ = node.send_remote_signal("create_acceptor", message);
|
||||
ITEM_ACCEPTOR_ASPECT_ALIAS_INFO.clone(),
|
||||
Some(&self.acceptor_aliases),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(acceptor_field_node) = acceptor.field.spatial.node() else {
|
||||
return;
|
||||
};
|
||||
let Ok(acceptor_field_alias) = Alias::create(
|
||||
&acceptor_field_node,
|
||||
&client,
|
||||
FIELD_ALIAS_INFO.clone(),
|
||||
Some(&self.acceptor_aliases),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
(acceptor.type_info.new_acceptor_fn)(&node, &acceptor_alias, &acceptor_field_alias);
|
||||
}
|
||||
fn handle_destroy_acceptor(&self, acceptor: &ItemAcceptor) {
|
||||
self.send_state("destroy_acceptor", acceptor.uid.as_str());
|
||||
self.acceptor_aliases.remove(&acceptor.uid);
|
||||
self.acceptor_field_aliases.remove(&acceptor.uid);
|
||||
let acceptor_alias = self.acceptor_aliases.get_from_aspect(acceptor).unwrap();
|
||||
let _ = item_ui_client::destroy_acceptor(&self.node.upgrade().unwrap(), acceptor_alias.id);
|
||||
|
||||
self.acceptor_aliases
|
||||
.remove_aspect(acceptor.spatial.as_ref());
|
||||
self.acceptor_field_aliases
|
||||
.remove_aspect(acceptor.field.as_ref());
|
||||
}
|
||||
}
|
||||
impl AspectIdentifier for ItemUI {
|
||||
impl_aspect_for_item_ui_aspect_id! {}
|
||||
}
|
||||
impl Aspect for ItemUI {
|
||||
impl_aspect_for_item_ui_aspect! {}
|
||||
}
|
||||
impl Drop for ItemUI {
|
||||
fn drop(&mut self) {
|
||||
*self.type_info.ui.lock() = Weak::new();
|
||||
@@ -292,88 +303,61 @@ impl Drop for ItemUI {
|
||||
}
|
||||
|
||||
pub struct ItemAcceptor {
|
||||
uid: String,
|
||||
node: Weak<Node>,
|
||||
spatial: Arc<Spatial>,
|
||||
pub type_info: &'static TypeInfo,
|
||||
field: Arc<Field>,
|
||||
accepted_aliases: LifeLinkedNodeMap<String>,
|
||||
accepted_aliases: AliasList,
|
||||
accepted_registry: Registry<Item>,
|
||||
}
|
||||
impl ItemAcceptor {
|
||||
fn add_to(node: &Arc<Node>, type_info: &'static TypeInfo, field: Arc<Field>) {
|
||||
let acceptor = type_info.acceptors.add(ItemAcceptor {
|
||||
uid: nanoid!(),
|
||||
node: Arc::downgrade(node),
|
||||
spatial: node.get_aspect::<Spatial>().unwrap(),
|
||||
type_info,
|
||||
field,
|
||||
accepted_aliases: Default::default(),
|
||||
accepted_aliases: AliasList::default(),
|
||||
accepted_registry: Registry::new(),
|
||||
});
|
||||
node.add_local_signal("capture", ItemAcceptor::capture_flex);
|
||||
if let Some(ui) = type_info.ui.lock().upgrade() {
|
||||
ui.handle_create_acceptor(&acceptor);
|
||||
}
|
||||
let _ = node.item_acceptor.set(acceptor);
|
||||
node.add_aspect_raw(acceptor.clone());
|
||||
}
|
||||
|
||||
fn capture_flex(node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
if !node.enabled.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let acceptor = node.item_acceptor.get().unwrap();
|
||||
let item_path: &str = deserialize(message.as_ref())?;
|
||||
let item_node = calling_client.get_node("Item", item_path)?;
|
||||
let item = item_node.get_aspect("Item", "item", |n| &n.item)?;
|
||||
capture(item, acceptor);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_aliases(&self, client: &Arc<Client>, parent: &str) -> Result<(Arc<Node>, Arc<Node>)> {
|
||||
let acceptor_node = &self.node.upgrade().unwrap();
|
||||
let acceptor_alias = Alias::create(
|
||||
client,
|
||||
parent,
|
||||
&self.uid,
|
||||
acceptor_node,
|
||||
AliasInfo {
|
||||
server_signals: vec!["capture"],
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let acceptor_field_alias = Alias::create(
|
||||
client,
|
||||
acceptor_alias.get_path(),
|
||||
"field",
|
||||
&self.field.spatial_ref().node.upgrade().unwrap(),
|
||||
AliasInfo::default(),
|
||||
)?;
|
||||
|
||||
Ok((acceptor_alias, acceptor_field_alias))
|
||||
}
|
||||
fn handle_capture(&self, item: &Arc<Item>) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let Some(client) = node.get_client() else {return};
|
||||
let Some(node) = self.spatial.node() else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = node.get_client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.accepted_registry.add_raw(item);
|
||||
if let Ok(alias_node) = item.make_alias(&client, &node.path) {
|
||||
self.accepted_aliases.add(item.uid.clone(), &alias_node);
|
||||
}
|
||||
let Ok(alias_node) = item.make_alias(&client, &self.accepted_aliases) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(serialized_data) = item.specialization.serialize_start_data(&item.uid) else {return};
|
||||
let _ = node.send_remote_signal("capture", serialized_data);
|
||||
item.specialization
|
||||
.send_acceptor_item_created(&node, &alias_node);
|
||||
}
|
||||
fn handle_release(&self, item: &Item) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
|
||||
self.accepted_registry.remove(item);
|
||||
self.accepted_aliases.remove(&item.uid);
|
||||
let Ok(message) = serialize(&item.uid) else {return};
|
||||
let _ = node.send_remote_signal("release", message);
|
||||
self.accepted_aliases.remove_aspect(item);
|
||||
|
||||
let Some(node) = self.spatial.node() else {
|
||||
return;
|
||||
};
|
||||
let alias = self.accepted_aliases.get_from_aspect(item).unwrap();
|
||||
let _ = item_acceptor_client::release_item(&node, alias.id);
|
||||
}
|
||||
}
|
||||
impl AspectIdentifier for ItemAcceptor {
|
||||
impl_aspect_for_item_acceptor_aspect_id! {}
|
||||
}
|
||||
impl Aspect for ItemAcceptor {
|
||||
impl_aspect_for_item_acceptor_aspect! {}
|
||||
}
|
||||
impl ItemAcceptorAspect for ItemAcceptor {}
|
||||
impl Drop for ItemAcceptor {
|
||||
fn drop(&mut self) {
|
||||
self.type_info.acceptors.remove(self);
|
||||
@@ -386,71 +370,38 @@ impl Drop for ItemAcceptor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_interface(client: &Arc<Client>) -> Result<()> {
|
||||
let node = Node::create(client, "", "item", false);
|
||||
node.add_local_signal("create_camera_item", camera::create_camera_item_flex);
|
||||
node.add_local_signal(
|
||||
"create_environment_item",
|
||||
environment::create_environment_item_flex,
|
||||
);
|
||||
node.add_local_signal("register_item_ui", register_item_ui_flex);
|
||||
node.add_local_signal("create_item_acceptor", create_item_acceptor_flex);
|
||||
node.add_to_scenegraph().map(|_| ())
|
||||
}
|
||||
|
||||
fn type_info(name: &str) -> Result<&'static TypeInfo> {
|
||||
match name {
|
||||
"environment" => Ok(&ITEM_TYPE_INFO_ENVIRONMENT),
|
||||
#[cfg(feature = "wayland")]
|
||||
"panel" => Ok(&ITEM_TYPE_INFO_PANEL),
|
||||
_ => Err(eyre!("Invalid item type")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_item_ui_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
type_info: &'static TypeInfo,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct RegisterItemUIInfo<'a> {
|
||||
item_type: &'a str,
|
||||
}
|
||||
let info: RegisterItemUIInfo = deserialize(message.as_ref())?;
|
||||
let type_info = type_info(info.item_type)?;
|
||||
let ui =
|
||||
Node::create(&calling_client, "/item", type_info.type_name, true).add_to_scenegraph()?;
|
||||
let ui = Node::from_id(&calling_client, type_info.ui_node_id, true).add_to_scenegraph()?;
|
||||
ItemUI::add_to(&ui, type_info)?;
|
||||
(type_info.add_ui_aspect)(&ui);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_item_acceptor_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
type_info: &'static TypeInfo,
|
||||
field: Arc<Node>,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateItemAcceptorInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
field_path: &'a str,
|
||||
item_type: &'a str,
|
||||
}
|
||||
let info: CreateItemAcceptorInfo = deserialize(message.as_ref())?;
|
||||
let space = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
let field = find_field(&calling_client, info.field_path)?;
|
||||
let type_info = type_info(info.item_type)?;
|
||||
let space = parent.get_aspect::<Spatial>()?;
|
||||
let field = field.get_aspect::<Field>()?;
|
||||
let transform = transform.to_mat4(true, true, false);
|
||||
|
||||
let node = Node::create(
|
||||
&calling_client,
|
||||
&format!("/item/{}/acceptor", type_info.type_name),
|
||||
info.name,
|
||||
true,
|
||||
)
|
||||
.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(space), transform, false)?;
|
||||
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(space.clone()), transform, false);
|
||||
ItemAcceptor::add_to(&node, type_info, field);
|
||||
(type_info.add_acceptor_aspect)(&node);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn acceptor_capture_item_flex(node: Arc<Node>, item: Arc<Node>) -> Result<()> {
|
||||
let acceptor = node.get_aspect::<ItemAcceptor>()?;
|
||||
let item = item.get_aspect::<Item>()?;
|
||||
capture(&item, &acceptor);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,241 +1,125 @@
|
||||
use super::camera::CameraItemAcceptor;
|
||||
use super::{create_item_acceptor_flex, register_item_ui_flex};
|
||||
use crate::bail;
|
||||
use crate::nodes::{
|
||||
Aspect, AspectIdentifier,
|
||||
items::{ITEM_ACCEPTOR_ASPECT_ALIAS_INFO, ITEM_ASPECT_ALIAS_INFO, ITEM_UI_ASPECT_ALIAS_INFO},
|
||||
};
|
||||
use crate::{
|
||||
core::{
|
||||
client::{get_env, state, Client, INTERNAL_CLIENT},
|
||||
client::{Client, INTERNAL_CLIENT, get_env, state},
|
||||
error::Result,
|
||||
registry::Registry,
|
||||
},
|
||||
nodes::{
|
||||
drawable::{model::ModelPart, Drawable},
|
||||
Node,
|
||||
drawable::model::ModelPart,
|
||||
items::{Item, ItemType, TypeInfo},
|
||||
spatial::Spatial,
|
||||
Message, Node,
|
||||
spatial::{Spatial, Transform},
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::{bail, eyre, Result};
|
||||
use glam::Mat4;
|
||||
use lazy_static::lazy_static;
|
||||
use mint::Vector2;
|
||||
use nanoid::nanoid;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{
|
||||
de::{Deserializer, Error, SeqAccess, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use stardust_xr::schemas::flex::{deserialize, serialize};
|
||||
use parking_lot::Mutex;
|
||||
use slotmap::{DefaultKey, Key, KeyData, SlotMap};
|
||||
use std::sync::{Arc, Weak};
|
||||
use tracing::debug;
|
||||
|
||||
stardust_xr_server_codegen::codegen_item_panel_protocol!();
|
||||
impl Default for Geometry {
|
||||
fn default() -> Self {
|
||||
Geometry {
|
||||
origin: [0, 0].into(),
|
||||
size: [0, 0].into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Copy for Geometry {}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref KEYMAPS: Mutex<SlotMap<DefaultKey, String>> = Mutex::new(SlotMap::default());
|
||||
pub static ref ITEM_TYPE_INFO_PANEL: TypeInfo = TypeInfo {
|
||||
type_name: "panel",
|
||||
aliased_local_signals: vec![
|
||||
"apply_surface_material",
|
||||
"close_toplevel",
|
||||
"auto_size_toplevel",
|
||||
"set_toplevel_size",
|
||||
"set_toplevel_focused_visuals",
|
||||
"pointer_motion",
|
||||
"pointer_button",
|
||||
"pointer_scroll",
|
||||
"keyboard_keymap",
|
||||
"keyboard_key",
|
||||
"touch_down",
|
||||
"touch_move",
|
||||
"touch_up",
|
||||
"reset_touches",
|
||||
],
|
||||
aliased_local_methods: vec![],
|
||||
aliased_remote_signals: vec![
|
||||
"toplevel_parent_changed",
|
||||
"toplevel_title_changed",
|
||||
"toplevel_app_id_changed",
|
||||
"toplevel_fullscreen_active",
|
||||
"toplevel_move_request",
|
||||
"toplevel_resize_request",
|
||||
"toplevel_size_changed",
|
||||
"set_cursor",
|
||||
"new_child",
|
||||
"reposition_child",
|
||||
"drop_child",
|
||||
],
|
||||
alias_info: PANEL_ITEM_ASPECT_ALIAS_INFO.clone(),
|
||||
ui_node_id: INTERFACE_NODE_ID,
|
||||
ui: Default::default(),
|
||||
items: Registry::new(),
|
||||
acceptors: Registry::new(),
|
||||
add_acceptor_aspect: |node| {
|
||||
node.add_aspect(PanelItemUi);
|
||||
},
|
||||
add_ui_aspect: |node| {
|
||||
node.add_aspect(PanelItemAcceptor);
|
||||
},
|
||||
new_acceptor_fn: |node, acceptor, acceptor_field| {
|
||||
let _ = panel_item_ui_client::create_acceptor(node, acceptor, acceptor_field);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// An ID for a surface inside this panel item
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub enum SurfaceID {
|
||||
Cursor,
|
||||
Toplevel,
|
||||
Child(String),
|
||||
}
|
||||
impl Default for SurfaceID {
|
||||
fn default() -> Self {
|
||||
Self::Toplevel
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for SurfaceID {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
deserializer.deserialize_seq(SurfaceIDVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct SurfaceIDVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for SurfaceIDVisitor {
|
||||
type Value = SurfaceID;
|
||||
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str("idk")
|
||||
}
|
||||
|
||||
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
|
||||
let Some(discrim) = seq.next_element()? else {
|
||||
return Err(A::Error::missing_field("discrim"));
|
||||
};
|
||||
|
||||
// idk if you wanna check for extraneous elements
|
||||
// I didn't bother
|
||||
|
||||
match discrim {
|
||||
"Cursor" => Ok(SurfaceID::Cursor),
|
||||
"Toplevel" => Ok(SurfaceID::Toplevel),
|
||||
"Child" => {
|
||||
let Some(text) = seq.next_element()? else {
|
||||
return Err(A::Error::missing_field("child_text"));
|
||||
};
|
||||
Ok(SurfaceID::Child(text))
|
||||
}
|
||||
_ => Err(A::Error::unknown_variant(
|
||||
discrim,
|
||||
&["Cursor", "Toplevel", "Child"],
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for SurfaceID {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match self {
|
||||
Self::Cursor => ["Cursor"].serialize(serializer),
|
||||
Self::Toplevel => ["Toplevel"].serialize(serializer),
|
||||
Self::Child(text) => ["Child", text].serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The origin and size of the surface's "solid" part.
|
||||
#[derive(Debug, Serialize, Clone, Copy)]
|
||||
pub struct Geometry {
|
||||
pub origin: Vector2<i32>,
|
||||
pub size: Vector2<u32>,
|
||||
}
|
||||
/// The state of the panel item's toplevel.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ToplevelInfo {
|
||||
/// The UID of the panel item of the parent of this toplevel, if it exists
|
||||
pub parent: Option<String>,
|
||||
/// Equivalent to the window title
|
||||
pub title: Option<String>,
|
||||
/// Application identifier, see <https://standards.freedesktop.org/desktop-entry-spec/>
|
||||
pub app_id: Option<String>,
|
||||
/// Current size in pixels
|
||||
pub size: Vector2<u32>,
|
||||
/// Recommended minimum size in pixels
|
||||
pub min_size: Option<Vector2<u32>>,
|
||||
/// Recommended maximum size in pixels
|
||||
pub max_size: Option<Vector2<u32>>,
|
||||
/// Surface geometry
|
||||
pub logical_rectangle: Geometry,
|
||||
}
|
||||
|
||||
/// Data on positioning a child
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ChildInfo {
|
||||
pub parent: SurfaceID,
|
||||
pub geometry: Geometry,
|
||||
}
|
||||
|
||||
/// The init data for the panel item.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PanelItemInitData {
|
||||
/// The cursor, if applicable.
|
||||
pub cursor: Option<Geometry>,
|
||||
/// Size of the toplevel surface in pixels.
|
||||
pub toplevel: ToplevelInfo,
|
||||
/// Vector of childs that already exist
|
||||
pub children: FxHashMap<String, ChildInfo>,
|
||||
/// The surface, if any, that has exclusive input to the pointer.
|
||||
pub pointer_grab: Option<SurfaceID>,
|
||||
/// The surface, if any, that has exclusive input to the keyboard.
|
||||
pub keyboard_grab: Option<SurfaceID>,
|
||||
}
|
||||
|
||||
pub trait Backend: Send + Sync + 'static {
|
||||
fn start_data(&self) -> Result<PanelItemInitData>;
|
||||
|
||||
fn apply_surface_material(&self, surface: SurfaceID, model_part: &Arc<ModelPart>);
|
||||
fn apply_cursor_material(&self, model_part: &Arc<ModelPart>);
|
||||
fn apply_surface_material(&self, surface: SurfaceId, model_part: &Arc<ModelPart>);
|
||||
|
||||
fn close_toplevel(&self);
|
||||
fn auto_size_toplevel(&self);
|
||||
fn set_toplevel_size(&self, size: Vector2<u32>);
|
||||
fn set_toplevel_focused_visuals(&self, focused: bool);
|
||||
|
||||
fn pointer_motion(&self, surface: &SurfaceID, position: Vector2<f32>);
|
||||
fn pointer_button(&self, surface: &SurfaceID, button: u32, pressed: bool);
|
||||
fn pointer_motion(&self, surface: &SurfaceId, position: Vector2<f32>);
|
||||
fn pointer_button(&self, surface: &SurfaceId, button: u32, pressed: bool);
|
||||
fn pointer_scroll(
|
||||
&self,
|
||||
surface: &SurfaceID,
|
||||
surface: &SurfaceId,
|
||||
scroll_distance: Option<Vector2<f32>>,
|
||||
scroll_steps: Option<Vector2<f32>>,
|
||||
);
|
||||
|
||||
fn keyboard_keys(&self, surface: &SurfaceID, keymap_id: &str, keys: Vec<i32>);
|
||||
fn keyboard_key(&self, surface: &SurfaceId, keymap_id: u64, key: u32, pressed: bool);
|
||||
|
||||
fn touch_down(&self, surface: &SurfaceID, id: u32, position: Vector2<f32>);
|
||||
fn touch_down(&self, surface: &SurfaceId, id: u32, position: Vector2<f32>);
|
||||
fn touch_move(&self, id: u32, position: Vector2<f32>);
|
||||
fn touch_up(&self, id: u32);
|
||||
fn reset_touches(&self);
|
||||
fn reset_input(&self);
|
||||
}
|
||||
|
||||
pub fn panel_item_from_node(node: &Node) -> Option<Arc<dyn PanelItemTrait>> {
|
||||
let ItemType::Panel(panel_item) = &node.item.get()?.specialization else {return None};
|
||||
let ItemType::Panel(panel_item) = &node.get_aspect::<Item>().ok()?.specialization else {
|
||||
return None;
|
||||
};
|
||||
Some(panel_item.clone())
|
||||
}
|
||||
|
||||
pub trait PanelItemTrait: Backend + Send + Sync + 'static {
|
||||
fn uid(&self) -> &str;
|
||||
fn serialize_start_data(&self, id: &str) -> Result<Message>;
|
||||
pub trait PanelItemTrait: Send + Sync + 'static {
|
||||
fn backend(&self) -> &dyn Backend;
|
||||
fn send_ui_item_created(&self, node: &Node, item: &Arc<Node>);
|
||||
fn send_acceptor_item_created(&self, node: &Node, item: &Arc<Node>);
|
||||
}
|
||||
|
||||
pub struct PanelItem<B: Backend + ?Sized> {
|
||||
pub uid: String,
|
||||
node: Weak<Node>,
|
||||
#[derive(Debug)]
|
||||
pub struct PanelItem<B: Backend> {
|
||||
pub node: Weak<Node>,
|
||||
pub backend: Box<B>,
|
||||
}
|
||||
impl<B: Backend + ?Sized> PanelItem<B> {
|
||||
pub fn create(backend: Box<B>, pid: Option<i32>) -> Arc<PanelItem<B>> {
|
||||
impl<B: Backend> PanelItem<B> {
|
||||
#[cfg_attr(not(feature = "wayland"), allow(dead_code))]
|
||||
pub fn create(backend: Box<B>, pid: Option<i32>) -> (Arc<Node>, Arc<PanelItem<B>>) {
|
||||
debug!(?pid, "Create panel item");
|
||||
|
||||
let startup_settings = pid
|
||||
.and_then(|pid| get_env(pid).ok())
|
||||
.and_then(|env| state(&env));
|
||||
|
||||
let uid = nanoid!();
|
||||
let node = Node::create(&INTERNAL_CLIENT, "/item/panel/item", &uid, true)
|
||||
.add_to_scenegraph()
|
||||
.unwrap();
|
||||
let spatial = Spatial::add_to(&node, None, Mat4::IDENTITY, false).unwrap();
|
||||
let node = Arc::new(Node::generate(&INTERNAL_CLIENT, true));
|
||||
let spatial = Spatial::add_to(&node, None, Mat4::IDENTITY, false);
|
||||
if let Some(startup_settings) = &startup_settings {
|
||||
spatial.set_local_transform(startup_settings.root);
|
||||
}
|
||||
|
||||
let panel_item = Arc::new(PanelItem {
|
||||
uid: uid.clone(),
|
||||
node: Arc::downgrade(&node),
|
||||
backend,
|
||||
});
|
||||
@@ -243,300 +127,399 @@ impl<B: Backend + ?Sized> PanelItem<B> {
|
||||
let generic_panel_item: Arc<dyn PanelItemTrait> = panel_item.clone();
|
||||
Item::add_to(
|
||||
&node,
|
||||
uid,
|
||||
&ITEM_TYPE_INFO_PANEL,
|
||||
ItemType::Panel(generic_panel_item),
|
||||
);
|
||||
node.add_aspect_raw(panel_item.clone());
|
||||
|
||||
node.add_local_signal("apply_surface_material", Self::apply_surface_material_flex);
|
||||
node.add_local_signal("close_toplevel", Self::close_toplevel_flex);
|
||||
node.add_local_signal("auto_size_toplevel", Self::auto_size_toplevel_flex);
|
||||
node.add_local_signal(
|
||||
"set_toplevel_size_changed",
|
||||
Self::set_toplevel_size_changed_flex,
|
||||
);
|
||||
|
||||
node.add_local_signal("pointer_motion", Self::pointer_motion_flex);
|
||||
node.add_local_signal("pointer_button", Self::pointer_button_flex);
|
||||
node.add_local_signal("pointer_scroll", Self::pointer_scroll_flex);
|
||||
|
||||
node.add_local_signal("keyboard_key", Self::keyboard_keys_flex);
|
||||
|
||||
node.add_local_signal("touch_down", Self::touch_down_flex);
|
||||
node.add_local_signal("touch_move", Self::touch_move_flex);
|
||||
node.add_local_signal("touch_up", Self::touch_up_flex);
|
||||
node.add_local_signal("reset_touches", Self::reset_touches_flex);
|
||||
|
||||
panel_item
|
||||
}
|
||||
pub fn drop_toplevel(&self) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
node.destroy();
|
||||
(node, panel_item)
|
||||
}
|
||||
}
|
||||
|
||||
// Remote signals
|
||||
impl<B: Backend + ?Sized> PanelItem<B> {
|
||||
pub fn toplevel_parent_changed(&self, parent: &str) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("toplevel_parent_changed", serialize(parent).unwrap());
|
||||
#[allow(unused)]
|
||||
impl<B: Backend> PanelItem<B> {
|
||||
pub fn toplevel_parent_changed(&self, parent: u64) {
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
panel_item_client::toplevel_parent_changed(&node, parent);
|
||||
}
|
||||
pub fn toplevel_title_changed(&self, title: &str) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("toplevel_title_changed", serialize(title).unwrap());
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
panel_item_client::toplevel_title_changed(&node, title);
|
||||
}
|
||||
pub fn toplevel_app_id_changed(&self, app_id: &str) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("toplevel_app_id_changed", serialize(app_id).unwrap());
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
panel_item_client::toplevel_app_id_changed(&node, app_id);
|
||||
}
|
||||
pub fn toplevel_fullscreen_active(&self, active: bool) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("toplevel_fullscreen_active", serialize(active).unwrap());
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
panel_item_client::toplevel_fullscreen_active(&node, active);
|
||||
}
|
||||
pub fn toplevel_move_request(&self) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("toplevel_move_request", Vec::<u8>::new());
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
panel_item_client::toplevel_move_request(&node);
|
||||
}
|
||||
pub fn toplevel_resize_request(&self, up: bool, down: bool, left: bool, right: bool) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal(
|
||||
"toplevel_resize_request",
|
||||
serialize((up, down, left, right)).unwrap(),
|
||||
);
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
panel_item_client::toplevel_resize_request(&node, up, down, left, right);
|
||||
}
|
||||
pub fn toplevel_size_changed(&self, size: Vector2<u32>) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("toplevel_size_changed", serialize(size).unwrap());
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
panel_item_client::toplevel_size_changed(&node, size);
|
||||
}
|
||||
|
||||
pub fn set_cursor(&self, geometry: Option<Geometry>) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("set_cursor", serialize(geometry).unwrap());
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
if let Some(geometry) = geometry {
|
||||
panel_item_client::set_cursor(&node, &geometry);
|
||||
} else {
|
||||
panel_item_client::hide_cursor(&node);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_child(&self, uid: &str, info: ChildInfo) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("new_child", serialize((uid, info)).unwrap());
|
||||
pub fn create_child(&self, id: u64, info: &ChildInfo) {
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
panel_item_client::create_child(&node, id, info);
|
||||
}
|
||||
pub fn reposition_child(&self, uid: &str, geometry: Geometry) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("reposition_child", serialize((uid, geometry)).unwrap());
|
||||
pub fn reposition_child(&self, id: u64, geometry: &Geometry) {
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
panel_item_client::reposition_child(&node, id, geometry);
|
||||
}
|
||||
pub fn drop_child(&self, uid: &str) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
let _ = node.send_remote_signal("drop_child", serialize(uid).unwrap());
|
||||
pub fn destroy_child(&self, id: u64) {
|
||||
let Some(node) = self.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
panel_item_client::destroy_child(&node, id);
|
||||
}
|
||||
}
|
||||
// Local signals
|
||||
macro_rules! flex_no_args {
|
||||
($fn_name: ident, $trait_fn: ident) => {
|
||||
fn $fn_name(node: &Node, _calling_client: Arc<Client>, _message: Message) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(node) else { return Ok(()) };
|
||||
panel_item.$trait_fn();
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
impl<B: Backend> AspectIdentifier for PanelItem<B> {
|
||||
impl_aspect_for_panel_item_aspect_id! {}
|
||||
}
|
||||
macro_rules! flex_deserialize {
|
||||
($fn_name: ident, $trait_fn: ident) => {
|
||||
fn $fn_name(node: &Node, _calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(node) else { return Ok(()) };
|
||||
panel_item.$trait_fn(deserialize(message.as_ref())?);
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
impl<B: Backend> Aspect for PanelItem<B> {
|
||||
impl_aspect_for_panel_item_aspect! {}
|
||||
}
|
||||
impl<B: Backend + ?Sized> PanelItem<B> {
|
||||
fn apply_surface_material_flex(
|
||||
node: &Node,
|
||||
#[allow(unused)]
|
||||
impl<B: Backend> PanelItemAspect for PanelItem<B> {
|
||||
#[doc = "Apply the cursor as a material to a model."]
|
||||
fn apply_cursor_material(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
model_part: Arc<Node>,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
let model_part = model_part.get_aspect::<ModelPart>()?;
|
||||
|
||||
panel_item.backend().apply_cursor_material(&model_part);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc = "Apply a surface's visuals as a material to a model."]
|
||||
fn apply_surface_material(
|
||||
node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
surface: SurfaceId,
|
||||
model_part: Arc<Node>,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(node) else { return Ok(()) };
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SurfaceMaterialInfo<'a> {
|
||||
surface: SurfaceID,
|
||||
model_node_path: &'a str,
|
||||
}
|
||||
|
||||
let info: SurfaceMaterialInfo = deserialize(message.as_ref())?;
|
||||
|
||||
let model_node = calling_client
|
||||
.scenegraph
|
||||
.get_node(info.model_node_path)
|
||||
.ok_or_else(|| eyre!("Model node not found"))?;
|
||||
let Some(Drawable::ModelPart(model_part)) = model_node.drawable.get() else {bail!("Node is not a model")};
|
||||
debug!(?info, "Apply surface material");
|
||||
|
||||
panel_item.apply_surface_material(info.surface, model_part);
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
let model_part = model_part.get_aspect::<ModelPart>()?;
|
||||
|
||||
panel_item
|
||||
.backend()
|
||||
.apply_surface_material(surface, &model_part);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
flex_no_args!(close_toplevel_flex, close_toplevel);
|
||||
flex_no_args!(auto_size_toplevel_flex, auto_size_toplevel);
|
||||
flex_deserialize!(set_toplevel_size_changed_flex, set_toplevel_size);
|
||||
#[doc = "Try to close the toplevel.\n \n The panel item UI handler or panel item acceptor will drop the panel item if this succeeds."]
|
||||
fn close_toplevel(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item.backend().close_toplevel();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pointer_motion_flex(
|
||||
node: &Node,
|
||||
#[doc = "Request a resize of the surface to whatever size the 2D app wants."]
|
||||
fn auto_size_toplevel(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item.backend().auto_size_toplevel();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc = "Request a resize of the surface (in pixels)."]
|
||||
fn set_toplevel_size(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
size: mint::Vector2<u32>,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(node) else { return Ok(()) };
|
||||
|
||||
let (surface_id, position): (SurfaceID, Vector2<f32>) = deserialize(message.as_ref())?;
|
||||
debug!(?surface_id, ?position, "Pointer deactivate");
|
||||
|
||||
panel_item.pointer_motion(&surface_id, position);
|
||||
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item.backend().set_toplevel_size(size);
|
||||
Ok(())
|
||||
}
|
||||
fn pointer_button_flex(
|
||||
node: &Node,
|
||||
|
||||
#[doc = "Tell the toplevel to appear focused visually if true, or unfocused if false."]
|
||||
fn set_toplevel_focused_visuals(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
focused: bool,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(node) else { return Ok(()) };
|
||||
|
||||
let (surface_id, button, state): (SurfaceID, u32, u32) = deserialize(message.as_ref())?;
|
||||
debug!(?surface_id, button, state, "Pointer button");
|
||||
|
||||
panel_item.pointer_button(&surface_id, button, state != 0);
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item.backend().set_toplevel_focused_visuals(focused);
|
||||
Ok(())
|
||||
}
|
||||
fn pointer_scroll_flex(
|
||||
node: &Node,
|
||||
|
||||
#[doc = "Send an event to set the pointer's position (in pixels, relative to top-left of surface). This will activate the pointer."]
|
||||
fn pointer_motion(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
surface: SurfaceId,
|
||||
position: mint::Vector2<f32>,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(node) else { return Ok(()) };
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PointerScrollInfo {
|
||||
surface_id: SurfaceID,
|
||||
axis_continuous: Option<Vector2<f32>>,
|
||||
axis_discrete: Option<Vector2<f32>>,
|
||||
}
|
||||
let info: PointerScrollInfo = deserialize(message.as_ref())?;
|
||||
debug!(?info, "Pointer scroll");
|
||||
|
||||
panel_item.pointer_scroll(&info.surface_id, info.axis_continuous, info.axis_discrete);
|
||||
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item.backend().pointer_motion(&surface, position);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keyboard_keys_flex(
|
||||
node: &Node,
|
||||
#[doc = "Send an event to set a pointer button's state if the pointer's active. The `button` is from the `input_event_codes` crate (e.g. BTN_LEFT for left click)."]
|
||||
fn pointer_button(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
surface: SurfaceId,
|
||||
button: u32,
|
||||
pressed: bool,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(node) else { return Ok(()) };
|
||||
let (surface_id, keymap_id, keys): (SurfaceID, &str, Vec<i32>) =
|
||||
deserialize(message.as_ref())?;
|
||||
debug!(?keys, "Set keyboard key state");
|
||||
|
||||
panel_item.keyboard_keys(&surface_id, keymap_id, keys);
|
||||
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item
|
||||
.backend()
|
||||
.pointer_button(&surface, button, pressed);
|
||||
Ok(())
|
||||
}
|
||||
pub fn grab_keyboard(&self, sid: Option<SurfaceID>) {
|
||||
let Some(node) = self.node.upgrade() else {return};
|
||||
|
||||
let Ok(message) = serialize(sid) else {return};
|
||||
let _ = node.send_remote_signal("grab_keyboard", message);
|
||||
}
|
||||
|
||||
fn touch_down_flex(node: &Node, _calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(node) else { return Ok(()) };
|
||||
|
||||
let (surface_id, id, position): (SurfaceID, u32, Vector2<f32>) =
|
||||
deserialize(message.as_ref())?;
|
||||
debug!(?surface_id, id, ?position, "Touch down");
|
||||
|
||||
panel_item.touch_down(&surface_id, id, position);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn touch_move_flex(node: &Node, _calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(node) else { return Ok(()) };
|
||||
|
||||
let (id, position): (u32, Vector2<f32>) = deserialize(message.as_ref())?;
|
||||
debug!(?position, "Touch move");
|
||||
|
||||
panel_item.touch_move(id, position);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
flex_deserialize!(touch_up_flex, touch_up);
|
||||
flex_no_args!(reset_touches_flex, reset_touches);
|
||||
}
|
||||
impl<B: Backend + ?Sized> PanelItemTrait for PanelItem<B> {
|
||||
fn uid(&self) -> &str {
|
||||
&self.uid
|
||||
}
|
||||
|
||||
fn serialize_start_data(&self, id: &str) -> Result<Message> {
|
||||
Ok(serialize((id, self.start_data()?))?.into())
|
||||
}
|
||||
}
|
||||
impl<B: Backend + ?Sized> Backend for PanelItem<B> {
|
||||
fn start_data(&self) -> Result<PanelItemInitData> {
|
||||
self.backend.start_data()
|
||||
}
|
||||
|
||||
fn apply_surface_material(&self, surface: SurfaceID, model_part: &Arc<ModelPart>) {
|
||||
self.backend.apply_surface_material(surface, model_part)
|
||||
}
|
||||
|
||||
fn close_toplevel(&self) {
|
||||
self.backend.close_toplevel()
|
||||
}
|
||||
fn auto_size_toplevel(&self) {
|
||||
self.backend.auto_size_toplevel()
|
||||
}
|
||||
fn set_toplevel_size(&self, size: Vector2<u32>) {
|
||||
self.backend.set_toplevel_size(size)
|
||||
}
|
||||
fn set_toplevel_focused_visuals(&self, focused: bool) {
|
||||
self.backend.set_toplevel_focused_visuals(focused)
|
||||
}
|
||||
|
||||
fn pointer_motion(&self, surface: &SurfaceID, position: Vector2<f32>) {
|
||||
self.backend.pointer_motion(surface, position)
|
||||
}
|
||||
fn pointer_button(&self, surface: &SurfaceID, button: u32, pressed: bool) {
|
||||
self.backend.pointer_button(surface, button, pressed)
|
||||
}
|
||||
#[doc = "Send an event to scroll the pointer if it's active.\nScroll distance is a value in pixels corresponding to the `distance` the surface should be scrolled.\nScroll steps is a value in columns/rows corresponding to the wheel clicks of a mouse or such. This also supports fractions of a wheel click."]
|
||||
fn pointer_scroll(
|
||||
&self,
|
||||
surface: &SurfaceID,
|
||||
scroll_distance: Option<Vector2<f32>>,
|
||||
scroll_steps: Option<Vector2<f32>>,
|
||||
) {
|
||||
self.backend
|
||||
.pointer_scroll(surface, scroll_distance, scroll_steps)
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
surface: SurfaceId,
|
||||
scroll_distance: mint::Vector2<f32>,
|
||||
scroll_steps: mint::Vector2<f32>,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item
|
||||
.backend()
|
||||
.pointer_scroll(&surface, Some(scroll_distance), Some(scroll_steps));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keyboard_keys(&self, surface: &SurfaceID, keymap_id: &str, keys: Vec<i32>) {
|
||||
self.backend.keyboard_keys(surface, keymap_id, keys)
|
||||
#[doc = "Send an event to stop scrolling the pointer."]
|
||||
fn pointer_stop_scroll(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
surface: SurfaceId,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item.backend().pointer_scroll(&surface, None, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn touch_down(&self, surface: &SurfaceID, id: u32, position: Vector2<f32>) {
|
||||
self.backend.touch_down(surface, id, position)
|
||||
#[doc = "Send a series of key presses and releases (positive keycode for pressed, negative for released)."]
|
||||
fn keyboard_key(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
surface: SurfaceId,
|
||||
keymap_id: u64,
|
||||
key: u32,
|
||||
pressed: bool,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item
|
||||
.backend()
|
||||
.keyboard_key(&surface, keymap_id, key, pressed);
|
||||
Ok(())
|
||||
}
|
||||
fn touch_move(&self, id: u32, position: Vector2<f32>) {
|
||||
self.backend.touch_move(id, position)
|
||||
|
||||
#[doc = "Put a touch down on this surface with the unique ID `uid` at `position` (in pixels) from top left corner of the surface."]
|
||||
fn touch_down(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
surface: SurfaceId,
|
||||
uid: u32,
|
||||
position: mint::Vector2<f32>,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item.backend().touch_down(&surface, uid, position);
|
||||
Ok(())
|
||||
}
|
||||
fn touch_up(&self, id: u32) {
|
||||
self.backend.touch_up(id)
|
||||
|
||||
#[doc = "Move an existing touch point."]
|
||||
fn touch_move(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
uid: u32,
|
||||
position: mint::Vector2<f32>,
|
||||
) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item.backend().touch_move(uid, position);
|
||||
Ok(())
|
||||
}
|
||||
fn reset_touches(&self) {
|
||||
self.backend.reset_touches()
|
||||
|
||||
#[doc = "Release a touch from its surface."]
|
||||
fn touch_up(node: Arc<Node>, _calling_client: Arc<Client>, uid: u32) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item.backend().touch_up(uid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc = "Reset all input, such as pressed keys and pointer clicks and touches. Useful for when it's newly captured into an item acceptor to make sure no input gets stuck."]
|
||||
fn reset_input(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
||||
let Some(panel_item) = panel_item_from_node(&node) else {
|
||||
return Ok(());
|
||||
};
|
||||
panel_item.backend().reset_input();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl<B: Backend + ?Sized> Drop for PanelItem<B> {
|
||||
fn drop(&mut self) {
|
||||
// Dropped panel item, basically just a debug breakpoint place
|
||||
|
||||
pub struct PanelItemUi;
|
||||
impl AspectIdentifier for PanelItemUi {
|
||||
impl_aspect_for_panel_item_ui_aspect_id! {}
|
||||
}
|
||||
impl Aspect for PanelItemUi {
|
||||
impl_aspect_for_panel_item_ui_aspect! {}
|
||||
}
|
||||
impl PanelItemUiAspect for PanelItemUi {}
|
||||
|
||||
pub struct PanelItemAcceptor;
|
||||
impl AspectIdentifier for PanelItemAcceptor {
|
||||
impl_aspect_for_panel_item_acceptor_aspect_id! {}
|
||||
}
|
||||
impl Aspect for PanelItemAcceptor {
|
||||
impl_aspect_for_panel_item_acceptor_aspect! {}
|
||||
}
|
||||
impl PanelItemAcceptorAspect for PanelItemAcceptor {
|
||||
fn capture_item(node: Arc<Node>, _calling_client: Arc<Client>, item: Arc<Node>) -> Result<()> {
|
||||
super::acceptor_capture_item_flex(node, item)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Backend> PanelItemTrait for PanelItem<B> {
|
||||
fn backend(&self) -> &dyn Backend {
|
||||
self.backend.as_ref()
|
||||
}
|
||||
fn send_ui_item_created(&self, node: &Node, item: &Arc<Node>) {
|
||||
let Ok(init_data) = self.backend.start_data() else {
|
||||
return;
|
||||
};
|
||||
let _ = panel_item_ui_client::create_item(node, item, init_data);
|
||||
}
|
||||
fn send_acceptor_item_created(&self, node: &Node, item: &Arc<Node>) {
|
||||
let Ok(init_data) = self.backend.start_data() else {
|
||||
return;
|
||||
};
|
||||
let _ = panel_item_acceptor_client::capture_item(node, item, init_data);
|
||||
}
|
||||
}
|
||||
|
||||
impl InterfaceAspect for Interface {
|
||||
#[doc = "Register this client to manage the items of a certain type and create default 3D UI for them."]
|
||||
fn register_panel_item_ui(node: Arc<Node>, calling_client: Arc<Client>) -> Result<()> {
|
||||
node.add_aspect(CameraItemAcceptor);
|
||||
register_item_ui_flex(calling_client, &ITEM_TYPE_INFO_PANEL)
|
||||
}
|
||||
|
||||
#[doc = "Create an item acceptor to allow temporary ownership of a given type of item. Creates a node at `/item/<item_type>/acceptor/<name>`."]
|
||||
fn create_panel_item_acceptor(
|
||||
node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
field: Arc<Node>,
|
||||
) -> Result<()> {
|
||||
node.add_aspect(PanelItemAcceptor);
|
||||
create_item_acceptor_flex(
|
||||
calling_client,
|
||||
id,
|
||||
parent,
|
||||
transform,
|
||||
&ITEM_TYPE_INFO_PANEL,
|
||||
field,
|
||||
)
|
||||
}
|
||||
|
||||
async fn register_keymap(
|
||||
_node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
keymap: String,
|
||||
) -> Result<u64> {
|
||||
let mut keymaps = KEYMAPS.lock();
|
||||
if let Some(found_keymap_id) = keymaps
|
||||
.iter()
|
||||
.filter(|(_k, v)| *v == &keymap)
|
||||
.map(|(k, _v)| k)
|
||||
.last()
|
||||
{
|
||||
return Ok(found_keymap_id.data().as_ffi());
|
||||
}
|
||||
|
||||
let key = keymaps.insert(keymap);
|
||||
Ok(key.data().as_ffi())
|
||||
}
|
||||
|
||||
async fn get_keymap(
|
||||
_node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
keymap_id: u64,
|
||||
) -> Result<String> {
|
||||
let keymaps = KEYMAPS.lock();
|
||||
let Some(keymap) = keymaps.get(KeyData::from_ffi(keymap_id).into()) else {
|
||||
bail!("Could not find keymap. Try registering it");
|
||||
};
|
||||
|
||||
Ok(keymap.clone())
|
||||
}
|
||||
}
|
||||
|
||||
377
src/nodes/mod.rs
377
src/nodes/mod.rs
@@ -1,42 +1,29 @@
|
||||
pub mod alias;
|
||||
pub mod audio;
|
||||
pub mod data;
|
||||
pub mod drawable;
|
||||
pub mod fields;
|
||||
pub mod hmd;
|
||||
pub mod input;
|
||||
pub mod items;
|
||||
pub mod root;
|
||||
pub mod spatial;
|
||||
|
||||
use color_eyre::eyre::{eyre, Result};
|
||||
use nanoid::nanoid;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use portable_atomic::{AtomicBool, Ordering};
|
||||
use rustc_hash::FxHashMap;
|
||||
use stardust_xr::messenger::MessageSenderHandle;
|
||||
use stardust_xr::scenegraph::ScenegraphError;
|
||||
use stardust_xr::schemas::flex::deserialize;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::vec::Vec;
|
||||
|
||||
use self::alias::Alias;
|
||||
use crate::core::client::Client;
|
||||
use crate::core::error::{Result, ServerError};
|
||||
use crate::core::registry::Registry;
|
||||
use crate::core::scenegraph::MethodResponseSender;
|
||||
|
||||
use self::alias::Alias;
|
||||
use self::audio::Sound;
|
||||
use self::data::{PulseReceiver, PulseSender};
|
||||
use self::drawable::Drawable;
|
||||
use self::fields::Field;
|
||||
use self::input::{InputHandler, InputMethod};
|
||||
use self::items::{Item, ItemAcceptor, ItemUI};
|
||||
use self::spatial::zone::Zone;
|
||||
use self::spatial::Spatial;
|
||||
use dashmap::DashMap;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use spatial::Spatial;
|
||||
use stardust_xr::messenger::MessageSenderHandle;
|
||||
use stardust_xr::scenegraph::ScenegraphError;
|
||||
use stardust_xr::schemas::flex::{deserialize, serialize};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::fmt::Debug;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::vec::Vec;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Message {
|
||||
@@ -57,118 +44,108 @@ impl AsRef<[u8]> for Message {
|
||||
}
|
||||
}
|
||||
|
||||
pub type Signal = fn(&Node, Arc<Client>, Message) -> Result<()>;
|
||||
pub type Method = fn(&Node, Arc<Client>, Message, MethodResponseSender);
|
||||
stardust_xr_server_codegen::codegen_node_protocol!();
|
||||
|
||||
pub struct Node {
|
||||
pub enabled: Arc<AtomicBool>,
|
||||
pub(super) uid: String,
|
||||
path: String,
|
||||
client: Weak<Client>,
|
||||
message_sender_handle: Option<MessageSenderHandle>,
|
||||
// trailing_slash_pos: usize,
|
||||
local_signals: Mutex<FxHashMap<String, Signal>>,
|
||||
local_methods: Mutex<FxHashMap<String, Method>>,
|
||||
destroyable: bool,
|
||||
pub struct Owned;
|
||||
impl AspectIdentifier for Owned {
|
||||
impl_aspect_for_owned_aspect_id! {}
|
||||
}
|
||||
impl Aspect for Owned {
|
||||
impl_aspect_for_owned_aspect! {}
|
||||
}
|
||||
impl OwnedAspect for Owned {
|
||||
fn set_enabled(node: Arc<Node>, _calling_client: Arc<Client>, enabled: bool) -> Result<()> {
|
||||
node.set_enabled(enabled);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub alias: OnceCell<Arc<Alias>>,
|
||||
aliases: Registry<Alias>,
|
||||
|
||||
pub spatial: OnceCell<Arc<Spatial>>,
|
||||
pub field: OnceCell<Arc<Field>>,
|
||||
pub zone: OnceCell<Arc<Zone>>,
|
||||
|
||||
// Data
|
||||
pub pulse_sender: OnceCell<Arc<PulseSender>>,
|
||||
pub pulse_receiver: OnceCell<Arc<PulseReceiver>>,
|
||||
|
||||
// Drawable
|
||||
pub drawable: OnceCell<Drawable>,
|
||||
|
||||
// Input
|
||||
pub input_method: OnceCell<Arc<InputMethod>>,
|
||||
pub input_handler: OnceCell<Arc<InputHandler>>,
|
||||
|
||||
// Item
|
||||
pub item: OnceCell<Arc<Item>>,
|
||||
pub item_acceptor: OnceCell<Arc<ItemAcceptor>>,
|
||||
pub item_ui: OnceCell<Arc<ItemUI>>,
|
||||
|
||||
// Sound
|
||||
pub sound: OnceCell<Arc<Sound>>,
|
||||
fn destroy(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
||||
if node.destroyable {
|
||||
node.destroy();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OwnedNode(pub Arc<Node>);
|
||||
impl Drop for OwnedNode {
|
||||
fn drop(&mut self) {
|
||||
self.0.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Node {
|
||||
enabled: AtomicBool,
|
||||
id: u64,
|
||||
client: Weak<Client>,
|
||||
message_sender_handle: Option<MessageSenderHandle>,
|
||||
|
||||
aliases: Registry<Alias>,
|
||||
aspects: Aspects,
|
||||
destroyable: bool,
|
||||
}
|
||||
impl Node {
|
||||
pub fn get_client(&self) -> Option<Arc<Client>> {
|
||||
self.client.upgrade()
|
||||
}
|
||||
// pub fn get_name(&self) -> &str {
|
||||
// &self.path[self.trailing_slash_pos + 1..]
|
||||
// }
|
||||
pub fn get_path(&self) -> &str {
|
||||
self.path.as_str()
|
||||
pub fn get_id(&self) -> u64 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn create(client: &Arc<Client>, parent: &str, name: &str, destroyable: bool) -> Self {
|
||||
let mut path = parent.to_string();
|
||||
path.push('/');
|
||||
path.push_str(name);
|
||||
pub fn generate(client: &Arc<Client>, destroyable: bool) -> Self {
|
||||
Self::from_id(client, client.generate_id(), destroyable)
|
||||
}
|
||||
pub fn from_id(client: &Arc<Client>, id: u64, destroyable: bool) -> Self {
|
||||
let node = Node {
|
||||
enabled: Arc::new(AtomicBool::new(true)),
|
||||
uid: nanoid!(),
|
||||
enabled: AtomicBool::new(true),
|
||||
client: Arc::downgrade(client),
|
||||
message_sender_handle: client.message_sender_handle.clone(),
|
||||
path,
|
||||
// trailing_slash_pos: parent.len(),
|
||||
local_signals: Default::default(),
|
||||
local_methods: Default::default(),
|
||||
id,
|
||||
aliases: Default::default(),
|
||||
aspects: Default::default(),
|
||||
destroyable,
|
||||
|
||||
alias: OnceCell::new(),
|
||||
aliases: Registry::new(),
|
||||
|
||||
spatial: OnceCell::new(),
|
||||
field: OnceCell::new(),
|
||||
zone: OnceCell::new(),
|
||||
pulse_sender: OnceCell::new(),
|
||||
pulse_receiver: OnceCell::new(),
|
||||
drawable: OnceCell::new(),
|
||||
input_method: OnceCell::new(),
|
||||
input_handler: OnceCell::new(),
|
||||
item: OnceCell::new(),
|
||||
item_acceptor: OnceCell::new(),
|
||||
item_ui: OnceCell::new(),
|
||||
sound: OnceCell::new(),
|
||||
};
|
||||
node.add_local_signal("set_enabled", Node::set_enabled_flex);
|
||||
node.add_local_signal("destroy", Node::destroy_flex);
|
||||
node.aspects.add(Owned);
|
||||
node
|
||||
}
|
||||
pub fn add_to_scenegraph(self) -> Result<Arc<Node>> {
|
||||
Ok(self
|
||||
.get_client()
|
||||
.ok_or_else(|| eyre!("Internal: Unable to get client"))?
|
||||
.ok_or(ServerError::NoClient)?
|
||||
.scenegraph
|
||||
.add_node(self))
|
||||
}
|
||||
pub fn add_to_scenegraph_owned(self) -> Result<OwnedNode> {
|
||||
Ok(OwnedNode(
|
||||
self.get_client()
|
||||
.ok_or(ServerError::NoClient)?
|
||||
.scenegraph
|
||||
.add_node(self),
|
||||
))
|
||||
}
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.enabled.load(Ordering::Relaxed)
|
||||
&& if let Ok(spatial) = self.get_aspect::<Spatial>() {
|
||||
spatial.visible()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
pub fn set_enabled(&self, enabled: bool) {
|
||||
self.enabled.store(enabled, Ordering::Relaxed);
|
||||
if let Ok(spatial) = self.get_aspect::<Spatial>() {
|
||||
spatial.mark_dirty();
|
||||
}
|
||||
}
|
||||
pub fn destroy(&self) {
|
||||
if let Some(client) = self.get_client() {
|
||||
client.scenegraph.remove_node(self.get_path());
|
||||
client.scenegraph.remove_node(self.get_id());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_enabled_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
node.enabled
|
||||
.store(deserialize(message.as_ref())?, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
// very much up for debate if we should allow this, as you can match objects using this
|
||||
// pub fn get_client_pid_flex(
|
||||
// node: &Node,
|
||||
// node: Arc<Node>,
|
||||
// _calling_client: Arc<Client>,
|
||||
// _message: Message,
|
||||
// ) -> Result<Message> {
|
||||
@@ -179,83 +156,67 @@ impl Node {
|
||||
// let pid = client.pid.ok_or_else(|| eyre!("Client PID is unknown"))?;
|
||||
// Ok(serialize(pid)?.into())
|
||||
// }
|
||||
pub fn destroy_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
_message: Message,
|
||||
) -> Result<()> {
|
||||
if node.destroyable {
|
||||
node.destroy();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_local_signal(&self, name: &str, signal: Signal) {
|
||||
self.local_signals.lock().insert(name.to_string(), signal);
|
||||
pub fn add_aspect<A: AspectIdentifier>(&self, aspect: A) -> Arc<A> {
|
||||
self.aspects.add(aspect)
|
||||
}
|
||||
pub fn add_local_method(&self, name: &str, method: Method) {
|
||||
self.local_methods.lock().insert(name.to_string(), method);
|
||||
pub fn add_aspect_raw<A: AspectIdentifier>(&self, aspect: Arc<A>) {
|
||||
self.aspects.add_raw(aspect)
|
||||
}
|
||||
|
||||
pub fn get_aspect<F, T>(
|
||||
&self,
|
||||
node_name: &'static str,
|
||||
aspect_type: &'static str,
|
||||
aspect_fn: F,
|
||||
) -> Result<&T>
|
||||
where
|
||||
F: FnOnce(&Node) -> &OnceCell<T>,
|
||||
{
|
||||
aspect_fn(self)
|
||||
.get()
|
||||
.ok_or_else(|| eyre!("{} is not a {} node", node_name, aspect_type))
|
||||
pub fn get_aspect<A: AspectIdentifier>(&self) -> Result<Arc<A>> {
|
||||
self.aspects.get()
|
||||
}
|
||||
|
||||
pub fn send_local_signal(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
calling_client: Arc<Client>,
|
||||
method: &str,
|
||||
aspect_id: u64,
|
||||
method: u64,
|
||||
message: Message,
|
||||
) -> Result<(), ScenegraphError> {
|
||||
if let Some(alias) = self.alias.get() {
|
||||
if !alias.info.server_signals.iter().any(|e| e == &method) {
|
||||
return Err(ScenegraphError::SignalNotFound);
|
||||
if let Ok(alias) = self.get_aspect::<Alias>() {
|
||||
if !alias.info.server_signals.contains(&method) {
|
||||
return Err(ScenegraphError::MemberNotFound);
|
||||
}
|
||||
alias
|
||||
.original
|
||||
.upgrade()
|
||||
.ok_or(ScenegraphError::BrokenAlias)?
|
||||
.send_local_signal(calling_client, method, message)
|
||||
.send_local_signal(calling_client, aspect_id, method, message)
|
||||
} else {
|
||||
let signal = self
|
||||
.local_signals
|
||||
.lock()
|
||||
.get(method)
|
||||
.cloned()
|
||||
.ok_or(ScenegraphError::SignalNotFound)?;
|
||||
signal(self, calling_client, message).map_err(|error| ScenegraphError::SignalError {
|
||||
error: error.to_string(),
|
||||
})
|
||||
let aspect = self
|
||||
.aspects
|
||||
.0
|
||||
.get(&aspect_id)
|
||||
.ok_or(ScenegraphError::AspectNotFound)?
|
||||
.clone();
|
||||
aspect
|
||||
.run_signal(calling_client, self.clone(), method, message)
|
||||
.map_err(|error| ScenegraphError::MemberError {
|
||||
error: error.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
pub fn execute_local_method(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
calling_client: Arc<Client>,
|
||||
method: &str,
|
||||
aspect_id: u64,
|
||||
method: u64,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
if let Some(alias) = self.alias.get() {
|
||||
if !alias.info.server_methods.iter().any(|e| e == &method) {
|
||||
response.send(Err(ScenegraphError::MethodNotFound));
|
||||
if let Ok(alias) = self.get_aspect::<Alias>() {
|
||||
if !alias.info.server_methods.contains(&method) {
|
||||
response.send_err(ScenegraphError::MemberNotFound);
|
||||
return;
|
||||
}
|
||||
let Some(alias) = alias.original.upgrade() else {
|
||||
response.send(Err(ScenegraphError::BrokenAlias));
|
||||
response.send_err(ScenegraphError::BrokenAlias);
|
||||
return;
|
||||
};
|
||||
alias.execute_local_method(
|
||||
calling_client,
|
||||
aspect_id,
|
||||
method,
|
||||
Message {
|
||||
data: message.data.clone(),
|
||||
@@ -264,14 +225,19 @@ impl Node {
|
||||
response,
|
||||
)
|
||||
} else {
|
||||
let Some(method) = self.local_methods.lock().get(method).cloned() else {
|
||||
response.send(Err(ScenegraphError::MethodNotFound));
|
||||
let Some(aspect) = self.aspects.0.get(&aspect_id).map(|v| v.clone()) else {
|
||||
response.send_err(ScenegraphError::AspectNotFound);
|
||||
return;
|
||||
};
|
||||
method(self, calling_client, message, response);
|
||||
aspect.run_method(calling_client, self.clone(), method, message, response);
|
||||
}
|
||||
}
|
||||
pub fn send_remote_signal(&self, method: &str, message: impl Into<Message>) -> Result<()> {
|
||||
pub fn send_remote_signal(
|
||||
&self,
|
||||
aspect_id: u64,
|
||||
method: u64,
|
||||
message: impl Into<Message>,
|
||||
) -> Result<()> {
|
||||
let message = message.into();
|
||||
self.aliases
|
||||
.get_valid_contents()
|
||||
@@ -281,6 +247,7 @@ impl Node {
|
||||
.for_each(|node| {
|
||||
// Beware! file descriptors will not be sent to aliases!!!
|
||||
let _ = node.send_remote_signal(
|
||||
aspect_id,
|
||||
method,
|
||||
Message {
|
||||
data: message.data.clone(),
|
||||
@@ -288,43 +255,39 @@ impl Node {
|
||||
},
|
||||
);
|
||||
});
|
||||
let path = self.path.clone();
|
||||
let method = method.to_string();
|
||||
if let Some(handle) = self.message_sender_handle.as_ref() {
|
||||
handle.signal(path.as_str(), method.as_str(), &message.data, message.fds)?;
|
||||
handle.signal(self.id, aspect_id, method, &message.data, message.fds)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn execute_remote_method(
|
||||
pub async fn execute_remote_method_typed<S: Serialize, D: DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
message: impl Into<Message>,
|
||||
) -> Result<impl Future<Output = Result<Message>>> {
|
||||
let message = message.into();
|
||||
aspect_id: u64,
|
||||
method: u64,
|
||||
input: S,
|
||||
fds: Vec<OwnedFd>,
|
||||
) -> Result<(D, Vec<OwnedFd>)> {
|
||||
let message_sender_handle = self
|
||||
.message_sender_handle
|
||||
.as_ref()
|
||||
.ok_or(eyre!("Messenger does not exist for this node"))?;
|
||||
.ok_or(ServerError::NoMessenger)?;
|
||||
|
||||
let future =
|
||||
message_sender_handle.method(self.path.as_str(), method, &message.data, message.fds)?;
|
||||
let serialized = serialize(input)?;
|
||||
let result = message_sender_handle
|
||||
.method(self.id, aspect_id, method, &serialized, fds)
|
||||
.await?
|
||||
.map_err(ServerError::RemoteMethodError)?;
|
||||
|
||||
Ok(async {
|
||||
match future.await {
|
||||
Ok(m) => {
|
||||
let (data, fds) = m.into_components();
|
||||
Ok(Message { data, fds })
|
||||
}
|
||||
Err(e) => Err(eyre!(e)),
|
||||
}
|
||||
})
|
||||
let (message, fds) = result.into_components();
|
||||
let deserialized: D = deserialize(&message)?;
|
||||
Ok((deserialized, fds))
|
||||
}
|
||||
}
|
||||
impl Debug for Node {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Node")
|
||||
.field("uid", &self.uid)
|
||||
.field("path", &self.path)
|
||||
.field("id", &self.id)
|
||||
.field("destroyable", &self.destroyable)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -333,3 +296,51 @@ impl Drop for Node {
|
||||
// Debug breakpoint
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AspectIdentifier: Aspect {
|
||||
const ID: u64;
|
||||
}
|
||||
pub trait Aspect: Any + Send + Sync + 'static {
|
||||
fn run_signal(
|
||||
&self,
|
||||
calling_client: Arc<Client>,
|
||||
node: Arc<Node>,
|
||||
signal: u64,
|
||||
message: Message,
|
||||
) -> Result<(), stardust_xr::scenegraph::ScenegraphError>;
|
||||
fn run_method(
|
||||
&self,
|
||||
calling_client: Arc<Client>,
|
||||
node: Arc<Node>,
|
||||
method: u64,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Aspects(DashMap<u64, Arc<dyn Aspect>>);
|
||||
impl Aspects {
|
||||
fn add<A: AspectIdentifier>(&self, t: A) -> Arc<A> {
|
||||
let aspect = Arc::new(t);
|
||||
self.add_raw(aspect.clone());
|
||||
aspect
|
||||
}
|
||||
fn add_raw<A: AspectIdentifier>(&self, aspect: Arc<A>) {
|
||||
self.0.insert(A::ID, aspect);
|
||||
}
|
||||
fn get<A: Aspect + AspectIdentifier>(&self) -> Result<Arc<A>> {
|
||||
self.0
|
||||
.get(&A::ID)
|
||||
// .cloned doesn't work for some reason
|
||||
.map(|v| v.clone())
|
||||
.and_then(|a| Arc::downcast(a).ok())
|
||||
.ok_or(ServerError::NoAspect(TypeId::of::<A>()))
|
||||
}
|
||||
}
|
||||
impl Drop for Aspects {
|
||||
fn drop(&mut self) {
|
||||
// why would this be needed? do drop impls not run otherwise?
|
||||
self.0.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,138 +1,105 @@
|
||||
use super::spatial::Spatial;
|
||||
use super::{Message, Node};
|
||||
use crate::core::client::Client;
|
||||
use crate::core::client_state::{ClientState, ClientStateInternal};
|
||||
use crate::core::registry::Registry;
|
||||
use crate::core::scenegraph::MethodResponseSender;
|
||||
use crate::wayland::WAYLAND_DISPLAY;
|
||||
use crate::STARDUST_INSTANCE;
|
||||
use color_eyre::eyre::Result;
|
||||
use super::{Aspect, AspectIdentifier, Node};
|
||||
use crate::bail;
|
||||
use crate::core::client::{CLIENTS, Client};
|
||||
use crate::core::client_state::ClientStateParsed;
|
||||
use crate::core::error::Result;
|
||||
use crate::nodes::spatial::SPATIAL_REF_ASPECT_ALIAS_INFO;
|
||||
use crate::session::connection_env;
|
||||
use glam::Mat4;
|
||||
use rustc_hash::FxHashMap;
|
||||
use stardust_xr::schemas::flex::{deserialize, serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
use std::future::Future;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tracing::info;
|
||||
|
||||
static ROOT_REGISTRY: Registry<Root> = Registry::new();
|
||||
stardust_xr_server_codegen::codegen_root_protocol!();
|
||||
|
||||
pub struct Root {
|
||||
pub node: Arc<Node>,
|
||||
send_frame_event: AtomicBool,
|
||||
node: Arc<Node>,
|
||||
connect_instant: Instant,
|
||||
}
|
||||
impl Root {
|
||||
pub fn create(client: &Arc<Client>) -> Result<Arc<Self>> {
|
||||
let node = Node::create(client, "", "", false);
|
||||
node.add_local_signal("subscribe_frame", Root::subscribe_frame_flex);
|
||||
node.add_local_signal("set_base_prefixes", Root::set_base_prefixes_flex);
|
||||
node.add_local_method("state_token", Root::state_token_flex);
|
||||
node.add_local_method(
|
||||
"get_connection_environment",
|
||||
get_connection_environment_flex,
|
||||
);
|
||||
pub fn create(client: &Arc<Client>, transform: Mat4) -> Result<Arc<Self>> {
|
||||
let node = Node::from_id(client, 0, false);
|
||||
let node = node.add_to_scenegraph()?;
|
||||
let _ = Spatial::add_to(&node, None, client.state.root, false);
|
||||
|
||||
Ok(ROOT_REGISTRY.add(Root {
|
||||
node,
|
||||
send_frame_event: AtomicBool::from(false),
|
||||
}))
|
||||
let _ = Spatial::add_to(&node, None, transform, false);
|
||||
let root_aspect = node.add_aspect(Root {
|
||||
node: node.clone(),
|
||||
connect_instant: Instant::now(),
|
||||
});
|
||||
Ok(root_aspect)
|
||||
}
|
||||
|
||||
fn subscribe_frame_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
_message: Message,
|
||||
) -> Result<()> {
|
||||
calling_client
|
||||
.root
|
||||
.get()
|
||||
.unwrap()
|
||||
.send_frame_event
|
||||
.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
pub fn send_frame_events(delta: f64) {
|
||||
if let Ok(data) = serialize((delta, 0.0)) {
|
||||
for root in ROOT_REGISTRY.get_valid_contents() {
|
||||
if root.send_frame_event.load(Ordering::Relaxed) {
|
||||
let _ = root.node.send_remote_signal("frame", data.clone());
|
||||
}
|
||||
}
|
||||
for client in CLIENTS.get_vec() {
|
||||
let Some(root) = client.root.get() else {
|
||||
continue;
|
||||
};
|
||||
let _ = root_client::frame(
|
||||
&root.node,
|
||||
&FrameInfo {
|
||||
delta: delta as f32,
|
||||
elapsed: root.connect_instant.elapsed().as_secs_f32(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_base_prefixes_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
*calling_client.base_resource_prefixes.lock() = deserialize(message.as_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn state_token_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(|| {
|
||||
let state: ClientStateInternal = deserialize(message.as_ref())?;
|
||||
let token = ClientState::from_deserialized(&calling_client, state).token();
|
||||
Ok(serialize(token)?.into())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_transform(&self, transform: Mat4) {
|
||||
let spatial = self.node.spatial.get().unwrap();
|
||||
spatial.set_spatial_parent(None).unwrap();
|
||||
let spatial = self.node.get_aspect::<Spatial>().unwrap();
|
||||
// spatial.set_spatial_parent(None).unwrap();
|
||||
spatial.set_local_transform(transform);
|
||||
}
|
||||
pub fn save_state(&self) -> impl Future<Output = Result<ClientStateInternal>> {
|
||||
let future = self
|
||||
.node
|
||||
.execute_remote_method("save_state", Message::default());
|
||||
async move { Ok(deserialize(&future?.await?.data)?) }
|
||||
pub async fn save_state(&self) -> Result<ClientState> {
|
||||
Ok(root_client::save_state(&self.node).await?.0)
|
||||
}
|
||||
}
|
||||
impl AspectIdentifier for Root {
|
||||
impl_aspect_for_root_aspect_id! {}
|
||||
}
|
||||
impl Aspect for Root {
|
||||
impl_aspect_for_root_aspect! {}
|
||||
}
|
||||
impl RootAspect for Root {
|
||||
async fn get_state(_node: Arc<Node>, calling_client: Arc<Client>) -> Result<ClientState> {
|
||||
let Some(state) = calling_client.state.get() else {
|
||||
bail!("Couldn't get state");
|
||||
};
|
||||
Ok(state.clone())
|
||||
}
|
||||
|
||||
impl Drop for Root {
|
||||
fn drop(&mut self) {
|
||||
ROOT_REGISTRY.remove(self);
|
||||
#[doc = "Get a hashmap of all the environment variables to connect a given app to the stardust server"]
|
||||
async fn get_connection_environment(
|
||||
_node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
) -> Result<stardust_xr::values::Map<String, String>> {
|
||||
Ok(connection_env())
|
||||
}
|
||||
|
||||
#[doc = "Generate a client state token and return it back.\n\n When launching a new client, set the environment variable `STARDUST_STARTUP_TOKEN` to the returned string.\n Make sure the environment variable shows in `/proc/{pid}/environ` as that's the only reliable way to pass the value to the server (suggestions welcome).\n"]
|
||||
async fn generate_state_token(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
state: ClientState,
|
||||
) -> Result<String> {
|
||||
Ok(ClientStateParsed::from_deserialized(&calling_client, state).token())
|
||||
}
|
||||
|
||||
#[doc = "Set initial list of folders to look for namespaced resources in"]
|
||||
fn set_base_prefixes(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
prefixes: Vec<String>,
|
||||
) -> Result<()> {
|
||||
info!(?calling_client, ?prefixes, "Set base prefixes");
|
||||
*calling_client.base_resource_prefixes.lock() =
|
||||
prefixes.into_iter().map(PathBuf::from).collect();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc = "Cleanly disconnect from the server"]
|
||||
fn disconnect(_node: Arc<Node>, calling_client: Arc<Client>) -> Result<()> {
|
||||
calling_client.disconnect(Ok(()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! var_env_insert {
|
||||
($env:ident, $name:ident) => {
|
||||
$env.insert(stringify!($name).to_string(), $name.get().unwrap().clone());
|
||||
};
|
||||
}
|
||||
pub fn get_connection_environment_flex(
|
||||
_node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
_message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
let mut env: FxHashMap<String, String> = FxHashMap::default();
|
||||
var_env_insert!(env, STARDUST_INSTANCE);
|
||||
#[cfg(feature = "wayland")]
|
||||
{
|
||||
var_env_insert!(env, WAYLAND_DISPLAY);
|
||||
#[cfg(feature = "xwayland")]
|
||||
var_env_insert!(env, DISPLAY);
|
||||
env.insert("GDK_BACKEND".to_string(), "wayland".to_string());
|
||||
env.insert("QT_QPA_PLATFORM".to_string(), "wayland".to_string());
|
||||
env.insert("MOZ_ENABLE_WAYLAND".to_string(), "1".to_string());
|
||||
env.insert("CLUTTER_BACKEND".to_string(), "wayland".to_string());
|
||||
env.insert("SDL_VIDEODRIVER".to_string(), "wayland".to_string());
|
||||
}
|
||||
|
||||
Ok(serialize(env)?.into())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,79 +1,181 @@
|
||||
pub mod zone;
|
||||
|
||||
use self::zone::{create_zone_flex, Zone};
|
||||
use super::{Message, Node};
|
||||
use self::zone::Zone;
|
||||
use super::alias::Alias;
|
||||
use super::fields::{Field, FieldTrait};
|
||||
use super::{Aspect, AspectIdentifier};
|
||||
use crate::bail;
|
||||
use crate::core::client::Client;
|
||||
use crate::core::entity_handle::EntityHandle;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::registry::Registry;
|
||||
use crate::core::scenegraph::MethodResponseSender;
|
||||
use color_eyre::eyre::{ensure, eyre, Result};
|
||||
use glam::{vec3a, Mat4, Quat};
|
||||
use crate::nodes::{Node, OWNED_ASPECT_ALIAS_INFO};
|
||||
use bevy::ecs::entity::EntityHashMap;
|
||||
use bevy::prelude::Transform as BevyTransform;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::primitives::Aabb;
|
||||
use color_eyre::eyre::OptionExt;
|
||||
use glam::{Mat4, Quat, Vec3, vec3a};
|
||||
use mint::Vector3;
|
||||
use nanoid::nanoid;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::schemas::flex::{deserialize, serialize};
|
||||
use stardust_xr::values::Transform;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::ptr;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{Arc, OnceLock, Weak};
|
||||
use stereokit::{bounds_grow_to_fit_box, Bounds};
|
||||
use std::{f32, ptr};
|
||||
|
||||
pub struct SpatialNodePlugin;
|
||||
impl Plugin for SpatialNodePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
(spawn_spatial_nodes, update_spatial_nodes)
|
||||
.chain()
|
||||
.before(TransformSystem::TransformPropagate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_spatial_nodes(mut cmds: Commands) {
|
||||
for spatial in SPATIAL_REGISTRY
|
||||
.get_valid_contents()
|
||||
.into_iter()
|
||||
.filter(|v| v.entity.read().is_none())
|
||||
{
|
||||
let entity = cmds
|
||||
.spawn((SpatialNode(Arc::downgrade(&spatial)), Name::new("Spatial")))
|
||||
.id();
|
||||
spatial.set_entity(EntityHandle::new(entity));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_spatial_nodes(
|
||||
mut query: Query<(&mut BevyTransform, &mut Visibility, Option<&ChildOf>)>,
|
||||
mut cmds: Commands,
|
||||
) {
|
||||
for (entity, (transform, parent_entity)) in UPDATED_SPATIALS_NODES.lock().drain() {
|
||||
let _span = debug_span!("updating spatial node").entered();
|
||||
let Ok((mut bevy_transform, mut vis, parent)) = query.get_mut(entity) else {
|
||||
continue;
|
||||
};
|
||||
// Set visibility based on node enabled state
|
||||
if let Some(transform) = transform {
|
||||
*vis = Visibility::Inherited;
|
||||
*bevy_transform = transform;
|
||||
} else {
|
||||
*vis = Visibility::Hidden;
|
||||
}
|
||||
|
||||
if parent.map(|v| v.0) != parent_entity {
|
||||
match parent_entity {
|
||||
Some(e) => cmds.entity(entity).insert(ChildOf(e)),
|
||||
None => cmds.entity(entity).remove::<ChildOf>(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static SPATIAL_REGISTRY: Registry<Spatial> = Registry::new();
|
||||
|
||||
#[derive(Clone, Component, Debug)]
|
||||
#[require(BevyTransform, Visibility)]
|
||||
pub struct SpatialNode(pub Weak<Spatial>);
|
||||
|
||||
const EPSILON: f32 = 0.00001;
|
||||
|
||||
stardust_xr_server_codegen::codegen_spatial_protocol!();
|
||||
impl Transform {
|
||||
pub fn to_mat4(&self, position: bool, rotation: bool, scale: bool) -> Mat4 {
|
||||
let position = position
|
||||
.then_some(self.translation)
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Vector3::from([0.0; 3]));
|
||||
let rotation = rotation
|
||||
.then_some(self.rotation)
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Quat::IDENTITY.into());
|
||||
|
||||
// Zero scale values break everything
|
||||
let scale = scale
|
||||
.then_some(self.scale)
|
||||
.flatten()
|
||||
.map(|s| Vector3 {
|
||||
x: if s.x == 0.0 { EPSILON } else { s.x },
|
||||
y: if s.y == 0.0 { EPSILON } else { s.y },
|
||||
z: if s.z == 0.0 { EPSILON } else { s.z },
|
||||
})
|
||||
.unwrap_or_else(|| Vector3::from([1.0; 3]));
|
||||
|
||||
Mat4::from_scale_rotation_translation(scale.into(), rotation.into(), position.into())
|
||||
}
|
||||
}
|
||||
impl AspectIdentifier for Zone {
|
||||
impl_aspect_for_zone_aspect_id! {}
|
||||
}
|
||||
impl Aspect for Zone {
|
||||
impl_aspect_for_zone_aspect! {}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref EXPORTED_SPATIALS: Mutex<FxHashMap<u64, Weak<Node>>> = Mutex::new(FxHashMap::default());
|
||||
}
|
||||
|
||||
static ZONEABLE_REGISTRY: Registry<Spatial> = Registry::new();
|
||||
|
||||
pub struct Spatial {
|
||||
uid: String,
|
||||
pub(super) node: Weak<Node>,
|
||||
self_ref: Weak<Spatial>,
|
||||
parent: Mutex<Option<Arc<Spatial>>>,
|
||||
old_parent: Mutex<Option<Arc<Spatial>>>,
|
||||
pub(super) transform: Mutex<Mat4>,
|
||||
zone: Mutex<Weak<Zone>>,
|
||||
pub node: Weak<Node>,
|
||||
entity: RwLock<Option<EntityHandle>>,
|
||||
parent: RwLock<Option<Arc<Spatial>>>,
|
||||
old_parent: RwLock<Option<Arc<Spatial>>>,
|
||||
transform: RwLock<Mat4>,
|
||||
zone: RwLock<Weak<Zone>>,
|
||||
children: Registry<Spatial>,
|
||||
pub(super) bounding_box_calc: OnceLock<fn(&Node) -> Bounds>,
|
||||
pub bounding_box_calc:
|
||||
OnceLock<for<'a> fn(&'a Node) -> Pin<Box<dyn Future<Output = Aabb> + 'a + Send + Sync>>>,
|
||||
}
|
||||
|
||||
impl Spatial {
|
||||
pub fn new(node: Weak<Node>, parent: Option<Arc<Spatial>>, transform: Mat4) -> Arc<Self> {
|
||||
Arc::new_cyclic(|self_ref| Spatial {
|
||||
uid: nanoid!(),
|
||||
let spatial = SPATIAL_REGISTRY.add(Spatial {
|
||||
node,
|
||||
self_ref: self_ref.clone(),
|
||||
parent: Mutex::new(parent),
|
||||
old_parent: Mutex::new(None),
|
||||
transform: Mutex::new(transform),
|
||||
zone: Mutex::new(Weak::new()),
|
||||
entity: RwLock::new(None),
|
||||
parent: RwLock::new(parent),
|
||||
old_parent: RwLock::new(None),
|
||||
transform: RwLock::new(transform),
|
||||
zone: RwLock::new(Weak::new()),
|
||||
children: Registry::new(),
|
||||
bounding_box_calc: OnceLock::default(),
|
||||
})
|
||||
});
|
||||
spatial.mark_dirty();
|
||||
spatial
|
||||
}
|
||||
pub fn set_entity(&self, entity: EntityHandle) {
|
||||
self.entity.write().replace(entity);
|
||||
self.mark_dirty();
|
||||
for child in self.children.get_valid_contents() {
|
||||
child.mark_dirty();
|
||||
}
|
||||
}
|
||||
pub fn get_entity(&self) -> Option<Entity> {
|
||||
self.entity.read().as_ref().map(|v| v.get())
|
||||
}
|
||||
pub fn add_to(
|
||||
node: &Arc<Node>,
|
||||
parent: Option<Arc<Spatial>>,
|
||||
transform: Mat4,
|
||||
zoneable: bool,
|
||||
) -> Result<Arc<Spatial>> {
|
||||
ensure!(
|
||||
node.spatial.get().is_none(),
|
||||
"Internal: Node already has a Spatial aspect!"
|
||||
);
|
||||
let spatial = Spatial::new(Arc::downgrade(node), parent, transform);
|
||||
node.add_local_method("get_bounding_box", Spatial::get_bounding_box_flex);
|
||||
node.add_local_method("get_transform", Spatial::get_transform_flex);
|
||||
node.add_local_signal("set_transform", Spatial::set_transform_flex);
|
||||
node.add_local_signal("set_spatial_parent", Spatial::set_spatial_parent_flex);
|
||||
node.add_local_signal(
|
||||
"set_spatial_parent_in_place",
|
||||
Spatial::set_spatial_parent_in_place_flex,
|
||||
);
|
||||
node.add_local_signal("set_zoneable", Spatial::set_zoneable_flex);
|
||||
node.add_local_method("field_distance", Spatial::field_distance_flex);
|
||||
node.add_local_method("field_normal", Spatial::field_normal_flex);
|
||||
node.add_local_method("field_closest_point", Spatial::field_closest_point_flex);
|
||||
) -> Arc<Spatial> {
|
||||
let spatial = Spatial::new(Arc::downgrade(node), parent.clone(), transform);
|
||||
if zoneable {
|
||||
ZONEABLE_REGISTRY.add_raw(&spatial);
|
||||
}
|
||||
let _ = node.spatial.set(spatial.clone());
|
||||
Ok(spatial)
|
||||
if let Some(parent) = parent {
|
||||
parent.children.add_raw(&spatial);
|
||||
}
|
||||
node.add_aspect_raw(spatial.clone());
|
||||
node.add_aspect(SpatialRef);
|
||||
spatial
|
||||
}
|
||||
|
||||
pub fn node(&self) -> Option<Arc<Node>> {
|
||||
@@ -87,34 +189,80 @@ impl Spatial {
|
||||
}
|
||||
|
||||
// the output bounds are probably way bigger than they need to be
|
||||
pub fn get_bounding_box(&self) -> Bounds {
|
||||
let Some(node) = self.node() else {return Bounds::default()};
|
||||
let mut bounds = self
|
||||
.bounding_box_calc
|
||||
.get()
|
||||
.map(|b| (b)(&node))
|
||||
.unwrap_or_default();
|
||||
pub async fn get_bounding_box(&self) -> Aabb {
|
||||
let Some(node) = self.node() else {
|
||||
return Aabb::default();
|
||||
};
|
||||
let mut bounds = match self.bounding_box_calc.get() {
|
||||
Some(f) => f(&node).await,
|
||||
None => Aabb::default(),
|
||||
};
|
||||
for child in self.children.get_valid_contents() {
|
||||
bounds = bounds_grow_to_fit_box(
|
||||
bounds,
|
||||
child.get_bounding_box(),
|
||||
Some(child.local_transform()),
|
||||
);
|
||||
let mat = child.local_transform();
|
||||
let child_aabb = Box::pin(child.get_bounding_box()).await;
|
||||
bounds = Aabb::enclosing([
|
||||
bounds.min().into(),
|
||||
bounds.max().into(),
|
||||
mat.transform_point3(child_aabb.min().into()),
|
||||
mat.transform_point3(child_aabb.max().into()),
|
||||
])
|
||||
.unwrap();
|
||||
}
|
||||
bounds
|
||||
}
|
||||
pub(super) fn mark_dirty(&self) {
|
||||
let Some(entity) = self.entity.read().as_ref().map(|v| v.get()) else {
|
||||
return;
|
||||
};
|
||||
let enabled = self
|
||||
.node()
|
||||
.is_none_or(|n| n.enabled.load(Ordering::Relaxed))
|
||||
&& self.local_visible();
|
||||
let transform = enabled.then(|| BevyTransform::from_matrix(self.local_transform()));
|
||||
let parent = self
|
||||
.get_parent()
|
||||
.and_then(|v| v.entity.read().as_ref().map(|v| v.get()));
|
||||
UPDATED_SPATIALS_NODES
|
||||
.lock()
|
||||
.insert(entity, (transform, parent));
|
||||
}
|
||||
|
||||
pub fn local_transform(&self) -> Mat4 {
|
||||
*self.transform.lock()
|
||||
*self.transform.read()
|
||||
}
|
||||
|
||||
fn local_visible(&self) -> bool {
|
||||
// Check our own scale by looking at matrix column lengths
|
||||
let mat = self.local_transform();
|
||||
let x_scale = mat.x_axis.length_squared();
|
||||
let y_scale = mat.y_axis.length_squared();
|
||||
let z_scale = mat.z_axis.length_squared();
|
||||
|
||||
x_scale >= EPSILON.powi(2) || y_scale >= EPSILON.powi(2) || z_scale >= EPSILON.powi(2)
|
||||
}
|
||||
/// Check if this node or any ancestor has zero scale (for visibility culling)
|
||||
pub fn visible(&self) -> bool {
|
||||
// Check parent chain
|
||||
if let Some(parent) = self.get_parent()
|
||||
&& !parent.visible()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check our own scale by looking at matrix column lengths
|
||||
self.local_visible()
|
||||
}
|
||||
pub fn global_transform(&self) -> Mat4 {
|
||||
match self.get_parent() {
|
||||
Some(value) => value.global_transform() * *self.transform.lock(),
|
||||
None => *self.transform.lock(),
|
||||
}
|
||||
let parent_transform = self
|
||||
.get_parent()
|
||||
.as_deref()
|
||||
.map(Self::global_transform)
|
||||
.unwrap_or_default();
|
||||
parent_transform * self.local_transform()
|
||||
}
|
||||
pub fn set_local_transform(&self, transform: Mat4) {
|
||||
*self.transform.lock() = transform;
|
||||
*self.transform.write() = transform;
|
||||
self.mark_dirty();
|
||||
}
|
||||
pub fn set_local_transform_components(
|
||||
&self,
|
||||
@@ -122,9 +270,7 @@ impl Spatial {
|
||||
transform: Transform,
|
||||
) {
|
||||
if reference_space == Some(self) {
|
||||
self.set_local_transform(
|
||||
parse_transform(transform, true, true, true) * self.local_transform(),
|
||||
);
|
||||
self.set_local_transform(transform.to_mat4(true, true, true) * self.local_transform());
|
||||
return;
|
||||
}
|
||||
let reference_to_parent_transform = reference_space
|
||||
@@ -137,7 +283,7 @@ impl Spatial {
|
||||
let (mut reference_space_scl, mut reference_space_rot, mut reference_space_pos) =
|
||||
local_transform_in_reference_space.to_scale_rotation_translation();
|
||||
|
||||
if let Some(pos) = transform.position {
|
||||
if let Some(pos) = transform.translation {
|
||||
reference_space_pos = pos.into()
|
||||
}
|
||||
if let Some(rot) = transform.rotation {
|
||||
@@ -175,277 +321,127 @@ impl Spatial {
|
||||
}
|
||||
|
||||
fn get_parent(&self) -> Option<Arc<Spatial>> {
|
||||
self.parent.lock().clone()
|
||||
self.parent.read().clone()
|
||||
}
|
||||
fn set_parent(&self, new_parent: Option<Arc<Spatial>>) {
|
||||
fn set_parent(self: &Arc<Self>, new_parent: &Arc<Spatial>) {
|
||||
if let Some(parent) = self.get_parent() {
|
||||
parent.children.remove(self);
|
||||
}
|
||||
if let Some(new_parent) = &new_parent {
|
||||
new_parent
|
||||
.children
|
||||
.add_raw(&self.self_ref.upgrade().unwrap());
|
||||
}
|
||||
new_parent.children.add_raw(self);
|
||||
|
||||
*self.parent.lock() = new_parent;
|
||||
*self.parent.write() = Some(new_parent.clone());
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
pub fn set_spatial_parent(&self, parent: Option<Arc<Spatial>>) -> Result<()> {
|
||||
let is_ancestor = parent
|
||||
.as_ref()
|
||||
.map(|parent| self.is_ancestor_of(parent.clone()))
|
||||
.unwrap_or(false);
|
||||
if is_ancestor {
|
||||
return Err(eyre!("Setting spatial parent would cause a loop"));
|
||||
pub fn set_spatial_parent(self: &Arc<Self>, parent: &Arc<Spatial>) -> Result<()> {
|
||||
if self.is_ancestor_of(parent.clone()) {
|
||||
bail!("Setting spatial parent would cause a loop");
|
||||
}
|
||||
self.set_parent(parent);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_spatial_parent_in_place(&self, parent: Option<Arc<Spatial>>) -> Result<()> {
|
||||
let is_ancestor = parent
|
||||
.as_ref()
|
||||
.map(|parent| self.is_ancestor_of(parent.clone()))
|
||||
.unwrap_or(false);
|
||||
if is_ancestor {
|
||||
return Err(eyre!("Setting spatial parent would cause a loop"));
|
||||
pub fn set_spatial_parent_in_place(self: &Arc<Self>, parent: &Arc<Spatial>) -> Result<()> {
|
||||
if self.is_ancestor_of(parent.clone()) {
|
||||
bail!("Setting spatial parent would cause a loop");
|
||||
}
|
||||
|
||||
self.set_local_transform(Spatial::space_to_space_matrix(
|
||||
Some(self),
|
||||
parent.as_deref(),
|
||||
));
|
||||
self.set_local_transform(Spatial::space_to_space_matrix(Some(self), Some(parent)));
|
||||
self.set_parent(parent);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_bounding_box_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
let this_spatial = node
|
||||
.spatial
|
||||
.get()
|
||||
.ok_or_else(|| eyre!("Node doesn't have a spatial?"))?;
|
||||
let relative_spatial_path: Option<&str> = deserialize(message.as_ref())?;
|
||||
let bounds = if let Some(relative_spatial_path) = relative_spatial_path {
|
||||
let relative_spatial =
|
||||
find_reference_space(&calling_client, relative_spatial_path)?;
|
||||
let center =
|
||||
Spatial::space_to_space_matrix(Some(&this_spatial), Some(&relative_spatial))
|
||||
.transform_point3([0.0; 3].into());
|
||||
let bounds: Bounds = Bounds {
|
||||
center,
|
||||
dimensions: [0.0; 3].into(),
|
||||
};
|
||||
bounds_grow_to_fit_box(
|
||||
bounds,
|
||||
this_spatial.get_bounding_box(),
|
||||
Some(Spatial::space_to_space_matrix(
|
||||
Some(&this_spatial),
|
||||
Some(&relative_spatial),
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
this_spatial.get_bounding_box()
|
||||
};
|
||||
|
||||
Ok(serialize((
|
||||
mint::Vector3::from(bounds.center),
|
||||
mint::Vector3::from(bounds.dimensions),
|
||||
))?
|
||||
.into())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_transform_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
let this_spatial = node
|
||||
.spatial
|
||||
.get()
|
||||
.ok_or_else(|| eyre!("Node doesn't have a spatial?"))?;
|
||||
let relative_spatial =
|
||||
find_reference_space(&calling_client, deserialize(message.as_ref())?)?;
|
||||
|
||||
let (scale, rotation, position) = Spatial::space_to_space_matrix(
|
||||
Some(this_spatial.as_ref()),
|
||||
Some(relative_spatial.as_ref()),
|
||||
)
|
||||
.to_scale_rotation_translation();
|
||||
|
||||
Ok(serialize((
|
||||
mint::Vector3::from(position),
|
||||
mint::Quaternion::from(rotation),
|
||||
mint::Vector3::from(scale),
|
||||
))?
|
||||
.into())
|
||||
});
|
||||
}
|
||||
pub fn set_transform_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct TransformArgs<'a> {
|
||||
reference_space_path: Option<&'a str>,
|
||||
transform: Transform,
|
||||
}
|
||||
let transform_args: TransformArgs = deserialize(message.as_ref())?;
|
||||
let reference_space_transform = transform_args
|
||||
.reference_space_path
|
||||
.map(|path| find_reference_space(&calling_client, path))
|
||||
.transpose()?;
|
||||
|
||||
node.spatial.get().unwrap().set_local_transform_components(
|
||||
reference_space_transform.as_deref(),
|
||||
transform_args.transform,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
pub fn set_spatial_parent_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let parent = find_spatial_parent(&calling_client, deserialize(message.as_ref())?)?;
|
||||
node.spatial.get().unwrap().set_spatial_parent(Some(parent))
|
||||
}
|
||||
pub fn set_spatial_parent_in_place_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let parent = find_spatial_parent(&calling_client, deserialize(message.as_ref())?)?;
|
||||
node.spatial
|
||||
.get()
|
||||
.unwrap()
|
||||
.set_spatial_parent_in_place(Some(parent))?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn set_zoneable_flex(
|
||||
node: &Node,
|
||||
_calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
let zoneable: bool = deserialize(message.as_ref())?;
|
||||
let spatial = node.spatial.get().unwrap();
|
||||
if zoneable {
|
||||
ZONEABLE_REGISTRY.add_raw(spatial);
|
||||
} else {
|
||||
ZONEABLE_REGISTRY.remove(spatial);
|
||||
zone::release(spatial);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn field_distance_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
let (point, fields): (Vector3<f32>, Vec<Option<&str>>) = deserialize(message.as_ref())?;
|
||||
let spatial = node.spatial.get().unwrap();
|
||||
|
||||
let output = fields
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
calling_client
|
||||
.get_node("Field", f?)
|
||||
.ok()?
|
||||
.get_aspect("Field", "field", |n| &n.field)
|
||||
.ok()
|
||||
.cloned()
|
||||
})
|
||||
.map(|f| f.map(|f| f.distance(spatial, point.into())))
|
||||
.collect::<Vec<Option<f32>>>();
|
||||
|
||||
Ok(serialize(output)?.into())
|
||||
});
|
||||
}
|
||||
pub fn field_normal_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
let (point, fields): (Vector3<f32>, Vec<Option<&str>>) = deserialize(message.as_ref())?;
|
||||
let spatial = node.spatial.get().unwrap();
|
||||
|
||||
let output = fields
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
calling_client
|
||||
.get_node("Field", f?)
|
||||
.ok()?
|
||||
.get_aspect("Field", "field", |n| &n.field)
|
||||
.ok()
|
||||
.cloned()
|
||||
})
|
||||
.map(|f| f.map(|f| Vector3::from(f.normal(spatial, point.into(), 0.001))))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(serialize(output)?.into())
|
||||
});
|
||||
}
|
||||
pub fn field_closest_point_flex(
|
||||
node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
response: MethodResponseSender,
|
||||
) {
|
||||
response.wrap_sync(move || {
|
||||
let (point, fields): (Vector3<f32>, Vec<Option<&str>>) = deserialize(message.as_ref())?;
|
||||
let spatial = node.spatial.get().unwrap();
|
||||
|
||||
let output = fields
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
calling_client
|
||||
.get_node("Field", f?)
|
||||
.ok()?
|
||||
.get_aspect("Field", "field", |n| &n.field)
|
||||
.ok()
|
||||
.cloned()
|
||||
})
|
||||
.map(|f| f.map(|f| Vector3::from(f.closest_point(spatial, point.into(), 0.001))))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(serialize(output)?.into())
|
||||
});
|
||||
}
|
||||
|
||||
pub(self) fn zone_distance(&self) -> f32 {
|
||||
self.zone
|
||||
.lock()
|
||||
.read()
|
||||
.upgrade()
|
||||
.and_then(|zone| zone.field.upgrade())
|
||||
.map(|zone| zone.field.clone())
|
||||
.map(|field| field.distance(self, vec3a(0.0, 0.0, 0.0)))
|
||||
.unwrap_or(f32::MAX)
|
||||
.unwrap_or(f32::NEG_INFINITY)
|
||||
}
|
||||
}
|
||||
static UPDATED_SPATIALS_NODES: Mutex<EntityHashMap<(Option<BevyTransform>, Option<Entity>)>> =
|
||||
Mutex::new(EntityHashMap::new());
|
||||
impl AspectIdentifier for Spatial {
|
||||
impl_aspect_for_spatial_aspect_id! {}
|
||||
}
|
||||
impl Aspect for Spatial {
|
||||
impl_aspect_for_spatial_aspect! {}
|
||||
}
|
||||
impl SpatialAspect for Spatial {
|
||||
fn set_local_transform(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
transform: Transform,
|
||||
) -> Result<()> {
|
||||
let this_spatial = node.get_aspect::<Spatial>()?;
|
||||
this_spatial.set_local_transform_components(None, transform);
|
||||
Ok(())
|
||||
}
|
||||
fn set_relative_transform(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
relative_to: Arc<Node>,
|
||||
transform: Transform,
|
||||
) -> Result<()> {
|
||||
let this_spatial = node.get_aspect::<Spatial>()?;
|
||||
let relative_spatial = relative_to.get_aspect::<Spatial>()?;
|
||||
|
||||
this_spatial.set_local_transform_components(Some(&relative_spatial), transform);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_spatial_parent(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
parent: Arc<Node>,
|
||||
) -> Result<()> {
|
||||
let this_spatial = node.get_aspect::<Spatial>()?;
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
|
||||
this_spatial.set_spatial_parent(&parent)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_spatial_parent_in_place(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
parent: Arc<Node>,
|
||||
) -> Result<()> {
|
||||
let this_spatial = node.get_aspect::<Spatial>()?;
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
|
||||
this_spatial.set_spatial_parent_in_place(&parent)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_zoneable(node: Arc<Node>, _calling_client: Arc<Client>, zoneable: bool) -> Result<()> {
|
||||
let spatial = node.get_aspect::<Spatial>()?;
|
||||
if zoneable {
|
||||
ZONEABLE_REGISTRY.add_raw(&spatial);
|
||||
} else {
|
||||
ZONEABLE_REGISTRY.remove(&spatial);
|
||||
zone::release(&spatial);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// legit gotta find a way to remove old ones, this just keeps the node alive
|
||||
async fn export_spatial(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<u64> {
|
||||
let id = rand::random();
|
||||
EXPORTED_SPATIALS.lock().insert(id, Arc::downgrade(&node));
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
impl PartialEq for Spatial {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.uid == other.uid
|
||||
self.node.as_ptr() == other.node.as_ptr()
|
||||
}
|
||||
}
|
||||
impl Debug for Spatial {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Spatial")
|
||||
.field("uid", &self.uid)
|
||||
.field("parent", &self.parent)
|
||||
.field("old_parent", &self.old_parent)
|
||||
.field("transform", &self.transform)
|
||||
@@ -454,75 +450,127 @@ impl Debug for Spatial {
|
||||
}
|
||||
impl Drop for Spatial {
|
||||
fn drop(&mut self) {
|
||||
ZONEABLE_REGISTRY.remove(self);
|
||||
zone::release(self);
|
||||
ZONEABLE_REGISTRY.remove(self);
|
||||
SPATIAL_REGISTRY.remove(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_transform(transform: Transform, position: bool, rotation: bool, scale: bool) -> Mat4 {
|
||||
let position = position
|
||||
.then_some(transform.position)
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Vector3::from([0.0; 3]));
|
||||
let rotation = rotation
|
||||
.then_some(transform.rotation)
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Quat::IDENTITY.into());
|
||||
let scale = scale
|
||||
.then_some(transform.scale)
|
||||
.flatten()
|
||||
.unwrap_or_else(|| Vector3::from([1.0; 3]));
|
||||
pub struct SpatialRef;
|
||||
impl AspectIdentifier for SpatialRef {
|
||||
impl_aspect_for_spatial_ref_aspect_id! {}
|
||||
}
|
||||
impl Aspect for SpatialRef {
|
||||
impl_aspect_for_spatial_ref_aspect! {}
|
||||
}
|
||||
impl SpatialRefAspect for SpatialRef {
|
||||
async fn get_local_bounding_box(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
) -> Result<BoundingBox> {
|
||||
let this_spatial = node.get_aspect::<Spatial>()?;
|
||||
let bounds = this_spatial.get_bounding_box().await;
|
||||
|
||||
Mat4::from_scale_rotation_translation(scale.into(), rotation.into(), position.into())
|
||||
Ok(BoundingBox {
|
||||
center: Vec3::from(bounds.center).into(),
|
||||
size: Vec3::from(bounds.half_extents * 2.0).into(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_relative_bounding_box(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
relative_to: Arc<Node>,
|
||||
) -> Result<BoundingBox> {
|
||||
let this_spatial = node.get_aspect::<Spatial>()?;
|
||||
let relative_spatial = relative_to.get_aspect::<Spatial>()?;
|
||||
let mat = Spatial::space_to_space_matrix(Some(&this_spatial), Some(&relative_spatial));
|
||||
let bb = this_spatial.get_bounding_box().await;
|
||||
let bounds = Aabb::enclosing([
|
||||
mat.transform_point3(bb.min().into()),
|
||||
mat.transform_point3(bb.max().into()),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
Ok(BoundingBox {
|
||||
center: Vec3::from(bounds.center).into(),
|
||||
size: Vec3::from(bounds.half_extents * 2.0).into(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_transform(
|
||||
node: Arc<Node>,
|
||||
_calling_client: Arc<Client>,
|
||||
relative_to: Arc<Node>,
|
||||
) -> Result<Transform> {
|
||||
let this_spatial = node.get_aspect::<Spatial>()?;
|
||||
let relative_spatial = relative_to.get_aspect::<Spatial>()?;
|
||||
|
||||
let (scale, rotation, position) = Spatial::space_to_space_matrix(
|
||||
Some(this_spatial.as_ref()),
|
||||
Some(relative_spatial.as_ref()),
|
||||
)
|
||||
.to_scale_rotation_translation();
|
||||
|
||||
Ok(Transform {
|
||||
translation: Some(position.into()),
|
||||
rotation: Some(rotation.into()),
|
||||
scale: Some(scale.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_spatial(
|
||||
calling_client: &Arc<Client>,
|
||||
node_name: &'static str,
|
||||
node_path: &str,
|
||||
) -> color_eyre::eyre::Result<Arc<Spatial>> {
|
||||
calling_client
|
||||
.get_node(node_name, node_path)?
|
||||
.get_aspect(node_name, "spatial", |n| &n.spatial)
|
||||
.cloned()
|
||||
}
|
||||
pub fn find_spatial_parent(
|
||||
calling_client: &Arc<Client>,
|
||||
node_path: &str,
|
||||
) -> color_eyre::eyre::Result<Arc<Spatial>> {
|
||||
find_spatial(calling_client, "Spatial parent", node_path)
|
||||
}
|
||||
pub fn find_reference_space(
|
||||
calling_client: &Arc<Client>,
|
||||
node_path: &str,
|
||||
) -> color_eyre::eyre::Result<Arc<Spatial>> {
|
||||
find_spatial(calling_client, "Reference space", node_path)
|
||||
}
|
||||
|
||||
pub fn create_interface(client: &Arc<Client>) -> Result<()> {
|
||||
let node = Node::create(client, "", "spatial", false);
|
||||
node.add_local_signal("create_spatial", create_spatial_flex);
|
||||
node.add_local_signal("create_zone", create_zone_flex);
|
||||
node.add_to_scenegraph().map(|_| ())
|
||||
}
|
||||
|
||||
pub fn create_spatial_flex(
|
||||
_node: &Node,
|
||||
calling_client: Arc<Client>,
|
||||
message: Message,
|
||||
) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateSpatialInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
impl InterfaceAspect for Interface {
|
||||
fn create_spatial(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
zoneable: bool,
|
||||
) -> Result<()> {
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
let transform = transform.to_mat4(true, true, true);
|
||||
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent.clone()), transform, zoneable);
|
||||
Ok(())
|
||||
}
|
||||
fn create_zone(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
id: u64,
|
||||
parent: Arc<Node>,
|
||||
transform: Transform,
|
||||
field: Arc<Node>,
|
||||
) -> Result<()> {
|
||||
let parent = parent.get_aspect::<Spatial>()?;
|
||||
let transform = transform.to_mat4(true, true, false);
|
||||
let field = field.get_aspect::<Field>()?;
|
||||
|
||||
let node = Node::from_id(&calling_client, id, true).add_to_scenegraph()?;
|
||||
let space = Spatial::add_to(&node, Some(parent.clone()), transform, false);
|
||||
Zone::add_to(&node, space, field);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn import_spatial_ref(
|
||||
_node: Arc<Node>,
|
||||
calling_client: Arc<Client>,
|
||||
uid: u64,
|
||||
) -> Result<Arc<Node>> {
|
||||
Ok(EXPORTED_SPATIALS
|
||||
.lock()
|
||||
.get(&uid)
|
||||
.and_then(|s| s.upgrade())
|
||||
.map(|s| {
|
||||
Alias::create(
|
||||
&s,
|
||||
&calling_client,
|
||||
SPATIAL_REF_ASPECT_ALIAS_INFO.clone(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.ok_or_eyre("Couldn't find spatial with that ID")?)
|
||||
}
|
||||
let info: CreateSpatialInfo = deserialize(message.as_ref())?;
|
||||
let node = Node::create(&calling_client, "/spatial/spatial", info.name, true);
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, true);
|
||||
let node = node.add_to_scenegraph()?;
|
||||
Spatial::add_to(&node, Some(parent), transform, info.zoneable)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,171 +1,164 @@
|
||||
use super::{find_spatial, Spatial, ZONEABLE_REGISTRY};
|
||||
use super::{
|
||||
SPATIAL_ASPECT_ALIAS_INFO, SPATIAL_REF_ASPECT_ALIAS_INFO, Spatial, ZONEABLE_REGISTRY,
|
||||
ZoneAspect,
|
||||
};
|
||||
use crate::{
|
||||
core::{client::Client, registry::Registry},
|
||||
core::{client::Client, error::Result, registry::Registry},
|
||||
nodes::{
|
||||
alias::{Alias, AliasInfo},
|
||||
fields::{find_field, Field},
|
||||
spatial::{find_spatial_parent, parse_transform},
|
||||
Message, Node,
|
||||
Node,
|
||||
alias::{Alias, AliasList, get_original},
|
||||
fields::{Field, FieldTrait},
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::vec3a;
|
||||
use parking_lot::Mutex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
use stardust_xr::{
|
||||
schemas::flex::{deserialize, serialize},
|
||||
values::Transform,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
pub fn capture(spatial: &Arc<Spatial>, zone: &Arc<Zone>) {
|
||||
let old_distance = spatial.zone_distance();
|
||||
let new_distance = zone
|
||||
.field
|
||||
.upgrade()
|
||||
.map(|field| field.distance(spatial, vec3a(0.0, 0.0, 0.0)))
|
||||
.unwrap_or(f32::MAX);
|
||||
if new_distance.abs() < old_distance.abs() {
|
||||
release(spatial);
|
||||
*spatial.old_parent.lock() = spatial.get_parent();
|
||||
*spatial.zone.lock() = Arc::downgrade(zone);
|
||||
zone.captured.add_raw(spatial);
|
||||
let Some(node) = zone.spatial.node.upgrade() else {return};
|
||||
let Ok(message) = serialize(&spatial.uid) else {return};
|
||||
let _ = node.send_remote_signal("capture", message);
|
||||
let new_distance = zone.field.distance(spatial, vec3a(0.0, 0.0, 0.0));
|
||||
if new_distance.abs() > old_distance.abs() {
|
||||
return;
|
||||
}
|
||||
|
||||
release(spatial);
|
||||
*spatial.old_parent.write() = spatial.get_parent();
|
||||
*spatial.zone.write() = Arc::downgrade(zone);
|
||||
let Some(zone_node) = zone.spatial.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(spatial_node) = spatial.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Ok(spatial_alias) = Alias::create(
|
||||
&spatial_node,
|
||||
&zone_node.get_client().unwrap(),
|
||||
SPATIAL_ASPECT_ALIAS_INFO.clone(),
|
||||
Some(&zone.captured),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let _ = super::zone_client::capture(&zone_node, &spatial_alias);
|
||||
}
|
||||
pub fn release(spatial: &Spatial) {
|
||||
let _ = spatial.set_spatial_parent_in_place(spatial.old_parent.lock().take());
|
||||
let mut spatial_zone = spatial.zone.lock();
|
||||
let Some(spatial_node) = spatial.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let spatial = spatial_node.get_aspect::<Spatial>().unwrap();
|
||||
|
||||
let Some(old_parent) = spatial.old_parent.read().clone() else {
|
||||
return;
|
||||
};
|
||||
let _ = spatial.set_spatial_parent_in_place(&old_parent);
|
||||
let mut spatial_zone = spatial.zone.write();
|
||||
|
||||
if let Some(spatial_zone) = spatial_zone.upgrade() {
|
||||
let Some(node) = spatial_zone.spatial.node.upgrade() else {return};
|
||||
spatial_zone.captured.remove(spatial);
|
||||
let Ok(message) = serialize(&spatial.uid) else {return};
|
||||
let _ = node.send_remote_signal("release", message);
|
||||
spatial_zone.captured.remove_aspect(spatial.as_ref());
|
||||
let Some(node) = spatial_zone.spatial.node.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let _ = super::zone_client::release(&node, spatial_node.id);
|
||||
}
|
||||
*spatial_zone = Weak::new();
|
||||
}
|
||||
|
||||
pub struct Zone {
|
||||
spatial: Arc<Spatial>,
|
||||
pub field: Weak<Field>,
|
||||
zoneables: Mutex<FxHashMap<String, Arc<Node>>>,
|
||||
captured: Registry<Spatial>,
|
||||
pub field: Arc<Field>,
|
||||
intersecting_spatials: Registry<Spatial>,
|
||||
intersecting: AliasList,
|
||||
captured: AliasList,
|
||||
}
|
||||
impl Zone {
|
||||
pub fn add_to(node: &Arc<Node>, spatial: Arc<Spatial>, field: &Arc<Field>) -> Arc<Zone> {
|
||||
pub fn add_to(node: &Arc<Node>, spatial: Arc<Spatial>, field: Arc<Field>) -> Arc<Zone> {
|
||||
let zone = Arc::new(Zone {
|
||||
spatial,
|
||||
field: Arc::downgrade(field),
|
||||
zoneables: Mutex::new(FxHashMap::default()),
|
||||
captured: Registry::new(),
|
||||
field,
|
||||
intersecting_spatials: Registry::default(),
|
||||
intersecting: AliasList::default(),
|
||||
captured: AliasList::default(),
|
||||
});
|
||||
node.add_local_signal("capture", Zone::capture_flex);
|
||||
node.add_local_signal("release", Zone::release_flex);
|
||||
node.add_local_signal("update", Zone::update);
|
||||
let _ = node.zone.set(zone.clone());
|
||||
node.add_aspect_raw(zone.clone());
|
||||
zone
|
||||
}
|
||||
fn capture_flex(node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
let zone = node.zone.get().unwrap();
|
||||
let capture_path: &str = deserialize(message.as_ref())?;
|
||||
let spatial = find_spatial(&calling_client, "Spatial", capture_path)?;
|
||||
capture(&spatial, zone);
|
||||
pub fn update(&self) -> Result<()> {
|
||||
let node = self.spatial.node().unwrap();
|
||||
|
||||
let current_zoneables = Registry::new();
|
||||
for zoneable in ZONEABLE_REGISTRY.get_valid_contents() {
|
||||
// Skip if the zoneable is an ancestor of the zone or the zone itself
|
||||
if zoneable.is_ancestor_of(self.spatial.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let distance = self.field.distance(&zoneable, [0.0; 3].into());
|
||||
if distance > 0.0 {
|
||||
continue;
|
||||
}
|
||||
if distance < zoneable.zone_distance() {
|
||||
continue;
|
||||
}
|
||||
current_zoneables.add_raw(&zoneable);
|
||||
}
|
||||
|
||||
let (added, removed) =
|
||||
Registry::get_changes(&self.intersecting_spatials, ¤t_zoneables);
|
||||
for added in added {
|
||||
let Some(added_node) = added.node() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(alias) = Alias::create(
|
||||
&added_node,
|
||||
&self.spatial.node().unwrap().get_client().unwrap(),
|
||||
SPATIAL_REF_ASPECT_ALIAS_INFO.clone(),
|
||||
Some(&self.intersecting),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let _ = super::zone_client::enter(&node, &alias);
|
||||
}
|
||||
for removed in removed {
|
||||
let Some(removed_node) = removed.node() else {
|
||||
continue;
|
||||
};
|
||||
release(&removed);
|
||||
let _ = super::zone_client::leave(&node, removed_node.id);
|
||||
self.intersecting.remove_aspect(removed.as_ref());
|
||||
}
|
||||
self.intersecting_spatials.set(¤t_zoneables);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn release_flex(_node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
let capture_path: &str = deserialize(message.as_ref())?;
|
||||
let spatial = find_spatial(&calling_client, "Spatial", capture_path)?;
|
||||
}
|
||||
impl ZoneAspect for Zone {
|
||||
fn update(node: Arc<Node>, _calling_client: Arc<Client>) -> Result<()> {
|
||||
let zone = node.get_aspect::<Zone>()?;
|
||||
let _ = zone.update();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn capture(node: Arc<Node>, _calling_client: Arc<Client>, spatial: Arc<Node>) -> Result<()> {
|
||||
let zone = node.get_aspect::<Zone>()?;
|
||||
let spatial = spatial.get_aspect()?;
|
||||
capture(&spatial, &zone);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(_node: Arc<Node>, _calling_client: Arc<Client>, spatial: Arc<Node>) -> Result<()> {
|
||||
let spatial = spatial.get_aspect()?;
|
||||
release(&spatial);
|
||||
Ok(())
|
||||
}
|
||||
fn update(node: &Node, _calling_client: Arc<Client>, _message: Message) -> Result<()> {
|
||||
let zone = node.zone.get().unwrap();
|
||||
let Some(field) = zone.field.upgrade() else { return Err(color_eyre::eyre::eyre!("Zone's field has been destroyed")) };
|
||||
let Some((zone_client, zone_node)) = zone
|
||||
.spatial
|
||||
.node
|
||||
.upgrade()
|
||||
.and_then(|n| n.get_client().zip(Some(n))) else { return Err(color_eyre::eyre::eyre!("No client on node?")) };
|
||||
let mut old_zoneables = zone.zoneables.lock();
|
||||
for (_uid, zoneable) in old_zoneables.iter() {
|
||||
zoneable.destroy();
|
||||
}
|
||||
let captured = zone.captured.get_valid_contents();
|
||||
let zoneables = ZONEABLE_REGISTRY
|
||||
.get_valid_contents()
|
||||
.into_iter()
|
||||
.filter(|zoneable| zoneable.node.upgrade().is_some())
|
||||
.filter(|zoneable| {
|
||||
if captured
|
||||
.iter()
|
||||
.any(|captured| Arc::ptr_eq(captured, zoneable))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
let spatial_zone_distance = zoneable.zone_distance();
|
||||
let self_zone_distance = field.distance(zoneable, vec3a(0.0, 0.0, 0.0));
|
||||
self_zone_distance < 0.0 && spatial_zone_distance > self_zone_distance
|
||||
})
|
||||
.filter_map(|zoneable| {
|
||||
let alias = Alias::create(
|
||||
&zone_client,
|
||||
zone_node.get_path(),
|
||||
&zoneable.uid,
|
||||
&zoneable.node.upgrade().unwrap(),
|
||||
AliasInfo {
|
||||
server_signals: vec![
|
||||
"set_transform",
|
||||
"set_spatial_parent",
|
||||
"set_spatial_parent_in_place",
|
||||
],
|
||||
server_methods: vec!["get_bounds", "get_transform"],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.ok()?;
|
||||
Some((zoneable.uid.clone(), alias))
|
||||
})
|
||||
.collect::<FxHashMap<String, Arc<Node>>>();
|
||||
|
||||
for entered_uid in zoneables.keys().filter(|k| !old_zoneables.contains_key(*k)) {
|
||||
node.send_remote_signal("enter", serialize(entered_uid)?)?;
|
||||
}
|
||||
for left_uid in old_zoneables.keys().filter(|k| !zoneables.contains_key(*k)) {
|
||||
node.send_remote_signal("leave", serialize(left_uid)?)?;
|
||||
}
|
||||
|
||||
*old_zoneables = zoneables;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Drop for Zone {
|
||||
fn drop(&mut self) {
|
||||
for captured in self.captured.get_valid_contents() {
|
||||
for captured in self
|
||||
.captured
|
||||
.get_aliases()
|
||||
.into_iter()
|
||||
.filter_map(|n| get_original(n, false))
|
||||
.filter_map(|n| n.get_aspect::<Spatial>().ok())
|
||||
{
|
||||
release(&captured);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_zone_flex(_node: &Node, calling_client: Arc<Client>, message: Message) -> Result<()> {
|
||||
#[derive(Deserialize)]
|
||||
struct CreateZoneInfo<'a> {
|
||||
name: &'a str,
|
||||
parent_path: &'a str,
|
||||
transform: Transform,
|
||||
field_path: &'a str,
|
||||
}
|
||||
let info: CreateZoneInfo = deserialize(message.as_ref())?;
|
||||
let parent = find_spatial_parent(&calling_client, info.parent_path)?;
|
||||
let transform = parse_transform(info.transform, true, true, false);
|
||||
let field = find_field(&calling_client, info.field_path)?;
|
||||
|
||||
let node =
|
||||
Node::create(&calling_client, "/spatial/zone", info.name, true).add_to_scenegraph()?;
|
||||
let space = Spatial::add_to(&node, Some(parent), transform, false)?;
|
||||
Zone::add_to(&node, space, &field);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
109
src/objects/hmd.rs
Normal file
109
src/objects/hmd.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_mod_openxr::{
|
||||
helper_traits::{ToQuat as _, ToVec3 as _},
|
||||
resources::{OxrFrameState, Pipelined},
|
||||
session::OxrSession,
|
||||
};
|
||||
use bevy_mod_xr::{
|
||||
session::{XrPreDestroySession, XrSessionCreated, session_running},
|
||||
spaces::{XrPrimaryReferenceSpace, XrSpace},
|
||||
};
|
||||
use openxr::SpaceLocationFlags;
|
||||
|
||||
use crate::{DbusConnection, PreFrameWait, get_time, nodes::spatial::Spatial};
|
||||
|
||||
use super::{ObjectHandle, SpatialRef, input::mouse_pointer::FlatscreenCam};
|
||||
|
||||
pub struct HmdPlugin;
|
||||
impl Plugin for HmdPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(XrPreDestroySession, destroy_view_space);
|
||||
app.add_systems(XrSessionCreated, create_view_space);
|
||||
app.add_systems(PreFrameWait, update_xr.run_if(session_running));
|
||||
app.add_systems(PreFrameWait, update_flat.run_if(not(session_running)));
|
||||
app.add_systems(Startup, setup);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(connection: Res<DbusConnection>, mut cmds: Commands) {
|
||||
let (spatial, _spatial_handle) = SpatialRef::create(&connection, "/org/stardustxr/HMD");
|
||||
let hmd = Hmd {
|
||||
spatial,
|
||||
_spatial_handle,
|
||||
space: None,
|
||||
};
|
||||
cmds.insert_resource(hmd);
|
||||
}
|
||||
|
||||
fn create_view_space(session: Res<OxrSession>, mut hmd: ResMut<Hmd>) {
|
||||
let space = session
|
||||
.create_reference_space(openxr::ReferenceSpaceType::VIEW, Transform::IDENTITY)
|
||||
.inspect_err(|err| error!("failed to create View XrSpace"))
|
||||
.ok();
|
||||
hmd.space = space.map(|v| v.0);
|
||||
}
|
||||
fn destroy_view_space(session: Res<OxrSession>, mut cmds: Commands, mut hmd: ResMut<Hmd>) {
|
||||
let Some(space) = hmd.space.take() else {
|
||||
return;
|
||||
};
|
||||
session.destroy_space(space);
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct Hmd {
|
||||
spatial: Arc<Spatial>,
|
||||
_spatial_handle: ObjectHandle<SpatialRef>,
|
||||
space: Option<XrSpace>,
|
||||
}
|
||||
|
||||
fn update_flat(cam: Single<&GlobalTransform, With<FlatscreenCam>>, hmd: Res<Hmd>) {
|
||||
// this shouldn't be parented to anything, so global and local spaces should be the same
|
||||
hmd.spatial.set_local_transform(cam.compute_matrix());
|
||||
}
|
||||
|
||||
fn update_xr(
|
||||
session: Option<Res<OxrSession>>,
|
||||
ref_space: Option<Res<XrPrimaryReferenceSpace>>,
|
||||
hmd: Res<Hmd>,
|
||||
state: Option<Res<OxrFrameState>>,
|
||||
pipelined: Option<Res<Pipelined>>,
|
||||
) {
|
||||
let (Some(session), Some(view), Some(ref_space), Some(state)) =
|
||||
(session, hmd.space, ref_space, state)
|
||||
else {
|
||||
// tokio::task::spawn({
|
||||
// let handle = hmd.tracked_handle.clone();
|
||||
// async move {
|
||||
// handle.set_tracked(false);
|
||||
// }
|
||||
// });
|
||||
return;
|
||||
};
|
||||
let time = get_time(pipelined.is_some(), &state);
|
||||
let location = session
|
||||
.locate_space(&view, &ref_space, time)
|
||||
.inspect_err(|err| error!("Error while Locating OpenXR Stage Space {err}"));
|
||||
if let Ok(location) = location {
|
||||
let is_tracked = location
|
||||
.location_flags
|
||||
.contains(SpaceLocationFlags::POSITION_VALID | SpaceLocationFlags::POSITION_TRACKED)
|
||||
|| location.location_flags.contains(
|
||||
SpaceLocationFlags::ORIENTATION_VALID | SpaceLocationFlags::ORIENTATION_TRACKED,
|
||||
);
|
||||
// tokio::task::spawn({
|
||||
// let handle = play_space.tracked_handle.clone();
|
||||
// async move {
|
||||
// handle.set_tracked(is_tracked);
|
||||
// }
|
||||
// });
|
||||
if is_tracked {
|
||||
hmd.spatial
|
||||
.set_local_transform(Mat4::from_rotation_translation(
|
||||
location.pose.orientation.to_quat(),
|
||||
location.pose.position.to_vec3(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
use crate::{
|
||||
core::client::INTERNAL_CLIENT,
|
||||
nodes::{
|
||||
input::{pointer::Pointer, InputMethod, InputType},
|
||||
Node, OwnedNode,
|
||||
fields::{FieldTrait, Ray},
|
||||
input::{INPUT_HANDLER_REGISTRY, InputDataType, InputMethod, Pointer},
|
||||
spatial::Spatial,
|
||||
Node,
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::Mat4;
|
||||
use nanoid::nanoid;
|
||||
use serde::Serialize;
|
||||
use stardust_xr::schemas::{flat::Datamap, flex::flexbuffers};
|
||||
use glam::{Mat4, vec3};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stardust_xr::values::Datamap;
|
||||
use std::sync::Arc;
|
||||
use stereokit::StereoKitMultiThread;
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
pub struct EyeDatamap {
|
||||
eye: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct KeyboardEvent {
|
||||
@@ -23,32 +27,25 @@ pub struct KeyboardEvent {
|
||||
}
|
||||
|
||||
pub struct EyePointer {
|
||||
node: OwnedNode,
|
||||
spatial: Arc<Spatial>,
|
||||
pointer: Arc<InputMethod>,
|
||||
}
|
||||
impl EyePointer {
|
||||
pub fn new() -> Result<Self> {
|
||||
let node = Node::create(&INTERNAL_CLIENT, "", &nanoid!(), false).add_to_scenegraph()?;
|
||||
let spatial = Spatial::add_to(&node, None, Mat4::IDENTITY, false).unwrap();
|
||||
let pointer =
|
||||
InputMethod::add_to(&node, InputType::Pointer(Pointer::default()), None).unwrap();
|
||||
let node = Node::generate(&INTERNAL_CLIENT, false).add_to_scenegraph_owned()?;
|
||||
let spatial = Spatial::add_to(&node.0, None, Mat4::IDENTITY, false);
|
||||
let pointer = InputMethod::add_to(
|
||||
&node.0,
|
||||
InputDataType::Pointer(Pointer::default()),
|
||||
Datamap::from_typed(EyeDatamap::default())?,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(EyePointer { spatial, pointer })
|
||||
}
|
||||
pub fn update(&self, sk: &impl StereoKitMultiThread) {
|
||||
let ray = sk.input_eyes();
|
||||
self.spatial
|
||||
.set_local_transform(Mat4::from_rotation_translation(
|
||||
ray.orientation,
|
||||
ray.position,
|
||||
));
|
||||
{
|
||||
// Set pointer input datamap
|
||||
let mut fbb = flexbuffers::Builder::default();
|
||||
let mut map = fbb.start_map();
|
||||
map.push("eye", 2);
|
||||
map.end_map();
|
||||
*self.pointer.datamap.lock() = Datamap::new(fbb.take_buffer()).ok();
|
||||
}
|
||||
Ok(EyePointer {
|
||||
node,
|
||||
spatial,
|
||||
pointer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,92 @@
|
||||
pub mod eye_pointer;
|
||||
pub mod mouse_pointer;
|
||||
pub mod sk_controller;
|
||||
pub mod sk_hand;
|
||||
pub mod oxr_controller;
|
||||
pub mod oxr_hand;
|
||||
|
||||
use crate::nodes::{
|
||||
fields::{Field, FieldTrait, Ray},
|
||||
input::{INPUT_HANDLER_REGISTRY, InputDataTrait, InputDataType, InputHandler, InputMethod},
|
||||
spatial::Spatial,
|
||||
};
|
||||
use glam::vec3;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CaptureManager {
|
||||
pub capture: Weak<InputHandler>,
|
||||
}
|
||||
impl CaptureManager {
|
||||
pub fn update_capture(&mut self, method: &InputMethod) {
|
||||
if let Some(capture) = &self.capture.upgrade()
|
||||
&& !method
|
||||
.capture_attempts
|
||||
.get_valid_contents()
|
||||
.contains(capture)
|
||||
{
|
||||
self.capture = Weak::new();
|
||||
}
|
||||
}
|
||||
pub fn set_new_capture(
|
||||
&mut self,
|
||||
method: &InputMethod,
|
||||
distance_calculator: DistanceCalculator,
|
||||
) {
|
||||
if self.capture.upgrade().is_none() {
|
||||
self.capture = find_closest_capture(method, distance_calculator);
|
||||
}
|
||||
}
|
||||
pub fn apply_capture(&self, method: &InputMethod) {
|
||||
method.captures.clear();
|
||||
if let Some(capture) = &self.capture.upgrade() {
|
||||
method.set_handler_order([capture].into_iter());
|
||||
method.captures.add_raw(capture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DistanceCalculator = fn(&Arc<Spatial>, &InputDataType, &Field) -> Option<f32>;
|
||||
|
||||
pub fn find_closest_capture(
|
||||
method: &InputMethod,
|
||||
distance_calculator: DistanceCalculator,
|
||||
) -> Weak<InputHandler> {
|
||||
method
|
||||
.capture_attempts
|
||||
.get_valid_contents()
|
||||
.into_iter()
|
||||
.filter_map(|h| {
|
||||
distance_calculator(&method.spatial, &method.data.lock(), &h.field)
|
||||
.map(|dist| (h.clone(), dist))
|
||||
})
|
||||
.min_by(|(_, dist_a), (_, dist_b)| dist_a.partial_cmp(dist_b).unwrap())
|
||||
.map(|(handler, _)| Arc::downgrade(&handler))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// sorts them greatest to least distance (so you can pop off the closest ones easily)
|
||||
pub fn get_sorted_handlers(
|
||||
method: &InputMethod,
|
||||
distance_calculator: DistanceCalculator,
|
||||
) -> Vec<(Arc<InputHandler>, f32)> {
|
||||
let mut handlers = INPUT_HANDLER_REGISTRY
|
||||
.get_valid_contents()
|
||||
.into_iter()
|
||||
.filter(|handler| handler.spatial.node().is_some_and(|node| node.enabled()))
|
||||
.filter(|handler| {
|
||||
handler
|
||||
.field
|
||||
.spatial
|
||||
.node()
|
||||
.is_some_and(|node| node.enabled())
|
||||
})
|
||||
.filter_map(|handler| {
|
||||
distance_calculator(&method.spatial, &method.data.lock(), &handler.field)
|
||||
.map(|distance| (handler, distance))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
handlers.sort_by(|(_, dist_a), (_, dist_b)| dist_a.partial_cmp(dist_b).unwrap());
|
||||
handlers
|
||||
}
|
||||
|
||||
@@ -1,221 +1,589 @@
|
||||
use super::{CaptureManager, DistanceCalculator, get_sorted_handlers};
|
||||
use crate::{
|
||||
core::{client::INTERNAL_CLIENT, typed_datamap::TypedDatamap},
|
||||
DbusConnection, ObjectRegistryRes,
|
||||
core::{client::INTERNAL_CLIENT, task},
|
||||
nodes::{
|
||||
data::{mask_matches, Mask, PulseSender, KEYMAPS, PULSE_RECEIVER_REGISTRY},
|
||||
fields::Ray,
|
||||
input::{pointer::Pointer, InputMethod, InputType},
|
||||
Node, OwnedNode,
|
||||
fields::{EXPORTED_FIELDS, Field, FieldTrait, Ray},
|
||||
input::{InputDataType, InputMethod, Pointer},
|
||||
items::panel::KEYMAPS,
|
||||
spatial::Spatial,
|
||||
Node,
|
||||
},
|
||||
objects::FieldRef,
|
||||
};
|
||||
use bevy::{
|
||||
input::{
|
||||
ButtonState,
|
||||
keyboard::{KeyboardInput, NativeKey, NativeKeyCode},
|
||||
mouse::{MouseMotion, MouseWheel},
|
||||
},
|
||||
prelude::*,
|
||||
window::PrimaryWindow,
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::{vec2, vec3, Mat4, Vec2, Vec3};
|
||||
use nanoid::nanoid;
|
||||
use dashmap::DashMap;
|
||||
use glam::{Mat4, Vec3, vec3};
|
||||
use mint::Vector2;
|
||||
use rustc_hash::{FxHashMap, FxHasher};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
use stereokit::{ray_from_mouse, ButtonState, Key, StereoKitMultiThread};
|
||||
use xkbcommon::xkb::{Context, Keymap, FORMAT_TEXT_V1};
|
||||
use slotmap::{DefaultKey, Key as SlotKey};
|
||||
use stardust_xr::{
|
||||
schemas::dbus::{
|
||||
ObjectInfo,
|
||||
interfaces::FieldRefProxy,
|
||||
list_query::{ListEvent, ObjectListQuery},
|
||||
object_registry::ObjectRegistry,
|
||||
query::{ObjectQuery, QueryContext, QueryEvent},
|
||||
},
|
||||
values::Datamap,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use tokio::sync::{Notify, mpsc, watch};
|
||||
use tokio::task::{AbortHandle, JoinSet};
|
||||
use tokio::time::{Duration, timeout};
|
||||
use xkbcommon_rs::{Context, Keymap, KeymapFormat, xkb_keymap::CompileFlags};
|
||||
use zbus::{Connection, names::OwnedInterfaceName};
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
#[derive(Clone)]
|
||||
struct HandlerInfo {
|
||||
handler: ObjectInfo,
|
||||
field_ref: Arc<Field>,
|
||||
keyboard_proxy: KeyboardHandlerProxy<'static>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct InputEvent {
|
||||
key: u32,
|
||||
pressed: bool,
|
||||
}
|
||||
|
||||
pub struct FlatscreenInputPlugin;
|
||||
impl Plugin for FlatscreenInputPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, setup);
|
||||
// yes the input method will be delayed by one frame, its only for debugging anyways
|
||||
app.add_systems(Update, update_pointer);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
#[require(Camera3d)]
|
||||
pub struct FlatscreenCam;
|
||||
|
||||
fn setup(mut cmds: Commands, object_registry: Res<ObjectRegistryRes>) {
|
||||
let Ok(pointer) = MousePointer::new(object_registry.0.clone())
|
||||
.inspect_err(|err| error!("unable to create mouse pointer: {err}"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
cmds.spawn((FlatscreenCam, Name::new("Flatscreen Camera")));
|
||||
cmds.insert_resource(pointer);
|
||||
}
|
||||
|
||||
fn update_pointer(
|
||||
window: Single<(&Window), With<PrimaryWindow>>,
|
||||
mut cam: Single<(&Camera, &GlobalTransform, &mut Transform), With<FlatscreenCam>>,
|
||||
mut pointer: ResMut<MousePointer>,
|
||||
connection: Res<DbusConnection>,
|
||||
object_registry: Res<ObjectRegistryRes>,
|
||||
mouse_buttons: Res<ButtonInput<MouseButton>>,
|
||||
keyboard_buttons: Res<ButtonInput<KeyCode>>,
|
||||
mut scroll: EventReader<MouseWheel>,
|
||||
mut motion: EventReader<MouseMotion>,
|
||||
mut keyboard_input_events: EventReader<KeyboardInput>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let (cam, cam_transform, mut cam_local_transform) = cam.into_inner();
|
||||
if keyboard_buttons.pressed(KeyCode::ShiftLeft) && mouse_buttons.pressed(MouseButton::Right) {
|
||||
let (mut yaw, mut pitch, _) = cam_local_transform.rotation.to_euler(EulerRot::YXZ);
|
||||
|
||||
for e in motion.read() {
|
||||
let scale = -0.003;
|
||||
pitch += e.delta.y * scale;
|
||||
yaw += e.delta.x * scale;
|
||||
}
|
||||
|
||||
cam_local_transform.rotation = Quat::from_rotation_y(yaw) * Quat::from_rotation_x(pitch);
|
||||
|
||||
let mut move_vec = Vec3::ZERO;
|
||||
move_vec.x += keyboard_buttons.pressed(KeyCode::KeyD) as u32 as f32;
|
||||
move_vec.x -= keyboard_buttons.pressed(KeyCode::KeyA) as u32 as f32;
|
||||
move_vec.z += keyboard_buttons.pressed(KeyCode::KeyS) as u32 as f32;
|
||||
move_vec.z -= keyboard_buttons.pressed(KeyCode::KeyW) as u32 as f32;
|
||||
move_vec.y += keyboard_buttons.pressed(KeyCode::KeyE) as u32 as f32;
|
||||
move_vec.y -= keyboard_buttons.pressed(KeyCode::KeyQ) as u32 as f32;
|
||||
|
||||
let move_vec = cam_local_transform.rotation * move_vec.normalize_or_zero();
|
||||
cam_local_transform.translation += move_vec * time.delta_secs() * 3.0;
|
||||
|
||||
return;
|
||||
}
|
||||
let Some(ray) = window
|
||||
.cursor_position()
|
||||
.and_then(|pos| get_viewport_pos(pos, cam))
|
||||
.and_then(|pos| cam.viewport_to_world(cam_transform, pos).ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
pointer.update(
|
||||
&connection,
|
||||
&object_registry,
|
||||
ray,
|
||||
&mouse_buttons,
|
||||
&keyboard_buttons,
|
||||
scroll,
|
||||
keyboard_input_events,
|
||||
);
|
||||
}
|
||||
|
||||
fn get_viewport_pos(logical_pos: Vec2, cam: &Camera) -> Option<Vec2> {
|
||||
if let Some(viewport_rect) = cam.logical_viewport_rect() {
|
||||
if !viewport_rect.contains(logical_pos) {
|
||||
return None;
|
||||
}
|
||||
Some(logical_pos - viewport_rect.min)
|
||||
} else {
|
||||
Some(logical_pos)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct MouseEvent {
|
||||
select: f32,
|
||||
middle: f32,
|
||||
context: f32,
|
||||
grab: f32,
|
||||
scroll_continuous: Vec2,
|
||||
scroll_discrete: Vec2,
|
||||
scroll_continuous: Vector2<f32>,
|
||||
scroll_discrete: Vector2<f32>,
|
||||
raw_input_events: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct KeyboardEvent {
|
||||
pub keyboard: (),
|
||||
pub xkbv1: (),
|
||||
pub keymap_id: String,
|
||||
pub keys: Vec<i32>,
|
||||
}
|
||||
impl Default for KeyboardEvent {
|
||||
impl Default for MouseEvent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
keyboard: (),
|
||||
xkbv1: (),
|
||||
keymap_id: "flatscreen".to_string(),
|
||||
keys: Default::default(),
|
||||
MouseEvent {
|
||||
select: 0.0,
|
||||
middle: 0.0,
|
||||
context: 0.0,
|
||||
grab: 0.0,
|
||||
scroll_continuous: [0.0; 2].into(),
|
||||
scroll_discrete: [0.0; 2].into(),
|
||||
raw_input_events: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[zbus::proxy(
|
||||
interface = "org.stardustxr.XKBv1",
|
||||
default_service = "org.stardustxr.XKBv1"
|
||||
)]
|
||||
trait KeyboardHandler {
|
||||
async fn keymap(&self, keymap_id: u64) -> zbus::Result<()>;
|
||||
async fn key_state(&self, key: u32, pressed: bool) -> zbus::Result<()>;
|
||||
async fn reset(&self) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
// Make KeyboardHandlerProxy queryable
|
||||
stardust_xr::schemas::impl_queryable_for_proxy!(KeyboardHandlerProxy);
|
||||
|
||||
// Query context for keyboard handlers
|
||||
#[derive(Debug, Clone)]
|
||||
struct KeyboardQueryContext;
|
||||
impl QueryContext for KeyboardQueryContext {}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct MousePointer {
|
||||
node: Arc<Node>,
|
||||
node: OwnedNode,
|
||||
keymap: DefaultKey,
|
||||
spatial: Arc<Spatial>,
|
||||
pointer: Arc<InputMethod>,
|
||||
mouse_datamap: TypedDatamap<MouseEvent>,
|
||||
keyboard_datamap: TypedDatamap<KeyboardEvent>,
|
||||
keyboard_sender: Arc<PulseSender>,
|
||||
capture_manager: CaptureManager,
|
||||
mouse_datamap: MouseEvent,
|
||||
// Task management
|
||||
focus_task_abort_handle: AbortHandle,
|
||||
input_delivery_task_abort_handle: AbortHandle,
|
||||
// Channels
|
||||
input_event_tx: mpsc::UnboundedSender<InputEvent>,
|
||||
// Notification for focus recalculation
|
||||
focus_notify: Arc<Notify>,
|
||||
}
|
||||
impl MousePointer {
|
||||
pub fn new() -> Result<Self> {
|
||||
let node = Node::create(&INTERNAL_CLIENT, "", &nanoid!(), false).add_to_scenegraph()?;
|
||||
let spatial = Spatial::add_to(&node, None, Mat4::IDENTITY, false).unwrap();
|
||||
let pointer =
|
||||
InputMethod::add_to(&node, InputType::Pointer(Pointer::default()), None).unwrap();
|
||||
pub fn new(object_registry: Arc<ObjectRegistry>) -> Result<Self> {
|
||||
let node = Node::generate(&INTERNAL_CLIENT, false).add_to_scenegraph_owned()?;
|
||||
let spatial = Spatial::add_to(&node.0, None, Mat4::IDENTITY, false);
|
||||
let pointer = InputMethod::add_to(
|
||||
&node.0,
|
||||
InputDataType::Pointer(Pointer::default()),
|
||||
Datamap::from_typed(MouseEvent::default())?,
|
||||
)?;
|
||||
|
||||
KEYMAPS.lock().insert(
|
||||
"flatscreen".to_string(),
|
||||
Keymap::new_from_names(&Context::new(0), "evdev", "", "", "", None, 0)
|
||||
let context = Context::new(0).unwrap();
|
||||
let keymap = KEYMAPS.lock().insert(
|
||||
Keymap::new_from_names(context, None, CompileFlags::NO_FLAGS)
|
||||
.unwrap()
|
||||
.get_as_string(FORMAT_TEXT_V1),
|
||||
.get_as_string(KeymapFormat::TextV1)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let keyboard_sender =
|
||||
PulseSender::add_to(&node, Mask::from_struct::<KeyboardEvent>()).unwrap();
|
||||
// Create channels and notification
|
||||
let (focused_handler_tx, focused_handler_rx) = watch::channel::<Option<HandlerInfo>>(None);
|
||||
let (input_event_tx, input_event_rx) = mpsc::unbounded_channel::<InputEvent>();
|
||||
let focus_notify = Arc::new(Notify::new());
|
||||
// Spawn input delivery task
|
||||
info!("Creating input delivery task");
|
||||
let input_delivery_task_abort_handle = task::new(
|
||||
|| "Mouse pointer input delivery task",
|
||||
Self::input_delivery_task(
|
||||
object_registry.get_connection().clone(),
|
||||
focused_handler_rx,
|
||||
input_event_rx,
|
||||
keymap.data().as_ffi(),
|
||||
),
|
||||
)?
|
||||
.abort_handle();
|
||||
info!("Input delivery task created successfully");
|
||||
|
||||
// Spawn focus tracking task
|
||||
info!("Creating focus tracking task");
|
||||
let focus_task_abort_handle = task::new(
|
||||
|| "Mouse pointer focus task",
|
||||
Self::focus_tracking_task(
|
||||
object_registry,
|
||||
focus_notify.clone(),
|
||||
spatial.clone(),
|
||||
pointer.clone(),
|
||||
focused_handler_tx,
|
||||
),
|
||||
)?
|
||||
.abort_handle();
|
||||
info!("Focus tracking task created successfully");
|
||||
|
||||
Ok(MousePointer {
|
||||
node,
|
||||
spatial,
|
||||
pointer,
|
||||
capture_manager: CaptureManager::default(),
|
||||
mouse_datamap: Default::default(),
|
||||
keyboard_datamap: Default::default(),
|
||||
keyboard_sender,
|
||||
keymap,
|
||||
focus_task_abort_handle,
|
||||
input_delivery_task_abort_handle,
|
||||
input_event_tx,
|
||||
focus_notify,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, sk: &impl StereoKitMultiThread) {
|
||||
let mouse = sk.input_mouse();
|
||||
pub fn update(
|
||||
&mut self,
|
||||
dbus_connection: &Connection,
|
||||
object_registry: &ObjectRegistry,
|
||||
ray: Ray3d,
|
||||
mouse_buttons: &ButtonInput<MouseButton>,
|
||||
keyboard_buttons: &ButtonInput<KeyCode>,
|
||||
mut scroll: EventReader<MouseWheel>,
|
||||
mut keyboard_input_events: EventReader<KeyboardInput>,
|
||||
) {
|
||||
let mut discrete = Vec2::ZERO;
|
||||
let mut continuous = Vec2::ZERO;
|
||||
for e in scroll.read() {
|
||||
match e.unit {
|
||||
bevy::input::mouse::MouseScrollUnit::Line => {
|
||||
discrete.x += e.x;
|
||||
discrete.y += e.y;
|
||||
}
|
||||
bevy::input::mouse::MouseScrollUnit::Pixel => {
|
||||
continuous.x += e.x;
|
||||
continuous.y += e.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ray = ray_from_mouse(mouse.pos).unwrap();
|
||||
self.spatial.set_local_transform(
|
||||
Mat4::look_to_rh(
|
||||
Vec3::from(ray.pos),
|
||||
Vec3::from(ray.dir),
|
||||
vec3(0.0, 1.0, 0.0),
|
||||
)
|
||||
.inverse(),
|
||||
Mat4::look_to_rh(ray.origin, Vec3::from(ray.direction), Vec3::Y).inverse(),
|
||||
);
|
||||
{
|
||||
// Set pointer input datamap
|
||||
self.mouse_datamap.select =
|
||||
if sk.input_key(Key::MouseLeft).contains(ButtonState::ACTIVE) {
|
||||
1.0f32
|
||||
} else {
|
||||
0.0f32
|
||||
};
|
||||
self.mouse_datamap.grab = if sk.input_key(Key::MouseRight).contains(ButtonState::ACTIVE)
|
||||
{
|
||||
1.0f32
|
||||
} else {
|
||||
0.0f32
|
||||
self.mouse_datamap = MouseEvent {
|
||||
select: mouse_buttons.pressed(MouseButton::Left) as u32 as f32,
|
||||
middle: mouse_buttons.pressed(MouseButton::Middle) as u32 as f32,
|
||||
context: mouse_buttons.pressed(MouseButton::Right) as u32 as f32,
|
||||
grab: mouse_buttons.pressed(MouseButton::Right) as u32 as f32, // Was Mouse 5
|
||||
scroll_continuous: continuous.into(),
|
||||
scroll_discrete: discrete.into(),
|
||||
raw_input_events: mouse_buttons
|
||||
.get_pressed()
|
||||
.map(|button| match button {
|
||||
MouseButton::Left => input_event_codes::BTN_LEFT!(),
|
||||
MouseButton::Right => input_event_codes::BTN_RIGHT!(),
|
||||
MouseButton::Middle => input_event_codes::BTN_MIDDLE!(),
|
||||
MouseButton::Back => input_event_codes::BTN_BACK!(),
|
||||
MouseButton::Forward => input_event_codes::BTN_FORWARD!(),
|
||||
MouseButton::Other(b) => *b as u32,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
self.mouse_datamap.scroll_continuous = vec2(0.0, mouse.scroll_change / 120.0);
|
||||
self.mouse_datamap.scroll_discrete = vec2(0.0, mouse.scroll_change / 120.0);
|
||||
*self.pointer.datamap.lock() = self.mouse_datamap.to_datamap().ok();
|
||||
*self.pointer.datamap.lock() = Datamap::from_typed(&self.mouse_datamap).unwrap();
|
||||
}
|
||||
self.send_keyboard_input(sk);
|
||||
self.target_pointer_input();
|
||||
|
||||
// Send keyboard input events via channel
|
||||
for event in keyboard_input_events.read() {
|
||||
if let Some(key) = map_key(event.key_code) {
|
||||
let input_event = InputEvent {
|
||||
key,
|
||||
pressed: matches!(event.state, ButtonState::Pressed),
|
||||
};
|
||||
info!(
|
||||
"Sending keyboard input event: key={}, pressed={}",
|
||||
key, input_event.pressed
|
||||
);
|
||||
if let Err(e) = self.input_event_tx.send(input_event) {
|
||||
error!("Failed to send keyboard input event: {}", e);
|
||||
}
|
||||
} else {
|
||||
warn!("Unable to map key code: {:?}", event.key_code);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify focus tracking task to recalculate focus
|
||||
self.focus_notify.notify_waiters();
|
||||
}
|
||||
fn target_pointer_input(&mut self) {
|
||||
let distance_calculator: DistanceCalculator = |space, data, field| {
|
||||
let result = field.ray_march(Ray {
|
||||
origin: vec3(0.0, 0.0, 0.0),
|
||||
direction: vec3(0.0, 0.0, -1.0),
|
||||
space: space.clone(),
|
||||
});
|
||||
let valid =
|
||||
result.deepest_point_distance > 0.0 && result.min_distance.is_sign_negative();
|
||||
valid.then_some(result.deepest_point_distance)
|
||||
};
|
||||
|
||||
self.capture_manager.update_capture(&self.pointer);
|
||||
self.capture_manager
|
||||
.set_new_capture(&self.pointer, distance_calculator);
|
||||
self.capture_manager.apply_capture(&self.pointer);
|
||||
|
||||
if self.capture_manager.capture.upgrade().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut handlers = get_sorted_handlers(&self.pointer, distance_calculator);
|
||||
let first_distance = handlers
|
||||
.first()
|
||||
.map(|(_, distance)| *distance)
|
||||
.unwrap_or(f32::NEG_INFINITY);
|
||||
|
||||
self.pointer.set_handler_order(
|
||||
handlers
|
||||
.iter()
|
||||
.filter(|(handler, distance)| (distance - first_distance).abs() <= 0.001)
|
||||
.map(|(handler, _)| handler),
|
||||
);
|
||||
}
|
||||
|
||||
fn send_keyboard_input(&mut self, sk: &impl StereoKitMultiThread) {
|
||||
let rx = PULSE_RECEIVER_REGISTRY
|
||||
.get_valid_contents()
|
||||
.into_iter()
|
||||
.filter(|rx| mask_matches(&rx.mask, &self.keyboard_sender.mask))
|
||||
.map(|rx| {
|
||||
let result = rx.field.ray_march(Ray {
|
||||
async fn focus_tracking_task(
|
||||
object_registry: Arc<ObjectRegistry>,
|
||||
focus_notify: Arc<Notify>,
|
||||
spatial: Arc<Spatial>,
|
||||
pointer: Arc<InputMethod>,
|
||||
focused_handler_tx: watch::Sender<Option<HandlerInfo>>,
|
||||
) {
|
||||
info!("Focus tracking task started");
|
||||
|
||||
// Create keyboard handler query inside the task
|
||||
let mut keyboard_query = ObjectQuery::<
|
||||
(FieldRefProxy<'static>, KeyboardHandlerProxy<'static>),
|
||||
_,
|
||||
>::new(object_registry.clone(), ());
|
||||
let (keyboard_handlers, mapper) = keyboard_query.to_list_query();
|
||||
task::new(
|
||||
|| "Focus tracking mapper",
|
||||
mapper.init(async |ev| match ev {
|
||||
ListEvent::NewMatch((field_ref, keyboard_proxy)) => {
|
||||
info!("New keyboard handler found");
|
||||
let uid = timeout(Duration::from_millis(100), field_ref.uid())
|
||||
.await
|
||||
.ok()?
|
||||
.ok()?;
|
||||
let field_node = EXPORTED_FIELDS.lock().get(&uid)?.upgrade()?;
|
||||
let field = field_node.get_aspect::<Field>();
|
||||
Some((field, keyboard_proxy))
|
||||
}
|
||||
ListEvent::Modified((field_ref, keyboard_proxy)) => {
|
||||
let uid = timeout(Duration::from_millis(100), field_ref.uid())
|
||||
.await
|
||||
.ok()?
|
||||
.ok()?;
|
||||
let field_node = EXPORTED_FIELDS.lock().get(&uid)?.upgrade()?;
|
||||
let field = field_node.get_aspect::<Field>();
|
||||
Some((field, keyboard_proxy))
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
);
|
||||
|
||||
// Main focus calculation loop
|
||||
loop {
|
||||
let mut closest_handler = None;
|
||||
let mut closest_distance = f32::MAX;
|
||||
|
||||
// Find closest handler
|
||||
for (handler, (field_ref, keyboard_proxy)) in &*keyboard_handlers.iter().await {
|
||||
let Ok(field_ref) = field_ref else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let result = field_ref.ray_march(Ray {
|
||||
origin: vec3(0.0, 0.0, 0.0),
|
||||
direction: vec3(0.0, 0.0, -1.0),
|
||||
space: self.spatial.clone(),
|
||||
space: spatial.clone(),
|
||||
});
|
||||
(rx, result)
|
||||
})
|
||||
.filter(|(_rx, result)| {
|
||||
result.deepest_point_distance > 0.0 && result.min_distance < 0.05
|
||||
})
|
||||
.reduce(|(rx_a, result_a), (rx_b, result_b)| {
|
||||
if result_a.deepest_point_distance < result_b.deepest_point_distance {
|
||||
(rx_a, result_a)
|
||||
} else {
|
||||
(rx_b, result_b)
|
||||
|
||||
if result.deepest_point_distance > 0.0
|
||||
&& result.min_distance < 0.05
|
||||
&& result.deepest_point_distance < closest_distance
|
||||
{
|
||||
closest_distance = result.deepest_point_distance;
|
||||
closest_handler = Some(HandlerInfo {
|
||||
handler: handler.clone(),
|
||||
field_ref: field_ref.clone(),
|
||||
keyboard_proxy: keyboard_proxy.clone(),
|
||||
});
|
||||
}
|
||||
})
|
||||
.map(|(rx, _)| rx);
|
||||
}
|
||||
|
||||
if let Some(rx) = rx {
|
||||
let keys = (8_u32..254)
|
||||
.filter_map(|i| Key::try_from(i).ok())
|
||||
.filter_map(|k| Some((map_key(k)?, sk.input_key(k))))
|
||||
.filter_map(|(i, k)| {
|
||||
if k.contains(ButtonState::JUST_ACTIVE) {
|
||||
Some(i as i32)
|
||||
} else if k.contains(ButtonState::JUST_INACTIVE) {
|
||||
Some(-(i as i32))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// Update focused handler
|
||||
if let Some(ref handler_info) = closest_handler {
|
||||
info!(
|
||||
"Focus tracking task: Focused on handler at distance {}",
|
||||
closest_distance
|
||||
);
|
||||
} else {
|
||||
debug!("Focus tracking task: No handler in focus");
|
||||
}
|
||||
let _ = focused_handler_tx.send(closest_handler);
|
||||
|
||||
self.keyboard_datamap.keys = keys;
|
||||
if !self.keyboard_datamap.keys.is_empty() {
|
||||
rx.send_data(&self.node.uid, self.keyboard_datamap.serialize().unwrap())
|
||||
.unwrap();
|
||||
// Wait for next frame signal
|
||||
focus_notify.notified().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn input_delivery_task(
|
||||
dbus_connection: Connection,
|
||||
mut focused_handler_rx: watch::Receiver<Option<HandlerInfo>>,
|
||||
mut input_event_rx: mpsc::UnboundedReceiver<InputEvent>,
|
||||
keymap_id: u64,
|
||||
) {
|
||||
info!("Input delivery task started");
|
||||
loop {
|
||||
// Handle input events
|
||||
while let Some(input_event) = input_event_rx.recv().await {
|
||||
info!(
|
||||
"Input delivery task: Received input event key={}, pressed={}",
|
||||
input_event.key, input_event.pressed
|
||||
);
|
||||
// Get current focused handler
|
||||
let current_handler = focused_handler_rx.borrow().clone();
|
||||
let Some(handler_info) = current_handler else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Send input to handler using cached proxy
|
||||
info!("Input delivery task: Sending to handler");
|
||||
let keyboard_handler = &handler_info.keyboard_proxy;
|
||||
|
||||
// Register keymap first
|
||||
if let Err(e) = keyboard_handler.keymap(keymap_id).await {
|
||||
warn!("Input delivery task: Failed to register keymap: {}", e);
|
||||
}
|
||||
|
||||
// Send key state
|
||||
if let Err(e) = keyboard_handler
|
||||
.key_state(input_event.key + 8, input_event.pressed)
|
||||
.await
|
||||
{
|
||||
error!("Input delivery task: Failed to send key state: {}", e);
|
||||
} else {
|
||||
info!(
|
||||
"Input delivery task: Successfully sent key {} (pressed={})",
|
||||
input_event.key + 8,
|
||||
input_event.pressed
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_key(key: Key) -> Option<u32> {
|
||||
impl Drop for MousePointer {
|
||||
fn drop(&mut self) {
|
||||
// Abort the persistent tasks when MousePointer is dropped
|
||||
self.focus_task_abort_handle.abort();
|
||||
self.input_delivery_task_abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
fn map_key(key: KeyCode) -> Option<u32> {
|
||||
use KeyCode as Key;
|
||||
match key {
|
||||
Key::Unidentified(NativeKeyCode::Xkb(code)) => Some(code),
|
||||
Key::Backspace => Some(input_event_codes::KEY_BACKSPACE!()),
|
||||
Key::Tab => Some(input_event_codes::KEY_TAB!()),
|
||||
Key::Return => Some(input_event_codes::KEY_ENTER!()),
|
||||
Key::Shift => Some(input_event_codes::KEY_LEFTSHIFT!()),
|
||||
Key::Ctrl => Some(input_event_codes::KEY_LEFTCTRL!()),
|
||||
Key::Alt => Some(input_event_codes::KEY_LEFTALT!()),
|
||||
Key::Enter => Some(input_event_codes::KEY_ENTER!()),
|
||||
Key::ShiftLeft => Some(input_event_codes::KEY_LEFTSHIFT!()),
|
||||
Key::ShiftRight => Some(input_event_codes::KEY_RIGHTSHIFT!()),
|
||||
Key::ControlLeft => Some(input_event_codes::KEY_LEFTCTRL!()),
|
||||
Key::ControlRight => Some(input_event_codes::KEY_RIGHTCTRL!()),
|
||||
Key::AltLeft => Some(input_event_codes::KEY_LEFTALT!()),
|
||||
Key::AltRight => Some(input_event_codes::KEY_RIGHTALT!()),
|
||||
Key::CapsLock => Some(input_event_codes::KEY_CAPSLOCK!()),
|
||||
Key::Esc => Some(input_event_codes::KEY_ESC!()),
|
||||
Key::Escape => Some(input_event_codes::KEY_ESC!()),
|
||||
Key::Space => Some(input_event_codes::KEY_SPACE!()),
|
||||
Key::End => Some(input_event_codes::KEY_END!()),
|
||||
Key::Home => Some(input_event_codes::KEY_HOME!()),
|
||||
Key::Left => Some(input_event_codes::KEY_LEFT!()),
|
||||
Key::Right => Some(input_event_codes::KEY_RIGHT!()),
|
||||
Key::Up => Some(input_event_codes::KEY_UP!()),
|
||||
Key::Down => Some(input_event_codes::KEY_DOWN!()),
|
||||
Key::ArrowLeft => Some(input_event_codes::KEY_LEFT!()),
|
||||
Key::ArrowRight => Some(input_event_codes::KEY_RIGHT!()),
|
||||
Key::ArrowUp => Some(input_event_codes::KEY_UP!()),
|
||||
Key::ArrowDown => Some(input_event_codes::KEY_DOWN!()),
|
||||
Key::PageUp => Some(input_event_codes::KEY_PAGEUP!()),
|
||||
Key::PageDown => Some(input_event_codes::KEY_PAGEDOWN!()),
|
||||
Key::PrintScreen => Some(input_event_codes::KEY_PRINT!()),
|
||||
Key::KeyInsert => Some(input_event_codes::KEY_INSERT!()),
|
||||
Key::Del => Some(input_event_codes::KEY_DELETE!()),
|
||||
Key::Key0 => Some(input_event_codes::KEY_0!()),
|
||||
Key::Key1 => Some(input_event_codes::KEY_1!()),
|
||||
Key::Key2 => Some(input_event_codes::KEY_2!()),
|
||||
Key::Key3 => Some(input_event_codes::KEY_3!()),
|
||||
Key::Key4 => Some(input_event_codes::KEY_4!()),
|
||||
Key::Key5 => Some(input_event_codes::KEY_5!()),
|
||||
Key::Key6 => Some(input_event_codes::KEY_6!()),
|
||||
Key::Key7 => Some(input_event_codes::KEY_7!()),
|
||||
Key::Key8 => Some(input_event_codes::KEY_8!()),
|
||||
Key::Key9 => Some(input_event_codes::KEY_9!()),
|
||||
Key::A => Some(input_event_codes::KEY_A!()),
|
||||
Key::B => Some(input_event_codes::KEY_B!()),
|
||||
Key::C => Some(input_event_codes::KEY_C!()),
|
||||
Key::D => Some(input_event_codes::KEY_D!()),
|
||||
Key::E => Some(input_event_codes::KEY_E!()),
|
||||
Key::F => Some(input_event_codes::KEY_F!()),
|
||||
Key::G => Some(input_event_codes::KEY_G!()),
|
||||
Key::H => Some(input_event_codes::KEY_H!()),
|
||||
Key::I => Some(input_event_codes::KEY_I!()),
|
||||
Key::J => Some(input_event_codes::KEY_J!()),
|
||||
Key::K => Some(input_event_codes::KEY_K!()),
|
||||
Key::L => Some(input_event_codes::KEY_L!()),
|
||||
Key::M => Some(input_event_codes::KEY_M!()),
|
||||
Key::N => Some(input_event_codes::KEY_N!()),
|
||||
Key::O => Some(input_event_codes::KEY_O!()),
|
||||
Key::P => Some(input_event_codes::KEY_P!()),
|
||||
Key::Q => Some(input_event_codes::KEY_Q!()),
|
||||
Key::R => Some(input_event_codes::KEY_R!()),
|
||||
Key::S => Some(input_event_codes::KEY_S!()),
|
||||
Key::T => Some(input_event_codes::KEY_T!()),
|
||||
Key::U => Some(input_event_codes::KEY_U!()),
|
||||
Key::V => Some(input_event_codes::KEY_V!()),
|
||||
Key::W => Some(input_event_codes::KEY_W!()),
|
||||
Key::X => Some(input_event_codes::KEY_X!()),
|
||||
Key::Y => Some(input_event_codes::KEY_Y!()),
|
||||
Key::Z => Some(input_event_codes::KEY_Z!()),
|
||||
Key::Insert => Some(input_event_codes::KEY_INSERT!()),
|
||||
Key::Delete => Some(input_event_codes::KEY_DELETE!()),
|
||||
Key::Digit0 => Some(input_event_codes::KEY_0!()),
|
||||
Key::Digit1 => Some(input_event_codes::KEY_1!()),
|
||||
Key::Digit2 => Some(input_event_codes::KEY_2!()),
|
||||
Key::Digit3 => Some(input_event_codes::KEY_3!()),
|
||||
Key::Digit4 => Some(input_event_codes::KEY_4!()),
|
||||
Key::Digit5 => Some(input_event_codes::KEY_5!()),
|
||||
Key::Digit6 => Some(input_event_codes::KEY_6!()),
|
||||
Key::Digit7 => Some(input_event_codes::KEY_7!()),
|
||||
Key::Digit8 => Some(input_event_codes::KEY_8!()),
|
||||
Key::Digit9 => Some(input_event_codes::KEY_9!()),
|
||||
Key::KeyA => Some(input_event_codes::KEY_A!()),
|
||||
Key::KeyB => Some(input_event_codes::KEY_B!()),
|
||||
Key::KeyC => Some(input_event_codes::KEY_C!()),
|
||||
Key::KeyD => Some(input_event_codes::KEY_D!()),
|
||||
Key::KeyE => Some(input_event_codes::KEY_E!()),
|
||||
Key::KeyF => Some(input_event_codes::KEY_F!()),
|
||||
Key::KeyG => Some(input_event_codes::KEY_G!()),
|
||||
Key::KeyH => Some(input_event_codes::KEY_H!()),
|
||||
Key::KeyI => Some(input_event_codes::KEY_I!()),
|
||||
Key::KeyJ => Some(input_event_codes::KEY_J!()),
|
||||
Key::KeyK => Some(input_event_codes::KEY_K!()),
|
||||
Key::KeyL => Some(input_event_codes::KEY_L!()),
|
||||
Key::KeyM => Some(input_event_codes::KEY_M!()),
|
||||
Key::KeyN => Some(input_event_codes::KEY_N!()),
|
||||
Key::KeyO => Some(input_event_codes::KEY_O!()),
|
||||
Key::KeyP => Some(input_event_codes::KEY_P!()),
|
||||
Key::KeyQ => Some(input_event_codes::KEY_Q!()),
|
||||
Key::KeyR => Some(input_event_codes::KEY_R!()),
|
||||
Key::KeyS => Some(input_event_codes::KEY_S!()),
|
||||
Key::KeyT => Some(input_event_codes::KEY_T!()),
|
||||
Key::KeyU => Some(input_event_codes::KEY_U!()),
|
||||
Key::KeyV => Some(input_event_codes::KEY_V!()),
|
||||
Key::KeyW => Some(input_event_codes::KEY_W!()),
|
||||
Key::KeyX => Some(input_event_codes::KEY_X!()),
|
||||
Key::KeyY => Some(input_event_codes::KEY_Y!()),
|
||||
Key::KeyZ => Some(input_event_codes::KEY_Z!()),
|
||||
Key::Numpad0 => Some(input_event_codes::KEY_NUMERIC_0!()),
|
||||
Key::Numpad1 => Some(input_event_codes::KEY_NUMERIC_1!()),
|
||||
Key::Numpad2 => Some(input_event_codes::KEY_NUMERIC_2!()),
|
||||
@@ -238,24 +606,64 @@ fn map_key(key: Key) -> Option<u32> {
|
||||
Key::F10 => Some(input_event_codes::KEY_F10!()),
|
||||
Key::F11 => Some(input_event_codes::KEY_F11!()),
|
||||
Key::F12 => Some(input_event_codes::KEY_F12!()),
|
||||
Key::F13 => Some(input_event_codes::KEY_F13!()),
|
||||
Key::F14 => Some(input_event_codes::KEY_F14!()),
|
||||
Key::F15 => Some(input_event_codes::KEY_F15!()),
|
||||
Key::F16 => Some(input_event_codes::KEY_F16!()),
|
||||
Key::F17 => Some(input_event_codes::KEY_F17!()),
|
||||
Key::F18 => Some(input_event_codes::KEY_F18!()),
|
||||
Key::F19 => Some(input_event_codes::KEY_F19!()),
|
||||
Key::F20 => Some(input_event_codes::KEY_F20!()),
|
||||
Key::F21 => Some(input_event_codes::KEY_F21!()),
|
||||
Key::F22 => Some(input_event_codes::KEY_F22!()),
|
||||
Key::F23 => Some(input_event_codes::KEY_F23!()),
|
||||
Key::F24 => Some(input_event_codes::KEY_F24!()),
|
||||
Key::Comma => Some(input_event_codes::KEY_COMMA!()),
|
||||
Key::Period => Some(input_event_codes::KEY_DOT!()),
|
||||
Key::SlashFwd => Some(input_event_codes::KEY_SLASH!()),
|
||||
Key::SlashBack => Some(input_event_codes::KEY_BACKSLASH!()),
|
||||
Key::Slash => Some(input_event_codes::KEY_SLASH!()),
|
||||
Key::Backslash => Some(input_event_codes::KEY_BACKSLASH!()),
|
||||
Key::Semicolon => Some(input_event_codes::KEY_SEMICOLON!()),
|
||||
Key::Apostrophe => Some(input_event_codes::KEY_APOSTROPHE!()),
|
||||
Key::BracketOpen => Some(input_event_codes::KEY_LEFTBRACE!()),
|
||||
Key::BracketClose => Some(input_event_codes::KEY_RIGHTBRACE!()),
|
||||
Key::Quote => Some(input_event_codes::KEY_APOSTROPHE!()),
|
||||
Key::BracketLeft => Some(input_event_codes::KEY_LEFTBRACE!()),
|
||||
Key::BracketRight => Some(input_event_codes::KEY_RIGHTBRACE!()),
|
||||
Key::Minus => Some(input_event_codes::KEY_MINUS!()),
|
||||
Key::Equals => Some(input_event_codes::KEY_EQUAL!()),
|
||||
Key::Backtick => None,
|
||||
Key::LCmd => Some(input_event_codes::KEY_LEFTMETA!()),
|
||||
Key::RCmd => Some(input_event_codes::KEY_RIGHTMETA!()),
|
||||
Key::Multiply => Some(input_event_codes::KEY_NUMERIC_STAR!()),
|
||||
Key::Add => Some(input_event_codes::KEY_KPPLUS!()),
|
||||
Key::Subtract => Some(input_event_codes::KEY_MINUS!()),
|
||||
Key::Decimal => Some(input_event_codes::KEY_DOT!()),
|
||||
Key::Divide => Some(input_event_codes::KEY_SLASH!()),
|
||||
Key::Equal => Some(input_event_codes::KEY_EQUAL!()),
|
||||
Key::Backquote => Some(input_event_codes::KEY_GRAVE!()),
|
||||
Key::SuperLeft => Some(input_event_codes::KEY_LEFTMETA!()),
|
||||
Key::SuperRight => Some(input_event_codes::KEY_RIGHTMETA!()),
|
||||
Key::NumpadMultiply => Some(input_event_codes::KEY_NUMERIC_STAR!()),
|
||||
Key::NumpadAdd => Some(input_event_codes::KEY_KPPLUS!()),
|
||||
Key::NumpadSubtract => Some(input_event_codes::KEY_MINUS!()),
|
||||
Key::NumpadDecimal => Some(input_event_codes::KEY_DOT!()),
|
||||
Key::NumpadDivide => Some(input_event_codes::KEY_SLASH!()),
|
||||
Key::ContextMenu => Some(input_event_codes::KEY_CONTEXT_MENU!()),
|
||||
Key::Help => Some(input_event_codes::KEY_HELP!()),
|
||||
Key::NumLock => Some(input_event_codes::KEY_NUMLOCK!()),
|
||||
Key::NumpadBackspace => Some(input_event_codes::KEY_BACKSPACE!()),
|
||||
Key::NumpadClear => Some(input_event_codes::KEY_CLEAR!()),
|
||||
Key::NumpadClearEntry => Some(input_event_codes::KEY_CLEAR!()),
|
||||
Key::NumpadComma => Some(input_event_codes::KEY_COMMA!()),
|
||||
Key::NumpadEnter => Some(input_event_codes::KEY_ENTER!()),
|
||||
Key::NumpadEqual => Some(input_event_codes::KEY_EQUAL!()),
|
||||
Key::NumpadHash => Some(input_event_codes::KEY_NUMERIC_POUND!()),
|
||||
Key::NumpadStar => Some(input_event_codes::KEY_KPASTERISK!()),
|
||||
Key::Fn => Some(input_event_codes::KEY_FN!()),
|
||||
Key::ScrollLock => Some(input_event_codes::KEY_SCROLLLOCK!()),
|
||||
Key::Pause => Some(input_event_codes::KEY_PAUSE!()),
|
||||
Key::Power => Some(input_event_codes::KEY_POWER!()),
|
||||
Key::Sleep => Some(input_event_codes::KEY_SLEEP!()),
|
||||
Key::Suspend => Some(input_event_codes::KEY_SUSPEND!()),
|
||||
Key::Again => Some(input_event_codes::KEY_AGAIN!()),
|
||||
Key::Copy => Some(input_event_codes::KEY_COPY!()),
|
||||
Key::Cut => Some(input_event_codes::KEY_CUT!()),
|
||||
Key::Find => Some(input_event_codes::KEY_FIND!()),
|
||||
Key::Open => Some(input_event_codes::KEY_OPEN!()),
|
||||
Key::Paste => Some(input_event_codes::KEY_PASTE!()),
|
||||
Key::Props => Some(input_event_codes::KEY_PROPS!()),
|
||||
Key::Select => Some(input_event_codes::KEY_SELECT!()),
|
||||
Key::Undo => Some(input_event_codes::KEY_UNDO!()),
|
||||
Key::Hiragana => Some(input_event_codes::KEY_HIRAGANA!()),
|
||||
Key::Katakana => Some(input_event_codes::KEY_KATAKANA!()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
417
src/objects/input/oxr_controller.rs
Normal file
417
src/objects/input/oxr_controller.rs
Normal file
@@ -0,0 +1,417 @@
|
||||
use super::{CaptureManager, get_sorted_handlers};
|
||||
use crate::{
|
||||
DbusConnection, PreFrameWait,
|
||||
core::client::INTERNAL_CLIENT,
|
||||
get_time,
|
||||
nodes::{
|
||||
Node, OwnedNode,
|
||||
drawable::{
|
||||
MaterialParameter,
|
||||
model::{Model, ModelPart},
|
||||
},
|
||||
fields::{Field, FieldTrait},
|
||||
input::{INPUT_HANDLER_REGISTRY, InputDataType, InputHandler, InputMethod, Tip},
|
||||
spatial::Spatial,
|
||||
},
|
||||
objects::{AsyncTracked, ObjectHandle, SpatialRef, Tracked},
|
||||
};
|
||||
use bevy::{asset::Handle, ecs::resource::Resource};
|
||||
use bevy::{math::Affine3, prelude::*};
|
||||
use bevy_mod_openxr::{
|
||||
action_binding::{OxrSendActionBindings, OxrSuggestActionBinding},
|
||||
helper_traits::{ToIsometry3d, ToVec2},
|
||||
resources::{OxrFrameState, OxrInstance, Pipelined},
|
||||
session::OxrSession,
|
||||
};
|
||||
use bevy_mod_xr::{
|
||||
hands::HandSide,
|
||||
session::{XrPreDestroySession, XrSessionCreated, XrSessionCreatedEvent},
|
||||
spaces::{XrPrimaryReferenceSpace, XrReferenceSpace, XrSpace},
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::{Affine3A, Mat4, Vec2, Vec3};
|
||||
use openxr::{Action, ActiveActionSet, SpaceLocationFlags};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stardust_xr::values::{Datamap, ResourceID, color::Rgb};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use tracing::instrument;
|
||||
use zbus::Connection;
|
||||
pub struct ControllerPlugin;
|
||||
const CURSOR_MODEL_PATH: &str = "/tmp/stardust_server/models/cursor.glb";
|
||||
impl Plugin for ControllerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
let cursor = include_bytes!("cursor.glb");
|
||||
fs::create_dir_all(
|
||||
PathBuf::from_str(CURSOR_MODEL_PATH)
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap(),
|
||||
);
|
||||
fs::write(CURSOR_MODEL_PATH, cursor).expect("can't write tmp cursor model file");
|
||||
app.add_systems(OxrSendActionBindings, suggest_bindings.run_if(run_once));
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
create_spaces.run_if(on_event::<XrSessionCreatedEvent>),
|
||||
);
|
||||
app.add_systems(XrPreDestroySession, destroy_spaces);
|
||||
app.add_systems(Startup, setup.run_if(resource_exists::<OxrInstance>));
|
||||
app.add_systems(PreFrameWait, update.run_if(resource_exists::<Controllers>));
|
||||
}
|
||||
}
|
||||
|
||||
// the api is just slightly nicer when using the bevy_mod_openxr solution okay?
|
||||
fn suggest_bindings(
|
||||
instance: Res<OxrInstance>,
|
||||
actions: Res<Actions>,
|
||||
mut suggest: EventWriter<OxrSuggestActionBinding>,
|
||||
) {
|
||||
let mut bind_all = |interaction_profile: &'static str,
|
||||
bindings: &[(openxr::sys::Action, &[&'static str])]| {
|
||||
for (action, bindings) in bindings {
|
||||
suggest.write(OxrSuggestActionBinding {
|
||||
action: *action,
|
||||
interaction_profile: interaction_profile.into(),
|
||||
bindings: bindings.iter().copied().map(Cow::Borrowed).collect(),
|
||||
});
|
||||
}
|
||||
};
|
||||
bind_all(
|
||||
"/interaction_profiles/oculus/touch_controller",
|
||||
&[
|
||||
(
|
||||
actions.trigger.as_raw(),
|
||||
&[
|
||||
"/user/hand/left/input/trigger/value",
|
||||
"/user/hand/right/input/trigger/value",
|
||||
],
|
||||
),
|
||||
(
|
||||
actions.stick_click.as_raw(),
|
||||
&[
|
||||
"/user/hand/left/input/thumbstick/click",
|
||||
"/user/hand/right/input/thumbstick/click",
|
||||
],
|
||||
),
|
||||
(
|
||||
actions.button.as_raw(),
|
||||
&[
|
||||
"/user/hand/left/input/x/click",
|
||||
"/user/hand/left/input/y/click",
|
||||
"/user/hand/right/input/a/click",
|
||||
"/user/hand/right/input/b/click",
|
||||
],
|
||||
),
|
||||
(
|
||||
actions.grip.as_raw(),
|
||||
&[
|
||||
"/user/hand/left/input/squeeze/value",
|
||||
"/user/hand/right/input/squeeze/value",
|
||||
],
|
||||
),
|
||||
(
|
||||
actions.stick.as_raw(),
|
||||
&[
|
||||
"/user/hand/left/input/thumbstick",
|
||||
"/user/hand/right/input/thumbstick",
|
||||
],
|
||||
),
|
||||
(
|
||||
actions.space.as_raw(),
|
||||
&[
|
||||
"/user/hand/left/input/aim/pose",
|
||||
"/user/hand/right/input/aim/pose",
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
bind_all(
|
||||
"/interaction_profiles/khr/simple_controller",
|
||||
&[(
|
||||
actions.space.as_raw(),
|
||||
&[
|
||||
"/user/hand/left/input/aim/pose",
|
||||
"/user/hand/right/input/aim/pose",
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
fn update(
|
||||
mut controllers: ResMut<Controllers>,
|
||||
actions: Res<Actions>,
|
||||
session: Option<Res<OxrSession>>,
|
||||
ref_space: Option<Res<XrPrimaryReferenceSpace>>,
|
||||
state: Option<Res<OxrFrameState>>,
|
||||
pipelined: Option<Res<Pipelined>>,
|
||||
) {
|
||||
let (Some(session), Some(state), Some(ref_space)) = (session, state, ref_space) else {
|
||||
controllers.left.set_enabled(false);
|
||||
controllers.right.set_enabled(false);
|
||||
return;
|
||||
};
|
||||
debug_span!("sync actions").in_scope(|| {
|
||||
session
|
||||
.sync_actions(&[ActiveActionSet::new(&actions.set)])
|
||||
.unwrap();
|
||||
});
|
||||
let time = get_time(pipelined.is_some(), &state);
|
||||
controllers
|
||||
.left
|
||||
.update(&session, &actions, time, ref_space.0);
|
||||
controllers
|
||||
.right
|
||||
.update(&session, &actions, time, ref_space.0);
|
||||
}
|
||||
|
||||
fn create_spaces(
|
||||
session: Res<OxrSession>,
|
||||
mut controllers: ResMut<Controllers>,
|
||||
actions: Res<Actions>,
|
||||
) {
|
||||
// if we ever need more actions than just these we should fully swith to the
|
||||
// bevy_mod_openxr provided stuff
|
||||
session.attach_action_sets(&[&actions.set]);
|
||||
session
|
||||
.sync_actions(&[ActiveActionSet::new(&actions.set)])
|
||||
.unwrap();
|
||||
|
||||
let instance = session.instance();
|
||||
let left = instance.string_to_path("/user/hand/left").unwrap();
|
||||
let right = instance.string_to_path("/user/hand/right").unwrap();
|
||||
let left = session
|
||||
.create_action_space(&actions.space, left, Isometry3d::IDENTITY)
|
||||
.unwrap();
|
||||
let right = session
|
||||
.create_action_space(&actions.space, right, Isometry3d::IDENTITY)
|
||||
.unwrap();
|
||||
controllers.left.space = Some(left);
|
||||
controllers.right.space = Some(right);
|
||||
}
|
||||
|
||||
fn destroy_spaces(session: Res<OxrSession>, mut controllers: ResMut<Controllers>) {
|
||||
if let Some(space) = controllers.left.space.take() {
|
||||
session.destroy_space(space);
|
||||
}
|
||||
if let Some(space) = controllers.right.space.take() {
|
||||
session.destroy_space(space);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(instance: Res<OxrInstance>, connection: Res<DbusConnection>, mut cmds: Commands) {
|
||||
tokio::task::spawn({
|
||||
let connection = connection.clone();
|
||||
async move {
|
||||
connection
|
||||
.request_name("org.stardustxr.Controllers")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
let set = instance
|
||||
.create_action_set("input_method_actions", "Input Method Action Source", 0)
|
||||
.unwrap();
|
||||
let paths = &[
|
||||
instance.string_to_path("/user/hand/left").unwrap(),
|
||||
instance.string_to_path("/user/hand/right").unwrap(),
|
||||
];
|
||||
let actions = Actions {
|
||||
trigger: set.create_action("trigger", "Select", paths).unwrap(),
|
||||
stick_click: set.create_action("stick_click", "Middle", paths).unwrap(),
|
||||
button: set.create_action("face_button", "Context", paths).unwrap(),
|
||||
grip: set.create_action("grip", "Grab", paths).unwrap(),
|
||||
stick: set.create_action("stick", "Scroll", paths).unwrap(),
|
||||
space: set.create_action("pose", "Location", paths).unwrap(),
|
||||
set,
|
||||
};
|
||||
let controllers = Controllers {
|
||||
left: OxrControllerInput::new(&connection, HandSide::Left).unwrap(),
|
||||
right: OxrControllerInput::new(&connection, HandSide::Right).unwrap(),
|
||||
};
|
||||
cmds.insert_resource(controllers);
|
||||
cmds.insert_resource(actions);
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
struct ControllerDatamap {
|
||||
select: f32,
|
||||
middle: f32,
|
||||
context: f32,
|
||||
grab: f32,
|
||||
scroll: Vec2,
|
||||
}
|
||||
#[derive(Resource)]
|
||||
struct Actions {
|
||||
set: openxr::ActionSet,
|
||||
trigger: openxr::Action<f32>,
|
||||
stick_click: openxr::Action<f32>,
|
||||
button: openxr::Action<f32>,
|
||||
grip: openxr::Action<f32>,
|
||||
space: openxr::Action<openxr::Posef>,
|
||||
stick: openxr::Action<openxr::Vector2f>,
|
||||
}
|
||||
#[derive(Resource)]
|
||||
struct Controllers {
|
||||
left: OxrControllerInput,
|
||||
right: OxrControllerInput,
|
||||
}
|
||||
|
||||
pub struct OxrControllerInput {
|
||||
object_handle: ObjectHandle<SpatialRef>,
|
||||
input: Arc<InputMethod>,
|
||||
side: HandSide,
|
||||
model: Arc<Model>,
|
||||
model_part: Arc<ModelPart>,
|
||||
capture_manager: CaptureManager,
|
||||
datamap: ControllerDatamap,
|
||||
tracked: AsyncTracked,
|
||||
space: Option<XrSpace>,
|
||||
_model_node: OwnedNode,
|
||||
}
|
||||
impl OxrControllerInput {
|
||||
fn new(connection: &Connection, side: HandSide) -> Result<Self> {
|
||||
let path = "/org/stardustxr/Controller/".to_string()
|
||||
+ match side {
|
||||
HandSide::Left => "left",
|
||||
HandSide::Right => "right",
|
||||
};
|
||||
let (spatial, object_handle) = SpatialRef::create(connection, &path);
|
||||
let tracked = AsyncTracked::new(connection, &path);
|
||||
let tip = InputDataType::Tip(Tip::default());
|
||||
let node = spatial.node().unwrap();
|
||||
node.set_enabled(false);
|
||||
let model_node = Arc::new(Node::generate(&INTERNAL_CLIENT, true));
|
||||
let model_spatial = Spatial::add_to(
|
||||
&model_node,
|
||||
Some(spatial.clone()),
|
||||
Mat4::from_scale(Vec3::splat(0.02)),
|
||||
false,
|
||||
);
|
||||
let model =
|
||||
Model::add_to(&model_node, ResourceID::Direct(CURSOR_MODEL_PATH.into())).unwrap();
|
||||
let model_part = model.get_model_part("Cursor".to_string()).unwrap();
|
||||
let input = InputMethod::add_to(
|
||||
&node,
|
||||
tip,
|
||||
Datamap::from_typed(ControllerDatamap::default())?,
|
||||
)?;
|
||||
Ok(OxrControllerInput {
|
||||
object_handle,
|
||||
input,
|
||||
side,
|
||||
model,
|
||||
model_part,
|
||||
capture_manager: CaptureManager::default(),
|
||||
datamap: Default::default(),
|
||||
tracked,
|
||||
space: None,
|
||||
_model_node: OwnedNode(model_node),
|
||||
})
|
||||
}
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub fn set_enabled(&self, enabled: bool) {
|
||||
if let Some(node) = self.input.spatial.node() {
|
||||
node.set_enabled(enabled);
|
||||
}
|
||||
self.tracked.set_tracked(enabled);
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
session: &OxrSession,
|
||||
actions: &Actions,
|
||||
time: openxr::Time,
|
||||
ref_space: XrReferenceSpace,
|
||||
) {
|
||||
let Some(space) = self.space.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let _span = debug_span!("locate space").entered();
|
||||
let Ok(location) = session
|
||||
.locate_space(space, &ref_space, time)
|
||||
.inspect_err(|err| error!("error while locating controller space: {err}"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let enabled = location.location_flags.contains(
|
||||
SpaceLocationFlags::POSITION_VALID
|
||||
| SpaceLocationFlags::POSITION_TRACKED
|
||||
| SpaceLocationFlags::ORIENTATION_VALID
|
||||
| SpaceLocationFlags::ORIENTATION_TRACKED,
|
||||
);
|
||||
drop(_span);
|
||||
self.set_enabled(enabled);
|
||||
if enabled {
|
||||
let world_transform = Mat4::from(Affine3A::from(location.pose.to_xr_pose()));
|
||||
self.model_part
|
||||
.set_material_parameter("roughness".to_string(), MaterialParameter::Float(1.0));
|
||||
self.model_part.set_material_parameter(
|
||||
"color".to_string(),
|
||||
MaterialParameter::Color(stardust_xr::values::Color::new(
|
||||
if self.capture_manager.capture.upgrade().is_none() {
|
||||
Rgb::new(1.0, 1.0, 1.0)
|
||||
} else {
|
||||
Rgb::new(0.0, 1.0, 0.75)
|
||||
},
|
||||
1.0,
|
||||
)),
|
||||
);
|
||||
self.input.spatial.set_local_transform(world_transform);
|
||||
}
|
||||
let path = session
|
||||
.instance()
|
||||
.string_to_path(match self.side {
|
||||
HandSide::Left => "/user/hand/left",
|
||||
HandSide::Right => "/user/hand/right",
|
||||
})
|
||||
.unwrap();
|
||||
if let Ok(path) = session.current_interaction_profile(path)
|
||||
&& let Ok(path) = session.instance().path_to_string(path)
|
||||
&& path == "/interaction_profiles/khr/simple_controller"
|
||||
{
|
||||
self.set_enabled(false);
|
||||
}
|
||||
|
||||
fn get<T: openxr::ActionInput + Default>(
|
||||
session: &OxrSession,
|
||||
path: openxr::Path,
|
||||
action: &Action<T>,
|
||||
) -> T {
|
||||
action
|
||||
.state(session, path)
|
||||
.map(|v| v.current_state)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
let _span = debug_span!("apply datamap").entered();
|
||||
self.datamap = ControllerDatamap {
|
||||
select: get(session, path, &actions.trigger),
|
||||
middle: get(session, path, &actions.stick_click) as u32 as f32,
|
||||
context: get(session, path, &actions.button) as u32 as f32,
|
||||
grab: get(session, path, &actions.grip),
|
||||
scroll: get(session, path, &actions.stick).to_vec2(),
|
||||
};
|
||||
*self.input.datamap.lock() = Datamap::from_typed(&self.datamap).unwrap();
|
||||
drop(_span);
|
||||
|
||||
let distance_calculator = |space: &Arc<Spatial>, _data: &InputDataType, field: &Field| {
|
||||
Some(field.distance(space, [0.0; 3].into()).abs())
|
||||
};
|
||||
|
||||
self.capture_manager.update_capture(&self.input);
|
||||
self.capture_manager
|
||||
.set_new_capture(&self.input, distance_calculator);
|
||||
self.capture_manager.apply_capture(&self.input);
|
||||
|
||||
if self.capture_manager.capture.upgrade().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let sorted_handlers = get_sorted_handlers(&self.input, distance_calculator);
|
||||
self.input
|
||||
.set_handler_order(sorted_handlers.iter().map(|(handler, _)| handler));
|
||||
}
|
||||
}
|
||||
360
src/objects/input/oxr_hand.rs
Normal file
360
src/objects/input/oxr_hand.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
use crate::core::client::INTERNAL_CLIENT;
|
||||
use crate::nodes::OwnedNode;
|
||||
use crate::nodes::fields::{Field, FieldTrait};
|
||||
use crate::nodes::input::{Finger, INPUT_HANDLER_REGISTRY, InputDataType, InputHandler, Thumb};
|
||||
use crate::nodes::{
|
||||
Node,
|
||||
input::{Hand, InputMethod, Joint},
|
||||
spatial::Spatial,
|
||||
};
|
||||
use crate::objects::{AsyncTracked, ObjectHandle, SpatialRef, Tracked};
|
||||
use crate::{BevyMaterial, DbusConnection, ObjectRegistryRes, PreFrameWait, get_time};
|
||||
use bevy::prelude::Transform as BevyTransform;
|
||||
use bevy::prelude::*;
|
||||
use bevy_mod_openxr::helper_traits::{ToQuat, ToVec3};
|
||||
use bevy_mod_openxr::resources::{OxrFrameState, Pipelined};
|
||||
use bevy_mod_openxr::session::OxrSession;
|
||||
use bevy_mod_xr::hands::{HandBone, HandSide, XrHandBoneEntities, XrHandBoneRadius};
|
||||
use bevy_mod_xr::session::{XrPreDestroySession, XrSessionCreated, session_available};
|
||||
use bevy_mod_xr::spaces::{XrPrimaryReferenceSpace, XrSpaceLocationFlags};
|
||||
use bevy_sk::hand::GRADIENT_TEXTURE_HANDLE;
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::{Mat4, Quat, Vec3};
|
||||
use openxr::{HandJointLocation, SpaceLocationFlags};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stardust_xr::values::Datamap;
|
||||
use std::sync::Arc;
|
||||
use zbus::Connection;
|
||||
|
||||
use super::{CaptureManager, get_sorted_handlers};
|
||||
|
||||
pub struct HandPlugin;
|
||||
impl Plugin for HandPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(PreFrameWait, update_hands.run_if(resource_exists::<Hands>));
|
||||
app.add_systems(XrSessionCreated, create_trackers);
|
||||
app.add_systems(XrPreDestroySession, destroy_trackers);
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
update_hand_material.run_if(resource_exists::<Hands>),
|
||||
);
|
||||
app.add_systems(Startup, setup.run_if(session_available));
|
||||
}
|
||||
}
|
||||
fn update_hands(
|
||||
mut hands: ResMut<Hands>,
|
||||
session: Option<Res<OxrSession>>,
|
||||
state: Option<Res<OxrFrameState>>,
|
||||
ref_space: Option<Res<XrPrimaryReferenceSpace>>,
|
||||
mut materials: ResMut<Assets<BevyMaterial>>,
|
||||
mut joint_query: Query<(
|
||||
&mut BevyTransform,
|
||||
&mut XrSpaceLocationFlags,
|
||||
&mut XrHandBoneRadius,
|
||||
)>,
|
||||
joints_query: Query<&XrHandBoneEntities>,
|
||||
pipelined: Option<Res<Pipelined>>,
|
||||
) {
|
||||
let (Some(session), Some(state), Some(ref_space)) = (session, state, ref_space) else {
|
||||
hands.left.tracked.set_tracked(false);
|
||||
hands.right.tracked.set_tracked(false);
|
||||
return;
|
||||
};
|
||||
let get_joints = |hand: &mut OxrHandInput| -> Option<openxr::HandJointLocations> {
|
||||
let Some(tracker) = hand.tracker.as_ref() else {
|
||||
hand.input.spatial.node().unwrap().set_enabled(false);
|
||||
hand.tracked.set_tracked(false);
|
||||
return None;
|
||||
};
|
||||
let time = get_time(pipelined.is_some(), &state);
|
||||
session
|
||||
.locate_hand_joints(tracker, &ref_space, time)
|
||||
.inspect_err(|err| error!("Error while locating hand joints"))
|
||||
.ok()
|
||||
.flatten()
|
||||
};
|
||||
let joints_left = get_joints(&mut hands.left);
|
||||
let joints_right = get_joints(&mut hands.right);
|
||||
hands.left.update(joints_left.as_ref(), &mut materials);
|
||||
hands.right.update(joints_right.as_ref(), &mut materials);
|
||||
}
|
||||
|
||||
fn pinch_between(joint_1: &Joint, joint_2: &Joint) -> f32 {
|
||||
const PINCH_MAX: f32 = 0.11;
|
||||
const PINCH_ACTIVACTION_DISTANCE: f32 = 0.01;
|
||||
let combined_radius = joint_1.radius + joint_2.radius;
|
||||
let pinch_dist =
|
||||
Vec3::from(joint_1.position).distance(Vec3::from(joint_2.position)) - combined_radius;
|
||||
(1.0 - ((pinch_dist - PINCH_ACTIVACTION_DISTANCE) / (PINCH_MAX - PINCH_ACTIVACTION_DISTANCE)))
|
||||
.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn create_trackers(session: Res<OxrSession>, mut hands: ResMut<Hands>) {
|
||||
hands.left.tracker = session
|
||||
.create_hand_tracker(openxr::HandEXT::LEFT)
|
||||
.inspect_err(|err| error!("failed to create left hand tracker"))
|
||||
.ok();
|
||||
hands.right.tracker = session
|
||||
.create_hand_tracker(openxr::HandEXT::RIGHT)
|
||||
.inspect_err(|err| error!("failed to create right hand tracker"))
|
||||
.ok();
|
||||
}
|
||||
fn destroy_trackers(mut hands: ResMut<Hands>) {
|
||||
hands.left.tracker.take();
|
||||
hands.right.tracker.take();
|
||||
}
|
||||
#[derive(Component)]
|
||||
struct CorrectHandMaterial;
|
||||
fn update_hand_material(
|
||||
query: Query<
|
||||
(Entity, &HandSide),
|
||||
(
|
||||
With<XrHandBoneEntities>,
|
||||
With<MeshMaterial3d<BevyMaterial>>,
|
||||
Without<CorrectHandMaterial>,
|
||||
),
|
||||
>,
|
||||
mut cmds: Commands,
|
||||
hands: Res<Hands>,
|
||||
) {
|
||||
for (entity, side) in &query {
|
||||
let handle = match side {
|
||||
HandSide::Left => hands.left.material.clone(),
|
||||
HandSide::Right => hands.right.material.clone(),
|
||||
};
|
||||
cmds.entity(entity)
|
||||
.insert(MeshMaterial3d(handle))
|
||||
.insert(CorrectHandMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(
|
||||
connection: Res<DbusConnection>,
|
||||
mut cmds: Commands,
|
||||
mut materials: ResMut<Assets<BevyMaterial>>,
|
||||
) {
|
||||
tokio::task::spawn({
|
||||
let connection = connection.clone();
|
||||
async move {
|
||||
connection
|
||||
.request_name("org.stardustxr.Hands")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
cmds.insert_resource(Hands {
|
||||
left: OxrHandInput::new(&connection, HandSide::Left, &mut materials).unwrap(),
|
||||
right: OxrHandInput::new(&connection, HandSide::Right, &mut materials).unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
fn convert_joint(joint: HandJointLocation) -> Joint {
|
||||
Joint {
|
||||
position: joint.pose.position.to_vec3().into(),
|
||||
rotation: joint.pose.orientation.to_quat().into(),
|
||||
radius: joint.radius,
|
||||
distance: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct Hands {
|
||||
left: OxrHandInput,
|
||||
right: OxrHandInput,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
struct HandDatamap {
|
||||
pinch_strength: f32,
|
||||
grab_strength: f32,
|
||||
}
|
||||
|
||||
pub struct OxrHandInput {
|
||||
_node: OwnedNode,
|
||||
palm_spatial: Arc<Spatial>,
|
||||
palm_object: ObjectHandle<SpatialRef>,
|
||||
side: HandSide,
|
||||
input: Arc<InputMethod>,
|
||||
capture_manager: CaptureManager,
|
||||
datamap: HandDatamap,
|
||||
tracked: AsyncTracked,
|
||||
tracker: Option<openxr::HandTracker>,
|
||||
captured: bool,
|
||||
material: Handle<BevyMaterial>,
|
||||
}
|
||||
impl OxrHandInput {
|
||||
pub fn new(
|
||||
connection: &Connection,
|
||||
side: HandSide,
|
||||
materials: &mut Assets<BevyMaterial>,
|
||||
) -> Result<Self> {
|
||||
let (palm_spatial, palm_object) = SpatialRef::create(
|
||||
connection,
|
||||
&("/org/stardustxr/Hand/".to_string()
|
||||
+ match side {
|
||||
HandSide::Left => "left",
|
||||
HandSide::Right => "right",
|
||||
} + "/palm"),
|
||||
);
|
||||
let tracked = AsyncTracked::new(
|
||||
connection,
|
||||
&("/org/stardustxr/Hand/".to_string()
|
||||
+ match side {
|
||||
HandSide::Left => "left",
|
||||
HandSide::Right => "right",
|
||||
}),
|
||||
);
|
||||
let node = Node::generate(&INTERNAL_CLIENT, false).add_to_scenegraph_owned()?;
|
||||
Spatial::add_to(&node.0, None, Mat4::IDENTITY, false);
|
||||
let hand = InputDataType::Hand(Hand {
|
||||
right: matches!(side, HandSide::Right),
|
||||
..Default::default()
|
||||
});
|
||||
let datamap = Datamap::from_typed(HandDatamap::default())?;
|
||||
let input = InputMethod::add_to(&node.0, hand, datamap)?;
|
||||
|
||||
let material = materials.add(BevyMaterial {
|
||||
base_color: Srgba::new(1.0, 1.0, 1.0, 1.0).into(),
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
base_color_texture: Some(GRADIENT_TEXTURE_HANDLE),
|
||||
perceptual_roughness: 1.0,
|
||||
..default()
|
||||
});
|
||||
Ok(OxrHandInput {
|
||||
_node: node,
|
||||
palm_spatial,
|
||||
palm_object,
|
||||
side,
|
||||
input,
|
||||
tracked,
|
||||
capture_manager: CaptureManager::default(),
|
||||
datamap: Default::default(),
|
||||
tracker: None,
|
||||
material,
|
||||
captured: false,
|
||||
})
|
||||
}
|
||||
pub fn set_enabled(&self, enabled: bool) {
|
||||
if let Some(node) = self.input.spatial.node() {
|
||||
node.set_enabled(enabled);
|
||||
}
|
||||
self.tracked.set_tracked(enabled);
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
joints: Option<&openxr::HandJointLocations>,
|
||||
materials: &mut ResMut<Assets<BevyMaterial>>,
|
||||
) {
|
||||
// TODO: use the hand data source ext
|
||||
let real_hand = true;
|
||||
let input_node = self.input.spatial.node().unwrap();
|
||||
let is_tracked = real_hand
|
||||
&& joints.is_some_and(|v| {
|
||||
v.iter().all(|v| {
|
||||
v.location_flags.contains(
|
||||
SpaceLocationFlags::POSITION_VALID | SpaceLocationFlags::POSITION_TRACKED,
|
||||
) || v.location_flags.contains(
|
||||
SpaceLocationFlags::ORIENTATION_VALID
|
||||
| SpaceLocationFlags::ORIENTATION_TRACKED,
|
||||
)
|
||||
})
|
||||
});
|
||||
self.set_enabled(is_tracked);
|
||||
if is_tracked {
|
||||
// cannot ever crash, is_tracked is only true of joints is some
|
||||
let joints = joints.unwrap();
|
||||
let new_hand = Hand {
|
||||
right: matches!(self.side, HandSide::Right),
|
||||
thumb: Thumb {
|
||||
tip: convert_joint(joints[HandBone::ThumbTip as usize]),
|
||||
distal: convert_joint(joints[HandBone::ThumbDistal as usize]),
|
||||
proximal: convert_joint(joints[HandBone::ThumbProximal as usize]),
|
||||
metacarpal: convert_joint(joints[HandBone::ThumbMetacarpal as usize]),
|
||||
},
|
||||
index: Finger {
|
||||
tip: convert_joint(joints[HandBone::IndexTip as usize]),
|
||||
distal: convert_joint(joints[HandBone::IndexDistal as usize]),
|
||||
intermediate: convert_joint(joints[HandBone::IndexIntermediate as usize]),
|
||||
proximal: convert_joint(joints[HandBone::IndexProximal as usize]),
|
||||
metacarpal: convert_joint(joints[HandBone::IndexMetacarpal as usize]),
|
||||
},
|
||||
middle: Finger {
|
||||
tip: convert_joint(joints[HandBone::MiddleTip as usize]),
|
||||
distal: convert_joint(joints[HandBone::MiddleDistal as usize]),
|
||||
intermediate: convert_joint(joints[HandBone::MiddleIntermediate as usize]),
|
||||
proximal: convert_joint(joints[HandBone::MiddleProximal as usize]),
|
||||
metacarpal: convert_joint(joints[HandBone::MiddleMetacarpal as usize]),
|
||||
},
|
||||
ring: Finger {
|
||||
tip: convert_joint(joints[HandBone::RingTip as usize]),
|
||||
distal: convert_joint(joints[HandBone::RingDistal as usize]),
|
||||
intermediate: convert_joint(joints[HandBone::RingIntermediate as usize]),
|
||||
proximal: convert_joint(joints[HandBone::RingProximal as usize]),
|
||||
metacarpal: convert_joint(joints[HandBone::RingMetacarpal as usize]),
|
||||
},
|
||||
little: Finger {
|
||||
tip: convert_joint(joints[HandBone::LittleTip as usize]),
|
||||
distal: convert_joint(joints[HandBone::LittleDistal as usize]),
|
||||
intermediate: convert_joint(joints[HandBone::LittleIntermediate as usize]),
|
||||
proximal: convert_joint(joints[HandBone::LittleProximal as usize]),
|
||||
metacarpal: convert_joint(joints[HandBone::LittleMetacarpal as usize]),
|
||||
},
|
||||
palm: convert_joint(joints[HandBone::Palm as usize]),
|
||||
wrist: convert_joint(joints[HandBone::Wrist as usize]),
|
||||
elbow: None,
|
||||
};
|
||||
self.palm_spatial
|
||||
.set_local_transform(Mat4::from_rotation_translation(
|
||||
new_hand.palm.rotation.into(),
|
||||
new_hand.palm.position.into(),
|
||||
));
|
||||
|
||||
self.datamap.pinch_strength = pinch_between(&new_hand.thumb.tip, &new_hand.index.tip);
|
||||
// this is how stereokit calculates grab
|
||||
self.datamap.grab_strength =
|
||||
pinch_between(&new_hand.ring.tip, &new_hand.ring.metacarpal);
|
||||
|
||||
*self.input.data.lock() = InputDataType::Hand(new_hand);
|
||||
*self.input.datamap.lock() = Datamap::from_typed(&self.datamap).unwrap();
|
||||
let captured = self.capture_manager.capture.upgrade().is_some();
|
||||
if captured && !self.captured {
|
||||
materials.get_mut(&self.material).unwrap().base_color =
|
||||
Srgba::rgb(0., 1., 0.75).into();
|
||||
} else if self.captured && !captured {
|
||||
materials.get_mut(&self.material).unwrap().base_color =
|
||||
Srgba::rgb(1., 1.0, 1.0).into();
|
||||
}
|
||||
self.captured = captured;
|
||||
}
|
||||
|
||||
let distance_calculator = |space: &Arc<Spatial>, data: &InputDataType, field: &Field| {
|
||||
let InputDataType::Hand(hand) = data else {
|
||||
return None;
|
||||
};
|
||||
let thumb_tip_distance = field.distance(space, hand.thumb.tip.position.into());
|
||||
let index_tip_distance = field.distance(space, hand.index.tip.position.into());
|
||||
let middle_tip_distance = field.distance(space, hand.middle.tip.position.into());
|
||||
let ring_tip_distance = field.distance(space, hand.ring.tip.position.into());
|
||||
|
||||
Some(
|
||||
(thumb_tip_distance * 0.3)
|
||||
+ (index_tip_distance * 0.4)
|
||||
+ (middle_tip_distance * 0.15)
|
||||
+ (ring_tip_distance * 0.15),
|
||||
)
|
||||
};
|
||||
|
||||
self.capture_manager.update_capture(&self.input);
|
||||
self.capture_manager
|
||||
.set_new_capture(&self.input, distance_calculator);
|
||||
self.capture_manager.apply_capture(&self.input);
|
||||
|
||||
if self.capture_manager.capture.upgrade().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let sorted_handlers = get_sorted_handlers(&self.input, distance_calculator);
|
||||
self.input
|
||||
.set_handler_order(sorted_handlers.iter().map(|(handler, _)| handler));
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
use crate::{
|
||||
core::{client::INTERNAL_CLIENT, typed_datamap::TypedDatamap},
|
||||
nodes::{
|
||||
input::{tip::Tip, InputMethod, InputType},
|
||||
spatial::Spatial,
|
||||
Node,
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::{Mat4, Vec2, Vec3};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use stereokit::{
|
||||
named_colors::WHITE, ButtonState, Handed, Model, RenderLayer, StereoKitDraw,
|
||||
StereoKitMultiThread,
|
||||
};
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
struct ControllerDatamap {
|
||||
select: f32,
|
||||
grab: f32,
|
||||
scroll: Vec2,
|
||||
}
|
||||
|
||||
pub struct SkController {
|
||||
_node: Arc<Node>,
|
||||
input: Arc<InputMethod>,
|
||||
model: Model,
|
||||
handed: Handed,
|
||||
datamap: TypedDatamap<ControllerDatamap>,
|
||||
}
|
||||
impl SkController {
|
||||
pub fn new(sk: &impl StereoKitMultiThread, handed: Handed) -> Result<Self> {
|
||||
let _node = Node::create(
|
||||
&INTERNAL_CLIENT,
|
||||
"",
|
||||
if handed == Handed::Left {
|
||||
"controller_left"
|
||||
} else {
|
||||
"controller_right"
|
||||
},
|
||||
false,
|
||||
)
|
||||
.add_to_scenegraph()?;
|
||||
Spatial::add_to(&_node, None, Mat4::IDENTITY, false)?;
|
||||
let model = sk.model_create_mem("cursor.glb", include_bytes!("cursor.glb"), None)?;
|
||||
let tip = InputType::Tip(Tip::default());
|
||||
let input = InputMethod::add_to(&_node, tip, None)?;
|
||||
Ok(SkController {
|
||||
_node,
|
||||
input,
|
||||
handed,
|
||||
model,
|
||||
datamap: Default::default(),
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, sk: &impl StereoKitDraw) {
|
||||
let controller = sk.input_controller(self.handed);
|
||||
*self.input.enabled.lock() = controller.tracked.contains(ButtonState::ACTIVE);
|
||||
if *self.input.enabled.lock() {
|
||||
let world_transform = Mat4::from_rotation_translation(
|
||||
controller.aim.orientation,
|
||||
controller.aim.position,
|
||||
);
|
||||
sk.model_draw(
|
||||
&self.model,
|
||||
world_transform * Mat4::from_scale(Vec3::ONE * 0.02),
|
||||
WHITE,
|
||||
RenderLayer::LAYER0,
|
||||
);
|
||||
self.input.spatial.set_local_transform(world_transform);
|
||||
}
|
||||
self.datamap.select = controller.trigger;
|
||||
self.datamap.grab = controller.grip;
|
||||
self.datamap.scroll = controller.stick;
|
||||
*self.input.datamap.lock() = self.datamap.to_datamap().ok();
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
use crate::{
|
||||
core::{client::INTERNAL_CLIENT, typed_datamap::TypedDatamap},
|
||||
nodes::{
|
||||
input::{hand::Hand, InputMethod, InputType},
|
||||
spatial::Spatial,
|
||||
Node,
|
||||
},
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::Mat4;
|
||||
use nanoid::nanoid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stardust_xr::schemas::flat::{Hand as FlatHand, Joint};
|
||||
use std::sync::Arc;
|
||||
use stereokit::{ButtonState, HandJoint, Handed, StereoKitMultiThread};
|
||||
|
||||
fn convert_joint(joint: HandJoint) -> Joint {
|
||||
Joint {
|
||||
position: joint.position.into(),
|
||||
rotation: joint.orientation.into(),
|
||||
radius: joint.radius,
|
||||
distance: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
struct HandDatamap {
|
||||
pinch_strength: f32,
|
||||
grab_strength: f32,
|
||||
}
|
||||
|
||||
pub struct SkHand {
|
||||
_node: Arc<Node>,
|
||||
input: Arc<InputMethod>,
|
||||
handed: Handed,
|
||||
datamap: TypedDatamap<HandDatamap>,
|
||||
}
|
||||
impl SkHand {
|
||||
pub fn new(handed: Handed) -> Result<Self> {
|
||||
let _node = Node::create(&INTERNAL_CLIENT, "", &nanoid!(), false).add_to_scenegraph()?;
|
||||
Spatial::add_to(&_node, None, Mat4::IDENTITY, false)?;
|
||||
let hand = InputType::Hand(Box::new(Hand {
|
||||
base: FlatHand {
|
||||
right: handed == Handed::Right,
|
||||
..Default::default()
|
||||
},
|
||||
}));
|
||||
let input = InputMethod::add_to(&_node, hand, None)?;
|
||||
Ok(SkHand {
|
||||
_node,
|
||||
input,
|
||||
handed,
|
||||
datamap: Default::default(),
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, controller_enabled: bool, sk: &impl StereoKitMultiThread) {
|
||||
let sk_hand = sk.input_hand(self.handed);
|
||||
if let InputType::Hand(hand) = &mut *self.input.specialization.lock() {
|
||||
let controller_active = controller_enabled
|
||||
&& sk
|
||||
.input_controller(self.handed)
|
||||
.tracked
|
||||
.contains(ButtonState::ACTIVE);
|
||||
*self.input.enabled.lock() =
|
||||
!controller_active && sk_hand.tracked_state.contains(ButtonState::ACTIVE);
|
||||
sk.input_hand_visible(self.handed, *self.input.enabled.lock());
|
||||
if *self.input.enabled.lock() {
|
||||
hand.base.thumb.tip = convert_joint(sk_hand.fingers[0][4]);
|
||||
hand.base.thumb.distal = convert_joint(sk_hand.fingers[0][3]);
|
||||
hand.base.thumb.proximal = convert_joint(sk_hand.fingers[0][2]);
|
||||
hand.base.thumb.metacarpal = convert_joint(sk_hand.fingers[0][1]);
|
||||
|
||||
for (finger, sk_finger) in [
|
||||
(&mut hand.base.index, sk_hand.fingers[1]),
|
||||
(&mut hand.base.middle, sk_hand.fingers[2]),
|
||||
(&mut hand.base.ring, sk_hand.fingers[3]),
|
||||
(&mut hand.base.little, sk_hand.fingers[4]),
|
||||
] {
|
||||
finger.tip = convert_joint(sk_finger[4]);
|
||||
finger.distal = convert_joint(sk_finger[3]);
|
||||
finger.intermediate = convert_joint(sk_finger[2]);
|
||||
finger.proximal = convert_joint(sk_finger[1]);
|
||||
finger.metacarpal = convert_joint(sk_finger[0]);
|
||||
}
|
||||
|
||||
hand.base.palm.position = sk_hand.palm.position.into();
|
||||
hand.base.palm.rotation = sk_hand.palm.orientation.into();
|
||||
hand.base.palm.radius =
|
||||
(sk_hand.fingers[2][0].radius + sk_hand.fingers[2][1].radius) * 0.5;
|
||||
|
||||
hand.base.wrist.position = sk_hand.wrist.position.into();
|
||||
hand.base.wrist.rotation = sk_hand.wrist.orientation.into();
|
||||
hand.base.wrist.radius =
|
||||
(sk_hand.fingers[0][0].radius + sk_hand.fingers[4][0].radius) * 0.5;
|
||||
|
||||
hand.base.elbow = None;
|
||||
}
|
||||
}
|
||||
self.datamap.pinch_strength = sk_hand.pinch_activation;
|
||||
self.datamap.grab_strength = sk_hand.grip_activation;
|
||||
*self.input.datamap.lock() = self.datamap.to_datamap().ok();
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,214 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use crate::{
|
||||
core::client::INTERNAL_CLIENT,
|
||||
nodes::{
|
||||
Node, OwnedNode,
|
||||
fields::{EXPORTED_FIELDS, Field, Shape},
|
||||
spatial::{EXPORTED_SPATIALS, Spatial},
|
||||
},
|
||||
};
|
||||
use glam::{Mat4, vec3};
|
||||
use input::{
|
||||
eye_pointer::EyePointer, mouse_pointer::MousePointer, oxr_controller::OxrControllerInput,
|
||||
oxr_hand::OxrHandInput,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use play_space::PlaySpaceBounds;
|
||||
use stardust_xr::schemas::dbus::object_registry::ObjectRegistry;
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
sync::{Arc, atomic::Ordering},
|
||||
};
|
||||
use tokio::{sync::mpsc, task::AbortHandle};
|
||||
use zbus::{Connection, interface, object_server::Interface, zvariant::OwnedObjectPath};
|
||||
|
||||
pub mod hmd;
|
||||
pub mod input;
|
||||
pub mod play_space;
|
||||
|
||||
pub struct ObjectHandle<I: Interface>(Connection, OwnedObjectPath, PhantomData<I>);
|
||||
|
||||
impl<I: Interface> Clone for ObjectHandle<I> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone(), self.1.clone(), PhantomData)
|
||||
}
|
||||
}
|
||||
impl<I: Interface> Drop for ObjectHandle<I> {
|
||||
fn drop(&mut self) {
|
||||
let connection = self.0.clone();
|
||||
let object_path = self.1.clone();
|
||||
tokio::task::spawn(async move {
|
||||
connection.object_server().remove::<I, _>(object_path);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around ObjectHandle<Tracked> that batches async updates
|
||||
/// instead of spawning a tokio task for each state change
|
||||
pub struct AsyncTracked {
|
||||
pub sender: mpsc::UnboundedSender<bool>,
|
||||
pub _handle: ObjectHandle<Tracked>,
|
||||
pub _abort_handle: AbortHandle,
|
||||
}
|
||||
|
||||
impl AsyncTracked {
|
||||
pub fn new(connection: &Connection, path: &str) -> Self {
|
||||
let handle = Tracked::new(connection, path);
|
||||
let (sender, mut receiver) = mpsc::unbounded_channel::<bool>();
|
||||
|
||||
// Spawn a single long-running task that processes state updates
|
||||
let task = tokio::task::spawn({
|
||||
let handle = handle.clone();
|
||||
async move {
|
||||
while let Some(is_tracked) = receiver.recv().await {
|
||||
let _ = handle.set_tracked(is_tracked).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
sender,
|
||||
_handle: handle,
|
||||
_abort_handle: task.abort_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tracked(&self, is_tracked: bool) {
|
||||
// Just send over channel instead of spawning a task
|
||||
let _ = self.sender.send(is_tracked);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AsyncTracked {
|
||||
fn drop(&mut self) {
|
||||
self._abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SpatialRef(u64, OwnedNode);
|
||||
impl SpatialRef {
|
||||
pub fn create(connection: &Connection, path: &str) -> (Arc<Spatial>, ObjectHandle<SpatialRef>) {
|
||||
let node = OwnedNode(Arc::new(Node::generate(&INTERNAL_CLIENT, false)));
|
||||
let spatial = Spatial::add_to(&node.0, None, Mat4::IDENTITY, false);
|
||||
let uid: u64 = rand::random();
|
||||
EXPORTED_SPATIALS
|
||||
.lock()
|
||||
.insert(uid, Arc::downgrade(&node.0));
|
||||
|
||||
tokio::task::spawn({
|
||||
let connection = connection.clone();
|
||||
let path = path.to_string();
|
||||
async move {
|
||||
connection
|
||||
.object_server()
|
||||
.at(path, Self(uid, node))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
(
|
||||
spatial,
|
||||
ObjectHandle(
|
||||
connection.clone(),
|
||||
OwnedObjectPath::try_from(path.to_string()).unwrap(),
|
||||
PhantomData,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
#[interface(name = "org.stardustxr.SpatialRef")]
|
||||
impl SpatialRef {
|
||||
#[zbus(property)]
|
||||
fn uid(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Tracked(bool);
|
||||
impl Tracked {
|
||||
pub fn new(connection: &Connection, path: &str) -> ObjectHandle<Tracked> {
|
||||
tokio::task::spawn({
|
||||
let connection = connection.clone();
|
||||
let path = path.to_string();
|
||||
async move {
|
||||
connection
|
||||
.object_server()
|
||||
.at(path, Self(false))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
ObjectHandle(
|
||||
connection.clone(),
|
||||
OwnedObjectPath::try_from(path.to_string()).unwrap(),
|
||||
PhantomData,
|
||||
)
|
||||
}
|
||||
}
|
||||
impl ObjectHandle<Tracked> {
|
||||
pub async fn set_tracked(&self, is_tracked: bool) -> zbus::Result<()> {
|
||||
let tracked_ref = self
|
||||
.0
|
||||
.object_server()
|
||||
.interface::<_, Tracked>(self.1.as_ref())
|
||||
.await?;
|
||||
let mut tracked = tracked_ref.get_mut().await;
|
||||
if tracked.0 != is_tracked {
|
||||
tracked.0 = is_tracked;
|
||||
tracked
|
||||
.is_tracked_changed(tracked_ref.signal_emitter())
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[interface(name = "org.stardustxr.Tracked")]
|
||||
impl Tracked {
|
||||
#[zbus(property)]
|
||||
fn is_tracked(&self) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FieldRef(u64, OwnedNode);
|
||||
impl FieldRef {
|
||||
pub fn create(
|
||||
connection: &Connection,
|
||||
path: &str,
|
||||
shape: Shape,
|
||||
) -> (Arc<Field>, ObjectHandle<FieldRef>) {
|
||||
let node = OwnedNode(Arc::new(Node::generate(&INTERNAL_CLIENT, false)));
|
||||
Spatial::add_to(&node.0, None, Mat4::IDENTITY, false);
|
||||
let field = Field::add_to(&node.0, shape).unwrap();
|
||||
let uid: u64 = rand::random();
|
||||
EXPORTED_FIELDS.lock().insert(uid, Arc::downgrade(&node.0));
|
||||
|
||||
tokio::task::spawn({
|
||||
let connection = connection.clone();
|
||||
let path = path.to_string();
|
||||
async move {
|
||||
connection
|
||||
.object_server()
|
||||
.at(path, Self(uid, node))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
(
|
||||
field,
|
||||
ObjectHandle(
|
||||
connection.clone(),
|
||||
OwnedObjectPath::try_from(path.to_string()).unwrap(),
|
||||
PhantomData,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
#[interface(name = "org.stardustxr.FieldRef")]
|
||||
impl FieldRef {
|
||||
#[zbus(property)]
|
||||
fn uid(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,153 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use glam::Mat4;
|
||||
use mint::Vector2;
|
||||
use nanoid::nanoid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stereokit::StereoKitMultiThread;
|
||||
|
||||
use crate::{
|
||||
core::client::INTERNAL_CLIENT,
|
||||
nodes::{
|
||||
data::{Mask, PulseReceiver},
|
||||
fields::{r#box::BoxField, Field},
|
||||
spatial::Spatial,
|
||||
Node,
|
||||
},
|
||||
use bevy::prelude::*;
|
||||
use bevy_mod_openxr::{
|
||||
helper_traits::{ToQuat, ToVec3},
|
||||
resources::{OxrFrameState, Pipelined},
|
||||
session::OxrSession,
|
||||
};
|
||||
use bevy_mod_xr::{
|
||||
session::{XrPreDestroySession, XrSessionCreated},
|
||||
spaces::{XrPrimaryReferenceSpace, XrReferenceSpace, XrSpace},
|
||||
};
|
||||
use openxr::SpaceLocationFlags;
|
||||
use parking_lot::RwLock;
|
||||
use zbus::{Connection, ObjectServer, interface};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct PlaySpaceMap {
|
||||
play_space: (),
|
||||
size: Vector2<f32>,
|
||||
use crate::{DbusConnection, PreFrameWait, get_time, nodes::spatial::Spatial};
|
||||
|
||||
use super::{AsyncTracked, ObjectHandle, SpatialRef, Tracked};
|
||||
|
||||
pub struct PlaySpacePlugin;
|
||||
impl Plugin for PlaySpacePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(XrPreDestroySession, destroy_stage_space);
|
||||
app.add_systems(XrSessionCreated, create_stage_space);
|
||||
app.add_systems(PreFrameWait, update);
|
||||
app.add_systems(Startup, setup);
|
||||
}
|
||||
}
|
||||
impl Default for PlaySpaceMap {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
play_space: (),
|
||||
size: [0.0; 2].into(),
|
||||
|
||||
fn setup(connection: Res<DbusConnection>, mut cmds: Commands) {
|
||||
let (spatial, spatial_handle) = SpatialRef::create(&connection, "/org/stardustxr/PlaySpace");
|
||||
// the OpenXR session might not exist quite yet
|
||||
let tracked = AsyncTracked::new(&connection, "/org/stardustxr/PlaySpace");
|
||||
let dbus_connection = connection.clone();
|
||||
let play_space_data = Arc::new(RwLock::default());
|
||||
tokio::task::spawn({
|
||||
let data = play_space_data.clone();
|
||||
async move {
|
||||
PlaySpaceBounds::create(&dbus_connection, data).await;
|
||||
dbus_connection
|
||||
.request_name("org.stardustxr.PlaySpace")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
cmds.insert_resource(PlaySpace {
|
||||
spatial,
|
||||
_spatial_handle: spatial_handle,
|
||||
tracked_handle: tracked,
|
||||
bounds: play_space_data,
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct StageSpace(XrSpace);
|
||||
fn create_stage_space(session: Res<OxrSession>, mut cmds: Commands) {
|
||||
let space = session
|
||||
.create_reference_space(openxr::ReferenceSpaceType::STAGE, Transform::IDENTITY)
|
||||
.inspect_err(|err| error!("failed to create Stage XrSpace"))
|
||||
.ok();
|
||||
if let Some(space) = space {
|
||||
cmds.insert_resource(StageSpace(space.0));
|
||||
}
|
||||
}
|
||||
fn destroy_stage_space(session: Res<OxrSession>, mut cmds: Commands, stage: Res<StageSpace>) {
|
||||
session.destroy_space(stage.0);
|
||||
cmds.remove_resource::<StageSpace>();
|
||||
}
|
||||
|
||||
/// TODO: impl this
|
||||
fn update(
|
||||
session: Option<Res<OxrSession>>,
|
||||
stage: Option<Res<StageSpace>>,
|
||||
ref_space: Option<Res<XrPrimaryReferenceSpace>>,
|
||||
play_space: Res<PlaySpace>,
|
||||
state: Option<Res<OxrFrameState>>,
|
||||
pipelined: Option<Res<Pipelined>>,
|
||||
) {
|
||||
let (Some(session), Some(stage), Some(ref_space), Some(state)) =
|
||||
(session, stage, ref_space, state)
|
||||
else {
|
||||
play_space.bounds.write().drain(..);
|
||||
play_space.tracked_handle.set_tracked(false);
|
||||
|
||||
play_space
|
||||
.spatial
|
||||
.set_local_transform(Mat4::from_translation(vec3(0.0, -1.65, 0.0)));
|
||||
return;
|
||||
};
|
||||
let time = get_time(pipelined.is_some(), &state);
|
||||
let location = session
|
||||
.locate_space(&stage.0, &ref_space, time)
|
||||
.inspect_err(|err| error!("Error while Locating OpenXR Stage Space {err}"));
|
||||
if let Ok(location) = location {
|
||||
let is_tracked = location.location_flags.contains(
|
||||
SpaceLocationFlags::POSITION_VALID
|
||||
| SpaceLocationFlags::POSITION_TRACKED
|
||||
| SpaceLocationFlags::ORIENTATION_VALID
|
||||
| SpaceLocationFlags::ORIENTATION_TRACKED,
|
||||
);
|
||||
play_space.tracked_handle.set_tracked(is_tracked);
|
||||
if is_tracked {
|
||||
play_space
|
||||
.spatial
|
||||
.set_local_transform(Mat4::from_rotation_translation(
|
||||
location.pose.orientation.to_quat(),
|
||||
location.pose.position.to_vec3(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// session.reference_space_bounds_rect(openxr::ReferenceSpaceType::STAGE);
|
||||
|
||||
// if (World::has_bounds()
|
||||
// && World::get_bounds_size().x != 0.0
|
||||
// && World::get_bounds_size().y != 0.0)
|
||||
// {
|
||||
// let bounds = World::get_bounds_size();
|
||||
// vec![
|
||||
// ((bounds.x).into(), (bounds.y).into()),
|
||||
// ((bounds.x).into(), (-bounds.y).into()),
|
||||
// ((-bounds.x).into(), (-bounds.y).into()),
|
||||
// ((-bounds.x).into(), (bounds.y).into()),
|
||||
// ]
|
||||
// } else {
|
||||
// vec![]
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct PlaySpace {
|
||||
_node: Arc<Node>,
|
||||
spatial: Arc<Spatial>,
|
||||
field: Arc<Field>,
|
||||
_pulse_rx: Arc<PulseReceiver>,
|
||||
_spatial_handle: ObjectHandle<SpatialRef>,
|
||||
tracked_handle: AsyncTracked,
|
||||
bounds: Arc<RwLock<Vec<(f64, f64)>>>,
|
||||
}
|
||||
impl PlaySpace {
|
||||
pub fn new() -> Result<Self> {
|
||||
let node = Node::create(&INTERNAL_CLIENT, "", &nanoid!(), false).add_to_scenegraph()?;
|
||||
let spatial = Spatial::add_to(&node, None, Mat4::IDENTITY, false)?;
|
||||
let field = BoxField::add_to(&node, [0.0; 3].into())?;
|
||||
|
||||
let pulse_rx =
|
||||
PulseReceiver::add_to(&node, field.clone(), Mask::from_struct::<PlaySpaceMap>())?;
|
||||
|
||||
Ok(PlaySpace {
|
||||
_node: node,
|
||||
spatial,
|
||||
field,
|
||||
_pulse_rx: pulse_rx,
|
||||
})
|
||||
}
|
||||
pub fn update(&self, sk: &impl StereoKitMultiThread) {
|
||||
let pose = sk.world_get_bounds_pose();
|
||||
self.spatial
|
||||
.set_local_transform(Mat4::from_rotation_translation(
|
||||
pose.orientation,
|
||||
pose.position,
|
||||
));
|
||||
let Field::Box(box_field) = self.field.as_ref() else {return};
|
||||
box_field.set_size(
|
||||
[
|
||||
sk.world_get_bounds_size().x,
|
||||
0.0,
|
||||
sk.world_get_bounds_size().y,
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
pub struct PlaySpaceBounds(Arc<RwLock<Vec<(f64, f64)>>>);
|
||||
impl PlaySpaceBounds {
|
||||
pub async fn create(connection: &Connection, data: Arc<RwLock<Vec<(f64, f64)>>>) {
|
||||
connection
|
||||
.object_server()
|
||||
.at("/org/stardustxr/PlaySpace", Self(data))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
#[interface(name = "org.stardustxr.PlaySpace")]
|
||||
impl PlaySpaceBounds {
|
||||
#[zbus(property)]
|
||||
fn bounds(&self) -> Vec<(f64, f64)> {
|
||||
self.0.read().clone()
|
||||
}
|
||||
}
|
||||
|
||||
112
src/session.rs
Normal file
112
src/session.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use crate::core::client::CLIENTS;
|
||||
use crate::core::client_state::ClientStateParsed;
|
||||
#[cfg(feature = "wayland")]
|
||||
use crate::wayland::WAYLAND_DISPLAY;
|
||||
use crate::{CliArgs, STARDUST_INSTANCE};
|
||||
use directories::ProjectDirs;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::time::Duration;
|
||||
use tokio::task::LocalSet;
|
||||
use tracing::info;
|
||||
|
||||
pub async fn save_session(project_dirs: &ProjectDirs) {
|
||||
let session_id = nanoid::nanoid!();
|
||||
let state_dir = project_dirs.state_dir().unwrap();
|
||||
let session_dir = state_dir.join(&session_id);
|
||||
std::fs::create_dir_all(&session_dir).unwrap();
|
||||
let _ = std::fs::remove_dir_all(state_dir.join("latest"));
|
||||
std::os::unix::fs::symlink(&session_dir, state_dir.join("latest")).unwrap();
|
||||
|
||||
let local_set = LocalSet::new();
|
||||
for client in CLIENTS.get_vec() {
|
||||
let session_dir = session_dir.clone();
|
||||
local_set.spawn_local(async move {
|
||||
tokio::select! {
|
||||
biased;
|
||||
s = client.save_state() => {if let Some(s) = s { s.to_file(&session_dir) }},
|
||||
_ = tokio::time::sleep(Duration::from_millis(100)) => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
local_set.await;
|
||||
info!("Session ID for restore is {session_id}");
|
||||
}
|
||||
|
||||
pub fn launch_start(cli_args: &CliArgs, project_dirs: &ProjectDirs) -> Vec<Child> {
|
||||
match (&cli_args.restore, &cli_args.startup_script) {
|
||||
(Some(session_id), _) => restore_session(
|
||||
&project_dirs.state_dir().unwrap().join(session_id),
|
||||
cli_args.debug_launched_clients,
|
||||
),
|
||||
(None, Some(startup_script)) => run_script(
|
||||
&startup_script.clone().canonicalize().unwrap_or_default(),
|
||||
cli_args.debug_launched_clients,
|
||||
),
|
||||
(None, None) => run_script(
|
||||
&project_dirs.config_dir().join("startup"),
|
||||
cli_args.debug_launched_clients,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore_session(session_dir: &Path, debug_launched_clients: bool) -> Vec<Child> {
|
||||
let Ok(clients) = session_dir.read_dir() else {
|
||||
return Vec::new();
|
||||
};
|
||||
clients
|
||||
.filter_map(Result::ok)
|
||||
.filter(|c| c.path().extension() == Some(OsStr::new("toml")))
|
||||
.filter_map(|c| ClientStateParsed::from_file(&c.path()))
|
||||
.filter_map(ClientStateParsed::launch_command)
|
||||
.filter_map(|c| run_client(c, debug_launched_clients))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn run_script(script_path: &Path, debug_launched_clients: bool) -> Vec<Child> {
|
||||
let _ = std::fs::set_permissions(script_path, std::fs::Permissions::from_mode(0o755));
|
||||
let startup_command = Command::new(script_path);
|
||||
run_client(startup_command, debug_launched_clients)
|
||||
.map(|c| vec![c])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn run_client(mut command: Command, debug_launched_clients: bool) -> Option<Child> {
|
||||
command.stdin(Stdio::null());
|
||||
if !debug_launched_clients {
|
||||
command.stdout(Stdio::null());
|
||||
command.stderr(Stdio::null());
|
||||
}
|
||||
for (var, value) in connection_env() {
|
||||
command.env(var, value);
|
||||
}
|
||||
let child = command.spawn().ok()?;
|
||||
Some(child)
|
||||
}
|
||||
|
||||
pub fn connection_env() -> FxHashMap<String, String> {
|
||||
let mut env: FxHashMap<String, String> = FxHashMap::default();
|
||||
env.insert(
|
||||
stringify!(STARDUST_INSTANCE).to_string(),
|
||||
STARDUST_INSTANCE.get().unwrap().clone(),
|
||||
);
|
||||
|
||||
if let Some(flat_wayland_display) = std::env::var_os("WAYLAND_DISPLAY") {
|
||||
env.insert(
|
||||
"FLAT_WAYLAND_DISPLAY".to_string(),
|
||||
flat_wayland_display.to_string_lossy().into_owned(),
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "wayland")]
|
||||
{
|
||||
env.insert(
|
||||
stringify!(WAYLAND_DISPLAY).to_string(),
|
||||
WAYLAND_DISPLAY.get().unwrap().to_string_lossy().to_string(),
|
||||
);
|
||||
env.insert("XDG_SESSION_TYPE".to_string(), "wayland".to_string());
|
||||
}
|
||||
env
|
||||
}
|
||||
39
src/spectator_cam.rs
Normal file
39
src/spectator_cam.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use bevy::{
|
||||
app::Plugin,
|
||||
core_pipeline::core_3d::Camera3d,
|
||||
ecs::{
|
||||
component::Component,
|
||||
system::{Commands, Res},
|
||||
},
|
||||
render::camera::{PerspectiveProjection, Projection},
|
||||
transform::components::Transform,
|
||||
};
|
||||
use bevy_mod_openxr::session::OxrSession;
|
||||
use bevy_mod_xr::session::XrSessionCreated;
|
||||
use openxr::ReferenceSpaceType;
|
||||
|
||||
pub struct SpectatorCameraPlugin;
|
||||
|
||||
impl Plugin for SpectatorCameraPlugin {
|
||||
fn build(&self, app: &mut bevy::app::App) {
|
||||
app.add_systems(XrSessionCreated, create);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
#[require(Camera3d)]
|
||||
struct SpectatorCam;
|
||||
|
||||
fn create(session: Res<OxrSession>, mut cmds: Commands) {
|
||||
cmds.spawn((
|
||||
SpectatorCam,
|
||||
session
|
||||
.create_reference_space(ReferenceSpaceType::VIEW, Transform::IDENTITY)
|
||||
.unwrap()
|
||||
.0,
|
||||
Projection::Perspective(PerspectiveProjection {
|
||||
fov: 100f32.to_radians(),
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
}
|
||||
82
src/tracking_offset.rs
Normal file
82
src/tracking_offset.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use bevy::{
|
||||
app::{Plugin, Update},
|
||||
ecs::{
|
||||
resource::Resource,
|
||||
schedule::{Condition, IntoScheduleConfigs, common_conditions::resource_exists},
|
||||
system::{Commands, Res, ResMut},
|
||||
},
|
||||
transform::components::Transform,
|
||||
};
|
||||
use bevy_mod_openxr::{
|
||||
helper_traits::ToTransform as _,
|
||||
poll_events::{OxrEventHandlerExt, OxrEventIn},
|
||||
resources::OxrFrameState,
|
||||
session::OxrSession,
|
||||
};
|
||||
use bevy_mod_xr::{session::XrSessionCreated, spaces::XrPrimaryReferenceSpace};
|
||||
use glam::{Quat, Vec3};
|
||||
use openxr::ReferenceSpaceType;
|
||||
|
||||
pub struct TrackingOffsetPlugin;
|
||||
|
||||
impl Plugin for TrackingOffsetPlugin {
|
||||
fn build(&self, app: &mut bevy::app::App) {
|
||||
app.add_oxr_event_handler(reset_offset);
|
||||
app.add_systems(XrSessionCreated, |mut cmds: Commands| {
|
||||
cmds.insert_resource(OffsetTag);
|
||||
});
|
||||
app.add_systems(
|
||||
Update,
|
||||
offset.run_if(resource_exists::<OffsetTag>.and(resource_exists::<OxrFrameState>)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct OffsetTag;
|
||||
|
||||
fn reset_offset(
|
||||
oxr_event: OxrEventIn,
|
||||
mut ref_space: ResMut<XrPrimaryReferenceSpace>,
|
||||
session: Res<OxrSession>,
|
||||
) {
|
||||
if let openxr::Event::ReferenceSpaceChangePending(v) = *oxr_event
|
||||
&& v.reference_space_type() == ReferenceSpaceType::LOCAL
|
||||
{
|
||||
let space = session
|
||||
.create_reference_space(ReferenceSpaceType::LOCAL, Transform::IDENTITY)
|
||||
.unwrap();
|
||||
session.destroy_space(ref_space.0.0).unwrap();
|
||||
ref_space.0 = space;
|
||||
}
|
||||
}
|
||||
|
||||
fn offset(
|
||||
session: Res<OxrSession>,
|
||||
state: Res<OxrFrameState>,
|
||||
mut primary_ref_space: ResMut<XrPrimaryReferenceSpace>,
|
||||
mut cmds: Commands,
|
||||
) {
|
||||
cmds.remove_resource::<OffsetTag>();
|
||||
let local = session
|
||||
.create_reference_space(ReferenceSpaceType::LOCAL, Transform::IDENTITY)
|
||||
.unwrap();
|
||||
let view = session
|
||||
.create_reference_space(ReferenceSpaceType::VIEW, Transform::IDENTITY)
|
||||
.unwrap();
|
||||
let view_pose = session
|
||||
.locate_space(&view, &local, state.predicted_display_time)
|
||||
.unwrap()
|
||||
.pose
|
||||
.to_transform();
|
||||
let offset = view_pose.with_rotation(Quat::from_axis_angle(
|
||||
Vec3::Y,
|
||||
view_pose.rotation.to_euler(glam::EulerRot::XYZ).1,
|
||||
));
|
||||
let offset = Transform::from_matrix(offset.compute_matrix());
|
||||
let local_offset = session
|
||||
.create_reference_space(ReferenceSpaceType::LOCAL, offset)
|
||||
.unwrap();
|
||||
session.destroy_space(primary_ref_space.0.0).unwrap();
|
||||
primary_ref_space.0 = local_offset;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
use crate::wayland::surface::CoreSurface;
|
||||
|
||||
use super::state::{ClientState, WaylandState};
|
||||
use portable_atomic::{AtomicU32, Ordering};
|
||||
#[cfg(feature = "xwayland")]
|
||||
use smithay::xwayland::XWaylandClientData;
|
||||
use smithay::{
|
||||
delegate_compositor,
|
||||
reexports::wayland_server::{protocol::wl_surface::WlSurface, Client},
|
||||
wayland::compositor::{self, CompositorClientState, CompositorHandler, CompositorState},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tracing::debug;
|
||||
|
||||
impl CompositorHandler for WaylandState {
|
||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||
&mut self.compositor_state
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
debug!(?surface, "Surface commit");
|
||||
let mut count = 0;
|
||||
let core_surface = compositor::with_states(surface, |data| {
|
||||
let count_new = data
|
||||
.data_map
|
||||
.insert_if_missing_threadsafe(|| AtomicU32::new(0));
|
||||
if !count_new {
|
||||
if let Some(stored_count) = data.data_map.get::<AtomicU32>() {
|
||||
count = stored_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
data.data_map.get::<Arc<CoreSurface>>().cloned()
|
||||
});
|
||||
if let Some(core_surface) = core_surface {
|
||||
core_surface.commit(count);
|
||||
}
|
||||
}
|
||||
|
||||
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
|
||||
if let Some(client_state) = client.get_data::<ClientState>() {
|
||||
&client_state.compositor_state
|
||||
} else {
|
||||
#[cfg(feature = "xwayland")]
|
||||
if let Some(xwayland_client_data) = client.get_data::<XWaylandClientData>() {
|
||||
return &xwayland_client_data.compositor_state;
|
||||
}
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_compositor!(WaylandState);
|
||||
112
src/wayland/core/buffer.rs
Normal file
112
src/wayland/core/buffer.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use crate::wayland::dmabuf::buffer_backing::DmabufBacking;
|
||||
use crate::wayland::{Client, Message, WaylandResult};
|
||||
use crate::wayland::{MessageSink, core::shm_buffer_backing::ShmBufferBacking, util::ClientExt};
|
||||
use bevy::{
|
||||
asset::{Assets, Handle},
|
||||
image::Image,
|
||||
};
|
||||
use bevy_dmabuf::import::ImportedDmatexs;
|
||||
use mint::Vector2;
|
||||
use std::sync::Arc;
|
||||
use waynest::ObjectId;
|
||||
pub use waynest_protocols::server::core::wayland::wl_buffer::*;
|
||||
use waynest_server::{Client as _, RequestDispatcher};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BufferUsage {
|
||||
pub buffer: Arc<Buffer>,
|
||||
message_sink: MessageSink,
|
||||
}
|
||||
impl BufferUsage {
|
||||
pub fn new(client: &Client, buffer: &Arc<Buffer>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
buffer: buffer.clone(),
|
||||
message_sink: client.message_sink(),
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Drop for BufferUsage {
|
||||
fn drop(&mut self) {
|
||||
let _ = self
|
||||
.message_sink
|
||||
.send(Message::ReleaseBuffer(self.buffer.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BufferBacking {
|
||||
Shm(ShmBufferBacking),
|
||||
Dmabuf(DmabufBacking),
|
||||
}
|
||||
|
||||
#[derive(Debug, RequestDispatcher)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct Buffer {
|
||||
pub id: ObjectId,
|
||||
backing: BufferBacking,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub fn new(
|
||||
client: &mut Client,
|
||||
id: ObjectId,
|
||||
backing: BufferBacking,
|
||||
) -> WaylandResult<Arc<Self>> {
|
||||
Ok(client.insert(id, Self { id, backing })?)
|
||||
}
|
||||
|
||||
/// Returns the tex if it was updated
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub fn update_tex(
|
||||
&self,
|
||||
dmatexes: &ImportedDmatexs,
|
||||
images: &mut Assets<Image>,
|
||||
) -> Option<Handle<Image>> {
|
||||
tracing::debug!("Updating texture for buffer {:?}", self.id);
|
||||
match &self.backing {
|
||||
BufferBacking::Shm(backing) => backing.update_tex(dmatexes, images),
|
||||
BufferBacking::Dmabuf(backing) => backing.update_tex(dmatexes, images),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub fn on_commit(&self) {
|
||||
tracing::debug!("running on_commit for buffer {:?}", self.id);
|
||||
match &self.backing {
|
||||
BufferBacking::Shm(backing) => backing.on_commit(),
|
||||
BufferBacking::Dmabuf(_backing) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
match &self.backing {
|
||||
BufferBacking::Shm(backing) => backing.is_transparent(),
|
||||
BufferBacking::Dmabuf(backing) => backing.is_transparent(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vector2<usize> {
|
||||
match &self.backing {
|
||||
BufferBacking::Shm(backing) => backing.size(),
|
||||
BufferBacking::Dmabuf(backing) => backing.size(),
|
||||
}
|
||||
}
|
||||
pub fn uses_buffer_usage(&self) -> bool {
|
||||
matches!(
|
||||
self.backing,
|
||||
BufferBacking::Dmabuf(_) | BufferBacking::Shm(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl WlBuffer for Buffer {
|
||||
type Connection = crate::wayland::Client;
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_buffer:request:destroy
|
||||
async fn destroy(&self, client: &mut Client, _sender_id: ObjectId) -> WaylandResult<()> {
|
||||
client.remove(self.id);
|
||||
tracing::info!("Destroying buffer {:?}", self.id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
11
src/wayland/core/callback.rs
Normal file
11
src/wayland/core/callback.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use waynest::ObjectId;
|
||||
pub use waynest_protocols::server::core::wayland::wl_callback::*;
|
||||
use waynest_server::RequestDispatcher;
|
||||
|
||||
#[derive(Debug, RequestDispatcher, Clone)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct Callback(pub ObjectId);
|
||||
/// https://wayland.app/protocols/wayland#wl_callback
|
||||
impl WlCallback for Callback {
|
||||
type Connection = crate::wayland::Client;
|
||||
}
|
||||
86
src/wayland/core/compositor.rs
Normal file
86
src/wayland/core/compositor.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use super::surface::WL_SURFACE_REGISTRY;
|
||||
use crate::wayland::{WaylandError, WaylandResult};
|
||||
use crate::wayland::{core::surface::Surface, util::ClientExt};
|
||||
use waynest::ObjectId;
|
||||
use waynest_protocols::server::core::wayland::wl_surface::WlSurface;
|
||||
pub use waynest_protocols::server::core::wayland::{wl_compositor::*, wl_region::*};
|
||||
use waynest_server::{Client as _, RequestDispatcher};
|
||||
|
||||
#[derive(Debug, waynest_server::RequestDispatcher, Default)]
|
||||
#[waynest(error = WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct Compositor;
|
||||
impl WlCompositor for Compositor {
|
||||
type Connection = crate::wayland::Client;
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_compositor:request:create_surface
|
||||
async fn create_surface(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
let surface = client.insert(id, Surface::new(client, id))?;
|
||||
if let Some(output) = client.display().output.get() {
|
||||
surface.enter(client, id, output.id).await?;
|
||||
}
|
||||
WL_SURFACE_REGISTRY.add_raw(&surface);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_compositor:request:create_region
|
||||
async fn create_region(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
client.insert(id, Region { id })?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, RequestDispatcher)]
|
||||
#[waynest(error = WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct Region {
|
||||
id: ObjectId,
|
||||
}
|
||||
impl WlRegion for Region {
|
||||
type Connection = crate::wayland::Client;
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_region:request:add
|
||||
async fn add(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
_x: i32,
|
||||
_y: i32,
|
||||
_width: i32,
|
||||
_height: i32,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_region:request:subtract
|
||||
async fn subtract(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
_x: i32,
|
||||
_y: i32,
|
||||
_width: i32,
|
||||
_height: i32,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_region:request:destroy
|
||||
async fn destroy(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
client.remove(self.id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
166
src/wayland/core/data_device.rs
Normal file
166
src/wayland/core/data_device.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use crate::wayland::{Client, WaylandResult};
|
||||
use std::os::fd::OwnedFd;
|
||||
use waynest::ObjectId;
|
||||
use waynest_protocols::server::core::wayland::{
|
||||
wl_data_device::*, wl_data_device_manager::*, wl_data_offer::WlDataOffer, wl_data_source::*,
|
||||
};
|
||||
use waynest_server::Client as _;
|
||||
|
||||
// TODO: actually implement this
|
||||
|
||||
#[derive(Debug, waynest_server::RequestDispatcher)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct DataDeviceManager;
|
||||
impl WlDataDeviceManager for DataDeviceManager {
|
||||
type Connection = Client;
|
||||
|
||||
async fn create_data_source(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
client.insert(id, DataSource { id })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_data_device(
|
||||
&self,
|
||||
client: &mut Client,
|
||||
_sender_id: ObjectId,
|
||||
id: ObjectId,
|
||||
_seat: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
client.insert(id, DataDevice)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, waynest_server::RequestDispatcher)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct DataSource {
|
||||
id: ObjectId,
|
||||
}
|
||||
impl WlDataSource for DataSource {
|
||||
type Connection = Client;
|
||||
|
||||
async fn offer(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
_mime_type: String,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn destroy(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
client.remove(self.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_actions(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
_dnd_actions: DndAction,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, waynest_server::RequestDispatcher)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct DataDevice;
|
||||
impl WlDataDevice for DataDevice {
|
||||
type Connection = Client;
|
||||
|
||||
async fn start_drag(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
_source: Option<ObjectId>,
|
||||
_origin: ObjectId,
|
||||
_icon: Option<ObjectId>,
|
||||
_serial: u32,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_selection(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
_source: Option<ObjectId>,
|
||||
_serial: u32,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn release(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, waynest_server::RequestDispatcher)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct DataOffer {
|
||||
id: ObjectId,
|
||||
}
|
||||
impl WlDataOffer for DataOffer {
|
||||
type Connection = Client;
|
||||
|
||||
async fn accept(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
_serial: u32,
|
||||
_mime_type: Option<String>,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
_mime_type: String,
|
||||
_fd: OwnedFd,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn destroy(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
client.remove(self.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn finish(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_actions(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
_dnd_actions: DndAction,
|
||||
_preferred_action: DndAction,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
258
src/wayland/core/keyboard.rs
Normal file
258
src/wayland/core/keyboard.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use crate::{
|
||||
nodes::items::panel::KEYMAPS,
|
||||
wayland::{Client, WaylandResult, core::surface::Surface, util::ClientExt},
|
||||
};
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use memfd::MemfdOptions;
|
||||
use slotmap::{DefaultKey, KeyData};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
io::Write,
|
||||
os::{
|
||||
fd::{AsFd, IntoRawFd},
|
||||
unix::io::{FromRawFd, OwnedFd},
|
||||
},
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use waynest::ObjectId;
|
||||
pub use waynest_protocols::server::core::wayland::wl_keyboard::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct ModifierState {
|
||||
pressed_keys: HashSet<u32>,
|
||||
mods_depressed: u32,
|
||||
mods_latched: u32,
|
||||
mods_locked: u32,
|
||||
group: u32,
|
||||
}
|
||||
|
||||
impl ModifierState {
|
||||
fn update_key(&mut self, key: u32, pressed: bool) -> bool {
|
||||
let changed = if pressed {
|
||||
self.pressed_keys.insert(key)
|
||||
} else {
|
||||
self.pressed_keys.remove(&key)
|
||||
};
|
||||
|
||||
if changed {
|
||||
self.update_modifiers();
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn update_modifiers(&mut self) {
|
||||
let mut mods = 0;
|
||||
|
||||
// Update modifier state based on currently pressed keys
|
||||
for key in &self.pressed_keys {
|
||||
match *key {
|
||||
input_event_codes::KEY_LEFTSHIFT!() | input_event_codes::KEY_RIGHTSHIFT!() => {
|
||||
mods |= 1
|
||||
}
|
||||
input_event_codes::KEY_LEFTCTRL!() | input_event_codes::KEY_RIGHTCTRL!() => {
|
||||
mods |= 4
|
||||
}
|
||||
input_event_codes::KEY_LEFTALT!() => mods |= 8,
|
||||
input_event_codes::KEY_RIGHTALT!() => mods |= 128,
|
||||
input_event_codes::KEY_LEFTMETA!() | input_event_codes::KEY_RIGHTMETA!() => {
|
||||
mods |= 64
|
||||
}
|
||||
input_event_codes::KEY_CAPSLOCK!() => self.mods_locked ^= 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.mods_depressed = mods;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(waynest_server::RequestDispatcher)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct Keyboard {
|
||||
pub id: ObjectId,
|
||||
focused_surface: Mutex<Weak<Surface>>,
|
||||
modifier_state: Mutex<ModifierState>,
|
||||
pressed_keys: DashMap<ObjectId, DashSet<u32>>,
|
||||
current_keymap_id: Mutex<u64>,
|
||||
}
|
||||
|
||||
impl Keyboard {
|
||||
pub fn new(id: ObjectId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
focused_surface: Mutex::new(Weak::new()),
|
||||
modifier_state: Mutex::new(ModifierState::default()),
|
||||
pressed_keys: DashMap::default(),
|
||||
current_keymap_id: Mutex::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_keymap(&self, client: &mut Client, keymap: &[u8]) -> WaylandResult<()> {
|
||||
let mut file = MemfdOptions::default()
|
||||
.create("stardust-keymap")?
|
||||
.into_file();
|
||||
file.set_len(keymap.len() as u64)?;
|
||||
file.write_all(keymap)?;
|
||||
file.flush()?;
|
||||
|
||||
let fd = unsafe { OwnedFd::from_raw_fd(file.into_raw_fd()) };
|
||||
|
||||
// Send keymap to client
|
||||
self.keymap(
|
||||
client,
|
||||
self.id,
|
||||
KeymapFormat::XkbV1,
|
||||
fd.as_fd(),
|
||||
keymap.len() as u32,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// has to be the wayland key, so -8 or whatever
|
||||
pub async fn handle_keyboard_key(
|
||||
&self,
|
||||
client: &mut Client,
|
||||
surface: Arc<Surface>,
|
||||
keymap_id: u64,
|
||||
key: u32,
|
||||
pressed: bool,
|
||||
) -> WaylandResult<()> {
|
||||
// KEYMAP UPDATES
|
||||
{
|
||||
let mut old_keymap_id = self.current_keymap_id.lock().await;
|
||||
|
||||
if *old_keymap_id != keymap_id {
|
||||
let keymap_key = DefaultKey::from(KeyData::from_ffi(keymap_id));
|
||||
|
||||
// Get keymap data and drop the lock immediately
|
||||
let keymap_data = {
|
||||
let keymap_lock = KEYMAPS.lock();
|
||||
keymap_lock
|
||||
.get(keymap_key)
|
||||
.map(|s| s.as_bytes().to_vec())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
// Now we can safely await
|
||||
self.send_keymap(client, &keymap_data).await?;
|
||||
};
|
||||
*old_keymap_id = keymap_id;
|
||||
drop(old_keymap_id);
|
||||
}
|
||||
|
||||
// PRESSED KEYS UPDATE
|
||||
let pressed_keys = self.pressed_keys.entry(surface.id).or_default();
|
||||
if pressed {
|
||||
pressed_keys.insert(key);
|
||||
} else {
|
||||
pressed_keys.remove(&key);
|
||||
}
|
||||
// println!("pressed keys: {:?}", &*pressed_keys);
|
||||
|
||||
// FOCUS UPDATES
|
||||
let mut focused = self.focused_surface.lock().await;
|
||||
let mut modifier_state = self.modifier_state.lock().await;
|
||||
|
||||
let refocus = focused.as_ptr() != Arc::as_ptr(&surface);
|
||||
// If we're entering a new surface
|
||||
if refocus {
|
||||
// Send leave to old surface if it exists and is still alive
|
||||
if let Some(old_surface) = focused.upgrade() {
|
||||
let serial = client.next_event_serial();
|
||||
self.leave(client, old_surface.id, serial, self.id).await?;
|
||||
// println!("Left surface {}", old_surface.id);
|
||||
}
|
||||
|
||||
// Send enter to new surface
|
||||
let serial = client.next_event_serial();
|
||||
self.enter(
|
||||
client,
|
||||
self.id,
|
||||
serial,
|
||||
surface.id,
|
||||
pressed_keys.iter().flat_map(|k| k.to_ne_bytes()).collect(),
|
||||
)
|
||||
.await?;
|
||||
// println!("Entered new surface {}", surface.id);
|
||||
|
||||
// Update focused surface
|
||||
*focused = Arc::downgrade(&surface);
|
||||
}
|
||||
|
||||
// KEY EVENT SENDING
|
||||
let serial = client.next_event_serial();
|
||||
// println!(
|
||||
// "Sent key {key} {}",
|
||||
// if pressed { "pressed" } else { "released" }
|
||||
// );
|
||||
self.key(
|
||||
client,
|
||||
self.id,
|
||||
serial,
|
||||
client.display().creation_time.elapsed().as_millis() as u32, // time
|
||||
key,
|
||||
if pressed {
|
||||
KeyState::Pressed
|
||||
} else {
|
||||
KeyState::Released
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// MODIFIER UPDATES
|
||||
// Update modifier state and send modifiers event if changed
|
||||
if refocus || modifier_state.update_key(key, pressed) {
|
||||
// println!("Update modifiers");
|
||||
let serial = client.next_event_serial();
|
||||
self.modifiers(
|
||||
client,
|
||||
self.id,
|
||||
serial,
|
||||
modifier_state.mods_depressed,
|
||||
modifier_state.mods_latched,
|
||||
modifier_state.mods_locked,
|
||||
modifier_state.group,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset(&self, client: &mut Client) -> WaylandResult<()> {
|
||||
let mut modifier_state = self.modifier_state.lock().await;
|
||||
modifier_state.pressed_keys.clear();
|
||||
modifier_state.mods_depressed = 0;
|
||||
modifier_state.mods_latched = 0;
|
||||
modifier_state.mods_locked = 0;
|
||||
modifier_state.group = 0;
|
||||
|
||||
let serial = client.next_event_serial();
|
||||
self.modifiers(
|
||||
client,
|
||||
self.id,
|
||||
serial,
|
||||
modifier_state.mods_depressed,
|
||||
modifier_state.mods_latched,
|
||||
modifier_state.mods_locked,
|
||||
modifier_state.group,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl WlKeyboard for Keyboard {
|
||||
type Connection = Client;
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_keyboard:request:release
|
||||
async fn release(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
13
src/wayland/core/mod.rs
Normal file
13
src/wayland/core/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod buffer;
|
||||
pub mod callback;
|
||||
pub mod compositor;
|
||||
pub mod data_device;
|
||||
pub mod keyboard;
|
||||
pub mod output;
|
||||
pub mod pointer;
|
||||
pub mod seat;
|
||||
pub mod shm;
|
||||
pub mod shm_buffer_backing;
|
||||
pub mod shm_pool;
|
||||
pub mod surface;
|
||||
pub mod touch;
|
||||
57
src/wayland/core/output.rs
Normal file
57
src/wayland/core/output.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use crate::wayland::{Client, WaylandResult};
|
||||
use waynest::ObjectId;
|
||||
pub use waynest_protocols::server::core::wayland::wl_output::*;
|
||||
|
||||
#[derive(Debug, waynest_server::RequestDispatcher)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct Output {
|
||||
pub id: ObjectId,
|
||||
pub version: u32,
|
||||
}
|
||||
impl Output {
|
||||
pub async fn advertise_outputs(&self, client: &mut Client) -> WaylandResult<()> {
|
||||
self.geometry(
|
||||
client,
|
||||
self.id,
|
||||
2048,
|
||||
2048,
|
||||
0,
|
||||
0,
|
||||
Subpixel::None,
|
||||
"Stardust Virtual Display".to_string(),
|
||||
"Stardust Virtual Display".to_string(),
|
||||
Transform::Normal,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if self.version >= 4 {
|
||||
self.name(client, self.id, "Stardust Virtual Display".to_string())
|
||||
.await?;
|
||||
self.description(
|
||||
client,
|
||||
self.id,
|
||||
"I needed this to account for dumb clients".to_string(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
self.mode(client, self.id, Mode::Current, 2048, 2048, i32::MAX)
|
||||
.await?;
|
||||
|
||||
if self.version >= 2 {
|
||||
self.done(client, self.id).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl WlOutput for Output {
|
||||
type Connection = Client;
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_output:request:release
|
||||
async fn release(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
252
src/wayland/core/pointer.rs
Normal file
252
src/wayland/core/pointer.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use super::surface::SurfaceRole;
|
||||
use crate::nodes::items::panel::Geometry;
|
||||
use crate::wayland::core::surface::Surface;
|
||||
use crate::wayland::{Client, WaylandResult};
|
||||
use mint::Vector2;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Weak;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing;
|
||||
use waynest::ObjectId;
|
||||
use waynest_server::Client as _;
|
||||
|
||||
pub use waynest_protocols::server::core::wayland::wl_pointer::*;
|
||||
|
||||
#[derive(waynest_server::RequestDispatcher)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct Pointer {
|
||||
pub id: ObjectId,
|
||||
version: u32,
|
||||
focused_surface: Mutex<Weak<Surface>>,
|
||||
cursor_surface: Mutex<Option<Arc<Surface>>>,
|
||||
}
|
||||
impl Pointer {
|
||||
pub fn new(id: ObjectId, version: u32) -> Self {
|
||||
Self {
|
||||
id,
|
||||
version,
|
||||
focused_surface: Mutex::new(Weak::new()),
|
||||
cursor_surface: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_pointer_motion(
|
||||
&self,
|
||||
client: &mut Client,
|
||||
surface: Arc<Surface>,
|
||||
position: Vector2<f32>,
|
||||
) -> WaylandResult<()> {
|
||||
tracing::debug!(
|
||||
"Handling pointer motion at ({}, {})",
|
||||
position.x,
|
||||
position.y
|
||||
);
|
||||
let mut focused = self.focused_surface.lock().await;
|
||||
|
||||
// If we're entering a new surface
|
||||
if focused.as_ptr() != Arc::as_ptr(&surface) {
|
||||
tracing::debug!("Surface transition detected");
|
||||
// Send leave to old surface if it exists and is still alive
|
||||
if let Some(old_surface) = focused.upgrade() {
|
||||
let serial = client.next_event_serial();
|
||||
tracing::debug!("Sending leave event with serial {}", serial);
|
||||
self.leave(client, self.id, serial, old_surface.id).await?;
|
||||
}
|
||||
|
||||
// Send enter to new surface
|
||||
let serial = client.next_event_serial();
|
||||
tracing::debug!(
|
||||
"Sending enter event with serial {} to surface {:?}",
|
||||
serial,
|
||||
surface.id
|
||||
);
|
||||
self.enter(
|
||||
client,
|
||||
self.id,
|
||||
serial,
|
||||
surface.id,
|
||||
(position.x as f64).into(),
|
||||
(position.y as f64).into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Update focused surface
|
||||
*focused = Arc::downgrade(&surface);
|
||||
}
|
||||
|
||||
// Send motion event to current surface
|
||||
tracing::debug!("Sending motion event to surface");
|
||||
self.motion(
|
||||
client,
|
||||
self.id,
|
||||
0, // time
|
||||
(position.x as f64).into(),
|
||||
(position.y as f64).into(),
|
||||
)
|
||||
.await?;
|
||||
if self.version >= 5 {
|
||||
self.frame(client, self.id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_pointer_button(
|
||||
&self,
|
||||
client: &mut Client,
|
||||
surface: Arc<Surface>,
|
||||
button: u32,
|
||||
pressed: bool,
|
||||
) -> WaylandResult<()> {
|
||||
tracing::debug!(
|
||||
"Handling pointer button {} {} on surface {:?}",
|
||||
button,
|
||||
if pressed { "pressed" } else { "released" },
|
||||
surface.id
|
||||
);
|
||||
let serial = client.next_event_serial();
|
||||
self.button(
|
||||
client,
|
||||
self.id,
|
||||
serial,
|
||||
0, // time
|
||||
button,
|
||||
if pressed {
|
||||
ButtonState::Pressed
|
||||
} else {
|
||||
ButtonState::Released
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.frame(client, self.id).await
|
||||
}
|
||||
pub async fn handle_pointer_scroll(
|
||||
&self,
|
||||
client: &mut Client,
|
||||
_surface: Arc<Surface>,
|
||||
scroll_distance: Option<Vector2<f32>>,
|
||||
scroll_steps: Option<Vector2<f32>>,
|
||||
) -> WaylandResult<()> {
|
||||
tracing::debug!(
|
||||
"Handling pointer scroll: distance={:?}, steps={:?}",
|
||||
scroll_distance,
|
||||
scroll_steps
|
||||
);
|
||||
if let Some(distance) = scroll_distance {
|
||||
self.axis(
|
||||
client,
|
||||
self.id,
|
||||
0, // time
|
||||
Axis::HorizontalScroll,
|
||||
(distance.x as f64).into(),
|
||||
)
|
||||
.await?;
|
||||
self.axis(
|
||||
client,
|
||||
self.id,
|
||||
0, // time
|
||||
Axis::VerticalScroll,
|
||||
(distance.y as f64).into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if self.version < 8
|
||||
&& self.version >= 5
|
||||
&& let Some(steps) = scroll_steps
|
||||
{
|
||||
self.axis_discrete(client, self.id, Axis::HorizontalScroll, steps.x as i32)
|
||||
.await?;
|
||||
self.axis_discrete(client, self.id, Axis::VerticalScroll, steps.y as i32)
|
||||
.await?;
|
||||
}
|
||||
if self.version >= 8
|
||||
&& let Some(steps) = scroll_steps
|
||||
{
|
||||
self.axis_value120(
|
||||
client,
|
||||
self.id,
|
||||
Axis::HorizontalScroll,
|
||||
(steps.x * 120.) as i32,
|
||||
)
|
||||
.await?;
|
||||
self.axis_value120(
|
||||
client,
|
||||
self.id,
|
||||
Axis::VerticalScroll,
|
||||
(steps.y * 120.) as i32,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if self.version >= 5 {
|
||||
self.frame(client, self.id).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset(&self, client: &mut Client) -> WaylandResult<()> {
|
||||
let mut focused = self.focused_surface.lock().await;
|
||||
if let Some(old_surface) = focused.upgrade() {
|
||||
let serial = client.next_event_serial();
|
||||
self.leave(client, self.id, serial, old_surface.id).await?;
|
||||
self.frame(client, self.id).await?;
|
||||
}
|
||||
*focused = Weak::new();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cursor_surface(&self) -> Option<Arc<Surface>> {
|
||||
self.cursor_surface.lock().await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl WlPointer for Pointer {
|
||||
type Connection = crate::wayland::Client;
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_pointer:request:set_cursor
|
||||
async fn set_cursor(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
_serial: u32,
|
||||
surface: Option<ObjectId>,
|
||||
hotspot_x: i32,
|
||||
hotspot_y: i32,
|
||||
) -> WaylandResult<()> {
|
||||
if let Some(focused_surface) = self.focused_surface.lock().await.upgrade()
|
||||
&& let Some(panel_item) = focused_surface.panel_item.lock().upgrade()
|
||||
{
|
||||
panel_item.set_cursor(surface.and_then(|s| client.get::<Surface>(s)).map(|s| {
|
||||
let size = s
|
||||
.current_state()
|
||||
.buffer
|
||||
.map(|b| b.buffer.size())
|
||||
.unwrap_or([16; 2].into());
|
||||
Geometry {
|
||||
origin: [hotspot_x, hotspot_y].into(),
|
||||
size: [size.x as u32, size.y as u32].into(),
|
||||
}
|
||||
}));
|
||||
}
|
||||
let Some(surface) = surface else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(surface) = client.get::<Surface>(surface) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
surface
|
||||
.try_set_role(SurfaceRole::Cursor, Error::Role)
|
||||
.await?;
|
||||
self.cursor_surface.lock().await.replace(surface);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_pointer:request:release
|
||||
async fn release(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
213
src/wayland/core/seat.rs
Normal file
213
src/wayland/core/seat.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use crate::wayland::Client;
|
||||
use crate::wayland::WaylandResult;
|
||||
use crate::wayland::core::{keyboard::Keyboard, pointer::Pointer, surface::Surface, touch::Touch};
|
||||
use mint::Vector2;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use waynest::ObjectId;
|
||||
pub use waynest_protocols::server::core::wayland::wl_seat::*;
|
||||
use waynest_server::Client as _;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SeatMessage {
|
||||
PointerMotion {
|
||||
surface: Arc<Surface>,
|
||||
position: Vector2<f32>,
|
||||
},
|
||||
PointerButton {
|
||||
surface: Arc<Surface>,
|
||||
button: u32,
|
||||
pressed: bool,
|
||||
},
|
||||
PointerScroll {
|
||||
surface: Arc<Surface>,
|
||||
scroll_distance: Option<Vector2<f32>>,
|
||||
scroll_steps: Option<Vector2<f32>>,
|
||||
},
|
||||
KeyboardKey {
|
||||
surface: Arc<Surface>,
|
||||
keymap_id: u64,
|
||||
key: u32,
|
||||
pressed: bool,
|
||||
},
|
||||
TouchDown {
|
||||
surface: Arc<Surface>,
|
||||
id: u32,
|
||||
position: Vector2<f32>,
|
||||
},
|
||||
TouchMove {
|
||||
id: u32,
|
||||
position: Vector2<f32>,
|
||||
},
|
||||
TouchUp {
|
||||
id: u32,
|
||||
},
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Default, waynest_server::RequestDispatcher)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct Seat {
|
||||
version: u32,
|
||||
pointer: OnceLock<Arc<Pointer>>,
|
||||
keyboard: OnceLock<Arc<Keyboard>>,
|
||||
touch: OnceLock<Arc<Touch>>,
|
||||
}
|
||||
|
||||
impl Seat {
|
||||
pub async fn new(client: &mut Client, id: ObjectId, version: u32) -> WaylandResult<Self> {
|
||||
let seat = Self {
|
||||
version,
|
||||
pointer: OnceLock::new(),
|
||||
keyboard: OnceLock::new(),
|
||||
touch: OnceLock::new(),
|
||||
};
|
||||
|
||||
if version >= 2 {
|
||||
seat.name(client, id, "theonlyseat".into()).await?;
|
||||
}
|
||||
|
||||
tracing::debug!("Advertising seat capabilities with id {}", id);
|
||||
let capabilities = Capability::Pointer | Capability::Keyboard | Capability::Touch;
|
||||
WlSeat::capabilities(&seat, client, id, capabilities).await?;
|
||||
tracing::debug!("Capabilities advertised: {:?}", capabilities);
|
||||
|
||||
Ok(seat)
|
||||
}
|
||||
|
||||
pub async fn handle_message(
|
||||
&self,
|
||||
client: &mut Client,
|
||||
message: SeatMessage,
|
||||
) -> WaylandResult<()> {
|
||||
match message {
|
||||
SeatMessage::PointerMotion { surface, position } => {
|
||||
if let Some(pointer) = self.pointer.get() {
|
||||
pointer
|
||||
.handle_pointer_motion(client, surface, position)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
SeatMessage::PointerButton {
|
||||
surface,
|
||||
button,
|
||||
pressed,
|
||||
} => {
|
||||
if let Some(pointer) = self.pointer.get() {
|
||||
pointer
|
||||
.handle_pointer_button(client, surface, button, pressed)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
SeatMessage::PointerScroll {
|
||||
surface,
|
||||
scroll_distance,
|
||||
scroll_steps,
|
||||
} => {
|
||||
if let Some(pointer) = self.pointer.get() {
|
||||
pointer
|
||||
.handle_pointer_scroll(client, surface, scroll_distance, scroll_steps)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
SeatMessage::KeyboardKey {
|
||||
surface,
|
||||
keymap_id,
|
||||
key,
|
||||
pressed,
|
||||
} => {
|
||||
if let Some(keyboard) = self.keyboard.get() {
|
||||
keyboard
|
||||
.handle_keyboard_key(client, surface, keymap_id, key - 8, pressed)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
SeatMessage::TouchDown {
|
||||
surface,
|
||||
id,
|
||||
position,
|
||||
} => {
|
||||
if let Some(touch) = self.touch.get() {
|
||||
touch
|
||||
.handle_touch_down(client, surface, id, position)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
SeatMessage::TouchMove { id, position } => {
|
||||
if let Some(touch) = self.touch.get() {
|
||||
touch.handle_touch_move(client, id, position).await?;
|
||||
}
|
||||
}
|
||||
SeatMessage::TouchUp { id } => {
|
||||
if let Some(touch) = self.touch.get() {
|
||||
touch.handle_touch_up(client, id).await?;
|
||||
}
|
||||
}
|
||||
SeatMessage::Reset => {
|
||||
if let Some(pointer) = self.pointer.get() {
|
||||
pointer.reset(client).await?;
|
||||
}
|
||||
if let Some(keyboard) = self.keyboard.get() {
|
||||
keyboard.reset(client).await?;
|
||||
}
|
||||
if let Some(touch) = self.touch.get() {
|
||||
touch.reset(client).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cursor_surface(&self) -> Option<Arc<Surface>> {
|
||||
self.pointer.get()?.cursor_surface().await
|
||||
}
|
||||
}
|
||||
impl WlSeat for Seat {
|
||||
type Connection = crate::wayland::Client;
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_seat:request:get_pointer
|
||||
async fn get_pointer(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
let pointer = client.insert(id, Pointer::new(id, self.version))?;
|
||||
let _ = self.pointer.set(pointer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_seat:request:get_keyboard
|
||||
async fn get_keyboard(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
tracing::info!("Getting keyboard");
|
||||
let keyboard = client.insert(id, Keyboard::new(id))?;
|
||||
let _ = self.keyboard.set(keyboard);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_seat:request:get_touch
|
||||
async fn get_touch(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
let touch = client.insert(id, Touch(id))?;
|
||||
let _ = self.touch.set(touch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_seat:request:release
|
||||
async fn release(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
47
src/wayland/core/shm.rs
Normal file
47
src/wayland/core/shm.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::wayland::{Client, WaylandResult, core::shm_pool::ShmPool};
|
||||
use std::os::fd::OwnedFd;
|
||||
use waynest::ObjectId;
|
||||
pub use waynest_protocols::server::core::wayland::wl_shm::*;
|
||||
use waynest_server::Client as _;
|
||||
|
||||
#[derive(Debug, waynest_server::RequestDispatcher, Default)]
|
||||
#[waynest(error = crate::wayland::WaylandError, connection = crate::wayland::Client)]
|
||||
pub struct Shm;
|
||||
impl Shm {
|
||||
pub async fn advertise_formats(
|
||||
&self,
|
||||
client: &mut Client,
|
||||
sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
self.format(client, sender_id, Format::Argb8888).await?;
|
||||
self.format(client, sender_id, Format::Xrgb8888).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl WlShm for Shm {
|
||||
type Connection = crate::wayland::Client;
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_shm:request:create_pool
|
||||
async fn create_pool(
|
||||
&self,
|
||||
client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
pool_id: ObjectId,
|
||||
fd: OwnedFd,
|
||||
size: i32,
|
||||
) -> WaylandResult<()> {
|
||||
client.insert(pool_id, ShmPool::new(fd, size, pool_id)?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// https://wayland.app/protocols/wayland#wl_shm:request:release
|
||||
async fn release(
|
||||
&self,
|
||||
_client: &mut Self::Connection,
|
||||
_sender_id: ObjectId,
|
||||
) -> WaylandResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
276
src/wayland/core/shm_buffer_backing.rs
Normal file
276
src/wayland/core/shm_buffer_backing.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use super::shm_pool::ShmPool;
|
||||
use crate::wayland::{RENDER_DEVICE, vulkano_data::VULKANO_CONTEXT};
|
||||
use bevy::{
|
||||
asset::{Assets, Handle},
|
||||
image::Image,
|
||||
};
|
||||
use bevy_dmabuf::{
|
||||
dmatex::{Dmatex, DmatexPlane, Resolution},
|
||||
import::{DmatexUsage, DropCallback, ImportedDmatexs, ImportedTexture, import_texture},
|
||||
};
|
||||
use drm_fourcc::DrmFourcc;
|
||||
use mint::Vector2;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
os::fd::OwnedFd,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use tracing::debug_span;
|
||||
use vulkano::{
|
||||
buffer::BufferUsage,
|
||||
command_buffer::{
|
||||
AutoCommandBufferBuilder, CommandBufferUsage, CopyBufferToImageInfo,
|
||||
PrimaryCommandBufferAbstract,
|
||||
},
|
||||
image::{
|
||||
ImageAspect, ImageCreateFlags, ImageCreateInfo, ImageMemory, ImageTiling, ImageUsage,
|
||||
sys::RawImage,
|
||||
},
|
||||
memory::{
|
||||
DedicatedAllocation, DeviceMemory, ExternalMemoryHandleType, MemoryAllocateInfo,
|
||||
ResourceMemory,
|
||||
allocator::{AllocationCreateInfo, MemoryTypeFilter},
|
||||
},
|
||||
sync::GpuFuture,
|
||||
};
|
||||
use waynest_protocols::server::core::wayland::wl_shm::Format;
|
||||
|
||||
/// Parameters for a shared memory buffer
|
||||
pub struct ShmBufferBacking {
|
||||
pool: Arc<ShmPool>,
|
||||
offset: usize,
|
||||
stride: usize,
|
||||
size: Vector2<usize>,
|
||||
wl_format: Format,
|
||||
image: Arc<vulkano::image::Image>,
|
||||
tex: OnceLock<Handle<Image>>,
|
||||
pending_imported_dmatex: Mutex<Option<ImportedTexture>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ShmBufferBacking {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ShmBufferBacking")
|
||||
.field("pool", &self.pool)
|
||||
.field("offset", &self.offset)
|
||||
.field("stride", &self.stride)
|
||||
.field("size", &self.size)
|
||||
.field("wl_format", &self.wl_format)
|
||||
.field("image", &self.image)
|
||||
.field("tex", &self.tex)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ShmBufferBacking {
|
||||
pub fn new(
|
||||
pool: Arc<ShmPool>,
|
||||
offset: usize,
|
||||
stride: usize,
|
||||
size: Vector2<usize>,
|
||||
wl_format: Format,
|
||||
) -> Self {
|
||||
let vk = VULKANO_CONTEXT.wait();
|
||||
let format = match wl_format {
|
||||
Format::Argb8888 | Format::Xrgb8888 => vulkano::format::Format::B8G8R8A8_SRGB,
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
let modifiers = vk
|
||||
.phys_dev
|
||||
.format_properties(format)
|
||||
.unwrap()
|
||||
.drm_format_modifier_properties
|
||||
.into_iter()
|
||||
.filter_map(|v| {
|
||||
(v.drm_format_modifier_plane_count == 1).then_some(v.drm_format_modifier)
|
||||
})
|
||||
.collect();
|
||||
let raw_image = RawImage::new(
|
||||
vk.dev.clone(),
|
||||
ImageCreateInfo {
|
||||
flags: ImageCreateFlags::empty(),
|
||||
image_type: vulkano::image::ImageType::Dim2d,
|
||||
format,
|
||||
extent: [size.x as u32, size.y as u32, 1],
|
||||
tiling: ImageTiling::DrmFormatModifier,
|
||||
usage: ImageUsage::TRANSFER_DST,
|
||||
drm_format_modifiers: modifiers,
|
||||
external_memory_handle_types: ExternalMemoryHandleType::DmaBuf.into(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let (modifier, num_planes) = raw_image.drm_format_modifier().unwrap();
|
||||
|
||||
let mem_reqs = raw_image.memory_requirements()[0];
|
||||
|
||||
let index = vk
|
||||
.phys_dev
|
||||
.memory_properties()
|
||||
.memory_types
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, _v)| i as u32)
|
||||
.find(|i| mem_reqs.memory_type_bits & (1 << i) != 0)
|
||||
.expect("no valid memory type");
|
||||
|
||||
let mem = ResourceMemory::new_dedicated(
|
||||
DeviceMemory::allocate(
|
||||
vk.dev.clone(),
|
||||
MemoryAllocateInfo {
|
||||
allocation_size: mem_reqs.layout.size(),
|
||||
memory_type_index: index,
|
||||
dedicated_allocation: Some(DedicatedAllocation::Image(&raw_image)),
|
||||
export_handle_types: ExternalMemoryHandleType::DmaBuf.into(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let Ok(image) = raw_image.bind_memory([mem]) else {
|
||||
panic!("unable to bind memory")
|
||||
};
|
||||
let image = Arc::new(image);
|
||||
let ImageMemory::Normal(mem) = image.memory() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let [mem] = mem.as_slice() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let fd = OwnedFd::from(
|
||||
mem.device_memory()
|
||||
.export_fd(ExternalMemoryHandleType::DmaBuf)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let planes = (0..num_planes)
|
||||
.filter_map(|i| {
|
||||
Some(match i {
|
||||
0 => ImageAspect::MemoryPlane0,
|
||||
1 => ImageAspect::MemoryPlane1,
|
||||
2 => ImageAspect::MemoryPlane2,
|
||||
3 => ImageAspect::MemoryPlane3,
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.map(|aspect| {
|
||||
let plane_layout = image.subresource_layout(aspect, 0, 0).unwrap();
|
||||
|
||||
DmatexPlane {
|
||||
dmabuf_fd: fd.try_clone().unwrap().into(),
|
||||
modifier,
|
||||
offset: plane_layout.offset as u32,
|
||||
stride: plane_layout.row_pitch as i32,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let dmatex = Dmatex {
|
||||
planes,
|
||||
res: Resolution {
|
||||
x: size.x as u32,
|
||||
y: size.y as u32,
|
||||
},
|
||||
format: DrmFourcc::Argb8888 as u32,
|
||||
flip_y: false,
|
||||
srgb: true,
|
||||
};
|
||||
let imported_dmatex = import_texture(
|
||||
RENDER_DEVICE.wait(),
|
||||
dmatex,
|
||||
DropCallback(None),
|
||||
DmatexUsage::Sampling,
|
||||
)
|
||||
.unwrap();
|
||||
Self {
|
||||
pool,
|
||||
offset,
|
||||
stride,
|
||||
size,
|
||||
wl_format,
|
||||
image,
|
||||
pending_imported_dmatex: Mutex::new(Some(imported_dmatex)),
|
||||
tex: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
pub fn on_commit(&self) {
|
||||
let vk = VULKANO_CONTEXT.wait();
|
||||
let data_len = self.size.x * self.size.y * 4;
|
||||
let gpu_buffer = vulkano::buffer::Buffer::new_slice::<u8>(
|
||||
vk.alloc.clone(),
|
||||
vulkano::buffer::BufferCreateInfo {
|
||||
usage: BufferUsage::TRANSFER_SRC,
|
||||
..Default::default()
|
||||
},
|
||||
AllocationCreateInfo {
|
||||
memory_type_filter: MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||
..Default::default()
|
||||
},
|
||||
data_len as u64,
|
||||
)
|
||||
.unwrap();
|
||||
{
|
||||
let _span = debug_span!("copy to gpu buffer").entered();
|
||||
let shm_data_lock = self.pool.data_lock();
|
||||
let mut gpu_slice = gpu_buffer.write().unwrap();
|
||||
for (shm_offset, gpu_offset) in
|
||||
(0..self.size.y).map(|v| (self.offset + (v * self.stride), (v * (self.size.x * 4))))
|
||||
{
|
||||
let line_slice = &shm_data_lock[shm_offset..(shm_offset + (self.size.x * 4))];
|
||||
let gpu_subslice = &mut gpu_slice[gpu_offset..(gpu_offset + (self.size.x * 4))];
|
||||
gpu_subslice.copy_from_slice(line_slice);
|
||||
}
|
||||
}
|
||||
let mut command_buffer = AutoCommandBufferBuilder::primary(
|
||||
vk.command_buffer_alloc.clone(),
|
||||
vk.queue.queue_family_index(),
|
||||
CommandBufferUsage::OneTimeSubmit,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
command_buffer
|
||||
.copy_buffer_to_image(CopyBufferToImageInfo::buffer_image(
|
||||
gpu_buffer.clone(),
|
||||
self.image.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let command_buffer = command_buffer.build().unwrap();
|
||||
command_buffer
|
||||
.execute(vk.queue.clone())
|
||||
.unwrap()
|
||||
.then_signal_fence_and_flush()
|
||||
.unwrap()
|
||||
.wait(None)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tracing::instrument("debug", skip_all)]
|
||||
pub fn update_tex(
|
||||
&self,
|
||||
dmatexes: &ImportedDmatexs,
|
||||
images: &mut Assets<Image>,
|
||||
) -> Option<Handle<Image>> {
|
||||
self.pending_imported_dmatex
|
||||
.lock()
|
||||
.take()
|
||||
.map(|tex| dmatexes.insert_imported_dmatex(images, tex))
|
||||
.inspect(|handle| {
|
||||
_ = self.tex.set(handle.clone());
|
||||
});
|
||||
self.tex.get().cloned()
|
||||
}
|
||||
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
match self.wl_format {
|
||||
Format::Xrgb8888 => false,
|
||||
Format::Argb8888 => true,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vector2<usize> {
|
||||
self.size
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user