Compare commits
1070 Commits
v3.0.0-dev
...
v3.28.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2064aa318 | ||
|
|
109709ce8b | ||
|
|
b200725762 | ||
|
|
9cf34bf987 | ||
|
|
be3a91bb14 | ||
|
|
fc948d8b27 | ||
|
|
8774658f4e | ||
|
|
0bf32148af | ||
|
|
97d158b615 | ||
|
|
b772dea188 | ||
|
|
5c8f0b35ec | ||
|
|
1b382653f2 | ||
|
|
c7cee63c97 | ||
|
|
ba9474fa62 | ||
|
|
b7ec7ea686 | ||
|
|
74560adb08 | ||
|
|
e6a303bdda | ||
|
|
7afcf0bb68 | ||
|
|
7aae2d0616 | ||
|
|
9255505a3b | ||
|
|
6c0dfd338d | ||
|
|
c306e988c8 | ||
|
|
86f7a8b273 | ||
|
|
2428a8efab | ||
|
|
cba12e487f | ||
|
|
e98d7931ca | ||
|
|
f8e2d2e3d0 | ||
|
|
4455bab6ac | ||
|
|
b21fb27041 | ||
|
|
5402ee0019 | ||
|
|
02654ea57b | ||
|
|
9cd4aeabaa | ||
|
|
d788511a83 | ||
|
|
378b5b8096 | ||
|
|
e26eb2f751 | ||
|
|
26264b15f7 | ||
|
|
309e3dd981 | ||
|
|
4ff5ed3b5d | ||
|
|
270a757756 | ||
|
|
79e4030ab4 | ||
|
|
fd86927e04 | ||
|
|
df3a7e5037 | ||
|
|
fefc6f14c9 | ||
|
|
923fbef0e4 | ||
|
|
601bd5ba7a | ||
|
|
5798f20c41 | ||
|
|
f2e26f91a8 | ||
|
|
e2cdc45be6 | ||
|
|
96493bc75e | ||
|
|
d78ad4a78e | ||
|
|
0c54b0dd6e | ||
|
|
65310e52de | ||
|
|
285d3cd9b8 | ||
|
|
10486abb64 | ||
|
|
8c1a9fc988 | ||
|
|
7bdd0b470b | ||
|
|
b7a51f12bf | ||
|
|
e002ac5474 | ||
|
|
211d339ce0 | ||
|
|
509fb14473 | ||
|
|
ef838a1b83 | ||
|
|
f84b5b633d | ||
|
|
8ec8c1170d | ||
|
|
920b98e4d1 | ||
|
|
c80f52d4bc | ||
|
|
0b6aa42daf | ||
|
|
04dc6427ef | ||
|
|
77e366b484 | ||
|
|
add7cfa0f2 | ||
|
|
14d4fa142c | ||
|
|
7fe1617f25 | ||
|
|
cbe4cb521c | ||
|
|
7c394525c1 | ||
|
|
12ed051fea | ||
|
|
014913c635 | ||
|
|
369e45c282 | ||
|
|
2892caec0c | ||
|
|
eb609e918a | ||
|
|
9217e58845 | ||
|
|
26b633618a | ||
|
|
e3054d6ee1 | ||
|
|
0b6294f4ec | ||
|
|
7b130dc0f3 | ||
|
|
d93c128c25 | ||
|
|
e1dd74775e | ||
|
|
e61e706607 | ||
|
|
a57d46deaa | ||
|
|
60efdc0071 | ||
|
|
4dc1155a9e | ||
|
|
5cabe6fb42 | ||
|
|
42239c305f | ||
|
|
fd3c763349 | ||
|
|
daa9bbf2c9 | ||
|
|
c7dad00908 | ||
|
|
24317717e8 | ||
|
|
dd3eac6db6 | ||
|
|
1606f8517f | ||
|
|
3a98401ada | ||
|
|
4764251241 | ||
|
|
3de04b7b9d | ||
|
|
04908495e9 | ||
|
|
3c835c848e | ||
|
|
0f1788f122 | ||
|
|
3d72e700a4 | ||
|
|
a5cf41e65f | ||
|
|
7029bc41d7 | ||
|
|
b87d40c844 | ||
|
|
9e154376d3 | ||
|
|
4a9fdbce57 | ||
|
|
775e335292 | ||
|
|
e553bf4deb | ||
|
|
db0e09ec6e | ||
|
|
ba50760f92 | ||
|
|
524e6d4f81 | ||
|
|
db93a669ab | ||
|
|
423f5b0502 | ||
|
|
2bc45c25fe | ||
|
|
d341463f67 | ||
|
|
19d1ecb36a | ||
|
|
ffe4047f97 | ||
|
|
15a3c29e7a | ||
|
|
6a32f85e60 | ||
|
|
42ff593004 | ||
|
|
5140af4e6f | ||
|
|
0f5b4c00a9 | ||
|
|
feb69f4987 | ||
|
|
84eb9a58ca | ||
|
|
fbd96f473a | ||
|
|
69228157dc | ||
|
|
6808f32b8d | ||
|
|
f29c62ec33 | ||
|
|
d77981230b | ||
|
|
e2b92c15f0 | ||
|
|
14614f4803 | ||
|
|
37d3489b07 | ||
|
|
f81225cc0d | ||
|
|
eb47f43940 | ||
|
|
7618a5e2c9 | ||
|
|
ab3ff842b2 | ||
|
|
82f0f92c15 | ||
|
|
545d9434d8 | ||
|
|
bbc43d5113 | ||
|
|
a6709acf65 | ||
|
|
509a027742 | ||
|
|
7244023233 | ||
|
|
c5f987d8b2 | ||
|
|
793696d4c0 | ||
|
|
305ca05f04 | ||
|
|
f4d7d1920a | ||
|
|
458aad0161 | ||
|
|
9e3132461f | ||
|
|
8301291215 | ||
|
|
daed14e228 | ||
|
|
7db82c5ba5 | ||
|
|
91d03c22c2 | ||
|
|
bc188f0d2b | ||
|
|
3981225824 | ||
|
|
1886d9d72f | ||
|
|
2a7dec8892 | ||
|
|
35d4a5b297 | ||
|
|
26345bfa50 | ||
|
|
8c9b8676dd | ||
|
|
5593c7a75f | ||
|
|
5b70c14ffe | ||
|
|
5e4d611044 | ||
|
|
7ab9d57156 | ||
|
|
9ea6f51126 | ||
|
|
649fe4f4f0 | ||
|
|
53df86c585 | ||
|
|
87c708e3c1 | ||
|
|
ba927b0490 | ||
|
|
2a09725c98 | ||
|
|
9fa0d17933 | ||
|
|
8d9f8a996a | ||
|
|
8814b60d0b | ||
|
|
541c07c53a | ||
|
|
6cbed80815 | ||
|
|
a3c1fdc0f4 | ||
|
|
ddf789b01c | ||
|
|
ab86cc0bf3 | ||
|
|
dc8fab5820 | ||
|
|
813c4f845a | ||
|
|
6ed42e9521 | ||
|
|
fb01ba60ec | ||
|
|
ea4210ded5 | ||
|
|
75e5cf54d6 | ||
|
|
7cebc85a95 | ||
|
|
c00faafa6d | ||
|
|
c9b14f0742 | ||
|
|
9624137c0d | ||
|
|
3eb433368f | ||
|
|
58691f4f5f | ||
|
|
5e9295abd5 | ||
|
|
6ed0ae55b2 | ||
|
|
84448d0aa1 | ||
|
|
31ced24966 | ||
|
|
24681840af | ||
|
|
5d28aa4f2e | ||
|
|
7dabdf3085 | ||
|
|
d7cbd5570c | ||
|
|
80011d4aea | ||
|
|
c6fe440a01 | ||
|
|
ba8d6dc3fa | ||
|
|
378f4f8304 | ||
|
|
aa414485a5 | ||
|
|
3a35a5d66a | ||
|
|
43b0a72b09 | ||
|
|
521ac2d13f | ||
|
|
30520c50c2 | ||
|
|
819f07eba3 | ||
|
|
d8d6aa7136 | ||
|
|
0bdcfea276 | ||
|
|
718f76313f | ||
|
|
ed75a365d8 | ||
|
|
f5ff13ffe4 | ||
|
|
44c69b1716 | ||
|
|
559ca7ffb8 | ||
|
|
524f34e8c1 | ||
|
|
d749be11f0 | ||
|
|
13dc9ff3cb | ||
|
|
24b4fce326 | ||
|
|
f506210bf8 | ||
|
|
cb0cbd06ce | ||
|
|
3356239089 | ||
|
|
9a5b2edc08 | ||
|
|
2d41b4bd83 | ||
|
|
58f7758ee1 | ||
|
|
9dbb642883 | ||
|
|
c5222e4d1d | ||
|
|
a5a695a17c | ||
|
|
7d1dc86cfb | ||
|
|
03224f914a | ||
|
|
1cf1f07232 | ||
|
|
838d36a74e | ||
|
|
6c9300d01b | ||
|
|
3059f7efef | ||
|
|
fbce7d9afa | ||
|
|
1c9f3ed9fa | ||
|
|
8c47d2d400 | ||
|
|
8a18f25b5d | ||
|
|
e7ae0058ed | ||
|
|
98bf3a3e33 | ||
|
|
379fcd4494 | ||
|
|
8589777bac | ||
|
|
c10a21ecbd | ||
|
|
eddc616b14 | ||
|
|
70fc1a9579 | ||
|
|
f27ec4d6a4 | ||
|
|
1e6e956e2d | ||
|
|
0a2a3530d1 | ||
|
|
9e4c820e26 | ||
|
|
05290bfe9e | ||
|
|
607f4ce353 | ||
|
|
4b819ead1d | ||
|
|
d07d9d3f31 | ||
|
|
d08776bf19 | ||
|
|
7a61fd44fd | ||
|
|
151da1487c | ||
|
|
e3f6dfad5b | ||
|
|
32080ce918 | ||
|
|
aedb2138c8 | ||
|
|
90e6938f1c | ||
|
|
eadff35250 | ||
|
|
487450ec64 | ||
|
|
f2f730bab5 | ||
|
|
ceecee37a7 | ||
|
|
394377bea9 | ||
|
|
2b47818deb | ||
|
|
9f72465052 | ||
|
|
ac33b4a322 | ||
|
|
911c844e54 | ||
|
|
12b9655565 | ||
|
|
2935717a06 | ||
|
|
2c8d2cfa21 | ||
|
|
d67c0eb757 | ||
|
|
7bcbcd5a7f | ||
|
|
1c06e7f36e | ||
|
|
407297adc0 | ||
|
|
b082b31562 | ||
|
|
fcbeb0f82f | ||
|
|
7094f8f265 | ||
|
|
48aaa13420 | ||
|
|
d2434cf91f | ||
|
|
8dbe0d2793 | ||
|
|
7b308cf984 | ||
|
|
520af504aa | ||
|
|
4bee130599 | ||
|
|
19a9ed3e19 | ||
|
|
0dac1b93ae | ||
|
|
e824863de1 | ||
|
|
9ff8becd62 | ||
|
|
fcaa1bcfa8 | ||
|
|
39f51bcc4f | ||
|
|
1b904ee2c9 | ||
|
|
e2baafc426 | ||
|
|
6dabe73002 | ||
|
|
d9b2b99c86 | ||
|
|
bdf23a7c4e | ||
|
|
c1723e0806 | ||
|
|
93590bd482 | ||
|
|
fbaa9d9e58 | ||
|
|
ba78a8558c | ||
|
|
513be04352 | ||
|
|
15317aa11b | ||
|
|
93e107f333 | ||
|
|
655b334b0a | ||
|
|
95cc1c58a6 | ||
|
|
0511d3e599 | ||
|
|
92a41b5c46 | ||
|
|
be16837c8c | ||
|
|
bf5f26cb12 | ||
|
|
ccbcef7eff | ||
|
|
d02a97b7f0 | ||
|
|
e0ca413c54 | ||
|
|
a272fc1c05 | ||
|
|
2c3f74d4ea | ||
|
|
c65b2fc0fd | ||
|
|
fd725adf27 | ||
|
|
ceba6da91f | ||
|
|
064e7d42ee | ||
|
|
cfdbf0c614 | ||
|
|
436777fe34 | ||
|
|
f08aa46222 | ||
|
|
e099ac514a | ||
|
|
873755f619 | ||
|
|
2094b7cf83 | ||
|
|
6ffdd81bb1 | ||
|
|
a69cb17602 | ||
|
|
7c82a9fd6e | ||
|
|
10d9a6c83d | ||
|
|
e474e9b090 | ||
|
|
837f9a6c74 | ||
|
|
c357aed7bb | ||
|
|
59ffddfd8d | ||
|
|
fb3accaa36 | ||
|
|
b3e79544ad | ||
|
|
2ee0f3bf97 | ||
|
|
a56b5edc4e | ||
|
|
f2d71b6551 | ||
|
|
ef560ddc03 | ||
|
|
2e30ffe1bc | ||
|
|
325b5e9297 | ||
|
|
ae9e04b8d4 | ||
|
|
ab0010122b | ||
|
|
08d736ecdc | ||
|
|
9c362c8ffd | ||
|
|
62c8778560 | ||
|
|
2fe0665e12 | ||
|
|
14a957f517 | ||
|
|
087010d0a1 | ||
|
|
f92657310a | ||
|
|
19e91400b5 | ||
|
|
7885fb7b4f | ||
|
|
331bec11ee | ||
|
|
f6ed650ef6 | ||
|
|
df9ce6add9 | ||
|
|
28ee47d501 | ||
|
|
df2da479ad | ||
|
|
46eb9d8baf | ||
|
|
b6be871a21 | ||
|
|
ce2367fc0a | ||
|
|
f219cd6c8b | ||
|
|
7789e8cfea | ||
|
|
e697624064 | ||
|
|
92ffdeb5bf | ||
|
|
ddefe7e542 | ||
|
|
fb4019c041 | ||
|
|
46026e047e | ||
|
|
51a9effcaa | ||
|
|
fc3b953a8e | ||
|
|
a2ded3cecc | ||
|
|
ffb1390b51 | ||
|
|
3b92591c05 | ||
|
|
f173ddcf00 | ||
|
|
f78306f624 | ||
|
|
9852c9301e | ||
|
|
2e4f3de604 | ||
|
|
300dd97353 | ||
|
|
8e29ade83a | ||
|
|
971c770f6a | ||
|
|
0dfad5a757 | ||
|
|
a0495f8aae | ||
|
|
7d31a06ae4 | ||
|
|
c5319ad7b1 | ||
|
|
f8bdb28ea6 | ||
|
|
2f8806d7c2 | ||
|
|
7d0a181c12 | ||
|
|
27cb7e53ed | ||
|
|
ee5154b510 | ||
|
|
080837201a | ||
|
|
656e6c0d94 | ||
|
|
b018f61bab | ||
|
|
b03b306848 | ||
|
|
19bf5c2b3e | ||
|
|
c22a716cf9 | ||
|
|
220c65da92 | ||
|
|
675f4b86f8 | ||
|
|
d31b3522b2 | ||
|
|
4ed2bab1d7 | ||
|
|
a572872806 | ||
|
|
e3ca23db0b | ||
|
|
67eb16a53f | ||
|
|
d7421cd1a3 | ||
|
|
c2e68ced66 | ||
|
|
10cf0db050 | ||
|
|
08f1a1dcfe | ||
|
|
11bf352295 | ||
|
|
5fd560b30a | ||
|
|
e6d01ca246 | ||
|
|
1610f05b83 | ||
|
|
8202b75cda | ||
|
|
4904bae5a6 | ||
|
|
04c06db02c | ||
|
|
0ddf2fa00d | ||
|
|
8776143ec2 | ||
|
|
080c8e7af3 | ||
|
|
a96f94b676 | ||
|
|
64998e762b | ||
|
|
b508da5ccc | ||
|
|
84a492655a | ||
|
|
9b9cfe4138 | ||
|
|
f362a7086a | ||
|
|
9e9ec57a5f | ||
|
|
da6a153985 | ||
|
|
b4bde3f510 | ||
|
|
8a5cebd635 | ||
|
|
ebdfc694c2 | ||
|
|
ddf2733d3c | ||
|
|
cf65bfbcd0 | ||
|
|
03cce830bc | ||
|
|
913cf4c3e9 | ||
|
|
2ccce0beaf | ||
|
|
52239f71cd | ||
|
|
d9265af2e8 | ||
|
|
5877f6a627 | ||
|
|
7f29340797 | ||
|
|
e3e982c051 | ||
|
|
17f09ff3de | ||
|
|
7a73416c03 | ||
|
|
0f799d44ad | ||
|
|
24ebd44f87 | ||
|
|
572b10e655 | ||
|
|
60361c176b | ||
|
|
b232a2c58f | ||
|
|
2a44ac56fb | ||
|
|
d0340a3257 | ||
|
|
e708a53ddb | ||
|
|
23cdd70198 | ||
|
|
ba4bc30a78 | ||
|
|
e516ea146d | ||
|
|
61af638fe4 | ||
|
|
7a0af4142f | ||
|
|
1aa8d596a3 | ||
|
|
a457810623 | ||
|
|
4bccb7ab84 | ||
|
|
fcd5b2ce0a | ||
|
|
57a1184a16 | ||
|
|
ef176efed8 | ||
|
|
9943e0317d | ||
|
|
ae11c1c904 | ||
|
|
f937e069ca | ||
|
|
c9326da47b | ||
|
|
4698c05f9c | ||
|
|
15fd2cd5a0 | ||
|
|
193eb11095 | ||
|
|
59cc0096cd | ||
|
|
6bd7390eb8 | ||
|
|
eabeb18f34 | ||
|
|
4b6f539ba3 | ||
|
|
ea55fc5f41 | ||
|
|
94787229a4 | ||
|
|
113d0b5141 | ||
|
|
163285b0a9 | ||
|
|
9f1cf5377a | ||
|
|
c37636215b | ||
|
|
0cb162022c | ||
|
|
5ff2ca311e | ||
|
|
44b8d452a8 | ||
|
|
82ccb1ab23 | ||
|
|
feb8b94e30 | ||
|
|
5adc73e277 | ||
|
|
c4ac983fc7 | ||
|
|
5ba7ba3aac | ||
|
|
e879479b3d | ||
|
|
7b49463297 | ||
|
|
1f77b19ced | ||
|
|
9853ebf02f | ||
|
|
6e13aa0bc9 | ||
|
|
66600c3373 | ||
|
|
19401c4bc6 | ||
|
|
bfc8660c5e | ||
|
|
6a83dc56ba | ||
|
|
82df9d8cad | ||
|
|
dd30fef078 | ||
|
|
79feb5a1cc | ||
|
|
0665524b11 | ||
|
|
d45367e840 | ||
|
|
1b7f0e0f1e | ||
|
|
18cb3360b5 | ||
|
|
cb0d988efc | ||
|
|
fc0c556967 | ||
|
|
00970164db | ||
|
|
7c3d76e9fe | ||
|
|
190c1f9620 | ||
|
|
f532325147 | ||
|
|
278dcb8808 | ||
|
|
6fec598b96 | ||
|
|
309c25e10b | ||
|
|
6df728ea3e | ||
|
|
dcf4ef6d74 | ||
|
|
4de1369a5a | ||
|
|
2ccfdb1280 | ||
|
|
9fbf800639 | ||
|
|
577daf64df | ||
|
|
0b1943e9b3 | ||
|
|
30bd2dd876 | ||
|
|
cecd4d4179 | ||
|
|
364baab18d | ||
|
|
bb3d4d2171 | ||
|
|
2355faf899 | ||
|
|
858e0b24ff | ||
|
|
f7d0ed3988 | ||
|
|
40096ecdfb | ||
|
|
43061b80b8 | ||
|
|
aa3d657d42 | ||
|
|
b0ef385769 | ||
|
|
dcf24e6292 | ||
|
|
2fdd77737c | ||
|
|
31c98ef1ba | ||
|
|
ceeec2c13a | ||
|
|
cc82e0cff8 | ||
|
|
29fc6c59e9 | ||
|
|
aa931fab7b | ||
|
|
8e2585a5c0 | ||
|
|
c115047f74 | ||
|
|
0ac58cb137 | ||
|
|
492e0977c3 | ||
|
|
e8a09e81f3 | ||
|
|
4fcc2c6208 | ||
|
|
e3523dc5fe | ||
|
|
acf6c31a36 | ||
|
|
339b2e696c | ||
|
|
6417fd49d6 | ||
|
|
374fd4db65 | ||
|
|
0b70dd9e38 | ||
|
|
55b19a7922 | ||
|
|
beb1b2655e | ||
|
|
6a81e48c3a | ||
|
|
f9841dd3df | ||
|
|
b563c773c1 | ||
|
|
dcda649d9d | ||
|
|
d6cfd23ae5 | ||
|
|
b69f62c9a4 | ||
|
|
582ee7d623 | ||
|
|
9e69f5dcfa | ||
|
|
7c4202186d | ||
|
|
7c56e4c09d | ||
|
|
b10b466c61 | ||
|
|
80d1986c61 | ||
|
|
7f9e413604 | ||
|
|
4dfbc3830f | ||
|
|
46cdefa9ee | ||
|
|
f857ea6095 | ||
|
|
994920f99f | ||
|
|
409ed9ad67 | ||
|
|
130d4096d5 | ||
|
|
d5659529d7 | ||
|
|
f6e06ab16e | ||
|
|
4245eaf1b2 | ||
|
|
c41bd702e2 | ||
|
|
3911145f87 | ||
|
|
350f5c4266 | ||
|
|
ed4056bc8b | ||
|
|
0d96fa21b7 | ||
|
|
0ee8d33d36 | ||
|
|
64cedec12b | ||
|
|
a16eaca42e | ||
|
|
366b3b1b75 | ||
|
|
33963c085a | ||
|
|
f3b18ef518 | ||
|
|
bca1b45fd4 | ||
|
|
3448d5ef03 | ||
|
|
0f5a6194ff | ||
|
|
9266faec59 | ||
|
|
94347c6dde | ||
|
|
7a07701be0 | ||
|
|
42519a4f75 | ||
|
|
c898c16430 | ||
|
|
318863bd18 | ||
|
|
ce94760d02 | ||
|
|
99759b5282 | ||
|
|
d9d8562ed9 | ||
|
|
dee55ea874 | ||
|
|
da413cc9e6 | ||
|
|
c72e93a13d | ||
|
|
21f910a3ca | ||
|
|
06e78e19d0 | ||
|
|
2c51edb4c2 | ||
|
|
da2c893721 | ||
|
|
e7ce693e50 | ||
|
|
29e8fe7904 | ||
|
|
baf3a6077e | ||
|
|
e030e7a32d | ||
|
|
125566ed75 | ||
|
|
c51cb67519 | ||
|
|
57f086b530 | ||
|
|
d1e17785b8 | ||
|
|
774328a1d8 | ||
|
|
175a0f48fa | ||
|
|
60b91ff032 | ||
|
|
2b003bc5b0 | ||
|
|
029a2fcab1 | ||
|
|
aa47f7fe4a | ||
|
|
a828113d9b | ||
|
|
ab4d509eda | ||
|
|
1296f32fa8 | ||
|
|
5aa1ec9876 | ||
|
|
f2cf1ab226 | ||
|
|
2d34c2a40b | ||
|
|
a7181e865c | ||
|
|
0a71b788b3 | ||
|
|
1ed3d84043 | ||
|
|
f472b75d0d | ||
|
|
072a9d1ccd | ||
|
|
8e26d1be68 | ||
|
|
c5871e9025 | ||
|
|
f26911b358 | ||
|
|
3a595b80b5 | ||
|
|
45760ddd41 | ||
|
|
447d068bf1 | ||
|
|
7e1642a4a3 | ||
|
|
7781267e78 | ||
|
|
f824fdcfed | ||
|
|
79dd441967 | ||
|
|
9dcf9c0785 | ||
|
|
83569462c6 | ||
|
|
947e169c3a | ||
|
|
e6b36c52d1 | ||
|
|
7fed3b84fa | ||
|
|
cbc941f193 | ||
|
|
a7ef0fb85f | ||
|
|
d10c36eaf5 | ||
|
|
e4c3a66753 | ||
|
|
f58f2cdc90 | ||
|
|
8f2676e91e | ||
|
|
89d397898e | ||
|
|
2dc32be9ee | ||
|
|
769220bd82 | ||
|
|
c75aa5dd52 | ||
|
|
88f1cfd8c4 | ||
|
|
108279c1aa | ||
|
|
8150490aac | ||
|
|
9497aa6362 | ||
|
|
3769da48a1 | ||
|
|
3c21fcd53a | ||
|
|
102ef2795d | ||
|
|
0befa253c2 | ||
|
|
87189cee58 | ||
|
|
3284f13fc6 | ||
|
|
70d219b120 | ||
|
|
a5ed3a08ea | ||
|
|
3665e7e999 | ||
|
|
9b583b23ae | ||
|
|
d602415e98 | ||
|
|
2c49a423e2 | ||
|
|
8a266e70c8 | ||
|
|
0df3bcd65d | ||
|
|
f5ecf5648e | ||
|
|
821f82fc3f | ||
|
|
92305fe628 | ||
|
|
17fe57b8a5 | ||
|
|
47433a51d3 | ||
|
|
e090827ced | ||
|
|
856e6a8b74 | ||
|
|
a813b4d40e | ||
|
|
98afc27442 | ||
|
|
9d4f28a395 | ||
|
|
50266d9a56 | ||
|
|
602a532cf2 | ||
|
|
b23d610c94 | ||
|
|
119c43d527 | ||
|
|
124feeb790 | ||
|
|
2c0e7e84da | ||
|
|
0d1e105343 | ||
|
|
f040c89ab3 | ||
|
|
5e9d8298ef | ||
|
|
7766ca2793 | ||
|
|
fb2f22f120 | ||
|
|
146fed3504 | ||
|
|
0b7a6e3375 | ||
|
|
f1fbdeaca0 | ||
|
|
ee7e37f6bc | ||
|
|
861f665ab3 | ||
|
|
456de23ad4 | ||
|
|
6d3578c17e | ||
|
|
57da7267e2 | ||
|
|
578b764406 | ||
|
|
f65a38a085 | ||
|
|
d187757bbc | ||
|
|
df83b24cf4 | ||
|
|
8e31ce0f5b | ||
|
|
4f69eb7963 | ||
|
|
4b9009216b | ||
|
|
895076c837 | ||
|
|
6e398ee64a | ||
|
|
c2177272b5 | ||
|
|
4877de5839 | ||
|
|
3cb9d10126 | ||
|
|
23c53cd9fb | ||
|
|
3471743a63 | ||
|
|
1b0b1809ef | ||
|
|
0833cffead | ||
|
|
a0a8ae88b7 | ||
|
|
e415cbeca4 | ||
|
|
4c15c93381 | ||
|
|
bd19822112 | ||
|
|
b87beb4a6e | ||
|
|
62a58facb2 | ||
|
|
5fa8178df7 | ||
|
|
d6043e7d1f | ||
|
|
8e432dfbb4 | ||
|
|
324ab3744b | ||
|
|
33ee4d0418 | ||
|
|
cbb104ccba | ||
|
|
0bda5461ae | ||
|
|
4096a03de1 | ||
|
|
fbee5f83df | ||
|
|
84a1b19850 | ||
|
|
1df5bd6d03 | ||
|
|
8bd4221a85 | ||
|
|
4d97ccdfb3 | ||
|
|
ca5e57ddbf | ||
|
|
ed6511799b | ||
|
|
9b1223ec15 | ||
|
|
97210ee67a | ||
|
|
308d1003ad | ||
|
|
ce9e193958 | ||
|
|
2a83afa8c1 | ||
|
|
8891fa328b | ||
|
|
a23c06c456 | ||
|
|
34e87121e1 | ||
|
|
6e5c20f442 | ||
|
|
8ac3bec451 | ||
|
|
5128d0f405 | ||
|
|
6ae2121391 | ||
|
|
749e0c5a47 | ||
|
|
7b55ef85e1 | ||
|
|
23ec35d1f9 | ||
|
|
ff089c2b9f | ||
|
|
1ab088718a | ||
|
|
0cb2e5857a | ||
|
|
7c5ae5d7ee | ||
|
|
6e2665d98d | ||
|
|
b3b4692237 | ||
|
|
55ff1d4999 | ||
|
|
511c839237 | ||
|
|
384cd6e5d9 | ||
|
|
6fd9dcc72e | ||
|
|
12ca06fe91 | ||
|
|
652f6c651b | ||
|
|
7dd808a772 | ||
|
|
945e55f8a7 | ||
|
|
866a30abe5 | ||
|
|
f0e33e1e4e | ||
|
|
8723ca38b4 | ||
|
|
efffca0026 | ||
|
|
6c5eb3035f | ||
|
|
0bf385f2ca | ||
|
|
9d5f51f513 | ||
|
|
a1448131d8 | ||
|
|
664cacc7ac | ||
|
|
1aec37dd05 | ||
|
|
3ff2c0840e | ||
|
|
5ca3c3ac52 | ||
|
|
714ee50965 | ||
|
|
ae1df3c5aa | ||
|
|
28afb39550 | ||
|
|
3466a8a024 | ||
|
|
90db3321f5 | ||
|
|
bf4e5ed7c2 | ||
|
|
d3b2c20c26 | ||
|
|
c3afabb4b1 | ||
|
|
18cc9790d1 | ||
|
|
d5ed3aa674 | ||
|
|
cfb9c9acfe | ||
|
|
67feef0b1d | ||
|
|
e0192ab5aa | ||
|
|
eae7c11c55 | ||
|
|
b8251e1ade | ||
|
|
2ff2b9f348 | ||
|
|
164e3f3343 | ||
|
|
4901a1bd87 | ||
|
|
cd194cca65 | ||
|
|
1748efbc18 | ||
|
|
4d60b40403 | ||
|
|
6e5a41879f | ||
|
|
c5f9eb54da | ||
|
|
aebbfeb061 | ||
|
|
a0a5a6b578 | ||
|
|
6bdafb85d7 | ||
|
|
0dd7debf5d | ||
|
|
962b9ee7af | ||
|
|
15bcc5df88 | ||
|
|
8495d834c8 | ||
|
|
7282399709 | ||
|
|
780bdd6e7e | ||
|
|
ad08e7c67f | ||
|
|
ff089964ca | ||
|
|
990191529a | ||
|
|
ce1bec12b4 | ||
|
|
703ea9af53 | ||
|
|
df0227ae1e | ||
|
|
486d12b6ac | ||
|
|
9c18375ab4 | ||
|
|
e1708aed68 | ||
|
|
a42d778b84 | ||
|
|
7692b59c7c | ||
|
|
82de9b36b3 | ||
|
|
49b3c8f65f | ||
|
|
90ad32d936 | ||
|
|
2509e91f1a | ||
|
|
80dc2219e4 | ||
|
|
931cb0fa7d | ||
|
|
4383f2ea90 | ||
|
|
add79dc242 | ||
|
|
b6e142f04c | ||
|
|
105f6c3041 | ||
|
|
4babbb65c1 | ||
|
|
878159f7ed | ||
|
|
c320386019 | ||
|
|
1e7a0159f0 | ||
|
|
bbf4f1d1d3 | ||
|
|
8cd1c69c76 | ||
|
|
2372a878ac | ||
|
|
9bec644997 | ||
|
|
650d38dff8 | ||
|
|
097277e397 | ||
|
|
82a4d5eedf | ||
|
|
2a00248812 | ||
|
|
9841c773cb | ||
|
|
b21a78ad14 | ||
|
|
7329fa597d | ||
|
|
f1d8f0ecb4 | ||
|
|
1d127f2364 | ||
|
|
b244405cc3 | ||
|
|
38adfe0ca6 | ||
|
|
6f0d798847 | ||
|
|
6e58bfd2b0 | ||
|
|
bc2e8d8ac4 | ||
|
|
7be654d47f | ||
|
|
bfe46e3604 | ||
|
|
008b597fc5 | ||
|
|
c4b4f2e3b1 | ||
|
|
76ee97301b | ||
|
|
289dc09eae | ||
|
|
f23f84f0f3 | ||
|
|
62259f3295 | ||
|
|
854a430a12 | ||
|
|
d27cdb5637 | ||
|
|
7d12d9ee90 | ||
|
|
d70a4ff347 | ||
|
|
49541558d1 | ||
|
|
7b00a1227c | ||
|
|
7800603c81 | ||
|
|
fca00c8116 | ||
|
|
00fa549e44 | ||
|
|
36181b6b87 | ||
|
|
88a95162e9 | ||
|
|
9c1d59a2c8 | ||
|
|
fdd894956a | ||
|
|
eb6dc0859d | ||
|
|
b99026bba2 | ||
|
|
6fa50eb8d5 | ||
|
|
e5046f15a9 | ||
|
|
09f1c066a0 | ||
|
|
ed2f0b34c9 | ||
|
|
c7f75861de | ||
|
|
ccd04dbc9d | ||
|
|
e71f8d2c10 | ||
|
|
d9b4c60239 | ||
|
|
103c1fca21 | ||
|
|
49559bf5fb | ||
|
|
26dceabf83 | ||
|
|
abe506182e | ||
|
|
582a0e2a38 | ||
|
|
2784ccf379 | ||
|
|
0ad1d578fe | ||
|
|
31fd1c9c68 | ||
|
|
9c961297a2 | ||
|
|
b920053349 | ||
|
|
0a5c764e4a | ||
|
|
b9a71c83ff | ||
|
|
3255f207d0 | ||
|
|
e3b4ca8862 | ||
|
|
6810793015 | ||
|
|
1feb3c2095 | ||
|
|
f2da6033d0 | ||
|
|
28bc212132 | ||
|
|
1a7c62eec6 | ||
|
|
de67dbacba | ||
|
|
57223a0f9a | ||
|
|
2ad0754b90 | ||
|
|
3ecb3af57b | ||
|
|
ec4f15f549 | ||
|
|
2458ea7b92 | ||
|
|
c5e6bedf11 | ||
|
|
8528e5a666 | ||
|
|
6ed232b3d9 | ||
|
|
f8aae8cbd1 | ||
|
|
00c2517045 | ||
|
|
99b043a929 | ||
|
|
5900e27e39 | ||
|
|
1b79d34907 | ||
|
|
fc52e29c92 | ||
|
|
df23b3c0fe | ||
|
|
5e25716c98 | ||
|
|
f70a10bc56 | ||
|
|
0ccb045f4e | ||
|
|
fa18d0d852 | ||
|
|
687c4342fb | ||
|
|
9459af46b8 | ||
|
|
fc5832747a | ||
|
|
01205d244b | ||
|
|
31a555255a | ||
|
|
fbb60c9493 | ||
|
|
9f953ef51c | ||
|
|
4871f1547c | ||
|
|
d6413529f4 | ||
|
|
724cf5a0da | ||
|
|
b6847907ca | ||
|
|
fb54a1aed7 | ||
|
|
9815318daf | ||
|
|
bc13b98111 | ||
|
|
238b70c121 | ||
|
|
dcd23bc0cb | ||
|
|
4694ea85fa | ||
|
|
bdb17743d7 | ||
|
|
8b76ff2461 | ||
|
|
ea5421002b | ||
|
|
76ac55917d | ||
|
|
5cfb2376c4 | ||
|
|
6b9d3fd80e | ||
|
|
a09752b62e | ||
|
|
3134e1d9f9 | ||
|
|
e94ecf2a0b | ||
|
|
1bd4d841a1 | ||
|
|
8e349f47a5 | ||
|
|
119c0a4231 | ||
|
|
f009f533e0 | ||
|
|
3ab0c1e509 | ||
|
|
e3d264e239 | ||
|
|
9bd60f8e8e | ||
|
|
bcec1d9637 | ||
|
|
1b431b1d20 | ||
|
|
23c2dcdfd4 | ||
|
|
dd415bf802 | ||
|
|
7bc0e9db7c | ||
|
|
ca10bb01db | ||
|
|
c0f14b7c33 | ||
|
|
b096f328fc | ||
|
|
88dbd43884 | ||
|
|
ade5e4d4b8 | ||
|
|
cb76b53a1b | ||
|
|
b9423f70d4 | ||
|
|
d61e18e6f3 | ||
|
|
ca4a725a79 | ||
|
|
17a18d5fea | ||
|
|
73be238ac4 | ||
|
|
796a034fec | ||
|
|
952b320975 | ||
|
|
be0f06ff0f | ||
|
|
6294ef2db2 | ||
|
|
78b5d505bd | ||
|
|
22afdffa15 | ||
|
|
d1056eddeb | ||
|
|
8655f4d85a | ||
|
|
f9deb54352 | ||
|
|
eae3c1b33a | ||
|
|
5c5f8aa741 | ||
|
|
ba68ac2e32 | ||
|
|
239fef281e | ||
|
|
c0880b647f | ||
|
|
039dc6a76b | ||
|
|
042a7625ad | ||
|
|
41827c478d | ||
|
|
9a73180c3c | ||
|
|
333ee85fdb | ||
|
|
fa8ca45b6a | ||
|
|
c2bae1aeb7 | ||
|
|
ada7a45fe6 | ||
|
|
2d09df55a9 | ||
|
|
ec2554537e | ||
|
|
f266dfadc6 | ||
|
|
99048eed61 | ||
|
|
fe63718b0c | ||
|
|
2c1200433c | ||
|
|
4a3252c929 | ||
|
|
5e052174ee | ||
|
|
cda0966105 | ||
|
|
c0a9716846 | ||
|
|
18c7395f9d | ||
|
|
d3da79f3dd | ||
|
|
5a215daca4 | ||
|
|
8527a3b3ef | ||
|
|
492dc1ba32 | ||
|
|
305a8ca802 | ||
|
|
6d2a35494f | ||
|
|
e76a08c73a | ||
|
|
a0fef0c20f | ||
|
|
605432ddd1 | ||
|
|
0c895071d8 | ||
|
|
79cd833ae6 | ||
|
|
0fee928e37 | ||
|
|
7f698336d7 | ||
|
|
cef04f192a | ||
|
|
4087c4c226 | ||
|
|
87bdcd2372 | ||
|
|
1dbcc0d7c8 | ||
|
|
4c93f01c64 | ||
|
|
0a18412da0 | ||
|
|
b67d16bdc4 | ||
|
|
976542d355 | ||
|
|
a2e5fda646 | ||
|
|
41b1b65d5f | ||
|
|
2ec2d1997f | ||
|
|
0bc65f3b72 | ||
|
|
9ed96b3599 | ||
|
|
b1cf9566f6 | ||
|
|
13ea97bd98 | ||
|
|
009a17a9ca | ||
|
|
ca38d9adb1 | ||
|
|
4e5a86e3db | ||
|
|
983ae4f8c2 | ||
|
|
7ce3531cc7 | ||
|
|
11f1a7fd1c | ||
|
|
47d7dd4d22 | ||
|
|
0d4f6bb5d9 | ||
|
|
b3a4e1976d | ||
|
|
8b3c0fd94e | ||
|
|
ab4a24d8ab | ||
|
|
7c93e9f834 | ||
|
|
90ea8cfebd | ||
|
|
7fc1866dac | ||
|
|
bb520ff424 | ||
|
|
f9d2e20cb9 | ||
|
|
6f13b67bf1 | ||
|
|
f37026a980 | ||
|
|
74f123265b | ||
|
|
ccfae65b01 | ||
|
|
bcfaef77c9 | ||
|
|
0b6243c0d1 | ||
|
|
472866d8ec | ||
|
|
471163f3d8 | ||
|
|
ab6106896d | ||
|
|
7ca624d04b | ||
|
|
bd3d18f43f | ||
|
|
21eb21b6dd | ||
|
|
a736fe7989 | ||
|
|
1970b7f249 | ||
|
|
24ad38e260 | ||
|
|
53bac83dff | ||
|
|
ab1578f667 | ||
|
|
4e70301b62 | ||
|
|
76ed2e9e11 | ||
|
|
aa1f081664 | ||
|
|
9a1d746170 | ||
|
|
ef87ce3507 | ||
|
|
2b9053eac4 | ||
|
|
116ef719be | ||
|
|
504406eb22 |
10
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<!-- Thank you for contributing to Mol* -->
|
||||
|
||||
# Description
|
||||
|
||||
|
||||
## Actions
|
||||
|
||||
- [ ] Added description of changes to the `[Unreleased]` section of `CHANGELOG.md`
|
||||
- [ ] Updated headers of modified files
|
||||
- [ ] Added my name to `package.json`'s `contributors`
|
||||
4
.github/workflows/node.yml
vendored
@@ -9,12 +9,12 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 17
|
||||
node-version: 16
|
||||
- run: npm ci
|
||||
- run: sudo apt-get install xvfb
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Test
|
||||
run: xvfb-run --auto-servernum npm run jest
|
||||
run: npm install --no-save "gl@^5.0.0" && xvfb-run --auto-servernum npm run jest
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
1
.vscode/extensions.json
vendored
@@ -6,7 +6,6 @@
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"firsttris.vscode-jest-runner",
|
||||
"msjsdiag.debugger-for-chrome",
|
||||
"slevesque.shader",
|
||||
"stpn.vscode-graphql",
|
||||
"wayou.vscode-todo-highlight"
|
||||
|
||||
430
CHANGELOG.md
@@ -6,6 +6,436 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v3.28.0] - 2022-12-20
|
||||
|
||||
- Show histogram in direct volume control point settings
|
||||
- Add `solidInterior` parameter to sphere/cylinder impostors
|
||||
- [Breaking] Tweak `ignoreHydrogens` non-polar handling (introduced in 3.27.0)
|
||||
- Add `meshes` and `volumes-and-segmentations` extensions
|
||||
- Fix missing support for info in `ParamDefinition.Converted`
|
||||
- Add support for multi-visual volume representations
|
||||
- Improve volume isosurface bounding-sphere
|
||||
- Add basic volume segmentation support to core
|
||||
- Add `Volume.Segment` model
|
||||
- Add `Segmentation` custom volume property
|
||||
- Add `SegmentRepresentation` representation
|
||||
- Add `volume-segment` color theme
|
||||
- Fix GPU marching cubes failing for large meshes with webgl2 (due to use of float16)
|
||||
|
||||
## [v3.27.0] - 2022-12-15
|
||||
|
||||
- Add an `includeTransparent` parameter to hide/show outlines of components that are transparent
|
||||
- Fix 'once' for animations of systems with many frames
|
||||
- Better guard against issue (black fringes) with bumpiness in impostors
|
||||
- Improve impostor shaders
|
||||
- Fix sphere near-clipping with orthographic projection
|
||||
- Fix cylinder near-clipping
|
||||
- Add interior cylinder caps
|
||||
- Add per-pixel object clipping
|
||||
- Fix `QualityAssessment` assignment bug for structures with different auth vs label sequence numbering
|
||||
- Refresh `ApplyActionControl`'s param definition when toggling expanded state
|
||||
- Fix `struct_conn` bond assignment for ions
|
||||
- Ability to show only polar hydrogens
|
||||
|
||||
## [v3.26.0] - 2022-12-04
|
||||
|
||||
- Support for ``powerPreference`` webgl attribute. Add ``PluginConfig.General.PowerPreference`` and ``power-preference`` Viewer GET param.
|
||||
- Excluded common protein caps `NME` and `ACE` from the ligand selection query
|
||||
- Add screen-space shadow post-processing effect
|
||||
- Add "Structure Molecular Surface" visual
|
||||
- Add `external-volume` theme (coloring of arbitrary geometries by user-selected volume)
|
||||
|
||||
## [v3.25.1] - 2022-11-20
|
||||
|
||||
- Fix edge-case in `Structure.eachUnitPair` with single-element units
|
||||
- Fix 'auto' structure-quality for coarse models
|
||||
|
||||
## [v3.25.0] - 2022-11-16
|
||||
|
||||
- Fix handling of gzipped assets (reverts #615)
|
||||
|
||||
## [v3.24.0] - 2022-11-13
|
||||
|
||||
- Make `PluginContext.initContainer` checkered canvas background optional
|
||||
- Store URL of downloaded assets to detect zip/gzip based on extension (#615)
|
||||
- Add optional `operator.key`; can be referenced in `IndexPairBonds`
|
||||
- Add overpaint/transparency/substance theme strength to representations
|
||||
- Fix viewport color for transparent background
|
||||
|
||||
## [v3.23.0] - 2022-10-19
|
||||
|
||||
- Add `PluginContext.initContainer/mount/unmount` methods; these should make it easier to reuse a plugin context with both custom and built-in UI
|
||||
- Add `PluginContext.canvas3dInitialized`
|
||||
- `createPluginUI` now resolves after the 3d canvas has been initialized
|
||||
- Change EM Volume Streaming default from `Whole Structure` to `Auto`
|
||||
|
||||
## [v3.22.0] - 2022-10-17
|
||||
|
||||
- Replace `VolumeIsosurfaceParams.pickingGranularity` param with `Volume.PickingGranuality`
|
||||
|
||||
## [v3.21.0] - 2022-10-17
|
||||
|
||||
- Add `VolumeIsosurfaceParams.pickingGranularity` param
|
||||
- Prevent component controls collapsing when option is selected
|
||||
|
||||
## [v3.20.0] - 2022-10-16
|
||||
|
||||
- [Breaking] Rename the ``model-index`` color theme to ``trajectory-index``
|
||||
- Add a new ``model-index`` color theme that uniquely colors each loaded model
|
||||
- Add the new ``model-index`` and ``structure-index`` color themes as an option for the carbon color in the ``element-symbol`` and ``ilustrative`` color themes
|
||||
- Add ``structure-index`` color theme that uniquely colors each root structure
|
||||
- Add ``nearest`` method to ``Lookup3D``
|
||||
- Add mipmap-based blur for skybox backgrounds
|
||||
|
||||
## [v3.19.0] - 2022-10-01
|
||||
|
||||
- Fix "empty textures" error on empty canvas
|
||||
- Optimize BinaryCIF integer packing encoder
|
||||
- Fix dual depth peeling when post-processing is off or when rendering direct-volumes
|
||||
- Add ``cameraClipping.minNear`` parameter
|
||||
- Fix black artifacts on specular highlights with transparent background
|
||||
|
||||
## [v3.18.0] - 2022-09-17
|
||||
|
||||
- Integration of Dual depth peeling - OIT method
|
||||
- Stereo camera improvements
|
||||
- Fix param updates not applied
|
||||
- Better param ranges and description
|
||||
- Add timer.mark for left/right camera
|
||||
|
||||
## [v3.17.0] - 2022-09-11
|
||||
|
||||
- [Fix] Clone ``Canvas3DParams`` when creating a ``Canvas3D`` instance to prevent shared state between multiple instances
|
||||
- Add ``includeResidueTest`` option to ``alignAndSuperposeWithSIFTSMapping``
|
||||
- Add ``parentDisplay`` param for interactions representation.
|
||||
- [Experimental] Add support for PyMOL, VMD, and Jmol atom expressions in selection scripts
|
||||
- Support for ``failIfMajorPerformanceCaveat`` webgl attribute. Add ``PluginConfig.General.AllowMajorPerformanceCaveat`` and ``allow-major-performance-caveat`` Viewer GET param.
|
||||
- Fix handling of PDB TER records (#549)
|
||||
- Add support for getting multiple loci from a representation (``.getAllLoci()``)
|
||||
- Add ``key`` property to intra- and inter-bonds for referencing source data
|
||||
- Fix click event triggered after move
|
||||
|
||||
## [v3.16.0] - 2022-08-25
|
||||
|
||||
- Support ``globalColorParams`` and ``globalSymmetryParams`` in common representation params
|
||||
- Support ``label`` parameter in ``Viewer.loadStructureFromUrl``
|
||||
- Fix ``ViewportHelpContent`` Mouse Controls section
|
||||
|
||||
## [v3.15.0] - 2022-08-23
|
||||
|
||||
- Fix wboit in Safari >=15 (add missing depth renderbuffer to wboit pass)
|
||||
- Add 'Around Camera' option to Volume streaming
|
||||
- Avoid queuing more than one update in Volume streaming
|
||||
|
||||
## [v3.14.0] - 2022-08-20
|
||||
|
||||
- Expose inter-bonds compute params in structure
|
||||
- Improve performance of inter/intra-bonds compute
|
||||
- Fix defaultAttribs handling in Canvas3DContext.fromCanvas
|
||||
- Confal pyramids extension improvements
|
||||
- Add custom labels to Confal pyramids
|
||||
- Improve naming of some internal types in Confal pyramids extension coordinate
|
||||
- Add example mmCIF file with categories necessary to display Confal pyramids
|
||||
- Change the lookup logic of NtC steps from residues
|
||||
- Add support for download of gzipped files
|
||||
- Don't filter IndexPairBonds by element-based rules in MOL/SDF and MOL2 (without symmetry) models
|
||||
- Fix Glycam Saccharide Names used by default
|
||||
- Fix GPU surfaces rendering in Safari with WebGL2
|
||||
- Add ``fov`` (Field of View) Canvas3D parameter
|
||||
- Add ``sceneRadiusFactor`` Canvas3D parameter
|
||||
- Add background pass (skybox, image, horizontal/radial gradient)
|
||||
- Set simple-settings presets via ``PluginConfig.Background.Styles``
|
||||
- Example presets in new backgrounds extension
|
||||
- Load skybox/image from URL or File (saved in session)
|
||||
- Opacity, saturation, lightness controls for skybox/image
|
||||
- Coverage (viewport or canvas) controls for image/gradient
|
||||
- [Breaking] ``AssetManager`` needs to be passed to various graphics related classes
|
||||
- Fix SSAO renderable initialization
|
||||
- Reduce number of webgl state changes
|
||||
- Add ``viewport`` and ``scissor`` to state object
|
||||
- Add ``hasOpaque`` to scene object
|
||||
- Handle edge cases where some renderables would not get (correctly) rendered
|
||||
- Fix text background rendering for opaque text
|
||||
- Fix helper scenes not shown when rendering directly to draw target
|
||||
- Fix ``CustomElementProperty`` coloring not working
|
||||
|
||||
## [v3.13.0] - 2022-07-24
|
||||
|
||||
- Fix: only update camera state if manualReset is off (#494)
|
||||
- Improve handling principal axes of points in a plane
|
||||
- Add 'material' annotation support for textures
|
||||
- More effort to avoid using ``flat`` qualifier in shaders: add ``dVaryingGroup``
|
||||
- Enable ``immediateUpdate`` for iso level in isosurface and volume streaming controls
|
||||
- Add support to download CCD from configurable URL
|
||||
|
||||
## [v3.12.1] - 2022-07-20
|
||||
|
||||
- Fix plugin behavior dispose logic to correctly unsubscribe observables.
|
||||
|
||||
## [v3.12.0] - 2022-07-17
|
||||
|
||||
- Add ``colorMarker`` option to Renderer. This disables the highlight and select marker at a shader level for faster rendering of large scenes in some cases.
|
||||
- Bind shared textures only once per pass, not for each render item
|
||||
- Fix missing 'material' annotation for some uniforms, causing unnecessary uniform updates
|
||||
- Remove use of ``isnan`` in impostor shaders, not needed and causing slowdown
|
||||
- Avoid using ``flat`` qualifier in shaders, causing slowdown
|
||||
- Improve CellPack's ``adjustStyle`` option (disable ``colorMarker``, set component options, enable marking w/o ghost)
|
||||
- Scan all entities when looking for ``struct_conn`` entries (fixes issue when the same ``label_asym_id`` is used in more than one entity)
|
||||
|
||||
## [v3.11.0] - 2022-07-04
|
||||
|
||||
- Add ``instanceGranularity`` option for marker, transparency, clipping, overpaint, substance data to save memory
|
||||
- CellPack extension tweaks
|
||||
- Use instancing to create DNA/RNA curves to save memory
|
||||
- Enable ``instanceGranularity`` by default
|
||||
- Add ``adjustStyle`` option to LoadCellPackModel action (stylized, no multi-sample, no far clipping, chain picking)
|
||||
- Structure Superposition now respects pivot's coordinate system
|
||||
|
||||
## [v3.10.2] - 2022-06-26
|
||||
|
||||
- Fix superfluous shader varying
|
||||
- Improve use of gl_VertexID when possible
|
||||
|
||||
## [v3.10.1] - 2022-06-26
|
||||
|
||||
- Fix groupCount when updating TextureMesh-based visuals
|
||||
|
||||
## [v3.10.0] - 2022-06-24
|
||||
|
||||
- Add support for Glycam saccharide names
|
||||
- Add ``PluginConfig.Viewport.ShowTrajectoryControls`` config option
|
||||
|
||||
## [v3.9.1] - 2022-06-19
|
||||
|
||||
- Fix missing ``super.componentWillUnmount()`` calls (@simeonborko)
|
||||
- Fix missing ``uGroupCount`` update for visuals
|
||||
- Fix missing aromatic bond display
|
||||
|
||||
## [v3.9.0] - 2022-05-30
|
||||
|
||||
- Improve picking by using drawbuffers (when available) to reduce number of drawcalls
|
||||
- GPU timing support
|
||||
- Add ``timing-mode`` Viewer GET param
|
||||
- Add support for webgl timer queries
|
||||
- Add timer marks around GPU render & compute operations
|
||||
- Volume Server CIF: Add check that a data block contains volume data before parsing
|
||||
- Fix ``Scene.clear`` not clearing primitives & volumes arrays (@JonStargaryen)
|
||||
- Fix rendering volumes when wboit is switched off and postprocessing is enabled
|
||||
|
||||
## [v3.8.2] - 2022-05-22
|
||||
|
||||
- Fix ``Scene.opacityAverage`` not taking xray shaded into account
|
||||
|
||||
## [v3.8.1] - 2022-05-14
|
||||
|
||||
- Fix issues with marking camera/handle helper (#433)
|
||||
- Fix issues with array uniforms when running with headless-gl
|
||||
- Fix Polymer Chain Instance coloring
|
||||
- Improve performance of scene marker/opacity average calculation
|
||||
|
||||
## [v3.8.0] - 2022-04-30
|
||||
|
||||
- Add support for outlines around transparent objects
|
||||
- Improve per-group transparency when wboit is switched off
|
||||
- Improve ``ColorTheme`` typing with ``ColorType`` generic.
|
||||
- Defaults to ``ColorTypeLocation``
|
||||
- Set when using ``ColorTypeDirect`` or ``ColorTypeGrid``
|
||||
- Fix case handling of ``struct_conf`` mmCIF enumeration field (#425)
|
||||
- Fix ``allowTransparentBackfaces`` for per-group transparency
|
||||
- Fix ``FormatRegistry.isApplicable`` returning true for unregistered formats
|
||||
- Fix: handle building of ``GridLookup3D`` with zero cell size
|
||||
- Fix ``ignoreLight`` for direct-volume rendering with webgl1
|
||||
- Fix (non-black) outlines when using transparent background
|
||||
|
||||
## [v3.7.0] - 2022-04-13
|
||||
|
||||
- Fix ``xrayShaded`` for texture-mesh geometries
|
||||
- [Breaking] Change ``allowTransparentBackfaces`` to ``transparentBackfaces`` with options ``off``, ``on``, ``opaque``. This was only added in 3.6.0, so allowing a breaking change here.
|
||||
- ``off``: don't show (default)
|
||||
- ``on``: show with transparency
|
||||
- ``opaque``: show fully opaque
|
||||
- Add option to disable file drop overlay.
|
||||
|
||||
## [v3.6.2] - 2022-04-05
|
||||
|
||||
- ModelServer ligand queries: fixes for alternate locations, additional atoms & UNL ligand
|
||||
- React 18 friendly ``useBehavior`` hook.
|
||||
|
||||
## [v3.6.1] - 2022-04-03
|
||||
|
||||
- Fix React18 related UI regressions.
|
||||
|
||||
## [v3.6.0] - 2022-04-03
|
||||
|
||||
- Check that model and coordinates have same element count when creating a trajectory
|
||||
- Fix aromatic rings assignment: do not mix flags and planarity test
|
||||
- Improve bonds assignment of coarse grained models: check for IndexPairBonds and exhaustive StructConn
|
||||
- Fix unit mapping in bondedAtomicPairs MolScript query
|
||||
- Improve pdb parsing: handle non unique atom and chain names (fixes #156)
|
||||
- Fix volume streaming for entries with multiple contour lists
|
||||
- Add ``allowTransparentBackfaces`` parameter to support double-sided rendering of transparent geometries
|
||||
- Fix handling of case insensitive mmCIF enumeration fields (including entity.type)
|
||||
- Fix ``disable-wboit`` Viewer GET param
|
||||
- Add support for React 18.
|
||||
- Used by importing ``createPluginUI`` from ``mol-plugin-ui/react18``;
|
||||
- In Mol* 4.0, React 18 will become the default option.
|
||||
|
||||
## [v3.5.0] - 2022-03-25
|
||||
|
||||
- Fix issues with bounding-sphere & color-smoothing (mostly for small geometries)
|
||||
- Support BCIF => CIF conversion in ``cif2bcif`` CLI tool
|
||||
|
||||
## [v3.4.0] - 2022-03-13
|
||||
|
||||
- Fix handling of mmcif with empty ``label_*`` fields
|
||||
- Improve saccharide detection (compare against list from CCD)
|
||||
- Fix legend label of hydrophobicity color theme
|
||||
- Add ``LoadTrajectory`` action
|
||||
- Add ``CustomImportControls`` to left panel
|
||||
- Add Zenodo import extension (load structures, trajectories, volumes, and zip files)
|
||||
- Fix loading of some compressed files within sessions
|
||||
- Fix wrong element assignment for atoms with Charmm ion names
|
||||
- Fix handling of empty symmetry cell data
|
||||
- Add support for ``trr`` and ``nctraj`` coordinates files
|
||||
- Add support for ``prmtop`` and ``top`` topology files
|
||||
|
||||
## [v3.3.1] - 2022-02-27
|
||||
|
||||
- Fix issue with unit boundary reuse (do at visual level instead)
|
||||
- Add option to ignore ions for inter-unit bond computation
|
||||
|
||||
## [v3.3.0] - 2022-02-27
|
||||
|
||||
- Fix parsing contour-level from emdb v3 header files
|
||||
- Fix invalid CSS (#376)
|
||||
- Fix "texture not renderable" & "texture not bound" warnings (#319)
|
||||
- Fix visual for bonds between two aromatic rings
|
||||
- Fix visual for delocalized bonds (parsed from mmcif and mol2)
|
||||
- Fix ring computation algorithm
|
||||
- Add ``UnitResonance`` property with info about delocalized triplets
|
||||
- Resolve marking in main renderer loop to improve overall performance
|
||||
- Use ``throttleTime`` instead of ``debounceTime`` in sequence viewer for better responsiveness
|
||||
- Change line geometry default ``scaleFactor`` to 2 (3 is too big after fixing line rendering)
|
||||
- Trajectory animation performance improvements
|
||||
- Reuse ``Model.CoarseGrained`` for coordinate trajectories
|
||||
- Avoid calculating ``InterUnitBonds`` when ``Structure.parent`` ones are empty
|
||||
- Reuse unit boundary if sphere has not changed too much
|
||||
- Don't show 'inter-bond' and 'element-cross' visuals in line representations of polymerAndLigand preset
|
||||
- Fix additional mononucleotides detected as polymer components
|
||||
- Fix and improve ``canRemap`` handling in ``IntraUnitBonds``
|
||||
- Reuse occlusion for secondary passes during multi-sampling
|
||||
- Check if marking passes are needed before doing them
|
||||
- Add ``resolutionScale`` parameter to allow trading quality of occlusion for performance
|
||||
|
||||
## [v3.2.0] - 2022-02-17
|
||||
|
||||
- Rename "best database mapping" to "SIFTS Mapping"
|
||||
- Add schema and export support for ``atom_site.pdbx_sifts_xref_*`` fields
|
||||
- Add schema export support for ``atom_site.pdbx_label_index`` field
|
||||
- Add `traceOnly` parameter to chain/UniProt-based structure alignment
|
||||
- Store ``IndexPairBonds`` as a dynamic property.
|
||||
|
||||
## [v3.1.0] - 2022-02-06
|
||||
|
||||
- Fix ``xrayShaded`` & ``ignoreLight`` params not working at the same time
|
||||
- Add ``ignoreLight`` to component params
|
||||
- Tweaks for cleaner default representation style
|
||||
- Cartoon: use ``nucleotide-ring`` instead of ``nucleotide-block``
|
||||
- Focus: use ``xrayShaded`` instead of opacity; adjust target size; don't show non-covalent interactions twice
|
||||
- Fix representation preset side effects (changing post-processing parameters, see #363)
|
||||
- Add Quick Styles panel (default, illustrative, stylized)
|
||||
- Fix exported structure missing secondary-structure categories (#364)
|
||||
- Fix volume streaming error message: distinguish between missing data and server error (#364)
|
||||
|
||||
## [v3.0.2] - 2022-01-30
|
||||
|
||||
- Fix color smoothing of elongated structures (by fixing ``Sphere.expand`` for spheres with highly directional extrema)
|
||||
- Fix entity label not displayed when multiple instances of the same entity are highlighted
|
||||
- Fix empty elements created in ``StructureElement.Loci.extendToAllInstances``
|
||||
- Measurement options tweaks (allow larger ``textSize``; make ``customText`` essential)
|
||||
- Fix visual visibility sync edge case when changing state snapshots
|
||||
|
||||
## [v3.0.1] - 2022-01-27
|
||||
|
||||
- Fix marking pass not working with ``transparentBackground``
|
||||
- Fix pdbe xray maps url not https
|
||||
- Fix entity-id color theme broken for non-IHM models
|
||||
- Improve/fix marking of ``InteractionsInterUnitVisual`` (mark when all contact-feature members are given)
|
||||
- Add missing "entity-id" and "enity-source" options for carbon coloring to "element-symbol" color theme
|
||||
- Fix VolumeServer/query CLI
|
||||
- Support automatic iso-value adjustment for VolumeServer data in ``Viewer.loadVolumeFromUrl``
|
||||
- Emit drag event whenever started within viewport (not only for non-empty loci)
|
||||
|
||||
## [v3.0.0] - 2022-01-23
|
||||
|
||||
- Assembly handling tweaks:
|
||||
- Do not include suffix for "identity assembly operators"
|
||||
- Do not include assembly-related categories to export if the structure was composed from an assembly
|
||||
- Special case for ``structAsymMap`` if Mol* asym id operator mapping is present
|
||||
- Support for opening ZIP files with multiple entries
|
||||
- Add Model Export extension
|
||||
- Bugfix: Automatically treat empty string as "non-present" value in BinaryCIF writer.
|
||||
- Fix coarse model support in entity-id color theme
|
||||
- Fix marking of carbohydrate visuals (whole chain could get marked instead of single residue)
|
||||
- Add custom colors to "element-symbol", "molecule-type", "residue-name", and "secondary-structure" themes
|
||||
- Support/bugfixes for ``atom_site.pdbx_sifts_xref`` categories
|
||||
- Improve/fix marking of ``InteractionsIntraUnitVisual`` (mark when all contact-feature members are given)
|
||||
|
||||
## [v3.0.0-dev.10] - 2022-01-17
|
||||
|
||||
- Fix ``getOperatorsForIndex``
|
||||
- Pass animation info (current frame & count) to state animations
|
||||
- Fix camera stutter for "camera spin" animation
|
||||
- Add formal charge parsing support for MOL/SDF files (thanks @ptourlas)
|
||||
- [Breaking] Cleaner looking ``MembraneOrientationVisuals`` defaults
|
||||
- [Breaking] Add rock animation to trackball controls
|
||||
- Add ``animate`` to ``TrackballControlsParams``, remove ``spin`` and ``spinSpeed``
|
||||
- Add ``animate`` to ``SimpleSettingsParams``, remove ``spin``
|
||||
- Add "camera rock" state animation
|
||||
- Add support for custom colors to "molecule-type" theme
|
||||
- [Breaking] Add style parameter to "illustrative" color theme
|
||||
- Defaults to "entity-id" style instead of "chain-id"
|
||||
- Add "illustrative" representation preset
|
||||
|
||||
## [v3.0.0-dev.9] - 2022-01-09
|
||||
|
||||
- Add PDBj as a ``pdb-provider`` option
|
||||
- Move Viewer APP to a separate file to allow use without importing light theme & index.html
|
||||
- Add symmetry support for mol2 files (only spacegroup setting 1)
|
||||
- Fix mol2 files element symbol assignment
|
||||
- Improve bond assignment from ``IndexPairBonds``
|
||||
- Add ``key`` field for mapping to source data
|
||||
- Fix assignment of bonds with unphysical length
|
||||
- Fix label/stats of single atom selection in multi-chain units
|
||||
|
||||
## [v3.0.0-dev.8] - 2021-12-31
|
||||
|
||||
- Add ``PluginFeatureDetection`` and disable WBOIT in Safari 15.
|
||||
- Add ``disable-wboit`` Viewer GET param
|
||||
- Add ``prefer-webgl1`` Viewer GET param
|
||||
- [Breaking] Refactor direct-volume rendering
|
||||
- Remove isosurface render-mode (use GPU MC instead)
|
||||
- Move coloring into theme (like for other geometries/renderables)
|
||||
- Add ``direct`` color type
|
||||
- Remove color from transfer-function (now only alpha)
|
||||
- Add direct-volume color theme support
|
||||
- Add volume-value color theme
|
||||
- [Breaking] Use size theme in molecular/gaussian surface & label representations
|
||||
- This is breaking because it was hardcoded to ``physical`` internally but the repr size theme default was ``uniform`` (now ``physical``)
|
||||
|
||||
## [v3.0.0-dev.7] - 2021-12-20
|
||||
|
||||
- Reduce number of created programs/shaders
|
||||
- Support specifying variants when creating graphics render-items
|
||||
- Change double-side shader param from define to uniform
|
||||
- Remove dMarkerType shader define (use uMarker as needed)
|
||||
- Support to ignore defines depending on the shader variant
|
||||
- Combine pickObject/pickInstance/pickGroup shader variants into one
|
||||
- Combine markingDepth/markingMask shader variants into one
|
||||
- Correctly set shader define flags for overpaint, transparency, substance, clipping
|
||||
- [Breaking] Add per-object clip rendering properties (variant/objects)
|
||||
- ``SimpleSettingsParams.clipping.variant/objects`` and ``RendererParams.clip`` were removed
|
||||
|
||||
## [v3.0.0-dev.6] - 2021-12-19
|
||||
|
||||
- Enable temporal multi-sampling by default
|
||||
|
||||
72
CITATION.cff
Normal file
@@ -0,0 +1,72 @@
|
||||
cff-version: 1.2.0
|
||||
title: >-
|
||||
Mol* library
|
||||
message: >-
|
||||
Please cite this software using the metadata from
|
||||
'preferred-citation'.
|
||||
authors:
|
||||
- given-names: Alexander S
|
||||
family-names: Rose
|
||||
orcid: 'https://orcid.org/0000-0002-0893-5551'
|
||||
- given-names: David
|
||||
family-names: Sehnal
|
||||
orcid: 'https://orcid.org/0000-0002-0682-3089'
|
||||
- given-names: Sebastian
|
||||
family-names: Bittrich
|
||||
orcid: 'https://orcid.org/0000-0003-3576-0387'
|
||||
- given-names: Áron Samuel
|
||||
family-names: Kovács
|
||||
- given-names: Ludovic
|
||||
family-names: Autin
|
||||
orcid: 'https://orcid.org/0000-0002-2197-191X'
|
||||
- given-names: Michal
|
||||
family-names: Malý
|
||||
- given-names: Jiří
|
||||
family-names: Černý
|
||||
- given-names: Panagiotis
|
||||
family-names: Tourlas
|
||||
type: software
|
||||
doi: 10.5281/zenodo.3947306
|
||||
preferred-citation:
|
||||
authors:
|
||||
- given-names: David
|
||||
family-names: Sehnal
|
||||
orcid: 'https://orcid.org/0000-0002-0682-3089'
|
||||
- given-names: Sebastian
|
||||
family-names: Bittrich
|
||||
orcid: 'https://orcid.org/0000-0003-3576-0387'
|
||||
- given-names: Mandar
|
||||
family-names: Deshpande
|
||||
orcid: 'https://orcid.org/0000-0002-9043-7665'
|
||||
- given-names: Radka
|
||||
family-names: Svobodová
|
||||
orcid: 'https://orcid.org/0000-0002-3840-8760'
|
||||
- given-names: Karel
|
||||
family-names: Berka
|
||||
orcid: 'https://orcid.org/0000-0001-9472-2589'
|
||||
- given-names: Václav
|
||||
family-names: Bazgier
|
||||
orcid: 'https://orcid.org/0000-0003-3393-3010'
|
||||
- given-names: Sameer
|
||||
family-names: Velankar
|
||||
orcid: 'https://orcid.org/0000-0002-8439-5964'
|
||||
- given-names: Stephen K
|
||||
family-names: Burley
|
||||
orcid: 'https://orcid.org/0000-0002-2487-9713'
|
||||
- given-names: Jaroslav
|
||||
family-names: Koča
|
||||
orcid: 'https://orcid.org/0000-0002-2780-4901'
|
||||
- given-names: Alexander S
|
||||
family-names: Rose
|
||||
orcid: 'https://orcid.org/0000-0002-0893-5551'
|
||||
title: >-
|
||||
Mol* Viewer: modern web app for 3D visualization
|
||||
and analysis of large biomolecular structures
|
||||
type: article
|
||||
doi: 10.1093/nar/gkab314
|
||||
journal: "Nucleic Acids Research"
|
||||
issue: W1
|
||||
volume: 49
|
||||
year: 2021
|
||||
month: 7
|
||||
pages: "W431–W437"
|
||||
19
README.md
@@ -11,6 +11,13 @@ When using Mol*, please cite:
|
||||
|
||||
David Sehnal, Sebastian Bittrich, Mandar Deshpande, Radka Svobodová, Karel Berka, Václav Bazgier, Sameer Velankar, Stephen K Burley, Jaroslav Koča, Alexander S Rose: [Mol* Viewer: modern web app for 3D visualization and analysis of large biomolecular structures](https://doi.org/10.1093/nar/gkab314), *Nucleic Acids Research*, 2021; https://doi.org/10.1093/nar/gkab314.
|
||||
|
||||
### Protein Data Bank Integrations
|
||||
|
||||
- The [pdbe-molstar](https://github.com/molstar/pdbe-molstar) library is the Mol* implementation used by EMBL-EBI data resources such as [PDBe](https://pdbe.org/), [PDBe-KB](https://pdbe-kb.org/) and [AlphaFold DB](https://alphafold.ebi.ac.uk/). This implementation can be used as a JS plugin and a Web component and supports property/attribute-based easy customisation. It provides helper methods to facilitate programmatic interactions between the web application and the 3D viewer. It also provides a superposition view for overlaying all the observed ligand molecules on representative protein conformations.
|
||||
|
||||
- [rcsb-molstar](https://github.com/molstar/rcsb-molstar) is the Mol* plugin used by [RCSB PDB](https://www.rcsb.org). The project provides additional presets for the visualization of structure alignments and structure motifs such as ligand binding sites. Furthermore, [rcsb-molstar](https://github.com/molstar/rcsb-molstar) allows to interactively add or hide of (parts of) chains, as seen in the [3D Protein Feature View](https://www.rcsb.org/3d-sequence/4hhb).
|
||||
|
||||
|
||||
## Project Structure Overview
|
||||
|
||||
The core of Mol* consists of these modules (see under `src/`):
|
||||
@@ -113,10 +120,13 @@ and navigate to `build/viewer`
|
||||
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-ions.js src/mol-model/structure/model/types/ions.ts
|
||||
|
||||
**Saccharide names**
|
||||
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-saccharides.js src/mol-model/structure/model/types/saccharides.ts
|
||||
|
||||
**GraphQL schemas**
|
||||
|
||||
node node_modules//@graphql-codegen/cli/bin -c src/extensions/rcsb/graphql/codegen.yml
|
||||
node node_modules/@graphql-codegen/cli/cjs/bin -c src/extensions/rcsb/graphql/codegen.yml
|
||||
|
||||
### Other scripts
|
||||
**Create chem comp bond table**
|
||||
@@ -131,7 +141,7 @@ and navigate to `build/viewer`
|
||||
|
||||
export NODE_PATH="lib"; node build/state-docs
|
||||
|
||||
**Convert any CIF to BinaryCIF**
|
||||
**Convert any CIF to BinaryCIF (or vice versa)**
|
||||
|
||||
node lib/commonjs/servers/model/preprocess -i file.cif -ob file.bcif
|
||||
|
||||
@@ -141,6 +151,11 @@ Or
|
||||
|
||||
node lib/commonjs/cli/cif2bcif
|
||||
|
||||
E.g.
|
||||
|
||||
node lib/commonjs/cli/cif2bcif src.cif out.bcif.gz
|
||||
node lib/commonjs/cli/cif2bcif src.bcif.gz out.cif
|
||||
|
||||
## Development
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -24,6 +24,11 @@ atom_site.auth_asym_id
|
||||
atom_site.auth_seq_id
|
||||
atom_site.pdbx_PDB_model_num
|
||||
atom_site.ihm_model_id
|
||||
atom_site.pdbx_label_index
|
||||
atom_site.pdbx_sifts_xref_db_name
|
||||
atom_site.pdbx_sifts_xref_db_acc
|
||||
atom_site.pdbx_sifts_xref_db_num
|
||||
atom_site.pdbx_sifts_xref_db_res
|
||||
|
||||
atom_site_anisotrop.id
|
||||
atom_site_anisotrop.U
|
||||
|
||||
|
44
docs/file-formats.md
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
Support file formats and their extensions.
|
||||
|
||||
## Structure
|
||||
|
||||
- MMCIF and CIFCORE (mmCIF and coreCIF schemas): cif, bcif, mmcif, mcif
|
||||
- GRO: gro
|
||||
- MOL: mol
|
||||
- MOL2: mol2
|
||||
- PDB/PDBQT: pdb, ent, pdbqt
|
||||
- SDF: sdf, sd
|
||||
- XYZ: xyz
|
||||
|
||||
|
||||
## Topology
|
||||
|
||||
Need to be loaded together with Coordinates.
|
||||
|
||||
- PRMTOP: prmtop, parm7
|
||||
- PSF: psf
|
||||
- TOP: top
|
||||
|
||||
## Coordinates
|
||||
|
||||
Need to be loaded together with a Structure or Topology.
|
||||
|
||||
- DCD: dcd
|
||||
- NCTRAJ: nc, nctraj
|
||||
- TRR: trr
|
||||
- XTC: xtc
|
||||
|
||||
|
||||
## Volume
|
||||
|
||||
- CCP4/MRC/MAP: ccp4, mrc, map
|
||||
- CUBE (may include a Structure): cub, cube
|
||||
- DSN6/BRIX: dsn6, brix
|
||||
- DX and DXBIN: dx, dxbin
|
||||
- DSCIF (DensityServer CIF schema): cif, bcif
|
||||
|
||||
|
||||
## Shape
|
||||
|
||||
- PLY
|
||||
@@ -13,6 +13,7 @@
|
||||
* DZ has C1 instead of N1 (e.g. 6I4N)
|
||||
* DP has N5 instead of C5 and C7 instead of N7 (e.g. 6I4N)
|
||||
* Beta & Gamma peptides (e.g. 1GAC, 6PQF)
|
||||
* Helices of D-amino acids (e.g. 7QDI)
|
||||
* Mixed (heterogeneous) all-atom/trace-only RNA model (1JGQ)
|
||||
* Polymers with residues with missing trace atoms (e.g. 2QFJ)
|
||||
* Modified RNA bases (1y26, 5L4O)
|
||||
@@ -34,6 +35,14 @@
|
||||
* ACE (many, e.g. 5AGU, 1E1X)
|
||||
* ACY in 7ABY
|
||||
* NH2 (many, e.g. 6Y13)
|
||||
* Ligands with many rings
|
||||
* STU (e.g. 1U59) - many fused rings
|
||||
* HT (e.g. 127D) - rings connected by a single bond
|
||||
* J2C (e.g. 7EFJ) - rings connected by a single atom
|
||||
* RBF (e.g. 7QF2) - three linearly fused rings
|
||||
* TA1 (e.g. 1JFF) - many fused rings (incl. a 8-member rings)
|
||||
* BPA (e.g. 1JDG) - many fused rings
|
||||
* CLR (e.g. 3GKI) - four fused rings
|
||||
|
||||
Assembly symmetries
|
||||
* 5M30 (Assembly 1, C3 local and pseudo)
|
||||
|
||||
1694
examples/1bna_confal_pyramids.cif
Normal file
28000
examples/long_animation.sdf
Normal file
45933
package-lock.json
generated
113
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "3.0.0-dev.6",
|
||||
"version": "3.28.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -13,14 +13,14 @@
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint-fix": "eslint . --fix",
|
||||
"test": "npm run lint && jest",
|
||||
"test": "npm install --no-save \"gl@^5.0.0\" && npm run lint && jest",
|
||||
"jest": "jest",
|
||||
"build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
|
||||
"clean": "node ./scripts/clean.js",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer",
|
||||
"build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"",
|
||||
"build-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/",
|
||||
"build-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/",
|
||||
"build-webpack": "webpack --mode production --config ./webpack.config.production.js",
|
||||
"build-webpack-viewer": "webpack --mode production --config ./webpack.config.viewer.js",
|
||||
"watch": "concurrently -c \"green,green,gray,gray\" --names \"tsc,srv,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-servers\" \"npm:watch-extra\" \"npm:watch-webpack\"",
|
||||
@@ -28,7 +28,7 @@
|
||||
"watch-viewer-debug": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer-debug\"",
|
||||
"watch-tsc": "tsc --watch --incremental",
|
||||
"watch-servers": "tsc --build tsconfig.commonjs.json --watch --incremental",
|
||||
"watch-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/ --watch",
|
||||
"watch-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/ --watch",
|
||||
"watch-webpack": "webpack -w --mode development --stats minimal",
|
||||
"watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js",
|
||||
"watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js",
|
||||
@@ -75,7 +75,9 @@
|
||||
"node_modules",
|
||||
"lib"
|
||||
],
|
||||
"testURL": "http://localhost/",
|
||||
"testEnvironmentOptions": {
|
||||
"url": "http://localhost/"
|
||||
},
|
||||
"testRegex": "\\.spec\\.ts$"
|
||||
},
|
||||
"author": "Mol* Contributors",
|
||||
@@ -86,77 +88,82 @@
|
||||
"Áron Samuel Kovács <aron.kovacs@mail.muni.cz>",
|
||||
"Ludovic Autin <autin@scripps.edu>",
|
||||
"Michal Malý <michal.maly@ibt.cas.cz>",
|
||||
"Jiří Černý <jiri.cerny@ibt.cas.cz>"
|
||||
"Jiří Černý <jiri.cerny@ibt.cas.cz>",
|
||||
"Panagiotis Tourlas <panagiot_tourlov@hotmail.com>",
|
||||
"Adam Midlik <midlik@gmail.com>",
|
||||
"Koya Sakuma <koya.sakuma.work@gmail.com>",
|
||||
"Gianluca Tomasello <giagitom@gmail.com>",
|
||||
"Jason Pattle <jpattle@exscientia.co.uk>",
|
||||
"David Williams <dwilliams@nobiastx.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/add": "^3.1.0",
|
||||
"@graphql-codegen/cli": "^2.3.0",
|
||||
"@graphql-codegen/time": "^3.1.0",
|
||||
"@graphql-codegen/typescript": "^2.4.1",
|
||||
"@graphql-codegen/typescript-graphql-files-modules": "^2.1.0",
|
||||
"@graphql-codegen/typescript-graphql-request": "^4.3.1",
|
||||
"@graphql-codegen/typescript-operations": "^2.2.1",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/gl": "^4.1.0",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.5.0",
|
||||
"@typescript-eslint/parser": "^5.5.0",
|
||||
"@graphql-codegen/add": "^3.2.3",
|
||||
"@graphql-codegen/cli": "^2.16.1",
|
||||
"@graphql-codegen/time": "^3.2.3",
|
||||
"@graphql-codegen/typescript": "^2.8.5",
|
||||
"@graphql-codegen/typescript-graphql-files-modules": "^2.2.1",
|
||||
"@graphql-codegen/typescript-graphql-request": "^4.5.8",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.10",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/gl": "^6.0.2",
|
||||
"@types/jest": "^29.2.4",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
||||
"@typescript-eslint/parser": "^5.47.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^6.4.0",
|
||||
"cpx2": "^4.0.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"cpx2": "^4.2.0",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"eslint": "^8.3.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "^8.30.0",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"graphql": "^15.7.2",
|
||||
"http-server": "^14.0.0",
|
||||
"jest": "^27.3.1",
|
||||
"mini-css-extract-plugin": "^2.4.5",
|
||||
"fs-extra": "^11.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^29.3.1",
|
||||
"mini-css-extract-plugin": "^2.7.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass": "^1.43.5",
|
||||
"sass-loader": "^12.3.0",
|
||||
"simple-git": "^2.47.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.57.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
"simple-git": "^3.15.1",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^27.0.7",
|
||||
"typescript": "^4.5.2",
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-cli": "^4.9.1"
|
||||
"ts-jest": "^29.0.3",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.10",
|
||||
"@types/benchmark": "^2.1.1",
|
||||
"@types/benchmark": "^2.1.2",
|
||||
"@types/compression": "1.7.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/node": "^16.11.10",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/node": "^16.18.10",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/swagger-ui-dist": "3.30.1",
|
||||
"argparse": "^2.0.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"body-parser": "^1.20.1",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.18.2",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^9.0.7",
|
||||
"immutable": "^3.8.2",
|
||||
"node-fetch": "^2.6.2",
|
||||
"rxjs": "^7.4.0",
|
||||
"swagger-ui-dist": "^4.1.1",
|
||||
"tslib": "^2.3.1",
|
||||
"immer": "^9.0.16",
|
||||
"immutable": "^4.1.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"rxjs": "^7.8.0",
|
||||
"swagger-ui-dist": "^4.15.5",
|
||||
"tslib": "^2.4.1",
|
||||
"util.promisify": "^1.1.1",
|
||||
"xhr2": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"gl": "^4.9.2"
|
||||
"react": "^18.1.0 || ^17.0.2 || ^16.14.0",
|
||||
"react-dom": "^18.1.0 || ^17.0.2 || ^16.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { PluginStateObject as PSO, PluginStateTransform } from '../../mol-plugin-state/objects';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { createPluginUI } from '../../mol-plugin-ui/react18';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
@@ -58,20 +58,22 @@ class Viewer {
|
||||
}
|
||||
|
||||
static async create(elementOrId: string | HTMLElement, colors = [Color(0x992211), Color(0xDDDDDD)], showButtons = true) {
|
||||
const o = { ...DefaultViewerOptions, ...{
|
||||
layoutIsExpanded: false,
|
||||
layoutShowControls: false,
|
||||
layoutShowRemoteState: false,
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: false,
|
||||
layoutShowLeftPanel: true,
|
||||
const o = {
|
||||
...DefaultViewerOptions, ...{
|
||||
layoutIsExpanded: false,
|
||||
layoutShowControls: false,
|
||||
layoutShowRemoteState: false,
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: false,
|
||||
layoutShowLeftPanel: true,
|
||||
|
||||
viewportShowExpand: true,
|
||||
viewportShowControls: false,
|
||||
viewportShowSettings: false,
|
||||
viewportShowSelectionMode: false,
|
||||
viewportShowAnimation: false,
|
||||
} };
|
||||
viewportShowExpand: true,
|
||||
viewportShowControls: false,
|
||||
viewportShowSettings: false,
|
||||
viewportShowSelectionMode: false,
|
||||
viewportShowAnimation: false,
|
||||
}
|
||||
};
|
||||
const defaultSpec = DefaultPluginUISpec();
|
||||
|
||||
const spec: PluginUISpec = {
|
||||
@@ -135,18 +137,16 @@ class Viewer {
|
||||
}
|
||||
};
|
||||
|
||||
plugin.behaviors.canvas3d.initialized.subscribe(v => {
|
||||
if (v) {
|
||||
PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
|
||||
renderer: {
|
||||
...plugin.canvas3d!.props.renderer,
|
||||
backgroundColor: ColorNames.white,
|
||||
},
|
||||
camera: {
|
||||
...plugin.canvas3d!.props.camera,
|
||||
helper: { axes: { name: 'off', params: {} } }
|
||||
}
|
||||
} });
|
||||
PluginCommands.Canvas3D.SetSettings(plugin, {
|
||||
settings: {
|
||||
renderer: {
|
||||
...plugin.canvas3d!.props.renderer,
|
||||
backgroundColor: ColorNames.white,
|
||||
},
|
||||
camera: {
|
||||
...plugin.canvas3d!.props.camera,
|
||||
helper: { axes: { name: 'off', params: {} } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,7 +166,7 @@ class Viewer {
|
||||
structures.push({ ref: structureProperties?.ref || structure.ref });
|
||||
}
|
||||
|
||||
// remove current structuresfrom hierarchy as they will be merged
|
||||
// remove current structures from hierarchy as they will be merged
|
||||
// TODO only works with using loadStructuresFromUrlsAndMerge once
|
||||
// need some more API metho to work with the hierarchy
|
||||
this.plugin.managers.structure.hierarchy.updateCurrent(this.plugin.managers.structure.hierarchy.current.structures, 'remove');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -31,7 +31,8 @@ function shinyStyle(plugin: PluginContext) {
|
||||
postprocessing: {
|
||||
...plugin.canvas3d!.props.postprocessing,
|
||||
occlusion: { name: 'off', params: {} },
|
||||
outline: { name: 'off', params: {} }
|
||||
shadow: { name: 'off', params: {} },
|
||||
outline: { name: 'off', params: {} },
|
||||
}
|
||||
} });
|
||||
}
|
||||
@@ -44,16 +45,19 @@ function occlusionStyle(plugin: PluginContext) {
|
||||
postprocessing: {
|
||||
...plugin.canvas3d!.props.postprocessing,
|
||||
occlusion: { name: 'on', params: {
|
||||
samples: 64,
|
||||
radius: 8,
|
||||
bias: 1.0,
|
||||
blurKernelSize: 13
|
||||
bias: 0.8,
|
||||
blurKernelSize: 15,
|
||||
radius: 5,
|
||||
samples: 32,
|
||||
resolutionScale: 1,
|
||||
} },
|
||||
outline: { name: 'on', params: {
|
||||
scale: 1.0,
|
||||
threshold: 0.33,
|
||||
color: Color(0x0000),
|
||||
} }
|
||||
includeTransparent: true,
|
||||
} },
|
||||
shadow: { name: 'off', params: {} },
|
||||
}
|
||||
} });
|
||||
}
|
||||
@@ -201,14 +205,14 @@ const InteractionsPreset = StructureRepresentationPresetProvider({
|
||||
const components = {
|
||||
ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
|
||||
surroundings: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandSurroundings, `surroundings`),
|
||||
interactions: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandPlusSurroundings, `interactions`)
|
||||
interactions: await presetStaticComponent(plugin, structureCell, 'ligand'),
|
||||
};
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.3 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
|
||||
ballAndStick: builder.buildRepresentation(update, components.surroundings, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ball-and-stick' }),
|
||||
interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams, material: CustomMaterial }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
|
||||
interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams, material: CustomMaterial, includeParent: true, parentDisplay: 'between' }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
|
||||
label: builder.buildRepresentation(update, components.surroundings, { type: 'label', typeParams: { ...typeParams, material: CustomMaterial, background: false, borderWidth: 0.1 }, color: 'uniform', colorParams: { value: Color(0x000000) } }, { tag: 'label' }),
|
||||
};
|
||||
|
||||
|
||||
510
src/apps/viewer/app.ts
Normal file
@@ -0,0 +1,510 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
|
||||
import { CellPack } from '../../extensions/cellpack';
|
||||
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
|
||||
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
|
||||
import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
|
||||
import { GeometryExport } from '../../extensions/geo-export';
|
||||
import { MAQualityAssessment } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { ModelExport } from '../../extensions/model-export';
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
|
||||
import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
|
||||
import { ZenodoImport } from '../../extensions/zenodo';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
|
||||
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
|
||||
import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
|
||||
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
|
||||
import { BuiltInTopologyFormat } from '../../mol-plugin-state/formats/topology';
|
||||
import { BuiltInCoordinatesFormat } from '../../mol-plugin-state/formats/coordinates';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model';
|
||||
import { createPluginUI } from '../../mol-plugin-ui/react18';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { StateObjectRef, StateObjectSelector } from '../../mol-state';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import '../../mol-util/polyfill';
|
||||
import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
|
||||
import { Backgrounds } from '../../extensions/backgrounds';
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
|
||||
|
||||
const CustomFormats = [
|
||||
['g3d', G3dProvider] as const
|
||||
];
|
||||
|
||||
const Extensions = {
|
||||
'volseg': PluginSpec.Behavior(Volseg),
|
||||
'backgrounds': PluginSpec.Behavior(Backgrounds),
|
||||
'cellpack': PluginSpec.Behavior(CellPack),
|
||||
'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
|
||||
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
|
||||
'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
|
||||
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
|
||||
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
|
||||
'g3d': PluginSpec.Behavior(G3DFormat),
|
||||
'model-export': PluginSpec.Behavior(ModelExport),
|
||||
'mp4-export': PluginSpec.Behavior(Mp4Export),
|
||||
'geo-export': PluginSpec.Behavior(GeometryExport),
|
||||
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
|
||||
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
|
||||
};
|
||||
|
||||
const DefaultViewerOptions = {
|
||||
customFormats: CustomFormats as [string, DataFormatProvider][],
|
||||
extensions: ObjectKeys(Extensions),
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: true,
|
||||
layoutShowRemoteState: true,
|
||||
layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: true,
|
||||
layoutShowLeftPanel: true,
|
||||
collapseLeftPanel: false,
|
||||
collapseRightPanel: false,
|
||||
disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
|
||||
pixelScale: PluginConfig.General.PixelScale.defaultValue,
|
||||
pickScale: PluginConfig.General.PickScale.defaultValue,
|
||||
pickPadding: PluginConfig.General.PickPadding.defaultValue,
|
||||
enableWboit: PluginConfig.General.EnableWboit.defaultValue,
|
||||
enableDpoit: PluginConfig.General.EnableDpoit.defaultValue,
|
||||
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
|
||||
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
|
||||
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
|
||||
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
|
||||
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
|
||||
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
|
||||
viewportShowTrajectoryControls: PluginConfig.Viewport.ShowTrajectoryControls.defaultValue,
|
||||
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
|
||||
volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
|
||||
volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
|
||||
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
|
||||
volumesAndSegmentationsDefaultServer: VolsegVolumeServerConfig.DefaultServer.defaultValue,
|
||||
};
|
||||
type ViewerOptions = typeof DefaultViewerOptions;
|
||||
|
||||
export class Viewer {
|
||||
constructor(public plugin: PluginUIContext) {
|
||||
}
|
||||
|
||||
static async create(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
|
||||
const definedOptions = {} as any;
|
||||
// filter for defined properies only so the default values
|
||||
// are property applied
|
||||
for (const p of Object.keys(options) as (keyof ViewerOptions)[]) {
|
||||
if (options[p] !== void 0) definedOptions[p] = options[p];
|
||||
}
|
||||
|
||||
const o: ViewerOptions = { ...DefaultViewerOptions, ...definedOptions };
|
||||
const defaultSpec = DefaultPluginUISpec();
|
||||
|
||||
const spec: PluginUISpec = {
|
||||
actions: defaultSpec.actions,
|
||||
behaviors: [
|
||||
...defaultSpec.behaviors,
|
||||
...o.extensions.map(e => Extensions[e]),
|
||||
],
|
||||
animations: [...defaultSpec.animations || []],
|
||||
customParamEditors: defaultSpec.customParamEditors,
|
||||
customFormats: o?.customFormats,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: o.layoutIsExpanded,
|
||||
showControls: o.layoutShowControls,
|
||||
controlsDisplay: o.layoutControlsDisplay,
|
||||
regionState: {
|
||||
bottom: 'full',
|
||||
left: o.collapseLeftPanel ? 'collapsed' : 'full',
|
||||
right: o.collapseRightPanel ? 'hidden' : 'full',
|
||||
top: 'full',
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
...defaultSpec.components,
|
||||
controls: {
|
||||
...defaultSpec.components?.controls,
|
||||
top: o.layoutShowSequence ? undefined : 'none',
|
||||
bottom: o.layoutShowLog ? undefined : 'none',
|
||||
left: o.layoutShowLeftPanel ? undefined : 'none',
|
||||
},
|
||||
remoteState: o.layoutShowRemoteState ? 'default' : 'none',
|
||||
},
|
||||
config: [
|
||||
[PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
|
||||
[PluginConfig.General.PixelScale, o.pixelScale],
|
||||
[PluginConfig.General.PickScale, o.pickScale],
|
||||
[PluginConfig.General.PickPadding, o.pickPadding],
|
||||
[PluginConfig.General.EnableWboit, o.enableWboit],
|
||||
[PluginConfig.General.EnableDpoit, o.enableDpoit],
|
||||
[PluginConfig.General.PreferWebGl1, o.preferWebgl1],
|
||||
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
|
||||
[PluginConfig.General.PowerPreference, o.powerPreference],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
|
||||
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
|
||||
[PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
|
||||
[PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation],
|
||||
[PluginConfig.Viewport.ShowTrajectoryControls, o.viewportShowTrajectoryControls],
|
||||
[PluginConfig.State.DefaultServer, o.pluginStateServer],
|
||||
[PluginConfig.State.CurrentServer, o.pluginStateServer],
|
||||
[PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer],
|
||||
[PluginConfig.VolumeStreaming.Enabled, !o.volumeStreamingDisabled],
|
||||
[PluginConfig.Download.DefaultPdbProvider, o.pdbProvider],
|
||||
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
|
||||
[PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
|
||||
[PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
|
||||
[VolsegVolumeServerConfig.DefaultServer, o.volumesAndSegmentationsDefaultServer],
|
||||
]
|
||||
};
|
||||
|
||||
const element = typeof elementOrId === 'string'
|
||||
? document.getElementById(elementOrId)
|
||||
: elementOrId;
|
||||
if (!element) throw new Error(`Could not get element with id '${elementOrId}'`);
|
||||
const plugin = await createPluginUI(element, spec, {
|
||||
onBeforeUIRender: plugin => {
|
||||
// the preset needs to be added before the UI renders otherwise
|
||||
// "Download Structure" wont be able to pick it up
|
||||
plugin.builders.structure.representation.registerPreset(ViewerAutoPreset);
|
||||
}
|
||||
});
|
||||
return new Viewer(plugin);
|
||||
}
|
||||
|
||||
setRemoteSnapshot(id: string) {
|
||||
const url = `${this.plugin.config.get(PluginConfig.State.CurrentServer)}/get/${id}`;
|
||||
return PluginCommands.State.Snapshots.Fetch(this.plugin, { url });
|
||||
}
|
||||
|
||||
loadSnapshotFromUrl(url: string, type: PluginState.SnapshotType) {
|
||||
return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
|
||||
}
|
||||
|
||||
loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions & { label?: string }) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'url',
|
||||
params: {
|
||||
url: Asset.Url(url),
|
||||
format: format as any,
|
||||
isBinary,
|
||||
label: options?.label,
|
||||
options: { ...params.source.params.options, representationParams: options?.representationParams as any },
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async loadAllModelsOrAssemblyFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
|
||||
const trajectory = await plugin.builders.structure.parseTrajectory(data, format);
|
||||
|
||||
await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'all-models', { useDefaultIfSingleModel: true, representationPresetParams: options?.representationParams });
|
||||
}
|
||||
|
||||
async loadStructureFromData(data: string | number[], format: BuiltInTrajectoryFormat, options?: { dataLabel?: string }) {
|
||||
const _data = await this.plugin.builders.data.rawData({ data, label: options?.dataLabel });
|
||||
const trajectory = await this.plugin.builders.structure.parseTrajectory(_data, format);
|
||||
await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
|
||||
}
|
||||
|
||||
loadPdb(pdb: string, options?: LoadStructureOptions) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
const provider = this.plugin.config.get(PluginConfig.Download.DefaultPdbProvider)!;
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'pdb' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: pdb,
|
||||
server: {
|
||||
name: provider,
|
||||
params: PdbDownloadProvider[provider].defaultValue as any
|
||||
}
|
||||
},
|
||||
options: { ...params.source.params.options, representationParams: options?.representationParams as any },
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadPdbDev(pdbDev: string) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'pdb-dev' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: pdbDev,
|
||||
encoding: 'bcif',
|
||||
},
|
||||
options: params.source.params.options,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadEmdb(emdb: string, options?: { detail?: number }) {
|
||||
const provider = this.plugin.config.get(PluginConfig.Download.DefaultEmdbProvider)!;
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, {
|
||||
source: {
|
||||
name: 'pdb-emd-ds' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: emdb,
|
||||
server: provider,
|
||||
},
|
||||
detail: options?.detail ?? 3,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadAlphaFoldDb(afdb: string) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'alphafolddb' as const,
|
||||
params: {
|
||||
id: afdb,
|
||||
options: {
|
||||
...params.source.params.options,
|
||||
representation: 'preset-structure-representation-ma-quality-assessment-plddt'
|
||||
},
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadModelArchive(id: string) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'modelarchive' as const,
|
||||
params: {
|
||||
id,
|
||||
options: params.source.params.options,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @example Load X-ray density from volume server
|
||||
viewer.loadVolumeFromUrl({
|
||||
url: 'https://www.ebi.ac.uk/pdbe/densities/x-ray/1tqn/cell?detail=3',
|
||||
format: 'dscif',
|
||||
isBinary: true
|
||||
}, [{
|
||||
type: 'relative',
|
||||
value: 1.5,
|
||||
color: 0x3362B2
|
||||
}, {
|
||||
type: 'relative',
|
||||
value: 3,
|
||||
color: 0x33BB33,
|
||||
volumeIndex: 1
|
||||
}, {
|
||||
type: 'relative',
|
||||
value: -3,
|
||||
color: 0xBB3333,
|
||||
volumeIndex: 1
|
||||
}], {
|
||||
entryId: ['2FO-FC', 'FO-FC'],
|
||||
isLazy: true
|
||||
});
|
||||
* *********************
|
||||
* @example Load EM density from volume server
|
||||
viewer.loadVolumeFromUrl({
|
||||
url: 'https://maps.rcsb.org/em/emd-30210/cell?detail=6',
|
||||
format: 'dscif',
|
||||
isBinary: true
|
||||
}, [{
|
||||
type: 'relative',
|
||||
value: 1,
|
||||
color: 0x3377aa
|
||||
}], {
|
||||
entryId: 'EMD-30210',
|
||||
isLazy: true
|
||||
});
|
||||
*/
|
||||
async loadVolumeFromUrl({ url, format, isBinary }: { url: string, format: BuildInVolumeFormat, isBinary: boolean }, isovalues: VolumeIsovalueInfo[], options?: { entryId?: string | string[], isLazy?: boolean }) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
if (!plugin.dataFormats.get(format)) {
|
||||
throw new Error(`Unknown density format: ${format}`);
|
||||
}
|
||||
|
||||
if (options?.isLazy) {
|
||||
const update = this.plugin.build();
|
||||
update.toRoot().apply(StateTransforms.Data.LazyVolume, {
|
||||
url,
|
||||
format,
|
||||
entryId: options?.entryId,
|
||||
isBinary,
|
||||
isovalues: isovalues.map(v => ({ alpha: 1, volumeIndex: 0, ...v }))
|
||||
});
|
||||
return update.commit();
|
||||
}
|
||||
|
||||
return plugin.dataTransaction(async () => {
|
||||
const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
|
||||
|
||||
const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId: options?.entryId });
|
||||
const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
|
||||
if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.');
|
||||
|
||||
const repr = plugin.build();
|
||||
for (const iso of isovalues) {
|
||||
const volume: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[iso.volumeIndex ?? 0] ?? parsed.volume;
|
||||
const volumeData = volume.cell!.obj!.data;
|
||||
repr
|
||||
.to(volume)
|
||||
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, {
|
||||
type: 'isosurface',
|
||||
typeParams: { alpha: iso.alpha ?? 1, isoValue: Volume.adjustedIsoValue(volumeData, iso.value, iso.type) },
|
||||
color: 'uniform',
|
||||
colorParams: { value: iso.color }
|
||||
}));
|
||||
}
|
||||
|
||||
await repr.commit();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* viewer.loadTrajectory({
|
||||
* model: { kind: 'model-url', url: 'villin.gro', format: 'gro' },
|
||||
* coordinates: { kind: 'coordinates-url', url: 'villin.xtc', format: 'xtc', isBinary: true },
|
||||
* preset: 'all-models' // or 'default'
|
||||
* });
|
||||
*/
|
||||
async loadTrajectory(params: LoadTrajectoryParams) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
let model: StateObjectSelector;
|
||||
|
||||
if (params.model.kind === 'model-data' || params.model.kind === 'model-url') {
|
||||
const data = params.model.kind === 'model-data'
|
||||
? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel })
|
||||
: await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
|
||||
|
||||
const trajectory = await plugin.builders.structure.parseTrajectory(data, params.model.format ?? 'mmcif');
|
||||
model = await plugin.builders.structure.createModel(trajectory);
|
||||
} else {
|
||||
const data = params.model.kind === 'topology-data'
|
||||
? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel })
|
||||
: await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
|
||||
|
||||
const provider = plugin.dataFormats.get(params.model.format);
|
||||
model = await provider!.parse(plugin, data);
|
||||
}
|
||||
|
||||
const data = params.coordinates.kind === 'coordinates-data'
|
||||
? await plugin.builders.data.rawData({ data: params.coordinates.data, label: params.coordinatesLabel })
|
||||
: await plugin.builders.data.download({ url: params.coordinates.url, isBinary: params.coordinates.isBinary, label: params.coordinatesLabel });
|
||||
|
||||
const provider = plugin.dataFormats.get(params.coordinates.format);
|
||||
const coords = await provider!.parse(plugin, data);
|
||||
|
||||
const trajectory = await plugin.build().toRoot()
|
||||
.apply(TrajectoryFromModelAndCoordinates, {
|
||||
modelRef: model.ref,
|
||||
coordinatesRef: coords.ref
|
||||
}, { dependsOn: [model.ref, coords.ref] })
|
||||
.commit();
|
||||
|
||||
const preset = await plugin.builders.structure.hierarchy.applyPreset(trajectory, params.preset ?? 'default');
|
||||
|
||||
return { model, coords, preset };
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.plugin.layout.events.updated.next(void 0);
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadStructureOptions {
|
||||
representationParams?: StructureRepresentationPresetProvider.CommonParams
|
||||
}
|
||||
|
||||
export interface VolumeIsovalueInfo {
|
||||
type: 'absolute' | 'relative',
|
||||
value: number,
|
||||
color: Color,
|
||||
alpha?: number,
|
||||
volumeIndex?: number
|
||||
}
|
||||
|
||||
export interface LoadTrajectoryParams {
|
||||
model: { kind: 'model-url', url: string, format?: BuiltInTrajectoryFormat /* mmcif */, isBinary?: boolean }
|
||||
| { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array, format?: BuiltInTrajectoryFormat /* mmcif */ }
|
||||
| { kind: 'topology-url', url: string, format: BuiltInTopologyFormat, isBinary?: boolean }
|
||||
| { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuiltInTopologyFormat },
|
||||
modelLabel?: string,
|
||||
coordinates: { kind: 'coordinates-url', url: string, format: BuiltInCoordinatesFormat, isBinary?: boolean }
|
||||
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuiltInCoordinatesFormat },
|
||||
coordinatesLabel?: string,
|
||||
preset?: keyof PresetTrajectoryHierarchy
|
||||
}
|
||||
|
||||
export const ViewerAutoPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure-representation-viewer-auto',
|
||||
display: {
|
||||
name: 'Automatic (w/ Annotation)', group: 'Annotation',
|
||||
description: 'Show standard automatic representation but colored by quality assessment (if available in the model).'
|
||||
},
|
||||
isApplicable(a) {
|
||||
return (
|
||||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')) ||
|
||||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))
|
||||
);
|
||||
},
|
||||
params: () => StructureRepresentationPresetProvider.CommonParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT'))) {
|
||||
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
|
||||
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
|
||||
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
|
||||
} else {
|
||||
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -38,6 +38,15 @@
|
||||
viewer.loadPdb('7bv2');
|
||||
viewer.loadEmdb('EMD-30210', { detail: 6 });
|
||||
// viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
|
||||
// viewer.loadStructureFromUrl('my url', 'pdb', false, {
|
||||
// representationParams: {
|
||||
// theme: {
|
||||
// globalName: 'uniform',
|
||||
// globalColorParams: { value: 0xff0000 }
|
||||
// }
|
||||
// },
|
||||
// label: 'my structure'
|
||||
// });
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -46,7 +46,10 @@
|
||||
}
|
||||
|
||||
var debugMode = getParam('debug-mode', '[^&]+').trim() === '1';
|
||||
if (debugMode) molstar.setDebugMode(debugMode, debugMode);
|
||||
if (debugMode) molstar.setDebugMode(debugMode);
|
||||
|
||||
var timingMode = getParam('timing-mode', '[^&]+').trim() === '1';
|
||||
if (timingMode) molstar.setTimingMode(timingMode);
|
||||
|
||||
var hideControls = getParam('hide-controls', '[^&]+').trim() === '1';
|
||||
var collapseLeftPanel = getParam('collapse-left-panel', '[^&]+').trim() === '1';
|
||||
@@ -56,6 +59,11 @@
|
||||
var pixelScale = getParam('pixel-scale', '[^&]+').trim();
|
||||
var pickScale = getParam('pick-scale', '[^&]+').trim();
|
||||
var pickPadding = getParam('pick-padding', '[^&]+').trim();
|
||||
var disableWboit = getParam('disable-wboit', '[^&]+').trim() === '1';
|
||||
var enableDpoit = getParam('enable-dpoit', '[^&]+').trim() === '1';
|
||||
var preferWebgl1 = getParam('prefer-webgl1', '[^&]+').trim() === '1' || void 0;
|
||||
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
|
||||
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
|
||||
|
||||
molstar.Viewer.create('app', {
|
||||
layoutShowControls: !hideControls,
|
||||
@@ -69,6 +77,11 @@
|
||||
pixelScale: parseFloat(pixelScale) || 1,
|
||||
pickScale: parseFloat(pickScale) || 0.25,
|
||||
pickPadding: isNaN(parseFloat(pickPadding)) ? 1 : parseFloat(pickPadding),
|
||||
enableWboit: (disableWboit || enableDpoit) ? false : void 0, // use default value if disable-wboit is not set
|
||||
enableDpoit: enableDpoit ? true : void 0,
|
||||
preferWebgl1: preferWebgl1,
|
||||
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
|
||||
powerPreference: powerPreference || 'high-performance',
|
||||
}).then(viewer => {
|
||||
var snapshotId = getParam('snapshot-id', '[^&]+').trim();
|
||||
if (snapshotId) viewer.setRemoteSnapshot(snapshotId);
|
||||
|
||||
@@ -1,482 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
|
||||
import { CellPack } from '../../extensions/cellpack';
|
||||
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
|
||||
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
|
||||
import { GeometryExport } from '../../extensions/geo-export';
|
||||
import { MAQualityAssessment } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
|
||||
import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
|
||||
import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
|
||||
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
|
||||
import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
|
||||
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
|
||||
import { BuildInStructureFormat } from '../../mol-plugin-state/formats/structure';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { StateObjectRef, StateObjectSelector } from '../../mol-state';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import '../../mol-util/polyfill';
|
||||
import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import './embedded.html';
|
||||
import './favicon.ico';
|
||||
import './index.html';
|
||||
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { setDebugMode, setProductionMode } from '../../mol-util/debug';
|
||||
|
||||
const CustomFormats = [
|
||||
['g3d', G3dProvider] as const
|
||||
];
|
||||
|
||||
const Extensions = {
|
||||
'cellpack': PluginSpec.Behavior(CellPack),
|
||||
'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
|
||||
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
|
||||
'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
|
||||
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
|
||||
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
|
||||
'g3d': PluginSpec.Behavior(G3DFormat),
|
||||
'mp4-export': PluginSpec.Behavior(Mp4Export),
|
||||
'geo-export': PluginSpec.Behavior(GeometryExport),
|
||||
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
|
||||
};
|
||||
|
||||
const DefaultViewerOptions = {
|
||||
customFormats: CustomFormats as [string, DataFormatProvider][],
|
||||
extensions: ObjectKeys(Extensions),
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: true,
|
||||
layoutShowRemoteState: true,
|
||||
layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: true,
|
||||
layoutShowLeftPanel: true,
|
||||
collapseLeftPanel: false,
|
||||
collapseRightPanel: false,
|
||||
disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
|
||||
pixelScale: PluginConfig.General.PixelScale.defaultValue,
|
||||
pickScale: PluginConfig.General.PickScale.defaultValue,
|
||||
pickPadding: PluginConfig.General.PickPadding.defaultValue,
|
||||
enableWboit: PluginConfig.General.EnableWboit.defaultValue,
|
||||
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
|
||||
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
|
||||
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
|
||||
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
|
||||
volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
|
||||
volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
|
||||
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
};
|
||||
type ViewerOptions = typeof DefaultViewerOptions;
|
||||
|
||||
export class Viewer {
|
||||
constructor(public plugin: PluginUIContext) {
|
||||
}
|
||||
|
||||
static async create(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
|
||||
const o = { ...DefaultViewerOptions, ...options };
|
||||
const defaultSpec = DefaultPluginUISpec();
|
||||
|
||||
const spec: PluginUISpec = {
|
||||
actions: defaultSpec.actions,
|
||||
behaviors: [
|
||||
...defaultSpec.behaviors,
|
||||
...o.extensions.map(e => Extensions[e]),
|
||||
],
|
||||
animations: [...defaultSpec.animations || []],
|
||||
customParamEditors: defaultSpec.customParamEditors,
|
||||
customFormats: o?.customFormats,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: o.layoutIsExpanded,
|
||||
showControls: o.layoutShowControls,
|
||||
controlsDisplay: o.layoutControlsDisplay,
|
||||
regionState: {
|
||||
bottom: 'full',
|
||||
left: o.collapseLeftPanel ? 'collapsed' : 'full',
|
||||
right: o.collapseRightPanel ? 'hidden' : 'full',
|
||||
top: 'full',
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
...defaultSpec.components,
|
||||
controls: {
|
||||
...defaultSpec.components?.controls,
|
||||
top: o.layoutShowSequence ? undefined : 'none',
|
||||
bottom: o.layoutShowLog ? undefined : 'none',
|
||||
left: o.layoutShowLeftPanel ? undefined : 'none',
|
||||
},
|
||||
remoteState: o.layoutShowRemoteState ? 'default' : 'none',
|
||||
},
|
||||
config: [
|
||||
[PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
|
||||
[PluginConfig.General.PixelScale, o.pixelScale],
|
||||
[PluginConfig.General.PickScale, o.pickScale],
|
||||
[PluginConfig.General.PickPadding, o.pickPadding],
|
||||
[PluginConfig.General.EnableWboit, o.enableWboit],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
|
||||
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
|
||||
[PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
|
||||
[PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation],
|
||||
[PluginConfig.State.DefaultServer, o.pluginStateServer],
|
||||
[PluginConfig.State.CurrentServer, o.pluginStateServer],
|
||||
[PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer],
|
||||
[PluginConfig.VolumeStreaming.Enabled, !o.volumeStreamingDisabled],
|
||||
[PluginConfig.Download.DefaultPdbProvider, o.pdbProvider],
|
||||
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
|
||||
[PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
|
||||
]
|
||||
};
|
||||
|
||||
const element = typeof elementOrId === 'string'
|
||||
? document.getElementById(elementOrId)
|
||||
: elementOrId;
|
||||
if (!element) throw new Error(`Could not get element with id '${elementOrId}'`);
|
||||
const plugin = await createPluginUI(element, spec, {
|
||||
onBeforeUIRender: plugin => {
|
||||
// the preset needs to be added before the UI renders otherwise
|
||||
// "Download Structure" wont be able to pick it up
|
||||
plugin.builders.structure.representation.registerPreset(ViewerAutoPreset);
|
||||
}
|
||||
});
|
||||
return new Viewer(plugin);
|
||||
}
|
||||
|
||||
setRemoteSnapshot(id: string) {
|
||||
const url = `${this.plugin.config.get(PluginConfig.State.CurrentServer)}/get/${id}`;
|
||||
return PluginCommands.State.Snapshots.Fetch(this.plugin, { url });
|
||||
}
|
||||
|
||||
loadSnapshotFromUrl(url: string, type: PluginState.SnapshotType) {
|
||||
return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
|
||||
}
|
||||
|
||||
loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'url',
|
||||
params: {
|
||||
url: Asset.Url(url),
|
||||
format: format as any,
|
||||
isBinary,
|
||||
options: { ...params.source.params.options, representationParams: options?.representationParams as any },
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async loadAllModelsOrAssemblyFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
|
||||
const trajectory = await plugin.builders.structure.parseTrajectory(data, format);
|
||||
|
||||
await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'all-models', { useDefaultIfSingleModel: true, representationPresetParams: options?.representationParams });
|
||||
}
|
||||
|
||||
async loadStructureFromData(data: string | number[], format: BuiltInTrajectoryFormat, options?: { dataLabel?: string }) {
|
||||
const _data = await this.plugin.builders.data.rawData({ data, label: options?.dataLabel });
|
||||
const trajectory = await this.plugin.builders.structure.parseTrajectory(_data, format);
|
||||
await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
|
||||
}
|
||||
|
||||
loadPdb(pdb: string, options?: LoadStructureOptions) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
const provider = this.plugin.config.get(PluginConfig.Download.DefaultPdbProvider)!;
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'pdb' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: pdb,
|
||||
server: {
|
||||
name: provider,
|
||||
params: PdbDownloadProvider[provider].defaultValue as any
|
||||
}
|
||||
},
|
||||
options: { ...params.source.params.options, representationParams: options?.representationParams as any },
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadPdbDev(pdbDev: string) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'pdb-dev' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: pdbDev,
|
||||
encoding: 'bcif',
|
||||
},
|
||||
options: params.source.params.options,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadEmdb(emdb: string, options?: { detail?: number }) {
|
||||
const provider = this.plugin.config.get(PluginConfig.Download.DefaultEmdbProvider)!;
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, {
|
||||
source: {
|
||||
name: 'pdb-emd-ds' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: emdb,
|
||||
server: provider,
|
||||
},
|
||||
detail: options?.detail ?? 3,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadAlphaFoldDb(afdb: string) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'alphafolddb' as const,
|
||||
params: {
|
||||
id: afdb,
|
||||
options: {
|
||||
...params.source.params.options,
|
||||
representation: 'preset-structure-representation-ma-quality-assessment-plddt'
|
||||
},
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadModelArchive(id: string) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'modelarchive' as const,
|
||||
params: {
|
||||
id,
|
||||
options: params.source.params.options,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @example Load X-ray density from volume server
|
||||
viewer.loadVolumeFromUrl({
|
||||
url: 'https://www.ebi.ac.uk/pdbe/densities/x-ray/1tqn/cell?detail=3',
|
||||
format: 'dscif',
|
||||
isBinary: true
|
||||
}, [{
|
||||
type: 'relative',
|
||||
value: 1.5,
|
||||
color: 0x3362B2
|
||||
}, {
|
||||
type: 'relative',
|
||||
value: 3,
|
||||
color: 0x33BB33,
|
||||
volumeIndex: 1
|
||||
}, {
|
||||
type: 'relative',
|
||||
value: -3,
|
||||
color: 0xBB3333,
|
||||
volumeIndex: 1
|
||||
}], {
|
||||
entryId: ['2FO-FC', 'FO-FC'],
|
||||
isLazy: true
|
||||
});
|
||||
* *********************
|
||||
* @example Load EM density from volume server
|
||||
viewer.loadVolumeFromUrl({
|
||||
url: 'https://maps.rcsb.org/em/emd-30210/cell?detail=6',
|
||||
format: 'dscif',
|
||||
isBinary: true
|
||||
}, [{
|
||||
type: 'relative',
|
||||
value: 1,
|
||||
color: 0x3377aa
|
||||
}], {
|
||||
entryId: 'EMD-30210',
|
||||
isLazy: true
|
||||
});
|
||||
*/
|
||||
async loadVolumeFromUrl({ url, format, isBinary }: { url: string, format: BuildInVolumeFormat, isBinary: boolean }, isovalues: VolumeIsovalueInfo[], options?: { entryId?: string | string[], isLazy?: boolean }) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
if (!plugin.dataFormats.get(format)) {
|
||||
throw new Error(`Unknown density format: ${format}`);
|
||||
}
|
||||
|
||||
if (options?.isLazy) {
|
||||
const update = this.plugin.build();
|
||||
update.toRoot().apply(StateTransforms.Data.LazyVolume, {
|
||||
url,
|
||||
format,
|
||||
entryId: options?.entryId,
|
||||
isBinary,
|
||||
isovalues: isovalues.map(v => ({ alpha: 1, volumeIndex: 0, ...v }))
|
||||
});
|
||||
return update.commit();
|
||||
}
|
||||
|
||||
return plugin.dataTransaction(async () => {
|
||||
const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
|
||||
|
||||
const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId: options?.entryId });
|
||||
const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
|
||||
if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.');
|
||||
|
||||
const repr = plugin.build();
|
||||
for (const iso of isovalues) {
|
||||
repr
|
||||
.to(parsed.volumes?.[iso.volumeIndex ?? 0] ?? parsed.volume)
|
||||
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, {
|
||||
type: 'isosurface',
|
||||
typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
|
||||
color: 'uniform',
|
||||
colorParams: { value: iso.color }
|
||||
}));
|
||||
}
|
||||
|
||||
await repr.commit();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* viewer.loadTrajectory({
|
||||
* model: { kind: 'model-url', url: 'villin.gro', format: 'gro' },
|
||||
* coordinates: { kind: 'coordinates-url', url: 'villin.xtc', format: 'xtc', isBinary: true },
|
||||
* preset: 'all-models' // or 'default'
|
||||
* });
|
||||
*/
|
||||
async loadTrajectory(params: LoadTrajectoryParams) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
let model: StateObjectSelector, coords: StateObjectSelector;
|
||||
|
||||
if (params.model.kind === 'model-data' || params.model.kind === 'model-url') {
|
||||
const data = params.model.kind === 'model-data'
|
||||
? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel })
|
||||
: await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
|
||||
|
||||
const trajectory = await plugin.builders.structure.parseTrajectory(data, params.model.format ?? 'mmcif');
|
||||
model = await plugin.builders.structure.createModel(trajectory);
|
||||
} else {
|
||||
const data = params.model.kind === 'topology-data'
|
||||
? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel })
|
||||
: await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
|
||||
|
||||
const provider = plugin.dataFormats.get(params.model.format);
|
||||
model = await provider!.parse(plugin, data);
|
||||
}
|
||||
|
||||
{
|
||||
const data = params.coordinates.kind === 'coordinates-data'
|
||||
? await plugin.builders.data.rawData({ data: params.coordinates.data, label: params.coordinatesLabel })
|
||||
: await plugin.builders.data.download({ url: params.coordinates.url, isBinary: params.coordinates.isBinary, label: params.coordinatesLabel });
|
||||
|
||||
const provider = plugin.dataFormats.get(params.coordinates.format);
|
||||
coords = await provider!.parse(plugin, data);
|
||||
}
|
||||
|
||||
const trajectory = await plugin.build().toRoot()
|
||||
.apply(TrajectoryFromModelAndCoordinates, {
|
||||
modelRef: model.ref,
|
||||
coordinatesRef: coords.ref
|
||||
}, { dependsOn: [model.ref, coords.ref] })
|
||||
.commit();
|
||||
|
||||
const preset = await plugin.builders.structure.hierarchy.applyPreset(trajectory, params.preset ?? 'default');
|
||||
|
||||
return { model, coords, preset };
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.plugin.layout.events.updated.next(void 0);
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadStructureOptions {
|
||||
representationParams?: StructureRepresentationPresetProvider.CommonParams
|
||||
}
|
||||
|
||||
export interface VolumeIsovalueInfo {
|
||||
type: 'absolute' | 'relative',
|
||||
value: number,
|
||||
color: Color,
|
||||
alpha?: number,
|
||||
volumeIndex?: number
|
||||
}
|
||||
|
||||
export interface LoadTrajectoryParams {
|
||||
model: { kind: 'model-url', url: string, format?: BuiltInTrajectoryFormat /* mmcif */, isBinary?: boolean }
|
||||
| { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array, format?: BuiltInTrajectoryFormat /* mmcif */ }
|
||||
| { kind: 'topology-url', url: string, format: BuildInStructureFormat, isBinary?: boolean }
|
||||
| { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat },
|
||||
modelLabel?: string,
|
||||
coordinates: { kind: 'coordinates-url', url: string, format: BuildInStructureFormat, isBinary?: boolean }
|
||||
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat },
|
||||
coordinatesLabel?: string,
|
||||
preset?: keyof PresetTrajectoryHierarchy
|
||||
}
|
||||
|
||||
export const ViewerAutoPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure-representation-viewer-auto',
|
||||
display: {
|
||||
name: 'Automatic (w/ Annotation)', group: 'Annotation',
|
||||
description: 'Show standard automatic representation but colored by quality assessment (if available in the model).'
|
||||
},
|
||||
isApplicable(a) {
|
||||
return (
|
||||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')) ||
|
||||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))
|
||||
);
|
||||
},
|
||||
params: () => StructureRepresentationPresetProvider.CommonParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT'))) {
|
||||
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
|
||||
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
|
||||
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
|
||||
} else {
|
||||
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
|
||||
}
|
||||
}
|
||||
});
|
||||
export * from './app';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Josh McMenemy <josh.mcmenemy@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
@@ -14,7 +15,7 @@ const writeFile = util.promisify(fs.writeFile);
|
||||
|
||||
import { DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
import { ensureDataAvailable, readCCD } from './util';
|
||||
import { DefaultDataOptions, ensureDataAvailable, readCCD } from './util';
|
||||
|
||||
function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
|
||||
const ionNames: string[] = [];
|
||||
@@ -31,11 +32,11 @@ function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
|
||||
|
||||
function writeIonNamesFile(filePath: string, ionNames: string[]) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated ion names params file. Names extracted from CCD components.
|
||||
*
|
||||
* @author molstar/chem-comp-dict/create-table cli
|
||||
* @author molstar/chem-comp-dict/create-ions cli
|
||||
*/
|
||||
|
||||
export const IonNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
@@ -43,8 +44,8 @@ export const IonNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").re
|
||||
writeFile(filePath, output);
|
||||
}
|
||||
|
||||
async function run(out: string, forceDownload = false) {
|
||||
await ensureDataAvailable(forceDownload);
|
||||
async function run(out: string, options = DefaultDataOptions) {
|
||||
await ensureDataAvailable(options);
|
||||
const ccd = await readCCD();
|
||||
const ionNames = extractIonNames(ccd);
|
||||
if (!fs.existsSync(path.dirname(out))) {
|
||||
@@ -64,10 +65,15 @@ parser.add_argument('--forceDownload', '-f', {
|
||||
action: 'store_true',
|
||||
help: 'Force download of CCD and PVCD.'
|
||||
});
|
||||
parser.add_argument('--ccdUrl', '-c', {
|
||||
help: 'Fetch the CCD from a custom URL. This forces download of the CCD.',
|
||||
required: false
|
||||
});
|
||||
interface Args {
|
||||
out: string,
|
||||
forceDownload?: boolean,
|
||||
ccdUrl?: string
|
||||
}
|
||||
const args: Args = parser.parse_args();
|
||||
|
||||
run(args.out, args.forceDownload);
|
||||
run(args.out, { forceDownload: args.forceDownload, ccdUrl: args.ccdUrl });
|
||||
|
||||
82
src/cli/chem-comp-dict/create-saccharides.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as path from 'path';
|
||||
import util from 'util';
|
||||
import fs from 'fs';
|
||||
require('util.promisify').shim();
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
|
||||
import { DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
import { DefaultDataOptions, ensureDataAvailable, readCCD } from './util';
|
||||
|
||||
function extractSaccharideNames(ccd: DatabaseCollection<CCD_Schema>) {
|
||||
const saccharideNames: string[] = [];
|
||||
for (const k in ccd) {
|
||||
const { chem_comp } = ccd[k];
|
||||
const type = chem_comp.type.value(0).toUpperCase();
|
||||
if (type.includes('SACCHARIDE')) {
|
||||
saccharideNames.push(chem_comp.id.value(0));
|
||||
}
|
||||
}
|
||||
// these are extra saccharides that don't have SACCHARIDE in their type
|
||||
saccharideNames.push(
|
||||
'UMQ', // UNDECYL-MALTOSIDE, via GlyFinder
|
||||
'SQD', // SULFOQUINOVOSYLDIACYLGLYCEROL, via GlyFinder
|
||||
);
|
||||
return saccharideNames;
|
||||
}
|
||||
|
||||
function writeSaccharideNamesFile(filePath: string, ionNames: string[]) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated ion names params file. Names extracted from CCD components.
|
||||
*
|
||||
* @author molstar/cli/chem-comp-dict/create-saccharides
|
||||
*/
|
||||
|
||||
export const SaccharideNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
writeFile(filePath, output);
|
||||
}
|
||||
|
||||
async function run(out: string, options = DefaultDataOptions) {
|
||||
await ensureDataAvailable(options);
|
||||
const ccd = await readCCD();
|
||||
const saccharideNames = extractSaccharideNames(ccd);
|
||||
if (!fs.existsSync(path.dirname(out))) {
|
||||
fs.mkdirSync(path.dirname(out));
|
||||
}
|
||||
writeSaccharideNamesFile(out, saccharideNames);
|
||||
}
|
||||
|
||||
const parser = new argparse.ArgumentParser({
|
||||
add_help: true,
|
||||
description: 'Extract and save SaccharideNames from CCD.'
|
||||
});
|
||||
parser.add_argument('out', {
|
||||
help: 'Generated file output path.'
|
||||
});
|
||||
parser.add_argument('--forceDownload', '-f', {
|
||||
action: 'store_true',
|
||||
help: 'Force download of CCD and PVCD.'
|
||||
});
|
||||
parser.add_argument('--ccdUrl', '-c', {
|
||||
help: 'Fetch the CCD from a custom URL. This forces download of the CCD.',
|
||||
required: false
|
||||
});
|
||||
interface Args {
|
||||
out: string,
|
||||
forceDownload?: boolean,
|
||||
ccdUrl?: string
|
||||
}
|
||||
const args: Args = parser.parse_args();
|
||||
|
||||
run(args.out, { forceDownload: args.forceDownload, ccdUrl: args.ccdUrl });
|
||||
@@ -18,7 +18,7 @@ import { SetUtils } from '../../mol-util/set';
|
||||
import { DefaultMap } from '../../mol-util/map';
|
||||
import { mmCIF_chemCompBond_schema } from '../../mol-io/reader/cif/schema/mmcif-extras';
|
||||
import { ccd_chemCompAtom_schema } from '../../mol-io/reader/cif/schema/ccd-extras';
|
||||
import { ensureDataAvailable, getEncodedCif, readCCD, readPVCD } from './util';
|
||||
import { DefaultDataOptions, ensureDataAvailable, getEncodedCif, readCCD, readPVCD } from './util';
|
||||
|
||||
type CCB = Table<CCD_Schema['chem_comp_bond']>
|
||||
type CCA = Table<CCD_Schema['chem_comp_atom']>
|
||||
@@ -239,8 +239,8 @@ function createAtoms(ccd: DatabaseCollection<CCD_Schema>, pvcd: DatabaseCollecti
|
||||
);
|
||||
}
|
||||
|
||||
async function run(out: string, binary = false, forceDownload = false, ccaOut?: string) {
|
||||
await ensureDataAvailable(forceDownload);
|
||||
async function run(out: string, binary = false, options = DefaultDataOptions, ccaOut?: string) {
|
||||
await ensureDataAvailable(options);
|
||||
const ccd = await readCCD();
|
||||
const pvcd = await readPVCD();
|
||||
|
||||
@@ -283,12 +283,22 @@ parser.add_argument('--ccaOut', '-a', {
|
||||
help: 'Optional generated file output path for chem_comp_atom data.',
|
||||
required: false
|
||||
});
|
||||
parser.add_argument('--ccdUrl', '-c', {
|
||||
help: 'Fetch the CCD from a custom URL. This forces download of the CCD.',
|
||||
required: false
|
||||
});
|
||||
parser.add_argument('--pvcdUrl', '-p', {
|
||||
help: 'Fetch the PVCD from a custom URL. This forces download of the PVCD.',
|
||||
required: false
|
||||
});
|
||||
interface Args {
|
||||
out: string,
|
||||
forceDownload?: boolean,
|
||||
binary?: boolean,
|
||||
ccaOut?: string
|
||||
ccaOut?: string,
|
||||
ccdUrl?: string,
|
||||
pvcdUrl?: string
|
||||
}
|
||||
const args: Args = parser.parse_args();
|
||||
|
||||
run(args.out, args.binary, args.forceDownload, args.ccaOut);
|
||||
run(args.out, args.binary, { forceDownload: args.forceDownload, ccdUrl: args.ccdUrl, pvcdUrl: args.pvcdUrl }, args.ccaOut);
|
||||
|
||||
@@ -35,9 +35,9 @@ export async function ensureAvailable(path: string, url: string, forceDownload =
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureDataAvailable(forceDownload = false) {
|
||||
await ensureAvailable(CCD_PATH, CCD_URL, forceDownload);
|
||||
await ensureAvailable(PVCD_PATH, PVCD_URL, forceDownload);
|
||||
export async function ensureDataAvailable(options: DataOptions) {
|
||||
await ensureAvailable(CCD_PATH, options.ccdUrl || CCD_URL, !!options.ccdUrl || options.forceDownload);
|
||||
await ensureAvailable(PVCD_PATH, options.pvcdUrl || PVCD_URL, !!options.pvcdUrl || options.forceDownload);
|
||||
}
|
||||
|
||||
export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
|
||||
@@ -68,6 +68,16 @@ export function getEncodedCif(name: string, database: Database<Database.Schema>,
|
||||
return encoder.getData();
|
||||
}
|
||||
|
||||
export type DataOptions = {
|
||||
ccdUrl?: string,
|
||||
pvcdUrl?: string,
|
||||
forceDownload?: boolean
|
||||
}
|
||||
|
||||
export const DefaultDataOptions: DataOptions = {
|
||||
forceDownload: false
|
||||
};
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '..', '..', '..', '..', 'build/data');
|
||||
const CCD_PATH = path.join(DATA_DIR, 'components.cif');
|
||||
const PVCD_PATH = path.join(DATA_DIR, 'aa-variants-v1.cif');
|
||||
|
||||
@@ -71,7 +71,7 @@ function classify(name: string, field: CifField): CifWriter.Field {
|
||||
}
|
||||
|
||||
export function convert(path: string, asText = false, hints?: EncodingStrategyHint[], filter?: string) {
|
||||
return Task.create<Uint8Array>('BinaryCIF', async ctx => {
|
||||
return Task.create<Uint8Array>('Convert CIF', async ctx => {
|
||||
const encodingProvider: BinaryEncodingProvider = hints
|
||||
? CifWriter.createEncodingProviderFromJsonConfig(hints)
|
||||
: { get: (c, f) => void 0 };
|
||||
|
||||
@@ -18,7 +18,7 @@ async function process(srcPath: string, outPath: string, configPath?: string, fi
|
||||
const config = configPath ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : void 0;
|
||||
const filter = filterPath ? fs.readFileSync(filterPath, 'utf8') : void 0;
|
||||
|
||||
const res = await convert(srcPath, false, config, filter);
|
||||
const res = await convert(srcPath, srcPath.toLowerCase().indexOf('.bcif') > 0, config, filter);
|
||||
await write(outPath, res);
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ function run(args: Args) {
|
||||
|
||||
const parser = new argparse.ArgumentParser({
|
||||
add_help: true,
|
||||
description: 'Convert any CIF file to a BCIF file'
|
||||
description: 'Convert any BCIF file to a CIF file or vice versa'
|
||||
});
|
||||
parser.add_argument('src', {
|
||||
help: 'Source CIF path'
|
||||
help: 'Source file path'
|
||||
});
|
||||
parser.add_argument('out', {
|
||||
help: 'Output BCIF path'
|
||||
help: 'Output file path'
|
||||
});
|
||||
parser.add_argument('-c', '--config', {
|
||||
help: 'Optional encoding strategy/precision config path',
|
||||
|
||||
@@ -161,7 +161,7 @@ const MMCIF_DIC_URL = 'http://mmcif.wwpdb.org/dictionaries/ascii/mmcif_pdbx_v50.
|
||||
const IHM_DIC_PATH = `${DIC_DIR}/ihm-extension.dic`;
|
||||
const IHM_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/IHM-dictionary/master/ihm-extension.dic';
|
||||
const MA_DIC_PATH = `${DIC_DIR}/ma-extension.dic`;
|
||||
const MA_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/MA-dictionary/master/mmcif_ma.dic';
|
||||
const MA_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/ModelCIF/master/dist/mmcif_ma.dic';
|
||||
|
||||
const CIF_CORE_DIC_PATH = `${DIC_DIR}/cif_core.dic`;
|
||||
const CIF_CORE_DIC_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/cif_core.dic';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -13,15 +13,17 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
switch (type) {
|
||||
// mmCIF
|
||||
case 'code':
|
||||
case 'ucode':
|
||||
case 'line':
|
||||
case 'uline':
|
||||
case 'text':
|
||||
case 'char':
|
||||
case 'uchar3':
|
||||
case 'uchar1':
|
||||
case 'boolean':
|
||||
return values && values.length ? EnumCol(values, 'str', description) : StrCol(description);
|
||||
case 'ucode':
|
||||
case 'uline':
|
||||
case 'uchar3':
|
||||
case 'uchar1':
|
||||
// only force lower-case for enums
|
||||
return values && values.length ? EnumCol(values.map(x => x.toLowerCase()), 'lstr', description) : StrCol(description);
|
||||
case 'aliasname':
|
||||
case 'name':
|
||||
case 'idname':
|
||||
@@ -49,6 +51,7 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
case 'operation_expression':
|
||||
case 'point_symmetry':
|
||||
case '4x3_matrix':
|
||||
case '3x4_matrix':
|
||||
case '3x4_matrices':
|
||||
case 'point_group':
|
||||
case 'point_group_helical':
|
||||
@@ -68,6 +71,7 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
case 'ec-type':
|
||||
case 'ucode-alphanum-csv':
|
||||
case 'id_list':
|
||||
case 'entity_id_list':
|
||||
return ListCol('str', ',', description);
|
||||
case 'id_list_spc':
|
||||
return ListCol('str', ' ', description);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -10,7 +10,7 @@ import { FieldPath } from '../../../mol-io/reader/cif/schema';
|
||||
|
||||
function header(name: string, info: string, moldataImportPath: string) {
|
||||
return `/**
|
||||
* Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated '${name}' schema file. ${info}
|
||||
*
|
||||
@@ -35,13 +35,17 @@ function getTypeShorthands(schema: Database, fields?: Filter) {
|
||||
const { columns } = schema.tables[table];
|
||||
Object.keys(columns).forEach(columnName => {
|
||||
if (fields && !fields[table][columnName]) return;
|
||||
types.add(schema.tables[table].columns[columnName].type);
|
||||
const col = schema.tables[table].columns[columnName];
|
||||
if (col.type === 'enum') types.add(col.subType);
|
||||
types.add(col.type);
|
||||
});
|
||||
});
|
||||
const shorthands: string[] = [];
|
||||
types.forEach(type => {
|
||||
switch (type) {
|
||||
case 'str': shorthands.push('const str = Schema.str;'); break;
|
||||
case 'ustr': shorthands.push('const ustr = Schema.ustr;'); break;
|
||||
case 'lstr': shorthands.push('const lstr = Schema.lstr;'); break;
|
||||
case 'int': shorthands.push('const int = Schema.int;'); break;
|
||||
case 'float': shorthands.push('const float = Schema.float;'); break;
|
||||
case 'coord': shorthands.push('const coord = Schema.coord;'); break;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -29,8 +29,8 @@ export function FloatCol(description: string): FloatCol { return { type: 'float'
|
||||
export type CoordCol = { type: 'coord' } & BaseCol
|
||||
export function CoordCol(description: string): CoordCol { return { type: 'coord', description }; }
|
||||
|
||||
export type EnumCol = { type: 'enum', subType: 'int' | 'str', values: string[] } & BaseCol
|
||||
export function EnumCol(values: string[], subType: 'int' | 'str', description: string): EnumCol {
|
||||
export type EnumCol = { type: 'enum', subType: 'int' | 'str' | 'ustr' | 'lstr', values: string[] } & BaseCol
|
||||
export function EnumCol(values: string[], subType: 'int' | 'str' | 'ustr' | 'lstr', description: string): EnumCol {
|
||||
return { type: 'enum', description, values, subType };
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ function print(volume: Volume) {
|
||||
}
|
||||
|
||||
async function doMesh(volume: Volume, filename: string) {
|
||||
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
|
||||
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
|
||||
console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
|
||||
|
||||
// Export the mesh in OBJ format.
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { AlphaOrbitalsExample } from '.';
|
||||
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
|
||||
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { PluginContextContainer } from '../../mol-plugin-ui/plugin';
|
||||
|
||||
export function mountControls(orbitals: AlphaOrbitalsExample, parent: Element) {
|
||||
ReactDOM.render(<PluginContextContainer plugin={orbitals.plugin}>
|
||||
createRoot(parent).render(<PluginContextContainer plugin={orbitals.plugin}>
|
||||
<Controls orbitals={orbitals} />
|
||||
</PluginContextContainer>, parent);
|
||||
</PluginContextContainer>);
|
||||
}
|
||||
|
||||
function Controls({ orbitals }: { orbitals: AlphaOrbitalsExample }) {
|
||||
|
||||
@@ -55,6 +55,17 @@
|
||||
</a>
|
||||
</div>
|
||||
<script>
|
||||
function getParam(name, regex) {
|
||||
var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
|
||||
return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
|
||||
}
|
||||
|
||||
var debugMode = getParam('debug-mode', '[^&]+').trim() === '1';
|
||||
if (debugMode) AlphaOrbitalsExample.setDebugMode(debugMode);
|
||||
|
||||
var timingMode = getParam('timing-mode', '[^&]+').trim() === '1';
|
||||
if (timingMode) AlphaOrbitalsExample.setTimingMode(timingMode);
|
||||
|
||||
AlphaOrbitalsExample.init('app')
|
||||
</script>
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -11,7 +11,7 @@ import { SphericalBasisOrder } from '../../extensions/alpha-orbitals/spherical-f
|
||||
import { BasisAndOrbitals, CreateOrbitalDensityVolume, CreateOrbitalRepresentation3D, CreateOrbitalVolume, StaticBasisAndOrbitals } from '../../extensions/alpha-orbitals/transforms';
|
||||
import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { createPluginUI } from '../../mol-plugin-ui/react18';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
@@ -25,6 +25,8 @@ import { DemoMoleculeSDF, DemoOrbitals } from './example-data';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
|
||||
import { setDebugMode, setTimingMode } from '../../mol-util/debug';
|
||||
|
||||
interface DemoInput {
|
||||
moleculeSdf: string,
|
||||
basis: Basis,
|
||||
@@ -97,7 +99,7 @@ export class AlphaOrbitalsExample {
|
||||
}
|
||||
|
||||
readonly params = new BehaviorSubject<ParamDefinition.For<Params>>({} as any);
|
||||
readonly state = new BehaviorSubject<Params>({ show: { name: 'orbital', params: { index: 32 } }, isoValue: 1, gpuSurface: false });
|
||||
readonly state = new BehaviorSubject<Params>({ show: { name: 'orbital', params: { index: 32 } }, isoValue: 1, gpuSurface: true });
|
||||
|
||||
private selectors?: Selectors = void 0;
|
||||
private basis?: StateObjectSelector<BasisAndOrbitals> = void 0;
|
||||
@@ -218,4 +220,6 @@ export class AlphaOrbitalsExample {
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).AlphaOrbitalsExample = new AlphaOrbitalsExample();
|
||||
(window as any).AlphaOrbitalsExample = new AlphaOrbitalsExample();
|
||||
(window as any).AlphaOrbitalsExample.setDebugMode = setDebugMode;
|
||||
(window as any).AlphaOrbitalsExample.setTimingMode = setTimingMode;
|
||||
@@ -9,7 +9,7 @@ import { EmptyLoci } from '../../mol-model/loci';
|
||||
import { StructureSelection } from '../../mol-model/structure';
|
||||
import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in/model-index';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { createPluginUI } from '../../mol-plugin-ui/react18';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
@@ -74,12 +74,20 @@ class BasicWrapper {
|
||||
toggleSpin() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
|
||||
const trackball = this.plugin.canvas3d.props.trackball;
|
||||
PluginCommands.Canvas3D.SetSettings(this.plugin, {
|
||||
settings: props => {
|
||||
props.trackball.spin = !props.trackball.spin;
|
||||
settings: {
|
||||
trackball: {
|
||||
...trackball,
|
||||
animate: trackball.animate.name === 'spin'
|
||||
? { name: 'off', params: {} }
|
||||
: { name: 'spin', params: { speed: 1 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!this.plugin.canvas3d.props.trackball.spin) PluginCommands.Camera.Reset(this.plugin, {});
|
||||
if (this.plugin.canvas3d.props.trackball.animate.name !== 'spin') {
|
||||
PluginCommands.Camera.Reset(this.plugin, {});
|
||||
}
|
||||
}
|
||||
|
||||
private animateModelIndexTargetFps() {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Canvas3DProps } from '../../mol-canvas3d/canvas3d';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { createPluginUI } from '../../mol-plugin-ui/react18';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
@@ -24,8 +24,9 @@ const Canvas3DPresets = {
|
||||
illustrative: {
|
||||
canvas3d: <Preset>{
|
||||
postprocessing: {
|
||||
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15 } },
|
||||
outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000) } }
|
||||
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
|
||||
outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000), includeTransparent: true, } },
|
||||
shadow: { name: 'off', params: {} },
|
||||
},
|
||||
renderer: {
|
||||
ambientIntensity: 1.0,
|
||||
@@ -36,8 +37,9 @@ const Canvas3DPresets = {
|
||||
occlusion: {
|
||||
canvas3d: <Preset>{
|
||||
postprocessing: {
|
||||
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15 } },
|
||||
outline: { name: 'off', params: {} }
|
||||
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
|
||||
outline: { name: 'off', params: {} },
|
||||
shadow: { name: 'off', params: {} },
|
||||
},
|
||||
renderer: {
|
||||
ambientIntensity: 0.4,
|
||||
@@ -50,7 +52,8 @@ const Canvas3DPresets = {
|
||||
canvas3d: <Preset>{
|
||||
postprocessing: {
|
||||
occlusion: { name: 'off', params: {} },
|
||||
outline: { name: 'off', params: {} }
|
||||
outline: { name: 'off', params: {} },
|
||||
shadow: { name: 'off', params: {} },
|
||||
},
|
||||
renderer: {
|
||||
ambientIntensity: 0.4,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in/mod
|
||||
import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
|
||||
import { PluginStateObject, PluginStateObject as PSO } from '../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { createPluginUI } from '../../mol-plugin-ui/react18';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { CreateVolumeStreamingInfo, InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
|
||||
@@ -256,7 +256,16 @@ class MolStarProteopediaWrapper {
|
||||
toggleSpin() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
const trackball = this.plugin.canvas3d.props.trackball;
|
||||
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } });
|
||||
PluginCommands.Canvas3D.SetSettings(this.plugin, {
|
||||
settings: {
|
||||
trackball: {
|
||||
...trackball,
|
||||
animate: trackball.animate.name === 'spin'
|
||||
? { name: 'off', params: {} }
|
||||
: { name: 'spin', params: { speed: 1 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
viewport = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -8,6 +8,7 @@ import { sortArray } from '../../mol-data/util';
|
||||
import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { AlphaOrbital, createGrid, CubeGrid, CubeGridComputationParams, initCubeGrid } from './data-model';
|
||||
import { gpuComputeAlphaOrbitalsDensityGridValues } from './gpu/compute';
|
||||
|
||||
@@ -19,9 +20,9 @@ export function createSphericalCollocationDensityGrid(
|
||||
|
||||
let matrix: Float32Array;
|
||||
if (canComputeGrid3dOnGPU(webgl)) {
|
||||
// console.time('gpu');
|
||||
matrix = await gpuComputeAlphaOrbitalsDensityGridValues(ctx, webgl!, cubeGrid, orbitals);
|
||||
// console.timeEnd('gpu');
|
||||
if (isTimingMode) webgl.timer.mark('createSphericalCollocationDensityGrid');
|
||||
matrix = await gpuComputeAlphaOrbitalsDensityGridValues(ctx, webgl, cubeGrid, orbitals);
|
||||
if (isTimingMode) webgl.timer.markEnd('createSphericalCollocationDensityGrid');
|
||||
} else {
|
||||
throw new Error('Missing OES_texture_float WebGL extension.');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Inspired by https://github.com/dgasmith/gau2grid.
|
||||
*
|
||||
@@ -10,12 +10,11 @@ import { sortArray } from '../../mol-data/util';
|
||||
import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { sphericalCollocation } from './collocation';
|
||||
import { AlphaOrbital, createGrid, CubeGrid, CubeGridComputationParams, initCubeGrid } from './data-model';
|
||||
import { gpuComputeAlphaOrbitalsGridValues } from './gpu/compute';
|
||||
|
||||
// setDebugMode(true);
|
||||
|
||||
export function createSphericalCollocationGrid(
|
||||
params: CubeGridComputationParams, orbital: AlphaOrbital, webgl?: WebGLContext
|
||||
): Task<CubeGrid> {
|
||||
@@ -24,9 +23,9 @@ export function createSphericalCollocationGrid(
|
||||
|
||||
let matrix: Float32Array;
|
||||
if (canComputeGrid3dOnGPU(webgl)) {
|
||||
// console.time('gpu');
|
||||
matrix = await gpuComputeAlphaOrbitalsGridValues(ctx, webgl!, cubeGrid, orbital);
|
||||
// console.timeEnd('gpu');
|
||||
if (isTimingMode) webgl.timer.mark('createSphericalCollocationGrid');
|
||||
matrix = await gpuComputeAlphaOrbitalsGridValues(ctx, webgl, cubeGrid, orbital);
|
||||
if (isTimingMode) webgl.timer.markEnd('createSphericalCollocationGrid');
|
||||
} else {
|
||||
// console.time('cpu');
|
||||
matrix = await sphericalCollocation(cubeGrid, orbital, ctx);
|
||||
|
||||
@@ -43,9 +43,9 @@ export type BilayerPlanesProps = PD.Values<BilayerPlanesParams>
|
||||
const BilayerRimsParams = {
|
||||
...Lines.Params,
|
||||
...SharedParams,
|
||||
lineSizeAttenuation: PD.Boolean(true),
|
||||
linesSize: PD.Numeric(0.3, { min: 0.01, max: 50, step: 0.01 }),
|
||||
dashedLines: PD.Boolean(true),
|
||||
lineSizeAttenuation: PD.Boolean(false),
|
||||
linesSize: PD.Numeric(0.5, { min: 0.01, max: 50, step: 0.01 }),
|
||||
dashedLines: PD.Boolean(false),
|
||||
};
|
||||
export type BilayerRimsParams = typeof BilayerRimsParams
|
||||
export type BilayerRimsProps = PD.Values<BilayerRimsParams>
|
||||
|
||||
BIN
src/extensions/backgrounds/images/cells.jpg
Normal file
|
After Width: | Height: | Size: 181 KiB |
91
src/extensions/backgrounds/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
|
||||
// from https://visualsonline.cancer.gov/details.cfm?imageid=2304, public domain
|
||||
import image_cells from './images/cells.jpg';
|
||||
|
||||
// created with http://alexcpeterson.com/spacescape/
|
||||
import face_nebula_nx from './skyboxes/nebula/nebula_left2.jpg';
|
||||
import face_nebula_ny from './skyboxes/nebula/nebula_bottom4.jpg';
|
||||
import face_nebula_nz from './skyboxes/nebula/nebula_back6.jpg';
|
||||
import face_nebula_px from './skyboxes/nebula/nebula_right1.jpg';
|
||||
import face_nebula_py from './skyboxes/nebula/nebula_top3.jpg';
|
||||
import face_nebula_pz from './skyboxes/nebula/nebula_front5.jpg';
|
||||
|
||||
export const Backgrounds = PluginBehavior.create<{ }>({
|
||||
name: 'extension-backgrounds',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'Backgrounds'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ }> {
|
||||
register(): void {
|
||||
this.ctx.config.set(PluginConfig.Background.Styles, [
|
||||
[{
|
||||
variant: {
|
||||
name: 'radialGradient',
|
||||
params: {
|
||||
centerColor: Color(0xFFFFFF),
|
||||
edgeColor: Color(0x808080),
|
||||
ratio: 0.2,
|
||||
coverage: 'viewport',
|
||||
}
|
||||
}
|
||||
}, 'Light Radial Gradient'],
|
||||
[{
|
||||
variant: {
|
||||
name: 'image',
|
||||
params: {
|
||||
source: {
|
||||
name: 'url',
|
||||
params: image_cells
|
||||
},
|
||||
lightness: 0,
|
||||
saturation: 0,
|
||||
opacity: 1,
|
||||
coverage: 'viewport',
|
||||
}
|
||||
}
|
||||
}, 'Normal Cells Image'],
|
||||
[{
|
||||
variant: {
|
||||
name: 'skybox',
|
||||
params: {
|
||||
faces: {
|
||||
name: 'urls',
|
||||
params: {
|
||||
nx: face_nebula_nx,
|
||||
ny: face_nebula_ny,
|
||||
nz: face_nebula_nz,
|
||||
px: face_nebula_px,
|
||||
py: face_nebula_py,
|
||||
pz: face_nebula_pz,
|
||||
}
|
||||
},
|
||||
lightness: 0,
|
||||
saturation: 0,
|
||||
opacity: 1,
|
||||
blur: 0.3,
|
||||
}
|
||||
}
|
||||
}, 'Purple Nebula Skybox'],
|
||||
]);
|
||||
}
|
||||
|
||||
update() {
|
||||
return false;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
this.ctx.config.set(PluginConfig.Background.Styles, []);
|
||||
}
|
||||
},
|
||||
params: () => ({ })
|
||||
});
|
||||
BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
10
src/extensions/backgrounds/typings.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
@@ -12,7 +12,7 @@ import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Ingredient, CellPacking, CompartmentPrimitives } from './data';
|
||||
import { getFromPdb, getFromCellPackDB, IngredientFiles, parseCif, parsePDBfile, getStructureMean, getFromOPM } from './util';
|
||||
import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure';
|
||||
import { trajectoryFromMmCIF, MmcifFormat } from '../../mol-model-formats/structure/mmcif';
|
||||
import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
|
||||
import { trajectoryFromPDB } from '../../mol-model-formats/structure/pdb';
|
||||
import { Mat4, Vec3, Quat } from '../../mol-math/linear-algebra';
|
||||
import { SymmetryOperator } from '../../mol-math/geometry';
|
||||
@@ -22,10 +22,6 @@ import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl, Structure
|
||||
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
|
||||
import { getMatFromResamplePoints } from './curve';
|
||||
import { compile } from '../../mol-script/runtime/query/compiler';
|
||||
import { CifCategory, CifField } from '../../mol-io/reader/cif';
|
||||
import { mmCIF_Schema } from '../../mol-io/reader/cif/schema/mmcif';
|
||||
import { Column } from '../../mol-data/db';
|
||||
import { createModels } from '../../mol-model-formats/structure/basic/parser';
|
||||
import { CellpackPackingPreset, CellpackMembranePreset } from './preset';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
import { Color } from '../../mol-util/color';
|
||||
@@ -141,7 +137,7 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi
|
||||
|
||||
function getTransformLegacy(trans: Vec3, rot: Quat) {
|
||||
const q: Quat = Quat.create(-rot[3], rot[0], rot[1], rot[2]);
|
||||
const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
|
||||
const m: Mat4 = Mat4.fromQuat(Mat4(), q);
|
||||
Mat4.transpose(m, m);
|
||||
Mat4.scale(m, m, Vec3.create(-1.0, 1.0, -1.0));
|
||||
Mat4.setTranslation(m, trans);
|
||||
@@ -150,7 +146,7 @@ function getTransformLegacy(trans: Vec3, rot: Quat) {
|
||||
|
||||
function getTransform(trans: Vec3, rot: Quat) {
|
||||
const q: Quat = Quat.create(-rot[0], rot[1], rot[2], -rot[3]);
|
||||
const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
|
||||
const m: Mat4 = Mat4.fromQuat(Mat4(), q);
|
||||
const p: Vec3 = Vec3.create(-trans[0], trans[1], trans[2]);
|
||||
Mat4.setTranslation(m, p);
|
||||
return m;
|
||||
@@ -213,110 +209,10 @@ function getAssembly(name: string, transforms: Mat4[], structure: Structure) {
|
||||
return builder.getStructure();
|
||||
}
|
||||
|
||||
function getCifCurve(name: string, transforms: Mat4[], model: Model) {
|
||||
if (!MmcifFormat.is(model.sourceData)) throw new Error('mmcif source data needed');
|
||||
|
||||
const { db } = model.sourceData.data;
|
||||
const d = db.atom_site;
|
||||
const n = d._rowCount;
|
||||
const rowCount = n * transforms.length;
|
||||
|
||||
const { offsets, count } = model.atomicHierarchy.chainAtomSegments;
|
||||
|
||||
const x = d.Cartn_x.toArray();
|
||||
const y = d.Cartn_y.toArray();
|
||||
const z = d.Cartn_z.toArray();
|
||||
|
||||
const Cartn_x = new Float32Array(rowCount);
|
||||
const Cartn_y = new Float32Array(rowCount);
|
||||
const Cartn_z = new Float32Array(rowCount);
|
||||
const map = new Uint32Array(rowCount);
|
||||
const seq = new Int32Array(rowCount);
|
||||
let offset = 0;
|
||||
for (let c = 0; c < count; ++c) {
|
||||
const cStart = offsets[c];
|
||||
const cEnd = offsets[c + 1];
|
||||
const cLength = cEnd - cStart;
|
||||
for (let t = 0, tl = transforms.length; t < tl; ++t) {
|
||||
const m = transforms[t];
|
||||
for (let j = cStart; j < cEnd; ++j) {
|
||||
const i = offset + j - cStart;
|
||||
const xj = x[j], yj = y[j], zj = z[j];
|
||||
Cartn_x[i] = m[0] * xj + m[4] * yj + m[8] * zj + m[12];
|
||||
Cartn_y[i] = m[1] * xj + m[5] * yj + m[9] * zj + m[13];
|
||||
Cartn_z[i] = m[2] * xj + m[6] * yj + m[10] * zj + m[14];
|
||||
map[i] = j;
|
||||
seq[i] = t + 1;
|
||||
}
|
||||
offset += cLength;
|
||||
}
|
||||
}
|
||||
|
||||
function multColumn<T>(column: Column<T>) {
|
||||
const array = column.toArray();
|
||||
return Column.ofLambda({
|
||||
value: row => array[map[row]],
|
||||
areValuesEqual: (rowA, rowB) => map[rowA] === map[rowB] || array[map[rowA]] === array[map[rowB]],
|
||||
rowCount, schema: column.schema
|
||||
});
|
||||
}
|
||||
|
||||
const _atom_site: CifCategory.SomeFields<mmCIF_Schema['atom_site']> = {
|
||||
auth_asym_id: CifField.ofColumn(multColumn(d.auth_asym_id)),
|
||||
auth_atom_id: CifField.ofColumn(multColumn(d.auth_atom_id)),
|
||||
auth_comp_id: CifField.ofColumn(multColumn(d.auth_comp_id)),
|
||||
auth_seq_id: CifField.ofNumbers(seq),
|
||||
|
||||
B_iso_or_equiv: CifField.ofColumn(Column.ofConst(0, rowCount, Column.Schema.float)),
|
||||
Cartn_x: CifField.ofNumbers(Cartn_x),
|
||||
Cartn_y: CifField.ofNumbers(Cartn_y),
|
||||
Cartn_z: CifField.ofNumbers(Cartn_z),
|
||||
group_PDB: CifField.ofColumn(Column.ofConst('ATOM', rowCount, Column.Schema.str)),
|
||||
id: CifField.ofColumn(Column.ofLambda({
|
||||
value: row => row,
|
||||
areValuesEqual: (rowA, rowB) => rowA === rowB,
|
||||
rowCount, schema: d.id.schema,
|
||||
})),
|
||||
|
||||
label_alt_id: CifField.ofColumn(multColumn(d.label_alt_id)),
|
||||
|
||||
label_asym_id: CifField.ofColumn(multColumn(d.label_asym_id)),
|
||||
label_atom_id: CifField.ofColumn(multColumn(d.label_atom_id)),
|
||||
label_comp_id: CifField.ofColumn(multColumn(d.label_comp_id)),
|
||||
label_seq_id: CifField.ofNumbers(seq),
|
||||
label_entity_id: CifField.ofColumn(Column.ofConst('1', rowCount, Column.Schema.str)),
|
||||
|
||||
occupancy: CifField.ofColumn(Column.ofConst(1, rowCount, Column.Schema.float)),
|
||||
type_symbol: CifField.ofColumn(multColumn(d.type_symbol)),
|
||||
|
||||
pdbx_PDB_ins_code: CifField.ofColumn(Column.ofConst('', rowCount, Column.Schema.str)),
|
||||
pdbx_PDB_model_num: CifField.ofColumn(Column.ofConst(1, rowCount, Column.Schema.int)),
|
||||
};
|
||||
|
||||
const categories = {
|
||||
entity: CifCategory.ofTable('entity', db.entity),
|
||||
chem_comp: CifCategory.ofTable('chem_comp', db.chem_comp),
|
||||
atom_site: CifCategory.ofFields('atom_site', _atom_site)
|
||||
};
|
||||
|
||||
return {
|
||||
header: name,
|
||||
categoryNames: Object.keys(categories),
|
||||
categories
|
||||
};
|
||||
}
|
||||
|
||||
async function getCurve(plugin: PluginContext, name: string, ingredient: Ingredient, transforms: Mat4[], model: Model) {
|
||||
const cif = getCifCurve(name, transforms, model);
|
||||
const curveModelTask = Task.create('Curve Model', async ctx => {
|
||||
const format = MmcifFormat.fromFrame(cif);
|
||||
const models = await createModels(format.data.db, format, ctx);
|
||||
return models.representative;
|
||||
});
|
||||
|
||||
const curveModel = await plugin.runTask(curveModelTask);
|
||||
// ingredient.source.selection = undefined;
|
||||
return getStructure(plugin, curveModel, ingredient);
|
||||
async function getCurve(name: string, transforms: Mat4[], model: Model) {
|
||||
const structure = Structure.ofModel(model);
|
||||
const assembly = getAssembly(name, transforms, structure);
|
||||
return assembly;
|
||||
}
|
||||
|
||||
async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredient, baseUrl: string, ingredientFiles: IngredientFiles, trajCache: TrajectoryCache, location: 'surface' | 'interior' | 'cytoplasme') {
|
||||
@@ -337,7 +233,7 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
|
||||
if (!model) return;
|
||||
let structure: Structure;
|
||||
if (nbCurve) {
|
||||
structure = await getCurve(plugin, name, ingredient, getCurveTransforms(ingredient), model);
|
||||
structure = await getCurve(name, getCurveTransforms(ingredient), model);
|
||||
} else {
|
||||
if ((!results || results.length === 0)) return;
|
||||
let bu: string|undefined = source.bu ? source.bu : undefined;
|
||||
@@ -361,7 +257,7 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
|
||||
structure = Structure.transform(structure, m1);
|
||||
if (ingredient.offset) {
|
||||
const o: Vec3 = Vec3.create(ingredient.offset[0], ingredient.offset[1], ingredient.offset[2]);
|
||||
if (!Vec3.exactEquals(o, Vec3.zero())) { // -1, 1, 4e-16 ??
|
||||
if (!Vec3.exactEquals(o, Vec3())) { // -1, 1, 4e-16 ??
|
||||
if (location !== 'surface') {
|
||||
Vec3.negate(o, o);
|
||||
}
|
||||
@@ -375,7 +271,7 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
|
||||
if (!Vec3.exactEquals(p, Vec3.unitZ)) {
|
||||
const q: Quat = Quat.identity();
|
||||
Quat.rotationTo(q, p, Vec3.unitZ);
|
||||
const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
|
||||
const m: Mat4 = Mat4.fromQuat(Mat4(), q);
|
||||
structure = Structure.transform(structure, m);
|
||||
}
|
||||
}
|
||||
@@ -519,6 +415,7 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
|
||||
.apply(StructureFromAssemblies, undefined, { state: { isGhost: true } })
|
||||
.commit({ revertOnError: true });
|
||||
const membraneParams = {
|
||||
ignoreLight: params.preset.adjustStyle,
|
||||
representation: params.preset.representation,
|
||||
};
|
||||
await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
|
||||
@@ -535,6 +432,7 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
|
||||
.apply(StateTransforms.Model.StructureFromModel, props, { state: { isGhost: true } })
|
||||
.commit({ revertOnError: true });
|
||||
const membraneParams = {
|
||||
ignoreLight: params.preset.adjustStyle,
|
||||
representation: params.preset.representation,
|
||||
};
|
||||
await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
|
||||
@@ -618,6 +516,7 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
|
||||
|
||||
const packingParams = {
|
||||
traceOnly: params.preset.traceOnly,
|
||||
ignoreLight: params.preset.adjustStyle,
|
||||
representation: params.preset.representation,
|
||||
};
|
||||
await CellpackPackingPreset.apply(packing, packingParams, plugin);
|
||||
@@ -669,7 +568,8 @@ const LoadCellPackModelParams = {
|
||||
ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredient files' }),
|
||||
preset: PD.Group({
|
||||
traceOnly: PD.Boolean(false),
|
||||
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation']))
|
||||
adjustStyle: PD.Boolean(true),
|
||||
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation'] as const))
|
||||
}, { isExpanded: true })
|
||||
};
|
||||
type LoadCellPackModelParams = PD.Values<typeof LoadCellPackModelParams>
|
||||
@@ -679,5 +579,53 @@ export const LoadCellPackModel = StateAction.build({
|
||||
params: LoadCellPackModelParams,
|
||||
from: PSO.Root
|
||||
})(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => {
|
||||
if (params.preset.adjustStyle) {
|
||||
ctx.managers.interactivity.setProps({ granularity: 'chain' });
|
||||
ctx.managers.structure.component.setOptions({
|
||||
... ctx.managers.structure.component.state.options,
|
||||
visualQuality: 'custom',
|
||||
ignoreLight: true,
|
||||
hydrogens: 'hide-all',
|
||||
});
|
||||
ctx.canvas3d?.setProps({
|
||||
multiSample: { mode: 'off' },
|
||||
cameraClipping: { far: false },
|
||||
renderer: { colorMarker: false },
|
||||
marking: {
|
||||
enabled: true,
|
||||
ghostEdgeStrength: 1,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
radius: 8,
|
||||
bias: 1,
|
||||
blurKernelSize: 15,
|
||||
resolutionScale: 1,
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.6,
|
||||
maxDistance: 80,
|
||||
steps: 3,
|
||||
tolerance: 1.0,
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
name: 'on',
|
||||
params: {
|
||||
scale: 1,
|
||||
threshold: 0.33,
|
||||
color: ColorNames.black,
|
||||
includeTransparent: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
await loadPackings(ctx, taskCtx, state, params);
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
@@ -13,7 +13,8 @@ import { CellPackGenerateColorThemeProvider } from './color/generate';
|
||||
|
||||
export const CellpackPackingPresetParams = {
|
||||
traceOnly: PD.Boolean(true),
|
||||
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['gaussian-surface', 'spacefill', 'point', 'orientation'])),
|
||||
ignoreLight: PD.Boolean(false),
|
||||
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['gaussian-surface', 'spacefill', 'point', 'orientation'] as const)),
|
||||
};
|
||||
export type CellpackPackingPresetParams = PD.ValuesFor<typeof CellpackPackingPresetParams>
|
||||
|
||||
@@ -27,7 +28,9 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
|
||||
|
||||
const reprProps = {
|
||||
ignoreHydrogens: true,
|
||||
traceOnly: params.traceOnly
|
||||
traceOnly: params.traceOnly,
|
||||
instanceGranularity: true,
|
||||
ignoreLight: params.ignoreLight,
|
||||
};
|
||||
const components = {
|
||||
polymer: await presetStaticComponent(plugin, structureCell, 'polymer')
|
||||
@@ -37,8 +40,8 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
|
||||
Object.assign(reprProps, {
|
||||
quality: 'custom', resolution: 10, radiusOffset: 2, doubleSided: false
|
||||
});
|
||||
} else if (params.representation === 'spacefill' && params.traceOnly) {
|
||||
Object.assign(reprProps, { sizeFactor: 2 });
|
||||
} else if (params.representation === 'spacefill') {
|
||||
Object.assign(reprProps, { sizeFactor: params.traceOnly ? 2 : 1 });
|
||||
}
|
||||
|
||||
// default is generated
|
||||
@@ -57,7 +60,8 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
|
||||
//
|
||||
|
||||
export const CellpackMembranePresetParams = {
|
||||
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['gaussian-surface', 'spacefill', 'point', 'orientation'])),
|
||||
ignoreLight: PD.Boolean(false),
|
||||
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['gaussian-surface', 'spacefill', 'point', 'orientation'] as const)),
|
||||
};
|
||||
export type CellpackMembranePresetParams = PD.ValuesFor<typeof CellpackMembranePresetParams>
|
||||
|
||||
@@ -71,6 +75,8 @@ export const CellpackMembranePreset = StructureRepresentationPresetProvider({
|
||||
|
||||
const reprProps = {
|
||||
ignoreHydrogens: true,
|
||||
instanceGranularity: true,
|
||||
ignoreLight: params.ignoreLight,
|
||||
};
|
||||
const components = {
|
||||
membrane: await presetStaticComponent(plugin, structureCell, 'all', { label: 'Membrane' })
|
||||
@@ -84,7 +90,7 @@ export const CellpackMembranePreset = StructureRepresentationPresetProvider({
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, {});
|
||||
const representations = {
|
||||
membrane: builder.buildRepresentation(update, components.membrane, { type: 'gaussian-surface', typeParams: { ...typeParams, ...reprProps }, color: 'uniform', colorParams: { value: ColorNames.lightgrey } }, { tag: 'all' })
|
||||
membrane: builder.buildRepresentation(update, components.membrane, { type: params.representation, typeParams: { ...typeParams, ...reprProps }, color: 'uniform', colorParams: { value: ColorNames.lightgrey } }, { tag: 'all' })
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { ConfalPyramidsColorThemeProvider } from './color';
|
||||
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
|
||||
import { ConfalPyramidsRepresentationProvider } from './representation';
|
||||
import { Loci } from '../../../mol-model/loci';
|
||||
import { ConfalPyramidsTypes } from './types';
|
||||
import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
|
||||
import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { StateObjectRef } from '../../../mol-state';
|
||||
@@ -56,21 +56,10 @@ export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean,
|
||||
description: 'Schematic depiction of conformer class and confal value.',
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> {
|
||||
|
||||
private provider = ConfalPyramidsProvider;
|
||||
|
||||
private labelConfalPyramids = {
|
||||
label: (loci: Loci): string | undefined => {
|
||||
if (!this.params.showToolTip) return void 0;
|
||||
|
||||
/* TODO: Implement this */
|
||||
return void 0;
|
||||
}
|
||||
};
|
||||
|
||||
register(): void {
|
||||
this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
|
||||
this.ctx.managers.lociLabels.addProvider(this.labelConfalPyramids);
|
||||
|
||||
this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider);
|
||||
this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider);
|
||||
@@ -88,7 +77,6 @@ export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean,
|
||||
|
||||
unregister() {
|
||||
this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name);
|
||||
this.ctx.managers.lociLabels.removeProvider(this.labelConfalPyramids);
|
||||
|
||||
this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider);
|
||||
this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider);
|
||||
@@ -101,3 +89,13 @@ export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean,
|
||||
showToolTip: PD.Boolean(true)
|
||||
})
|
||||
});
|
||||
|
||||
export function confalPyramidLabel(halfPyramid: ConfalPyramidsTypes.HalfPyramid) {
|
||||
const { step } = halfPyramid;
|
||||
return `
|
||||
<b>${step.auth_asym_id_1}</b> |
|
||||
<b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}
|
||||
${step.label_comp_id_2} ${step.auth_seq_id_2}${step.PDB_ins_code_2}${step.label_alt_id_2.length > 0 ? ` (alt ${step.label_alt_id_2})` : ''} </b><br />
|
||||
<i>NtC:</i> ${step.NtC} | <i>Confal score:</i> ${step.confal_score} | <i>RMSD:</i> ${step.rmsd.toFixed(2)}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -11,149 +11,248 @@ import { Location } from '../../../mol-model/location';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { ColorTheme } from '../../../mol-theme/color';
|
||||
import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { Color, ColorMap } from '../../../mol-util/color';
|
||||
import { getColorMapParams } from '../../../mol-util/color/params';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TableLegend } from '../../../mol-util/legend';
|
||||
import { iterableToArray } from '../../../mol-data/util';
|
||||
import { ObjectKeys } from '../../../mol-util/type-helpers';
|
||||
|
||||
const DefaultColor = Color(0xCCCCCC);
|
||||
const Description = 'Assigns colors to confal pyramids';
|
||||
|
||||
const DefaultClassColors = {
|
||||
A: 0xFFC1C1,
|
||||
B: 0xC8CFFF,
|
||||
BII: 0x0059DA,
|
||||
miB: 0x3BE8FB,
|
||||
Z: 0x01F60E,
|
||||
IC: 0xFA5CFB,
|
||||
OPN: 0xE90000,
|
||||
SYN: 0xFFFF01,
|
||||
N: 0xF2F2F2,
|
||||
};
|
||||
const ErrorColor = Color(0xFFA10A);
|
||||
|
||||
type ConformerClasses = 'A' | 'B' | 'BII' | 'miB' | 'Z' | 'IC' | 'OPN' | 'SYN' | 'N';
|
||||
const PyramidsColors = ColorMap({
|
||||
NANT_Upr: DefaultClassColors.N,
|
||||
NANT_Lwr: DefaultClassColors.N,
|
||||
AA00_Upr: DefaultClassColors.A,
|
||||
AA00_Lwr: DefaultClassColors.A,
|
||||
AA02_Upr: DefaultClassColors.A,
|
||||
AA02_Lwr: DefaultClassColors.A,
|
||||
AA03_Upr: DefaultClassColors.A,
|
||||
AA03_Lwr: DefaultClassColors.A,
|
||||
AA04_Upr: DefaultClassColors.A,
|
||||
AA04_Lwr: DefaultClassColors.A,
|
||||
AA08_Upr: DefaultClassColors.A,
|
||||
AA08_Lwr: DefaultClassColors.A,
|
||||
AA09_Upr: DefaultClassColors.A,
|
||||
AA09_Lwr: DefaultClassColors.A,
|
||||
AA01_Upr: DefaultClassColors.A,
|
||||
AA01_Lwr: DefaultClassColors.A,
|
||||
AA05_Upr: DefaultClassColors.A,
|
||||
AA05_Lwr: DefaultClassColors.A,
|
||||
AA06_Upr: DefaultClassColors.A,
|
||||
AA06_Lwr: DefaultClassColors.A,
|
||||
AA10_Upr: DefaultClassColors.A,
|
||||
AA10_Lwr: DefaultClassColors.A,
|
||||
AA11_Upr: DefaultClassColors.A,
|
||||
AA11_Lwr: DefaultClassColors.A,
|
||||
AA07_Upr: DefaultClassColors.A,
|
||||
AA07_Lwr: DefaultClassColors.A,
|
||||
AA12_Upr: DefaultClassColors.A,
|
||||
AA12_Lwr: DefaultClassColors.A,
|
||||
AA13_Upr: DefaultClassColors.A,
|
||||
AA13_Lwr: DefaultClassColors.A,
|
||||
AB01_Upr: DefaultClassColors.A,
|
||||
AB01_Lwr: DefaultClassColors.B,
|
||||
AB02_Upr: DefaultClassColors.A,
|
||||
AB02_Lwr: DefaultClassColors.B,
|
||||
AB03_Upr: DefaultClassColors.A,
|
||||
AB03_Lwr: DefaultClassColors.B,
|
||||
AB04_Upr: DefaultClassColors.A,
|
||||
AB04_Lwr: DefaultClassColors.B,
|
||||
AB05_Upr: DefaultClassColors.A,
|
||||
AB05_Lwr: DefaultClassColors.B,
|
||||
BA01_Upr: DefaultClassColors.B,
|
||||
BA01_Lwr: DefaultClassColors.A,
|
||||
BA05_Upr: DefaultClassColors.B,
|
||||
BA05_Lwr: DefaultClassColors.A,
|
||||
BA09_Upr: DefaultClassColors.B,
|
||||
BA09_Lwr: DefaultClassColors.A,
|
||||
BA08_Upr: DefaultClassColors.BII,
|
||||
BA08_Lwr: DefaultClassColors.A,
|
||||
BA10_Upr: DefaultClassColors.B,
|
||||
BA10_Lwr: DefaultClassColors.A,
|
||||
BA13_Upr: DefaultClassColors.BII,
|
||||
BA13_Lwr: DefaultClassColors.A,
|
||||
BA16_Upr: DefaultClassColors.BII,
|
||||
BA16_Lwr: DefaultClassColors.A,
|
||||
BA17_Upr: DefaultClassColors.BII,
|
||||
BA17_Lwr: DefaultClassColors.A,
|
||||
BB00_Upr: DefaultClassColors.B,
|
||||
BB00_Lwr: DefaultClassColors.B,
|
||||
BB01_Upr: DefaultClassColors.B,
|
||||
BB01_Lwr: DefaultClassColors.B,
|
||||
BB17_Upr: DefaultClassColors.B,
|
||||
BB17_Lwr: DefaultClassColors.B,
|
||||
BB02_Upr: DefaultClassColors.B,
|
||||
BB02_Lwr: DefaultClassColors.B,
|
||||
BB03_Upr: DefaultClassColors.B,
|
||||
BB03_Lwr: DefaultClassColors.B,
|
||||
BB11_Upr: DefaultClassColors.B,
|
||||
BB11_Lwr: DefaultClassColors.B,
|
||||
BB16_Upr: DefaultClassColors.B,
|
||||
BB16_Lwr: DefaultClassColors.B,
|
||||
BB04_Upr: DefaultClassColors.B,
|
||||
BB04_Lwr: DefaultClassColors.BII,
|
||||
BB05_Upr: DefaultClassColors.B,
|
||||
BB05_Lwr: DefaultClassColors.BII,
|
||||
BB07_Upr: DefaultClassColors.BII,
|
||||
BB07_Lwr: DefaultClassColors.BII,
|
||||
BB08_Upr: DefaultClassColors.BII,
|
||||
BB08_Lwr: DefaultClassColors.BII,
|
||||
BB10_Upr: DefaultClassColors.miB,
|
||||
BB10_Lwr: DefaultClassColors.miB,
|
||||
BB12_Upr: DefaultClassColors.miB,
|
||||
BB12_Lwr: DefaultClassColors.miB,
|
||||
BB13_Upr: DefaultClassColors.miB,
|
||||
BB13_Lwr: DefaultClassColors.miB,
|
||||
BB14_Upr: DefaultClassColors.miB,
|
||||
BB14_Lwr: DefaultClassColors.miB,
|
||||
BB15_Upr: DefaultClassColors.miB,
|
||||
BB15_Lwr: DefaultClassColors.miB,
|
||||
BB20_Upr: DefaultClassColors.miB,
|
||||
BB20_Lwr: DefaultClassColors.miB,
|
||||
IC01_Upr: DefaultClassColors.IC,
|
||||
IC01_Lwr: DefaultClassColors.IC,
|
||||
IC02_Upr: DefaultClassColors.IC,
|
||||
IC02_Lwr: DefaultClassColors.IC,
|
||||
IC03_Upr: DefaultClassColors.IC,
|
||||
IC03_Lwr: DefaultClassColors.IC,
|
||||
IC04_Upr: DefaultClassColors.IC,
|
||||
IC04_Lwr: DefaultClassColors.IC,
|
||||
IC05_Upr: DefaultClassColors.IC,
|
||||
IC05_Lwr: DefaultClassColors.IC,
|
||||
IC06_Upr: DefaultClassColors.IC,
|
||||
IC06_Lwr: DefaultClassColors.IC,
|
||||
IC07_Upr: DefaultClassColors.IC,
|
||||
IC07_Lwr: DefaultClassColors.IC,
|
||||
OP01_Upr: DefaultClassColors.OPN,
|
||||
OP01_Lwr: DefaultClassColors.OPN,
|
||||
OP02_Upr: DefaultClassColors.OPN,
|
||||
OP02_Lwr: DefaultClassColors.OPN,
|
||||
OP03_Upr: DefaultClassColors.OPN,
|
||||
OP03_Lwr: DefaultClassColors.OPN,
|
||||
OP04_Upr: DefaultClassColors.OPN,
|
||||
OP04_Lwr: DefaultClassColors.OPN,
|
||||
OP05_Upr: DefaultClassColors.OPN,
|
||||
OP05_Lwr: DefaultClassColors.OPN,
|
||||
OP06_Upr: DefaultClassColors.OPN,
|
||||
OP06_Lwr: DefaultClassColors.OPN,
|
||||
OP07_Upr: DefaultClassColors.OPN,
|
||||
OP07_Lwr: DefaultClassColors.OPN,
|
||||
OP08_Upr: DefaultClassColors.OPN,
|
||||
OP08_Lwr: DefaultClassColors.OPN,
|
||||
OP09_Upr: DefaultClassColors.OPN,
|
||||
OP09_Lwr: DefaultClassColors.OPN,
|
||||
OP10_Upr: DefaultClassColors.OPN,
|
||||
OP10_Lwr: DefaultClassColors.OPN,
|
||||
OP11_Upr: DefaultClassColors.OPN,
|
||||
OP11_Lwr: DefaultClassColors.OPN,
|
||||
OP12_Upr: DefaultClassColors.OPN,
|
||||
OP12_Lwr: DefaultClassColors.OPN,
|
||||
OP13_Upr: DefaultClassColors.OPN,
|
||||
OP13_Lwr: DefaultClassColors.OPN,
|
||||
OP14_Upr: DefaultClassColors.OPN,
|
||||
OP14_Lwr: DefaultClassColors.OPN,
|
||||
OP15_Upr: DefaultClassColors.OPN,
|
||||
OP15_Lwr: DefaultClassColors.OPN,
|
||||
OP16_Upr: DefaultClassColors.OPN,
|
||||
OP16_Lwr: DefaultClassColors.OPN,
|
||||
OP17_Upr: DefaultClassColors.OPN,
|
||||
OP17_Lwr: DefaultClassColors.OPN,
|
||||
OP18_Upr: DefaultClassColors.OPN,
|
||||
OP18_Lwr: DefaultClassColors.OPN,
|
||||
OP19_Upr: DefaultClassColors.OPN,
|
||||
OP19_Lwr: DefaultClassColors.OPN,
|
||||
OP20_Upr: DefaultClassColors.OPN,
|
||||
OP20_Lwr: DefaultClassColors.OPN,
|
||||
OP21_Upr: DefaultClassColors.OPN,
|
||||
OP21_Lwr: DefaultClassColors.OPN,
|
||||
OP22_Upr: DefaultClassColors.OPN,
|
||||
OP22_Lwr: DefaultClassColors.OPN,
|
||||
OP23_Upr: DefaultClassColors.OPN,
|
||||
OP23_Lwr: DefaultClassColors.OPN,
|
||||
OP24_Upr: DefaultClassColors.OPN,
|
||||
OP24_Lwr: DefaultClassColors.OPN,
|
||||
OP25_Upr: DefaultClassColors.OPN,
|
||||
OP25_Lwr: DefaultClassColors.OPN,
|
||||
OP26_Upr: DefaultClassColors.OPN,
|
||||
OP26_Lwr: DefaultClassColors.OPN,
|
||||
OP27_Upr: DefaultClassColors.OPN,
|
||||
OP27_Lwr: DefaultClassColors.OPN,
|
||||
OP28_Upr: DefaultClassColors.OPN,
|
||||
OP28_Lwr: DefaultClassColors.OPN,
|
||||
OP29_Upr: DefaultClassColors.OPN,
|
||||
OP29_Lwr: DefaultClassColors.OPN,
|
||||
OP30_Upr: DefaultClassColors.OPN,
|
||||
OP30_Lwr: DefaultClassColors.OPN,
|
||||
OP31_Upr: DefaultClassColors.OPN,
|
||||
OP31_Lwr: DefaultClassColors.OPN,
|
||||
OPS1_Upr: DefaultClassColors.OPN,
|
||||
OPS1_Lwr: DefaultClassColors.OPN,
|
||||
OP1S_Upr: DefaultClassColors.OPN,
|
||||
OP1S_Lwr: DefaultClassColors.SYN,
|
||||
AAS1_Upr: DefaultClassColors.SYN,
|
||||
AAS1_Lwr: DefaultClassColors.A,
|
||||
AB1S_Upr: DefaultClassColors.A,
|
||||
AB1S_Lwr: DefaultClassColors.SYN,
|
||||
AB2S_Upr: DefaultClassColors.A,
|
||||
AB2S_Lwr: DefaultClassColors.SYN,
|
||||
BB1S_Upr: DefaultClassColors.B,
|
||||
BB1S_Lwr: DefaultClassColors.SYN,
|
||||
BB2S_Upr: DefaultClassColors.B,
|
||||
BB2S_Lwr: DefaultClassColors.SYN,
|
||||
BBS1_Upr: DefaultClassColors.SYN,
|
||||
BBS1_Lwr: DefaultClassColors.B,
|
||||
ZZ01_Upr: DefaultClassColors.Z,
|
||||
ZZ01_Lwr: DefaultClassColors.Z,
|
||||
ZZ02_Upr: DefaultClassColors.Z,
|
||||
ZZ02_Lwr: DefaultClassColors.Z,
|
||||
ZZ1S_Upr: DefaultClassColors.Z,
|
||||
ZZ1S_Lwr: DefaultClassColors.SYN,
|
||||
ZZ2S_Upr: DefaultClassColors.Z,
|
||||
ZZ2S_Lwr: DefaultClassColors.SYN,
|
||||
ZZS1_Upr: DefaultClassColors.SYN,
|
||||
ZZS1_Lwr: DefaultClassColors.Z,
|
||||
ZZS2_Upr: DefaultClassColors.SYN,
|
||||
ZZS2_Lwr: DefaultClassColors.Z,
|
||||
});
|
||||
type PyramidsColors = typeof PyramidsColors;
|
||||
|
||||
const ColorMapping: ReadonlyMap<ConformerClasses, Color> = new Map([
|
||||
['A', Color(0xFFC1C1)],
|
||||
['B', Color(0xC8CFFF)],
|
||||
['BII', Color(0x0059DA)],
|
||||
['miB', Color(0x3BE8FB)],
|
||||
['Z', Color(0x01F60E)],
|
||||
['IC', Color(0xFA5CFB)],
|
||||
['OPN', Color(0xE90000)],
|
||||
['SYN', Color(0xFFFF01)],
|
||||
['N', Color(0xF2F2F2)],
|
||||
]);
|
||||
export const ConfalPyramidsColorThemeParams = {
|
||||
colors: PD.MappedStatic('default', {
|
||||
'default': PD.EmptyGroup(),
|
||||
'custom': PD.Group(getColorMapParams(PyramidsColors))
|
||||
}),
|
||||
};
|
||||
export type ConfalPyramidsColorThemeParams = typeof ConfalPyramidsColorThemeParams;
|
||||
|
||||
const NtCToClasses: ReadonlyMap<string, [ConformerClasses, ConformerClasses]> = new Map([
|
||||
['NANT', ['N', 'N']],
|
||||
['AA00', ['A', 'A']],
|
||||
['AA02', ['A', 'A']],
|
||||
['AA03', ['A', 'A']],
|
||||
['AA04', ['A', 'A']],
|
||||
['AA08', ['A', 'A']],
|
||||
['AA09', ['A', 'A']],
|
||||
['AA01', ['A', 'A']],
|
||||
['AA05', ['A', 'A']],
|
||||
['AA06', ['A', 'A']],
|
||||
['AA10', ['A', 'A']],
|
||||
['AA11', ['A', 'A']],
|
||||
['AA07', ['A', 'A']],
|
||||
['AA12', ['A', 'A']],
|
||||
['AA13', ['A', 'A']],
|
||||
['AB01', ['A', 'B']],
|
||||
['AB02', ['A', 'B']],
|
||||
['AB03', ['A', 'B']],
|
||||
['AB04', ['A', 'B']],
|
||||
['AB05', ['A', 'B']],
|
||||
['BA01', ['B', 'A']],
|
||||
['BA05', ['B', 'A']],
|
||||
['BA09', ['B', 'A']],
|
||||
['BA08', ['BII', 'A']],
|
||||
['BA10', ['B', 'A']],
|
||||
['BA13', ['BII', 'A']],
|
||||
['BA16', ['BII', 'A']],
|
||||
['BA17', ['BII', 'A']],
|
||||
['BB00', ['B', 'B']],
|
||||
['BB01', ['B', 'B']],
|
||||
['BB17', ['B', 'B']],
|
||||
['BB02', ['B', 'B']],
|
||||
['BB03', ['B', 'B']],
|
||||
['BB11', ['B', 'B']],
|
||||
['BB16', ['B', 'B']],
|
||||
['BB04', ['B', 'BII']],
|
||||
['BB05', ['B', 'BII']],
|
||||
['BB07', ['BII', 'BII']],
|
||||
['BB08', ['BII', 'BII']],
|
||||
['BB10', ['miB', 'miB']],
|
||||
['BB12', ['miB', 'miB']],
|
||||
['BB13', ['miB', 'miB']],
|
||||
['BB14', ['miB', 'miB']],
|
||||
['BB15', ['miB', 'miB']],
|
||||
['BB20', ['miB', 'miB']],
|
||||
['IC01', ['IC', 'IC']],
|
||||
['IC02', ['IC', 'IC']],
|
||||
['IC03', ['IC', 'IC']],
|
||||
['IC04', ['IC', 'IC']],
|
||||
['IC05', ['IC', 'IC']],
|
||||
['IC06', ['IC', 'IC']],
|
||||
['IC07', ['IC', 'IC']],
|
||||
['OP01', ['OPN', 'OPN']],
|
||||
['OP02', ['OPN', 'OPN']],
|
||||
['OP03', ['OPN', 'OPN']],
|
||||
['OP04', ['OPN', 'OPN']],
|
||||
['OP05', ['OPN', 'OPN']],
|
||||
['OP06', ['OPN', 'OPN']],
|
||||
['OP07', ['OPN', 'OPN']],
|
||||
['OP08', ['OPN', 'OPN']],
|
||||
['OP09', ['OPN', 'OPN']],
|
||||
['OP10', ['OPN', 'OPN']],
|
||||
['OP11', ['OPN', 'OPN']],
|
||||
['OP12', ['OPN', 'OPN']],
|
||||
['OP13', ['OPN', 'OPN']],
|
||||
['OP14', ['OPN', 'OPN']],
|
||||
['OP15', ['OPN', 'OPN']],
|
||||
['OP16', ['OPN', 'OPN']],
|
||||
['OP17', ['OPN', 'OPN']],
|
||||
['OP18', ['OPN', 'OPN']],
|
||||
['OP19', ['OPN', 'OPN']],
|
||||
['OP20', ['OPN', 'OPN']],
|
||||
['OP21', ['OPN', 'OPN']],
|
||||
['OP22', ['OPN', 'OPN']],
|
||||
['OP23', ['OPN', 'OPN']],
|
||||
['OP24', ['OPN', 'OPN']],
|
||||
['OP25', ['OPN', 'OPN']],
|
||||
['OP26', ['OPN', 'OPN']],
|
||||
['OP27', ['OPN', 'OPN']],
|
||||
['OP28', ['OPN', 'OPN']],
|
||||
['OP29', ['OPN', 'OPN']],
|
||||
['OP30', ['OPN', 'OPN']],
|
||||
['OP31', ['OPN', 'OPN']],
|
||||
['OPS1', ['OPN', 'OPN']],
|
||||
['OP1S', ['OPN', 'SYN']],
|
||||
['AAS1', ['SYN', 'A']],
|
||||
['AB1S', ['A', 'SYN']],
|
||||
['AB2S', ['A', 'SYN']],
|
||||
['BB1S', ['B', 'SYN']],
|
||||
['BB2S', ['B', 'SYN']],
|
||||
['BBS1', ['SYN', 'B']],
|
||||
['ZZ01', ['Z', 'Z']],
|
||||
['ZZ02', ['Z', 'Z']],
|
||||
['ZZ1S', ['Z', 'SYN']],
|
||||
['ZZ2S', ['Z', 'SYN']],
|
||||
['ZZS1', ['SYN', 'Z']],
|
||||
['ZZS2', ['SYN', 'Z']],
|
||||
]);
|
||||
|
||||
function getConformerColor(ntc: string, useLower: boolean): Color {
|
||||
const item = NtCToClasses.get(ntc);
|
||||
if (!item) return ErrorColor;
|
||||
return ColorMapping.get(useLower ? item[1] : item[0]) ?? ErrorColor;
|
||||
}
|
||||
|
||||
export const ConfalPyramidsColorThemeParams = {};
|
||||
export type ConfalPyramidsColorThemeParams = typeof ConfalPyramidsColorThemeParams
|
||||
export function getConfalPyramidsColorThemeParams(ctx: ThemeDataContext) {
|
||||
return ConfalPyramidsColorThemeParams; // TODO return copy
|
||||
return PD.clone(ConfalPyramidsColorThemeParams);
|
||||
}
|
||||
|
||||
export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values<ConfalPyramidsColorThemeParams>): ColorTheme<ConfalPyramidsColorThemeParams> {
|
||||
const colorMap = props.colors.name === 'default' ? PyramidsColors : props.colors.params;
|
||||
|
||||
function color(location: Location, isSecondary: boolean): Color {
|
||||
if (CPT.isLocation(location)) {
|
||||
const { pyramid, isLower } = location.data;
|
||||
return getConformerColor(pyramid.NtC, isLower);
|
||||
const { step, isLower } = location.data;
|
||||
const key = step.NtC + `_${isLower ? 'Lwr' : 'Upr'}` as keyof PyramidsColors;
|
||||
return colorMap[key] ?? ErrorColor;
|
||||
}
|
||||
|
||||
return DefaultColor;
|
||||
return ErrorColor;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -162,12 +261,7 @@ export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values
|
||||
color,
|
||||
props,
|
||||
description: Description,
|
||||
legend: TableLegend(iterableToArray(ColorMapping.entries()).map(([conformer, color]) => {
|
||||
return [conformer, color] as [string, Color];
|
||||
}).concat([
|
||||
['Error', ErrorColor],
|
||||
['Unknown', DefaultColor]
|
||||
]))
|
||||
legend: TableLegend(ObjectKeys(colorMap).map(k => [k.replace('_', ' '), colorMap[k]] as [string, Color]).concat([['Error', ErrorColor]])),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
|
||||
export type ConfalPyramids = PropertyWrapper<CPT.PyramidsData | undefined >;
|
||||
export type ConfalPyramids = PropertyWrapper<CPT.Steps | undefined>;
|
||||
|
||||
export namespace ConfalPyramids {
|
||||
export const Schema = {
|
||||
@@ -105,36 +105,42 @@ export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramids
|
||||
|
||||
type StepsSummaryTable = Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step_summary>;
|
||||
|
||||
function createPyramidsFromCif(model: Model,
|
||||
steps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
|
||||
stepsSummary: StepsSummaryTable): CPT.PyramidsData {
|
||||
const pyramids = new Array<CPT.Pyramid>();
|
||||
const names = new Map<string, number>();
|
||||
const locations = new Array<CPT.Location>();
|
||||
let hasMultipleModels = false;
|
||||
function createPyramidsFromCif(
|
||||
model: Model,
|
||||
cifSteps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
|
||||
stepsSummary: StepsSummaryTable
|
||||
): CPT.Steps {
|
||||
const steps = new Array<CPT.Step>();
|
||||
const mapping = new Array<CPT.MappedChains>();
|
||||
|
||||
const {
|
||||
id, PDB_model_number, name,
|
||||
auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1,
|
||||
auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2,
|
||||
_rowCount } = steps;
|
||||
_rowCount
|
||||
} = cifSteps;
|
||||
|
||||
if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
|
||||
|
||||
for (let i = 0; i < _rowCount; i++) {
|
||||
const model_num = PDB_model_number.value(i);
|
||||
if (model_num !== model.modelNum) {
|
||||
hasMultipleModels = true;
|
||||
continue; // We are only interested in data for the current model
|
||||
}
|
||||
const {
|
||||
NtC,
|
||||
confal_score,
|
||||
rmsd
|
||||
} = getSummaryData(id.value(i), i, stepsSummary);
|
||||
const modelNum = PDB_model_number.value(i);
|
||||
const chainId = auth_asym_id_1.value(i);
|
||||
const seqId = auth_seq_id_1.value(i);
|
||||
const modelIdx = modelNum - 1;
|
||||
|
||||
const { _NtC, _confal_score } = getNtCAndConfalScore(id.value(i), i, stepsSummary);
|
||||
if (mapping.length <= modelIdx || !mapping[modelIdx])
|
||||
mapping[modelIdx] = new Map<string, CPT.MappedResidues>();
|
||||
|
||||
const pyramid = {
|
||||
PDB_model_number: model_num,
|
||||
const step = {
|
||||
PDB_model_number: modelNum,
|
||||
name: name.value(i),
|
||||
auth_asym_id_1: auth_asym_id_1.value(i),
|
||||
auth_seq_id_1: auth_seq_id_1.value(i),
|
||||
auth_asym_id_1: chainId,
|
||||
auth_seq_id_1: seqId,
|
||||
label_comp_id_1: label_comp_id_1.value(i),
|
||||
label_alt_id_1: label_alt_id_1.value(i),
|
||||
PDB_ins_code_1: PDB_ins_code_1.value(i),
|
||||
@@ -143,30 +149,41 @@ function createPyramidsFromCif(model: Model,
|
||||
label_comp_id_2: label_comp_id_2.value(i),
|
||||
label_alt_id_2: label_alt_id_2.value(i),
|
||||
PDB_ins_code_2: PDB_ins_code_2.value(i),
|
||||
confal_score: _confal_score,
|
||||
NtC: _NtC
|
||||
confal_score,
|
||||
NtC,
|
||||
rmsd,
|
||||
};
|
||||
|
||||
pyramids.push(pyramid);
|
||||
names.set(pyramid.name, pyramids.length - 1);
|
||||
steps.push(step);
|
||||
|
||||
locations.push(CPT.Location(pyramid, false));
|
||||
locations.push(CPT.Location(pyramid, true));
|
||||
const mappedChains = mapping[modelIdx];
|
||||
const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
|
||||
const stepsForResidue = residuesOnChain.get(seqId) ?? [];
|
||||
stepsForResidue.push(steps.length - 1);
|
||||
|
||||
residuesOnChain.set(seqId, stepsForResidue);
|
||||
mappedChains.set(chainId, residuesOnChain);
|
||||
mapping[modelIdx] = mappedChains;
|
||||
}
|
||||
|
||||
return { pyramids, names, locations, hasMultipleModels };
|
||||
return { steps, mapping };
|
||||
}
|
||||
|
||||
function getNtCAndConfalScore(id: number, i: number, stepsSummary: StepsSummaryTable) {
|
||||
const { step_id, confal_score, assigned_NtC } = stepsSummary;
|
||||
function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
|
||||
const {
|
||||
step_id,
|
||||
confal_score,
|
||||
assigned_NtC,
|
||||
cartesian_rmsd_closest_NtC_representative,
|
||||
} = stepsSummary;
|
||||
|
||||
// Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step
|
||||
for (let j = i; j < stepsSummary._rowCount; j++) {
|
||||
if (id === step_id.value(j)) return { _NtC: assigned_NtC.value(j), _confal_score: confal_score.value(j) };
|
||||
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
|
||||
}
|
||||
// Safety net for cases where the previous assumption is not met
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (id === step_id.value(j)) return { _NtC: assigned_NtC.value(j), _confal_score: confal_score.value(j) };
|
||||
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
|
||||
}
|
||||
throw new Error('Inconsistent mmCIF data');
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
|
||||
import { ConfalPyramidsUtil } from './util';
|
||||
import { ConfalPyramidsIterator } from './util';
|
||||
import { ConfalPyramidsTypes as CPT } from './types';
|
||||
import { Interval } from '../../../mol-data/int';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
@@ -16,14 +16,14 @@ import { PrimitiveBuilder } from '../../../mol-geo/primitive/primitive';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { EmptyLoci, Loci } from '../../../mol-model/loci';
|
||||
import { Structure, StructureProperties, Unit } from '../../../mol-model/structure';
|
||||
import { Structure, Unit } from '../../../mol-model/structure';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
|
||||
import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation';
|
||||
import { UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { VisualContext } from '../../../mol-repr/visual';
|
||||
import { getAltResidueLociFromId, StructureGroup } from '../../../mol-repr/structure/visual/util/common';
|
||||
import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
|
||||
import { NullLocation } from '../../../mol-model/location';
|
||||
@@ -32,6 +32,12 @@ const t = Mat4.identity();
|
||||
const w = Vec3.zero();
|
||||
const mp = Vec3.zero();
|
||||
|
||||
const posO3 = Vec3();
|
||||
const posP = Vec3();
|
||||
const posOP1 = Vec3();
|
||||
const posOP2 = Vec3();
|
||||
const posO5 = Vec3();
|
||||
|
||||
function calcMidpoint(mp: Vec3, v: Vec3, w: Vec3) {
|
||||
Vec3.sub(mp, v, w);
|
||||
Vec3.scale(mp, mp, 0.5);
|
||||
@@ -53,64 +59,76 @@ function createConfalPyramidsIterator(structureGroup: StructureGroup): LocationI
|
||||
const { structure, group } = structureGroup;
|
||||
const instanceCount = group.units.length;
|
||||
|
||||
const prop = ConfalPyramidsProvider.get(structure.model).value;
|
||||
if (prop === undefined || prop.data === undefined) {
|
||||
return LocationIterator(0, 1, 1, () => NullLocation);
|
||||
}
|
||||
const data = ConfalPyramidsProvider.get(structure.model)?.value?.data;
|
||||
if (!data) return LocationIterator(0, 1, 1, () => NullLocation);
|
||||
|
||||
const { locations } = prop.data;
|
||||
const halfPyramidsCount = data.steps.length * 2;
|
||||
|
||||
const getLocation = (groupIndex: number, instanceIndex: number) => {
|
||||
if (locations.length <= groupIndex) return NullLocation;
|
||||
return locations[groupIndex];
|
||||
if (halfPyramidsCount <= groupIndex) return NullLocation;
|
||||
const idx = Math.floor(groupIndex / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation
|
||||
return CPT.Location(data.steps[idx], groupIndex % 2 === 1);
|
||||
};
|
||||
return LocationIterator(locations.length, instanceCount, 1, getLocation);
|
||||
return LocationIterator(halfPyramidsCount, instanceCount, 1, getLocation);
|
||||
}
|
||||
|
||||
function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ConfalPyramidsMeshParams>, mesh?: Mesh) {
|
||||
if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
|
||||
|
||||
const prop = ConfalPyramidsProvider.get(structure.model).value;
|
||||
if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh);
|
||||
const data = ConfalPyramidsProvider.get(structure.model)?.value?.data;
|
||||
if (!data) return Mesh.createEmpty(mesh);
|
||||
|
||||
const { pyramids } = prop.data;
|
||||
if (pyramids.length === 0) return Mesh.createEmpty(mesh);
|
||||
const { steps, mapping } = data;
|
||||
if (steps.length === 0) return Mesh.createEmpty(mesh);
|
||||
const vertexCount = (6 * steps.length) / mapping.length;
|
||||
|
||||
const mb = MeshBuilder.createState(512, 512, mesh);
|
||||
const mb = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh);
|
||||
|
||||
const handler = (pyramid: CPT.Pyramid, first: ConfalPyramidsUtil.FirstResidueAtoms, second: ConfalPyramidsUtil.SecondResidueAtoms, firsLocIndex: number, secondLocIndex: number) => {
|
||||
if (firsLocIndex === -1 || secondLocIndex === -1)
|
||||
throw new Error('Invalid location index');
|
||||
const it = new ConfalPyramidsIterator(structure, unit);
|
||||
while (it.hasNext) {
|
||||
const allPoints = it.move();
|
||||
|
||||
const scale = (pyramid.confal_score - 20.0) / 100.0;
|
||||
const O3 = first.O3.pos;
|
||||
const OP1 = second.OP1.pos; const OP2 = second.OP2.pos; const O5 = second.O5.pos; const P = second.P.pos;
|
||||
for (const points of allPoints) {
|
||||
const { O3, P, OP1, OP2, O5, confalScore } = points;
|
||||
const scale = (confalScore - 20.0) / 100.0;
|
||||
// Steps can be drawn in a different order than they are stored.
|
||||
// To make sure that we can get from the drawn pyramid back to the step in represents,
|
||||
// we need to use an appropriate groupId. The stepIdx passed from the iterator
|
||||
// is an index into the array of all steps in the structure.
|
||||
// Since a step is drawn as two "half-pyramids" we need two ids to map to a single step.
|
||||
// To do that, we just multiply the index by 2. idx*2 marks the "upper" half-pyramid,
|
||||
// (idx*2)+1 the "lower" half-pyramid.
|
||||
const groupIdx = points.stepIdx * 2;
|
||||
|
||||
shiftVertex(O3, P, scale);
|
||||
shiftVertex(OP1, P, scale);
|
||||
shiftVertex(OP2, P, scale);
|
||||
shiftVertex(O5, P, scale);
|
||||
calcMidpoint(mp, O3, O5);
|
||||
unit.conformation.invariantPosition(O3, posO3);
|
||||
unit.conformation.invariantPosition(P, posP);
|
||||
unit.conformation.invariantPosition(OP1, posOP1);
|
||||
unit.conformation.invariantPosition(OP2, posOP2);
|
||||
unit.conformation.invariantPosition(O5, posO5);
|
||||
|
||||
mb.currentGroup = firsLocIndex;
|
||||
let pb = PrimitiveBuilder(3);
|
||||
/* Upper part (for first residue in step) */
|
||||
pb.add(O3, OP1, OP2);
|
||||
pb.add(O3, mp, OP1);
|
||||
pb.add(O3, OP2, mp);
|
||||
MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
|
||||
shiftVertex(posO3, posP, scale);
|
||||
shiftVertex(posOP1, posP, scale);
|
||||
shiftVertex(posOP2, posP, scale);
|
||||
shiftVertex(posO5, posP, scale);
|
||||
calcMidpoint(mp, posO3, posO5);
|
||||
|
||||
/* Lower part (for second residue in step */
|
||||
mb.currentGroup = secondLocIndex;
|
||||
pb = PrimitiveBuilder(3);
|
||||
pb.add(mp, O5, OP1);
|
||||
pb.add(mp, OP2, O5);
|
||||
pb.add(O5, OP2, OP1);
|
||||
MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
|
||||
};
|
||||
mb.currentGroup = groupIdx;
|
||||
let pb = PrimitiveBuilder(3);
|
||||
/* Upper part (for first residue in step) */
|
||||
pb.add(posO3, posOP1, posOP2);
|
||||
pb.add(posO3, mp, posOP1);
|
||||
pb.add(posO3, posOP2, mp);
|
||||
MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
|
||||
|
||||
const walker = new ConfalPyramidsUtil.UnitWalker(structure, unit, handler);
|
||||
walker.walk();
|
||||
/* Lower part (for second residue in step) */
|
||||
mb.currentGroup = groupIdx + 1;
|
||||
pb = PrimitiveBuilder(3);
|
||||
pb.add(mp, posO5, posOP1);
|
||||
pb.add(mp, posOP2, posO5);
|
||||
pb.add(posO5, posOP2, posOP1);
|
||||
MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
|
||||
}
|
||||
}
|
||||
|
||||
return MeshBuilder.getMesh(mb);
|
||||
}
|
||||
@@ -124,16 +142,17 @@ function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGro
|
||||
const unit = structureGroup.group.units[instanceId];
|
||||
if (!Unit.isAtomic(unit)) return EmptyLoci;
|
||||
|
||||
const prop = ConfalPyramidsProvider.get(structure.model).value;
|
||||
if (prop === undefined || prop.data === undefined) return EmptyLoci;
|
||||
const data = ConfalPyramidsProvider.get(structure.model)?.value?.data;
|
||||
if (!data) return EmptyLoci;
|
||||
|
||||
const { locations } = prop.data;
|
||||
const halfPyramidsCount = data.steps.length * 2;
|
||||
|
||||
if (locations.length <= groupId) return EmptyLoci;
|
||||
const altId = StructureProperties.atom.label_alt_id(CPT.toElementLocation(locations[groupId]));
|
||||
const rI = unit.residueIndex[locations[groupId].element.element];
|
||||
if (halfPyramidsCount <= groupId) return EmptyLoci;
|
||||
|
||||
return getAltResidueLociFromId(structure, unit, rI, altId);
|
||||
const idx = Math.floor(groupId / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation
|
||||
const step = data.steps[idx];
|
||||
|
||||
return CPT.Loci({ step, isLower: groupId % 2 === 1 }, [{}]);
|
||||
}
|
||||
|
||||
function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
*/
|
||||
|
||||
import { DataLocation } from '../../../mol-model/location';
|
||||
import { ElementIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
|
||||
import { DataLoci } from '../../../mol-model/loci';
|
||||
import { confalPyramidLabel } from './behavior';
|
||||
|
||||
export namespace ConfalPyramidsTypes {
|
||||
export type Pyramid = {
|
||||
export const DataTag = 'dnatco-confal-half-pyramid';
|
||||
|
||||
export type Step = {
|
||||
PDB_model_number: number,
|
||||
name: string,
|
||||
auth_asym_id_1: string,
|
||||
@@ -23,38 +26,40 @@ export namespace ConfalPyramidsTypes {
|
||||
label_alt_id_2: string,
|
||||
PDB_ins_code_2: string,
|
||||
confal_score: number,
|
||||
NtC: string
|
||||
NtC: string,
|
||||
rmsd: number,
|
||||
}
|
||||
|
||||
export interface PyramidsData {
|
||||
pyramids: Array<Pyramid>,
|
||||
names: Map<string, number>,
|
||||
locations: Array<Location>,
|
||||
hasMultipleModels: boolean
|
||||
export type MappedChains = Map<string, MappedResidues>;
|
||||
export type MappedResidues = Map<number, number[]>;
|
||||
|
||||
export interface Steps {
|
||||
steps: Array<Step>,
|
||||
mapping: MappedChains[],
|
||||
}
|
||||
|
||||
export interface LocationData {
|
||||
readonly pyramid: Pyramid
|
||||
readonly isLower: boolean;
|
||||
export interface HalfPyramid {
|
||||
step: Step,
|
||||
isLower: boolean,
|
||||
}
|
||||
|
||||
export interface Element {
|
||||
structure: Structure;
|
||||
unit: Unit.Atomic;
|
||||
element: ElementIndex;
|
||||
}
|
||||
export interface Location extends DataLocation<HalfPyramid, {}> {}
|
||||
|
||||
export interface Location extends DataLocation<LocationData, Element> {}
|
||||
|
||||
export function Location(pyramid: Pyramid, isLower: boolean, structure?: Structure, unit?: Unit.Atomic, element?: ElementIndex) {
|
||||
return DataLocation('pyramid', { pyramid, isLower }, { structure: structure as any, unit: unit as any, element: element as any });
|
||||
export function Location(step: Step, isLower: boolean) {
|
||||
return DataLocation(DataTag, { step, isLower }, {});
|
||||
}
|
||||
|
||||
export function isLocation(x: any): x is Location {
|
||||
return !!x && x.kind === 'data-location' && x.tag === 'pyramid';
|
||||
return !!x && x.kind === 'data-location' && x.tag === DataTag;
|
||||
}
|
||||
|
||||
export function toElementLocation(location: Location) {
|
||||
return StructureElement.Location.create(location.element.structure, location.element.unit, location.element.element);
|
||||
export interface Loci extends DataLoci<HalfPyramid, {}> {}
|
||||
|
||||
export function Loci(data: HalfPyramid, elements: ReadonlyArray<{}>): Loci {
|
||||
return DataLoci(DataTag, data, elements, undefined, () => confalPyramidLabel(data));
|
||||
}
|
||||
|
||||
export function isLoci(x: any): x is Loci {
|
||||
return !!x && x.kind === 'data-loci' && x.tag === DataTag;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,293 +7,121 @@
|
||||
|
||||
import { ConfalPyramidsProvider } from './property';
|
||||
import { ConfalPyramidsTypes as CPT } from './types';
|
||||
import { OrderedSet, Segmentation } from '../../../mol-data/int';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { Segmentation } from '../../../mol-data/int';
|
||||
import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
|
||||
|
||||
export namespace ConfalPyramidsUtil {
|
||||
type Residue = Segmentation.Segment<ResidueIndex>;
|
||||
type Residue = Segmentation.Segment<ResidueIndex>;
|
||||
|
||||
export type AtomInfo = {
|
||||
pos: Vec3,
|
||||
index: ElementIndex,
|
||||
fakeAltId: string,
|
||||
};
|
||||
export type Pyramid = {
|
||||
O3: ElementIndex,
|
||||
P: ElementIndex,
|
||||
OP1: ElementIndex,
|
||||
OP2: ElementIndex,
|
||||
O5: ElementIndex,
|
||||
confalScore: number,
|
||||
stepIdx: number,
|
||||
};
|
||||
|
||||
export type FirstResidueAtoms = {
|
||||
O3: AtomInfo,
|
||||
};
|
||||
const EmptyStepIndices = new Array<number>();
|
||||
|
||||
export type SecondResidueAtoms = {
|
||||
OP1: AtomInfo,
|
||||
OP2: AtomInfo,
|
||||
O5: AtomInfo,
|
||||
P: AtomInfo,
|
||||
};
|
||||
function copyResidue(r?: Residue) {
|
||||
return r ? { index: r.index, start: r.start, end: r.end } : void 0;
|
||||
}
|
||||
|
||||
type ResidueInfo = {
|
||||
PDB_model_num: number,
|
||||
asym_id: string,
|
||||
auth_asym_id: string,
|
||||
seq_id: number,
|
||||
auth_seq_id: number,
|
||||
comp_id: string,
|
||||
alt_id: string,
|
||||
ins_code: string,
|
||||
};
|
||||
function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string): ElementIndex {
|
||||
for (let eI = residue.start; eI < residue.end; eI++) {
|
||||
loc.element = loc.unit.elements[eI];
|
||||
const elName = StructureProperties.atom.label_atom_id(loc);
|
||||
const elAltId = StructureProperties.atom.label_alt_id(loc);
|
||||
|
||||
export type Handler = (pyramid: CPT.Pyramid, first: FirstResidueAtoms, second: SecondResidueAtoms, firstLocIndex: number, secondLocIndex: number) => void;
|
||||
|
||||
function residueInfoFromLocation(loc: StructureElement.Location): ResidueInfo {
|
||||
return {
|
||||
PDB_model_num: StructureProperties.unit.model_num(loc),
|
||||
asym_id: StructureProperties.chain.label_asym_id(loc),
|
||||
auth_asym_id: StructureProperties.chain.auth_asym_id(loc),
|
||||
seq_id: StructureProperties.residue.label_seq_id(loc),
|
||||
auth_seq_id: StructureProperties.residue.auth_seq_id(loc),
|
||||
comp_id: StructureProperties.atom.label_comp_id(loc),
|
||||
alt_id: StructureProperties.atom.label_alt_id(loc),
|
||||
ins_code: StructureProperties.residue.pdbx_PDB_ins_code(loc)
|
||||
};
|
||||
if (names.includes(elName) && (elAltId === altId || elAltId.length === 0))
|
||||
return loc.element;
|
||||
}
|
||||
|
||||
export function hasMultipleModels(unit: Unit.Atomic): boolean {
|
||||
return -1 as ElementIndex;
|
||||
}
|
||||
|
||||
function getPyramid(loc: StructureElement.Location, one: Residue, two: Residue, altIdOne: string, altIdTwo: string, confalScore: number, stepIdx: number): Pyramid {
|
||||
const O3 = getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne);
|
||||
const P = getAtomIndex(loc, two, ['P'], altIdTwo);
|
||||
const OP1 = getAtomIndex(loc, two, ['OP1'], altIdTwo);
|
||||
const OP2 = getAtomIndex(loc, two, ['OP2'], altIdTwo);
|
||||
const O5 = getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo);
|
||||
|
||||
return { O3, P, OP1, OP2, O5, confalScore, stepIdx };
|
||||
}
|
||||
|
||||
export class ConfalPyramidsIterator {
|
||||
private chainIt: Segmentation.SegmentIterator<ChainIndex>;
|
||||
private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
|
||||
private residueOne?: Residue;
|
||||
private residueTwo: Residue;
|
||||
private data?: CPT.Steps;
|
||||
private loc: StructureElement.Location;
|
||||
|
||||
private getStepIndices(r: Residue) {
|
||||
this.loc.element = this.loc.unit.elements[r.start];
|
||||
|
||||
const modelIdx = StructureProperties.unit.model_num(this.loc) - 1;
|
||||
const chainId = StructureProperties.chain.auth_asym_id(this.loc);
|
||||
const seqId = StructureProperties.residue.auth_seq_id(this.loc);
|
||||
|
||||
const chains = this.data!.mapping[modelIdx];
|
||||
if (!chains) return EmptyStepIndices;
|
||||
const residues = chains.get(chainId);
|
||||
if (!residues) return EmptyStepIndices;
|
||||
return residues.get(seqId) ?? EmptyStepIndices;
|
||||
}
|
||||
|
||||
private moveStep() {
|
||||
this.residueOne = copyResidue(this.residueTwo);
|
||||
this.residueTwo = copyResidue(this.residueIt.move())!;
|
||||
|
||||
return this.toPyramids(this.residueOne!, this.residueTwo);
|
||||
}
|
||||
|
||||
private toPyramids(one: Residue, two: Residue) {
|
||||
const indices = this.getStepIndices(one);
|
||||
|
||||
const points = [];
|
||||
for (const idx of indices) {
|
||||
const step = this.data!.steps[idx];
|
||||
points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.confal_score, idx));
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
constructor(structure: Structure, unit: Unit) {
|
||||
this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
|
||||
this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
|
||||
|
||||
const prop = ConfalPyramidsProvider.get(unit.model).value;
|
||||
if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data');
|
||||
return prop.data.hasMultipleModels;
|
||||
this.data = prop?.data;
|
||||
|
||||
if (this.chainIt.hasNext) {
|
||||
this.residueIt.setSegment(this.chainIt.move());
|
||||
if (this.residueIt.hasNext)
|
||||
this.residueTwo = this.residueIt.move();
|
||||
}
|
||||
|
||||
this.loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
|
||||
}
|
||||
|
||||
function getPossibleAltIdsIndices(eIFirst: ElementIndex, eILast: ElementIndex, structure: Structure, unit: Unit.Atomic): string[] {
|
||||
const loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
|
||||
|
||||
const uIFirst = OrderedSet.indexOf(unit.elements, eIFirst);
|
||||
const uILast = OrderedSet.indexOf(unit.elements, eILast);
|
||||
|
||||
const possibleAltIds: string[] = [];
|
||||
for (let uI = uIFirst; uI <= uILast; uI++) {
|
||||
loc.element = unit.elements[uI];
|
||||
const altId = StructureProperties.atom.label_alt_id(loc);
|
||||
if (altId !== '' && !possibleAltIds.includes(altId)) possibleAltIds.push(altId);
|
||||
}
|
||||
|
||||
return possibleAltIds;
|
||||
get hasNext() {
|
||||
if (!this.data)
|
||||
return false;
|
||||
return this.residueIt.hasNext
|
||||
? true
|
||||
: this.chainIt.hasNext;
|
||||
}
|
||||
|
||||
function getPossibleAltIdsResidue(residue: Residue, structure: Structure, unit: Unit.Atomic): string[] {
|
||||
return getPossibleAltIdsIndices(unit.elements[residue.start], unit.elements[residue.end - 1], structure, unit);
|
||||
}
|
||||
|
||||
class Utility {
|
||||
protected getPyramidByName(name: string): { pyramid: CPT.Pyramid | undefined, index: number } {
|
||||
const index = this.data.names.get(name);
|
||||
if (index === undefined) return { pyramid: undefined, index: -1 };
|
||||
|
||||
return { pyramid: this.data.pyramids[index], index };
|
||||
move() {
|
||||
if (this.residueIt.hasNext) {
|
||||
return this.moveStep();
|
||||
} else {
|
||||
this.residueIt.setSegment(this.chainIt.move());
|
||||
return this.moveStep();
|
||||
}
|
||||
|
||||
protected stepToName(entry_id: string, modelNum: number, locFirst: StructureElement.Location, locSecond: StructureElement.Location, fakeAltId_1: string, fakeAltId_2: string) {
|
||||
const first = residueInfoFromLocation(locFirst);
|
||||
const second = residueInfoFromLocation(locSecond);
|
||||
const model_id = this.hasMultipleModels ? `-m${modelNum}` : '';
|
||||
const alt_id_1 = fakeAltId_1 !== '' ? `.${fakeAltId_1}` : (first.alt_id.length ? `.${first.alt_id}` : '');
|
||||
const alt_id_2 = fakeAltId_2 !== '' ? `.${fakeAltId_2}` : (second.alt_id.length ? `.${second.alt_id}` : '');
|
||||
const ins_code_1 = first.ins_code.length ? `.${first.ins_code}` : '';
|
||||
const ins_code_2 = second.ins_code.length ? `.${second.ins_code}` : '';
|
||||
|
||||
return `${entry_id}${model_id}_${first.auth_asym_id}_${first.comp_id}${alt_id_1}_${first.auth_seq_id}${ins_code_1}_${second.comp_id}${alt_id_2}_${second.auth_seq_id}${ins_code_2}`;
|
||||
}
|
||||
|
||||
constructor(unit: Unit.Atomic) {
|
||||
const prop = ConfalPyramidsProvider.get(unit.model).value;
|
||||
if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data');
|
||||
|
||||
this.data = prop.data;
|
||||
this.hasMultipleModels = hasMultipleModels(unit);
|
||||
|
||||
this.entryId = unit.model.entryId.toLowerCase();
|
||||
this.modelNum = unit.model.modelNum;
|
||||
}
|
||||
|
||||
protected readonly data: CPT.PyramidsData;
|
||||
protected readonly hasMultipleModels: boolean;
|
||||
protected readonly entryId: string;
|
||||
protected readonly modelNum: number;
|
||||
}
|
||||
|
||||
export class UnitWalker extends Utility {
|
||||
private getAtomIndices(names: string[], residue: Residue): ElementIndex[] {
|
||||
let rI = residue.start;
|
||||
const rILast = residue.end - 1;
|
||||
const indices: ElementIndex[] = [];
|
||||
|
||||
for (; rI !== rILast; rI++) {
|
||||
const eI = this.unit.elements[rI];
|
||||
const loc = StructureElement.Location.create(this.structure, this.unit, eI);
|
||||
const thisName = StructureProperties.atom.label_atom_id(loc);
|
||||
if (names.includes(thisName)) indices.push(eI);
|
||||
}
|
||||
|
||||
if (indices.length === 0)
|
||||
throw new Error(`Element ${name} not found on residue ${residue.index}`);
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
private getAtomPositions(indices: ElementIndex[]): Vec3[] {
|
||||
const pos = this.unit.conformation.invariantPosition;
|
||||
const positions: Vec3[] = [];
|
||||
|
||||
for (const eI of indices) {
|
||||
const v = Vec3.zero();
|
||||
pos(eI, v);
|
||||
positions.push(v);
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
private handleStep(firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[]) {
|
||||
const modelNum = this.hasMultipleModels ? this.modelNum : -1;
|
||||
let ok = false;
|
||||
|
||||
const firstLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex);
|
||||
const secondLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex);
|
||||
for (let i = 0; i < firstAtoms.length; i++) {
|
||||
const first = firstAtoms[i];
|
||||
for (let j = 0; j < secondAtoms.length; j++) {
|
||||
const second = secondAtoms[j];
|
||||
firstLoc.element = first.O3.index;
|
||||
secondLoc.element = second.OP1.index;
|
||||
|
||||
const name = this.stepToName(this.entryId, modelNum, firstLoc, secondLoc, first.O3.fakeAltId, second.OP1.fakeAltId);
|
||||
const { pyramid, index } = this.getPyramidByName(name);
|
||||
if (pyramid !== undefined) {
|
||||
const setLoc = (loc: CPT.Location, eI: ElementIndex) => {
|
||||
loc.element.structure = this.structure;
|
||||
loc.element.unit = this.unit;
|
||||
loc.element.element = eI;
|
||||
};
|
||||
|
||||
const locIndex = index * 2;
|
||||
setLoc(this.data.locations[locIndex], firstLoc.element);
|
||||
setLoc(this.data.locations[locIndex + 1], secondLoc.element);
|
||||
this.handler(pyramid, first, second, locIndex, locIndex + 1);
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok) throw new Error('Bogus step');
|
||||
}
|
||||
|
||||
private processFirstResidue(residue: Residue, possibleAltIds: string[]) {
|
||||
const indO3 = this.getAtomIndices(['O3\'', 'O3*'], residue);
|
||||
const posO3 = this.getAtomPositions(indO3);
|
||||
|
||||
const altPos: FirstResidueAtoms[] = [
|
||||
{ O3: { pos: posO3[0], index: indO3[0], fakeAltId: '' } }
|
||||
];
|
||||
|
||||
for (let i = 1; i < indO3.length; i++) {
|
||||
altPos.push({ O3: { pos: posO3[i], index: indO3[i], fakeAltId: '' } });
|
||||
}
|
||||
|
||||
if (altPos.length === 1 && possibleAltIds.length > 1) {
|
||||
/* We have some alternate positions on the residue but O3 does not have any - fake them */
|
||||
altPos[0].O3.fakeAltId = possibleAltIds[0];
|
||||
|
||||
for (let i = 1; i < possibleAltIds.length; i++)
|
||||
altPos.push({ O3: { pos: posO3[0], index: indO3[0], fakeAltId: possibleAltIds[i] } });
|
||||
}
|
||||
|
||||
return altPos;
|
||||
}
|
||||
|
||||
private processSecondResidue(residue: Residue, possibleAltIds: string[]) {
|
||||
const indOP1 = this.getAtomIndices(['OP1'], residue);
|
||||
const indOP2 = this.getAtomIndices(['OP2'], residue);
|
||||
const indO5 = this.getAtomIndices(['O5\'', 'O5*'], residue);
|
||||
const indP = this.getAtomIndices(['P'], residue);
|
||||
|
||||
const posOP1 = this.getAtomPositions(indOP1);
|
||||
const posOP2 = this.getAtomPositions(indOP2);
|
||||
const posO5 = this.getAtomPositions(indO5);
|
||||
const posP = this.getAtomPositions(indP);
|
||||
|
||||
const infoOP1: AtomInfo[] = [];
|
||||
/* We use OP1 as "pivotal" atom. There is no specific reason
|
||||
* to pick OP1, it is as good a choice as any other atom
|
||||
*/
|
||||
if (indOP1.length === 1 && possibleAltIds.length > 1) {
|
||||
/* No altIds on OP1, fake them */
|
||||
for (const altId of possibleAltIds)
|
||||
infoOP1.push({ pos: posOP1[0], index: indOP1[0], fakeAltId: altId });
|
||||
} else {
|
||||
for (let i = 0; i < indOP1.length; i++)
|
||||
infoOP1.push({ pos: posOP1[i], index: indOP1[i], fakeAltId: '' });
|
||||
}
|
||||
|
||||
const mkInfo = (i: number, indices: ElementIndex[], positions: Vec3[], altId: string) => {
|
||||
if (i >= indices.length) {
|
||||
const last = indices.length - 1;
|
||||
return { pos: positions[last], index: indices[last], fakeAltId: altId };
|
||||
}
|
||||
|
||||
return { pos: positions[i], index: indices[i], fakeAltId: altId };
|
||||
};
|
||||
|
||||
const altPos: SecondResidueAtoms[] = [];
|
||||
for (let i = 0; i < infoOP1.length; i++) {
|
||||
const altId = infoOP1[i].fakeAltId;
|
||||
|
||||
const OP2 = mkInfo(i, indOP2, posOP2, altId);
|
||||
const O5 = mkInfo(i, indO5, posO5, altId);
|
||||
const P = mkInfo(i, indP, posP, altId);
|
||||
|
||||
altPos.push({ OP1: infoOP1[i], OP2, O5, P });
|
||||
}
|
||||
|
||||
return altPos;
|
||||
}
|
||||
|
||||
private step(residue: Residue): { firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[] } {
|
||||
const firstPossibleAltIds = getPossibleAltIdsResidue(residue, this.structure, this.unit);
|
||||
const firstAtoms = this.processFirstResidue(residue, firstPossibleAltIds);
|
||||
|
||||
residue = this.residueIt.move();
|
||||
|
||||
const secondPossibleAltIds = getPossibleAltIdsResidue(residue, this.structure, this.unit);
|
||||
const secondAtoms = this.processSecondResidue(residue, secondPossibleAltIds);
|
||||
|
||||
return { firstAtoms, secondAtoms };
|
||||
}
|
||||
|
||||
walk() {
|
||||
while (this.chainIt.hasNext) {
|
||||
this.residueIt.setSegment(this.chainIt.move());
|
||||
|
||||
let residue = this.residueIt.move();
|
||||
while (this.residueIt.hasNext) {
|
||||
try {
|
||||
const { firstAtoms, secondAtoms } = this.step(residue);
|
||||
|
||||
this.handleStep(firstAtoms, secondAtoms);
|
||||
} catch (error) {
|
||||
/* Skip and move along */
|
||||
residue = this.residueIt.move();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private structure: Structure, private unit: Unit.Atomic, private handler: Handler) {
|
||||
super(unit);
|
||||
|
||||
this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
|
||||
this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
|
||||
}
|
||||
|
||||
private chainIt: Segmentation.SegmentIterator<ChainIndex>;
|
||||
private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { sizeDataFactor } from '../../mol-geo/geometry/size-data';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { decodeFloatRGB } from '../../mol-util/float-packing';
|
||||
import { unpackRGBToInt } from '../../mol-util/number-packing';
|
||||
import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
|
||||
import { readAlphaTexture, readTexture } from '../../mol-gl/compute/util';
|
||||
|
||||
@@ -65,7 +65,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const r = tSize.array[i * 3];
|
||||
const g = tSize.array[i * 3 + 1];
|
||||
const b = tSize.array[i * 3 + 2];
|
||||
return decodeFloatRGB(r, g, b) / sizeDataFactor;
|
||||
return unpackRGBToInt(r, g, b) / sizeDataFactor;
|
||||
}
|
||||
|
||||
private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number): number {
|
||||
@@ -95,9 +95,9 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const g = groups[i4 + 1];
|
||||
const b = groups[i4 + 2];
|
||||
if (groups instanceof Float32Array) {
|
||||
return decodeFloatRGB(r * 255 + 0.5, g * 255 + 0.5, b * 255 + 0.5);
|
||||
return unpackRGBToInt(r * 255 + 0.5, g * 255 + 0.5, b * 255 + 0.5);
|
||||
}
|
||||
return decodeFloatRGB(r, g, b);
|
||||
return unpackRGBToInt(r, g, b);
|
||||
}
|
||||
|
||||
protected static getInterpolatedColors(webgl: WebGLContext, input: { vertices: Float32Array, vertexCount: number, values: BaseValues, stride: 3 | 4, colorType: 'volume' | 'volumeInstance' }) {
|
||||
|
||||
@@ -60,6 +60,8 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
|
||||
const merged = merge(
|
||||
this.controls.behaviors.params,
|
||||
this.plugin.canvas3d!.reprCount
|
||||
@@ -71,6 +73,7 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this._controls?.dispose();
|
||||
this._controls = void 0;
|
||||
}
|
||||
|
||||
35
src/extensions/meshes/choice.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
|
||||
|
||||
/**
|
||||
* Represents a set of values to choose from, with a default value. Example:
|
||||
* ```
|
||||
* export const MyChoice = new Choice({ yes: 'I agree', no: 'Nope' }, 'yes');
|
||||
* export type MyChoiceType = Choice.Values<typeof MyChoice>; // 'yes'|'no'
|
||||
* ```
|
||||
*/
|
||||
export class Choice<T extends string, D extends T> {
|
||||
readonly defaultValue: D;
|
||||
readonly options: [T, string][];
|
||||
private readonly nameDict: { [value in T]: string };
|
||||
constructor(opts: { [value in T]: string }, defaultValue: D) {
|
||||
this.defaultValue = defaultValue;
|
||||
this.options = Object.keys(opts).map(k => [k as T, opts[k as T]]);
|
||||
this.nameDict = opts;
|
||||
}
|
||||
PDSelect(defaultValue?: T, info?: PD.Info): PD.Select<T> {
|
||||
return PD.Select<T>(defaultValue ?? this.defaultValue, this.options, info);
|
||||
}
|
||||
prettyName(value: T): string {
|
||||
return this.nameDict[value];
|
||||
}
|
||||
}
|
||||
export namespace Choice {
|
||||
export type Values<T extends Choice<any, any>> = T extends Choice<infer R, any> ? R : any;
|
||||
}
|
||||
225
src/extensions/meshes/examples.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
/** Testing examples for using mesh-extension.ts. */
|
||||
|
||||
import { ParseMeshlistTransformer, MeshShapeTransformer, MeshlistData } from './mesh-extension';
|
||||
import * as MeshUtils from './mesh-utils';
|
||||
import { BACKGROUND_OPACITY, FOREROUND_OPACITY, InitMeshStreaming } from './mesh-streaming/transformers';
|
||||
import { MeshServerInfo } from './mesh-streaming/server-info';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectRef, StateObjectSelector } from '../../mol-state';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { Download } from '../../mol-plugin-state/transforms/data';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { Box3D } from '../../mol-math/geometry';
|
||||
import { ShapeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
import { CIF } from '../../mol-io/reader/cif';
|
||||
|
||||
|
||||
export const DB_URL = '/db'; // local
|
||||
|
||||
|
||||
export async function runMeshExtensionExamples(plugin: PluginUIContext, db_url: string = DB_URL) {
|
||||
console.time('TIME MESH EXAMPLES');
|
||||
// await runIsosurfaceExample(plugin, db_url);
|
||||
// await runMolsurfaceExample(plugin);
|
||||
|
||||
// Focused Ion Beam-Scanning Electron Microscopy of mitochondrial reticulum in murine skeletal muscle: https://www.ebi.ac.uk/empiar/EMPIAR-10070/
|
||||
// await runMeshExample(plugin, 'all', db_url);
|
||||
// await runMeshExample(plugin, 'fg', db_url);
|
||||
// await runMultimeshExample(plugin, 'fg', 'worst', db_url);
|
||||
// await runCifMeshExample(plugin);
|
||||
// await runMeshExample2(plugin, 'fg');
|
||||
await runMeshStreamingExample(plugin);
|
||||
|
||||
console.timeEnd('TIME MESH EXAMPLES');
|
||||
}
|
||||
|
||||
/** Example for downloading multiple separate segments, each containing 1 mesh. */
|
||||
export async function runMeshExample(plugin: PluginUIContext, segments: 'fg' | 'all', db_url: string = DB_URL) {
|
||||
const detail = 2;
|
||||
const segmentIds = (segments === 'all') ?
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17] // segment-16 has no detail-2
|
||||
: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 17]; // segment-13 and segment-15 are quasi background
|
||||
|
||||
for (const segmentId of segmentIds) {
|
||||
await createMeshFromUrl(plugin, `${db_url}/empiar-10070-mesh-rounded/segment-${segmentId}/detail-${detail}`, segmentId, detail, true, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/** Example for downloading multiple separate segments, each containing 1 mesh. */
|
||||
export async function runMeshExample2(plugin: PluginUIContext, segments: 'one' | 'few' | 'fg' | 'all') {
|
||||
const detail = 1;
|
||||
const segmentIds = (segments === 'one') ? [15]
|
||||
: (segments === 'few') ? [1, 4, 7, 10, 16]
|
||||
: (segments === 'all') ? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17] // segment-16 has no detail-2
|
||||
: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 17]; // segment-13 and segment-15 are quasi background
|
||||
|
||||
for (const segmentId of segmentIds) {
|
||||
await createMeshFromUrl(plugin, `http://localhost:9000/v2/empiar/empiar-10070/mesh_bcif/${segmentId}/${detail}`, segmentId, detail, false, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/** Example for downloading a single segment containing multiple meshes. */
|
||||
export async function runMultimeshExample(plugin: PluginUIContext, segments: 'fg' | 'all', detailChoice: 'best' | 'worst', db_url: string = DB_URL) {
|
||||
const urlDetail = (detailChoice === 'best') ? '2' : 'worst';
|
||||
const numDetail = (detailChoice === 'best') ? 2 : 1000;
|
||||
await createMeshFromUrl(plugin, `${db_url}/empiar-10070-multimesh-rounded/segments-${segments}/detail-${urlDetail}`, 0, numDetail, false, undefined);
|
||||
}
|
||||
|
||||
/** Download data and create state tree hierarchy down to visual representation. */
|
||||
export async function createMeshFromUrl(plugin: PluginContext, meshDataUrl: string, segmentId: number, detail: number,
|
||||
collapseTree: boolean, color?: Color, parent?: StateObjectSelector | StateObjectRef, transparentIfBboxAbove?: number,
|
||||
name?: string, ownerId?: string) {
|
||||
|
||||
const update = parent ? plugin.build().to(parent) : plugin.build().toRoot();
|
||||
const rawDataNodeRef = update.apply(Download,
|
||||
{ url: meshDataUrl, isBinary: true, label: `Downloaded Data ${segmentId}` },
|
||||
{ state: { isCollapsed: collapseTree } }
|
||||
).ref;
|
||||
const parsedDataNode = await update.to(rawDataNodeRef)
|
||||
.apply(StateTransforms.Data.ParseCif)
|
||||
.apply(ParseMeshlistTransformer,
|
||||
{ label: undefined, segmentId: segmentId, segmentName: name ?? `Segment ${segmentId}`, detail: detail, ownerId: ownerId },
|
||||
{}
|
||||
)
|
||||
.commit();
|
||||
|
||||
let transparent = false;
|
||||
if (transparentIfBboxAbove !== undefined && parsedDataNode.data) {
|
||||
const bbox = MeshlistData.bbox(parsedDataNode.data) || Box3D.zero();
|
||||
transparent = Box3D.volume(bbox) > transparentIfBboxAbove;
|
||||
}
|
||||
|
||||
await plugin.build().to(parsedDataNode)
|
||||
.apply(MeshShapeTransformer, { color: color },)
|
||||
.apply(ShapeRepresentation3D,
|
||||
{ alpha: transparent ? BACKGROUND_OPACITY : FOREROUND_OPACITY },
|
||||
{ tags: ['mesh-segment-visual', `segment-${segmentId}`] }
|
||||
)
|
||||
.commit();
|
||||
|
||||
return rawDataNodeRef;
|
||||
}
|
||||
|
||||
export async function runMeshStreamingExample(plugin: PluginUIContext, source: MeshServerInfo.MeshSource = 'empiar', entryId: string = 'empiar-10070', serverUrl?: string, parent?: StateObjectSelector) {
|
||||
const params = ParamDefinition.getDefaultValues(MeshServerInfo.Params);
|
||||
if (serverUrl) params.serverUrl = serverUrl;
|
||||
params.source = source;
|
||||
params.entryId = entryId;
|
||||
await plugin.runTask(plugin.state.data.applyAction(InitMeshStreaming, params, parent?.ref), { useOverlay: false });
|
||||
}
|
||||
|
||||
/** Example for downloading a protein structure and visualizing molecular surface. */
|
||||
export async function runMolsurfaceExample(plugin: PluginUIContext) {
|
||||
const entryId = 'pdb-7etq';
|
||||
|
||||
// Node "https://www.ebi.ac.uk/pdbe/entry-files/download/7etq.bcif" ("transformer": "ms-plugin.download") -> var data
|
||||
const data = await plugin.builders.data.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/7etq.bcif', isBinary: true }, { state: { isGhost: false } });
|
||||
console.log('formats:', plugin.dataFormats.list);
|
||||
|
||||
// Node "CIF File" ("transformer": "ms-plugin.parse-cif")
|
||||
// Node "7ETQ 1 model" ("transformer": "ms-plugin.trajectory-from-mmcif") -> var trajectory
|
||||
const parsed = await plugin.dataFormats.get('mmcif')!.parse(plugin, data, { entryId });
|
||||
const trajectory: StateObjectSelector<PluginStateObject.Molecule.Trajectory> = parsed.trajectory;
|
||||
console.log('parsed', parsed);
|
||||
console.log('trajectory', trajectory);
|
||||
|
||||
// Node "Model 1" ("transformer": "ms-plugin.model-from-trajectory") -> var model
|
||||
const model = await plugin.build().to(trajectory).apply(StateTransforms.Model.ModelFromTrajectory).commit();
|
||||
console.log('model:', model);
|
||||
|
||||
// Node "Model 91 elements" ("transformer": "ms-plugin.structure-from-model") -> var structure
|
||||
const structure = await plugin.build().to(model).apply(StateTransforms.Model.StructureFromModel,).commit();
|
||||
console.log('structure:', structure);
|
||||
|
||||
// Node "Molecular Surface" ("transformer": "ms-plugin.structure-representation-3d") -> var repr
|
||||
const reprParams = createStructureRepresentationParams(plugin, undefined, { type: 'molecular-surface' });
|
||||
const repr = await plugin.build().to(structure).apply(StateTransforms.Representation.StructureRepresentation3D, reprParams).commit();
|
||||
console.log('repr:', repr);
|
||||
}
|
||||
|
||||
/** Example for downloading an EMDB density data and visualizing isosurface. */
|
||||
export async function runIsosurfaceExample(plugin: PluginUIContext, db_url: string = DB_URL) {
|
||||
const entryId = 'emd-1832';
|
||||
const isoLevel = 2.73;
|
||||
|
||||
let root = await plugin.build();
|
||||
const data = await plugin.builders.data.download({ url: `${db_url}/emd-1832-box`, isBinary: true }, { state: { isGhost: false } });
|
||||
const parsed = await plugin.dataFormats.get('dscif')!.parse(plugin, data, { entryId });
|
||||
|
||||
const volume: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[0] ?? parsed.volume;
|
||||
const volumeData = volume.cell!.obj!.data;
|
||||
console.log('data:', data);
|
||||
console.log('parsed:', parsed);
|
||||
console.log('volume:', volume);
|
||||
console.log('volumeData:', volumeData);
|
||||
|
||||
root = await plugin.build();
|
||||
console.log('root:', root);
|
||||
console.log('to:', root.to(volume));
|
||||
console.log('toRoot:', root.toRoot());
|
||||
|
||||
let volumeParams;
|
||||
volumeParams = createVolumeRepresentationParams(plugin, volumeData, {
|
||||
type: 'isosurface',
|
||||
typeParams: {
|
||||
alpha: 0.5,
|
||||
isoValue: Volume.adjustedIsoValue(volumeData, isoLevel, 'relative'),
|
||||
visuals: ['solid'],
|
||||
sizeFactor: 1,
|
||||
},
|
||||
color: 'uniform',
|
||||
colorParams: { value: Color(0x00aaaa) },
|
||||
|
||||
});
|
||||
root.to(volume).apply(StateTransforms.Representation.VolumeRepresentation3D, volumeParams);
|
||||
|
||||
volumeParams = createVolumeRepresentationParams(plugin, volumeData, {
|
||||
type: 'isosurface',
|
||||
typeParams: {
|
||||
alpha: 1.0,
|
||||
isoValue: Volume.adjustedIsoValue(volumeData, isoLevel, 'relative'),
|
||||
visuals: ['wireframe'],
|
||||
sizeFactor: 1,
|
||||
},
|
||||
color: 'uniform',
|
||||
colorParams: { value: Color(0x8800aa) },
|
||||
|
||||
});
|
||||
root.to(volume).apply(StateTransforms.Representation.VolumeRepresentation3D, volumeParams);
|
||||
await root.commit();
|
||||
}
|
||||
|
||||
|
||||
export async function runCifMeshExample(plugin: PluginUIContext, api: string = 'http://localhost:9000/v2',
|
||||
source: MeshServerInfo.MeshSource = 'empiar', entryId: string = 'empiar-10070',
|
||||
segmentId: number = 1, detail: number = 10,
|
||||
) {
|
||||
const url = `${api}/${source}/${entryId}/mesh_bcif/${segmentId}/${detail}`;
|
||||
getMeshFromBcif(plugin, url);
|
||||
}
|
||||
|
||||
async function getMeshFromBcif(plugin: PluginUIContext, url: string) {
|
||||
const urlAsset = Asset.getUrlAsset(plugin.managers.asset, url); // QUESTION how is urlAsset better than normal `fetch`
|
||||
const asset = await plugin.runTask(plugin.managers.asset.resolve(urlAsset, 'binary'));
|
||||
const parsed = await plugin.runTask(CIF.parseBinary(asset.data));
|
||||
if (parsed.isError) {
|
||||
plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
|
||||
return;
|
||||
}
|
||||
console.log('blocks:', parsed.result.blocks);
|
||||
const mesh = await MeshUtils.meshFromCif(parsed.result);
|
||||
console.log(mesh);
|
||||
}
|
||||
40
src/extensions/meshes/mesh-cif-schema.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column, Database } from '../../mol-data/db';
|
||||
import { CifFrame } from '../../mol-io/reader/cif';
|
||||
import { toDatabase } from '../../mol-io/reader/cif/schema';
|
||||
|
||||
|
||||
const int = Column.Schema.int;
|
||||
const float = Column.Schema.float;
|
||||
|
||||
|
||||
// TODO in future, move to molstar/src/mol-io/reader/cif/schema/mesh.ts
|
||||
export const Mesh_Data_Schema = {
|
||||
mesh: {
|
||||
id: int,
|
||||
},
|
||||
mesh_vertex: {
|
||||
mesh_id: int,
|
||||
vertex_id: int,
|
||||
x: float,
|
||||
y: float,
|
||||
z: float,
|
||||
},
|
||||
/** Table of triangles, 3 rows per triangle */
|
||||
mesh_triangle: {
|
||||
mesh_id: int,
|
||||
/** Indices of vertices within mesh */
|
||||
vertex_id: int,
|
||||
}
|
||||
};
|
||||
export type Mesh_Data_Schema = typeof Mesh_Data_Schema;
|
||||
export interface Mesh_Data_Database extends Database<Mesh_Data_Schema> {}
|
||||
|
||||
|
||||
// TODO in future, move to molstar/src/mol-io/reader/cif.ts: CIF.schema.mesh
|
||||
export const CIF_schema_mesh = (frame: CifFrame) => toDatabase<Mesh_Data_Schema, Mesh_Data_Database>(Mesh_Data_Schema, frame);
|
||||
227
src/extensions/meshes/mesh-extension.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
/** Defines new types of State tree transformers for dealing with mesh data. */
|
||||
|
||||
|
||||
import { BaseGeometry, VisualQuality, VisualQualityOptions } from '../../mol-geo/geometry/base';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { CifFile } from '../../mol-io/reader/cif';
|
||||
import { Box3D } from '../../mol-math/geometry';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { ShapeProvider } from '../../mol-model/shape/provider';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { Material } from '../../mol-util/material';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import * as MeshUtils from './mesh-utils';
|
||||
|
||||
|
||||
export const VolsegTransform: StateTransformer.Builder.Root = StateTransformer.builderFactory('volseg');
|
||||
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // //
|
||||
// Parsed data
|
||||
|
||||
/** Data type for `MeshlistStateObject` - list of meshes */
|
||||
export interface MeshlistData {
|
||||
segmentId: number,
|
||||
segmentName: string,
|
||||
detail: number,
|
||||
meshIds: number[],
|
||||
mesh: Mesh,
|
||||
/** Reference to the object which created this meshlist (e.g. `MeshStreaming.Behavior`) */
|
||||
ownerId?: string,
|
||||
}
|
||||
|
||||
export namespace MeshlistData {
|
||||
export function empty(): MeshlistData {
|
||||
return {
|
||||
segmentId: 0,
|
||||
segmentName: 'Empty',
|
||||
detail: 0,
|
||||
meshIds: [],
|
||||
mesh: Mesh.createEmpty(),
|
||||
};
|
||||
};
|
||||
export async function fromCIF(data: CifFile, segmentId: number, segmentName: string, detail: number): Promise<MeshlistData> {
|
||||
const { mesh, meshIds } = await MeshUtils.meshFromCif(data);
|
||||
return {
|
||||
segmentId,
|
||||
segmentName,
|
||||
detail,
|
||||
meshIds,
|
||||
mesh,
|
||||
};
|
||||
}
|
||||
export function stats(meshListData: MeshlistData): string {
|
||||
return `Meshlist "${meshListData.segmentName}" (detail ${meshListData.detail}): ${meshListData.meshIds.length} meshes, ${meshListData.mesh.vertexCount} vertices, ${meshListData.mesh.triangleCount} triangles`;
|
||||
}
|
||||
export function getShape(data: MeshlistData, color: Color): Shape<Mesh> {
|
||||
const mesh = data.mesh;
|
||||
const meshShape: Shape<Mesh> = Shape.create(data.segmentName, data, mesh,
|
||||
() => color,
|
||||
() => 1,
|
||||
// group => `${data.segmentName} | Segment ${data.segmentId} | Detail ${data.detail} | Mesh ${group}`,
|
||||
group => data.segmentName,
|
||||
);
|
||||
return meshShape;
|
||||
}
|
||||
|
||||
export function combineBBoxes(boxes: (Box3D | null)[]): Box3D | null {
|
||||
let result = null;
|
||||
for (const box of boxes) {
|
||||
if (!box) continue;
|
||||
if (result) {
|
||||
Vec3.min(result.min, result.min, box.min);
|
||||
Vec3.max(result.max, result.max, box.max);
|
||||
} else {
|
||||
result = Box3D.zero();
|
||||
Box3D.copy(result, box);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function bbox(data: MeshlistData): Box3D | null {
|
||||
return MeshUtils.bbox(data.mesh);
|
||||
}
|
||||
|
||||
export function allVerticesUsed(data: MeshlistData): boolean {
|
||||
const unusedVertices = new Set();
|
||||
for (let i = 0; i < data.mesh.vertexCount; i++) {
|
||||
unusedVertices.add(i);
|
||||
}
|
||||
for (let i = 0; i < 3 * data.mesh.triangleCount; i++) {
|
||||
const v = data.mesh.vertexBuffer.ref.value[i];
|
||||
unusedVertices.delete(v);
|
||||
}
|
||||
return unusedVertices.size === 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // //
|
||||
// Raw Data -> Parsed data
|
||||
|
||||
export class MeshlistStateObject extends PluginStateObject.Create<MeshlistData>({ name: 'Parsed Meshlist', typeClass: 'Object' }) { }
|
||||
// QUESTION: is typeClass just for color, or does do something?
|
||||
|
||||
export const ParseMeshlistTransformer = VolsegTransform({
|
||||
name: 'meshlist-from-string',
|
||||
from: PluginStateObject.Format.Cif,
|
||||
to: MeshlistStateObject,
|
||||
params: {
|
||||
label: PD.Text(MeshlistStateObject.type.name, { isHidden: true }), // QUESTION: Is this the right way to pass a value to apply() without exposing it in GUI?
|
||||
segmentId: PD.Numeric(1, {}, { isHidden: true }),
|
||||
segmentName: PD.Text('Segment'),
|
||||
detail: PD.Numeric(1, {}, { isHidden: true }),
|
||||
/** Reference to the object which manages this meshlist (e.g. `MeshStreaming.Behavior`) */
|
||||
ownerId: PD.Text('', { isHidden: true }),
|
||||
}
|
||||
})({
|
||||
apply({ a, params }, globalCtx) { // `a` is the parent node, params are 2nd argument to To.apply(), `globalCtx` is the plugin
|
||||
return Task.create('Create Parsed Meshlist', async ctx => {
|
||||
const meshlistData = await MeshlistData.fromCIF(a.data, params.segmentId, params.segmentName, params.detail);
|
||||
meshlistData.ownerId = params.ownerId;
|
||||
const es = meshlistData.meshIds.length === 1 ? '' : 'es';
|
||||
return new MeshlistStateObject(meshlistData, { label: params.label, description: `${meshlistData.segmentName} (${meshlistData.meshIds.length} mesh${es})` });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // //
|
||||
// Parsed data -> Shape
|
||||
|
||||
/** Data type for PluginStateObject.Shape.Provider */
|
||||
type MeshShapeProvider = ShapeProvider<MeshlistData, Mesh, Mesh.Params>;
|
||||
namespace MeshShapeProvider {
|
||||
export function fromMeshlistData(meshlist: MeshlistData, color?: Color): MeshShapeProvider {
|
||||
const theColor = color ?? MeshUtils.ColorGenerator.next().value;
|
||||
return {
|
||||
label: 'Mesh',
|
||||
data: meshlist,
|
||||
params: meshParamDef, // TODO how to pass the real params correctly?
|
||||
geometryUtils: Mesh.Utils,
|
||||
getShape: (ctx, data: MeshlistData) => MeshlistData.getShape(data, theColor),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Params for MeshShapeTransformer */
|
||||
const meshShapeParamDef = {
|
||||
color: PD.Value<Color | undefined>(undefined), // undefined means random color
|
||||
};
|
||||
|
||||
const meshParamDef: Mesh.Params = {
|
||||
// These are basically original MS.Mesh.Params:
|
||||
// BaseGeometry.Params
|
||||
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { label: 'Opacity', isEssential: true, description: 'How opaque/transparent the representation is rendered.' }),
|
||||
quality: PD.Select<VisualQuality>('custom', VisualQualityOptions, { isEssential: true, description: 'Visual/rendering quality of the representation.' }), // use 'custom' when wanting to apply doubleSided
|
||||
material: Material.getParam(),
|
||||
clip: Mesh.Params.clip, // PD.Group(MS.Clip.Params),
|
||||
instanceGranularity: PD.Boolean(false, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory.' }),
|
||||
// Mesh.Params
|
||||
doubleSided: PD.Boolean(true, BaseGeometry.CustomQualityParamInfo), // default: false (set true, to show at least something in weird cases)
|
||||
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory), // default: false (set true to see the real mesh vertices and triangles)
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: PD.Boolean(false, BaseGeometry.ShadingCategory), // this is like better opacity (angle-dependent), nice
|
||||
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque']), BaseGeometry.ShadingCategory),
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
// TODO when I change values here, it has effect, but not if I change them in GUI
|
||||
};
|
||||
|
||||
export const MeshShapeTransformer = VolsegTransform({
|
||||
name: 'shape-from-meshlist',
|
||||
display: { name: 'Shape from Meshlist', description: 'Create Shape from Meshlist data' },
|
||||
from: MeshlistStateObject,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: meshShapeParamDef
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
// you can look for example at ShapeFromPly in mol-plugin-state/tansforms/model.ts as an example
|
||||
const shapeProvider = MeshShapeProvider.fromMeshlistData(a.data, params.color);
|
||||
return new PluginStateObject.Shape.Provider(shapeProvider, { label: PluginStateObject.Shape.Provider.type.name, description: a.description });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // //
|
||||
// Shape -> Repr
|
||||
|
||||
// type MeshRepr = MS.PluginStateObject.Representation3DData<MS.ShapeRepresentation<MS.ShapeProvider<any,any,any>, MS.Mesh, MS.Mesh.Params>, any>;
|
||||
|
||||
// export const CustomMeshReprTransformer = VolsegTransform({
|
||||
// name: 'custom-repr',
|
||||
// from: MS.PluginStateObject.Shape.Provider, // later we can change this
|
||||
// to: MS.PluginStateObject.Shape.Representation3D,
|
||||
// })({
|
||||
// apply({ a }, globalCtx) {
|
||||
// const repr: MeshRepr = createRepr(a.data); // TODO implement createRepr
|
||||
// // have a look at MS.StateTransforms.Representation.ShapeRepresentation3D if you want to try implementing yourself
|
||||
// return new MS.PluginStateObject.Shape.Representation3D(repr)
|
||||
// },
|
||||
// })
|
||||
|
||||
// export async function createMeshRepr(plugin: MS.PluginContext, data: any) {
|
||||
// await plugin.build()
|
||||
// .toRoot()
|
||||
// .apply(CreateMyShapeTransformer, { data })
|
||||
// .apply(MS.StateTransforms.Representation.ShapeRepresentation3D) // this should work
|
||||
// // or .apply(CustomMeshRepr)
|
||||
// .commit();
|
||||
// }
|
||||
|
||||
// export function createRepr(reprData: MS.ShapeProvider<any,any,any>): MeshRepr {
|
||||
// throw new Error('NotImplemented');
|
||||
// return {} as MeshRepr;
|
||||
// }
|
||||
335
src/extensions/meshes/mesh-streaming/behavior.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { distinctUntilChanged, map } from 'rxjs';
|
||||
|
||||
import { CIF } from '../../../mol-io/reader/cif';
|
||||
import { Box3D } from '../../../mol-math/geometry';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { PluginBehavior } from '../../../mol-plugin/behavior';
|
||||
import { PluginCommand } from '../../../mol-plugin/command';
|
||||
import { PluginCommands } from '../../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { UUID } from '../../../mol-util';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
import { Choice } from '../choice';
|
||||
import { MeshlistData } from '../mesh-extension';
|
||||
import { Metadata } from '../metadata';
|
||||
import { MeshServerInfo } from './server-info';
|
||||
|
||||
|
||||
const DEFAULT_SEGMENT_NAME = 'Untitled segment';
|
||||
const DEFAULT_SEGMENT_COLOR = ColorNames.lightgray;
|
||||
export const NO_SEGMENT = -1;
|
||||
/** Maximum (worst) detail level available in GUI (TODO set actual maximum possible value) */
|
||||
const MAX_DETAIL = 10;
|
||||
const DEFAULT_DETAIL = 7; // TODO decide a reasonable default
|
||||
/** Segments whose bounding box volume is above this value (relative to the overall bounding box) are considered as background segments */
|
||||
export const BACKGROUND_SEGMENT_VOLUME_THRESHOLD = 0.5;
|
||||
// const DEBUG_IGNORED_SEGMENTS = new Set([13, 15]); // TODO remove
|
||||
const DEBUG_IGNORED_SEGMENTS = new Set(); // TODO remove
|
||||
|
||||
|
||||
export class MeshStreaming extends PluginStateObject.CreateBehavior<MeshStreaming.Behavior>({ name: 'Mesh Streaming' }) { }
|
||||
|
||||
export namespace MeshStreaming {
|
||||
|
||||
export namespace Params {
|
||||
export const ViewTypeChoice = new Choice({ off: 'Off', select: 'Select', all: 'All' }, 'select'); // TODO add camera target?
|
||||
export type ViewType = Choice.Values<typeof ViewTypeChoice>;
|
||||
|
||||
export function create(options: MeshServerInfo.Data) {
|
||||
return {
|
||||
view: PD.MappedStatic('select', {
|
||||
'off': PD.Group({}),
|
||||
'select': PD.Group({
|
||||
baseDetail: PD.Numeric(DEFAULT_DETAIL, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for the non-selected segments (lower number = better)' }),
|
||||
focusDetail: PD.Numeric(1, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for the selected segment (lower number = better)' }),
|
||||
selectedSegment: PD.Numeric(NO_SEGMENT, {}, { isHidden: true }),
|
||||
}, { isFlat: true }),
|
||||
'all': PD.Group({
|
||||
detail: PD.Numeric(DEFAULT_DETAIL, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for all segments (lower number = better)' })
|
||||
}, { isFlat: true }),
|
||||
}, { description: '"Off" hides all segments. \n"Select" shows all segments in lower detail, clicked segment in better detail. "All" shows all segment in the same level.' }),
|
||||
};
|
||||
}
|
||||
|
||||
export type Definition = ReturnType<typeof create>
|
||||
export type Values = PD.Values<Definition>
|
||||
|
||||
export function copyValues(params: Values): Values {
|
||||
return {
|
||||
view: {
|
||||
name: params.view.name,
|
||||
params: { ...params.view.params } as any,
|
||||
}
|
||||
};
|
||||
}
|
||||
export function valuesEqual(p: Values, q: Values): boolean {
|
||||
if (p.view.name !== q.view.name) return false;
|
||||
for (const key in p.view.params) {
|
||||
if ((p.view.params as any)[key] !== (q.view.params as any)[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
export function detailsEqual(p: Values, q: Values): boolean {
|
||||
switch (p.view.name) {
|
||||
case 'off':
|
||||
return q.view.name === 'off';
|
||||
case 'select':
|
||||
return q.view.name === 'select' && p.view.params.baseDetail === q.view.params.baseDetail && p.view.params.focusDetail === q.view.params.focusDetail;
|
||||
case 'all':
|
||||
return q.view.name === 'all' && p.view.params.detail === q.view.params.detail;
|
||||
default:
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface VisualInfo {
|
||||
tag: string, // e.g. high-2, low-1 // ? remove if can be omitted
|
||||
segmentId: number, // ? remove if unused
|
||||
segmentName: string, // ? remove if unused
|
||||
detailType: VisualInfo.DetailType, // ? remove if unused
|
||||
detail: number, // ? remove if unused
|
||||
color: Color, // move to MeshlistData?
|
||||
visible: boolean,
|
||||
data?: MeshlistData,
|
||||
}
|
||||
export namespace VisualInfo {
|
||||
export type DetailType = 'low' | 'high';
|
||||
export const DetailTypes: DetailType[] = ['low', 'high'];
|
||||
export function tagFor(segmentId: number, detail: DetailType) {
|
||||
return `${detail}-${segmentId}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Behavior extends PluginBehavior.WithSubscribers<Params.Values> {
|
||||
private id: string;
|
||||
private ref: string = '';
|
||||
public parentData: MeshServerInfo.Data;
|
||||
private metadata?: Metadata;
|
||||
public visuals?: { [tag: string]: VisualInfo };
|
||||
public backgroundSegments: { [segmentId: number]: boolean } = {};
|
||||
private focusObservable = this.plugin.behaviors.interaction.click.pipe( // QUESTION is this OK way to get focused segment?
|
||||
map(evt => evt.current.loci),
|
||||
map(loci => (loci.kind === 'group-loci') ? loci.shape.sourceData as MeshlistData : null),
|
||||
map(data => (data?.ownerId === this.id) ? data : null), // do not process shapes created by others
|
||||
distinctUntilChanged((old, current) => old?.segmentId === current?.segmentId),
|
||||
);
|
||||
private focusSubscription?: PluginCommand.Subscription = undefined;
|
||||
private backgroundSegmentsInitialized = false;
|
||||
|
||||
constructor(plugin: PluginContext, data: MeshServerInfo.Data, params: Params.Values) {
|
||||
super(plugin, params);
|
||||
this.id = UUID.create22();
|
||||
this.parentData = data;
|
||||
}
|
||||
|
||||
register(ref: string): void {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
unregister(): void {
|
||||
if (this.focusSubscription) {
|
||||
this.focusSubscription.unsubscribe();
|
||||
this.focusSubscription = undefined;
|
||||
}
|
||||
// TODO empty cache here (if used)
|
||||
}
|
||||
|
||||
selectSegment(segmentId: number) {
|
||||
if (this.params.view.name === 'select') {
|
||||
if (this.params.view.params.selectedSegment === segmentId) return;
|
||||
const newParams = Params.copyValues(this.params);
|
||||
if (newParams.view.name === 'select') {
|
||||
newParams.view.params.selectedSegment = segmentId;
|
||||
}
|
||||
const state = this.plugin.state.data;
|
||||
const update = state.build().to(this.ref).update(newParams);
|
||||
PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
|
||||
}
|
||||
}
|
||||
|
||||
async update(params: Params.Values) {
|
||||
const oldParams = this.params;
|
||||
this.params = params;
|
||||
|
||||
if (!this.metadata) {
|
||||
const response = await fetch(this.getMetadataUrl());
|
||||
this.metadata = await response.json();
|
||||
}
|
||||
|
||||
if (!this.visuals) {
|
||||
this.initVisualInfos();
|
||||
} else if (!Params.detailsEqual(this.params, oldParams)) {
|
||||
this.updateVisualInfoDetails();
|
||||
}
|
||||
|
||||
switch (params.view.name) {
|
||||
case 'off':
|
||||
await this.disableVisuals();
|
||||
break;
|
||||
case 'select':
|
||||
await this.enableVisuals(params.view.params.selectedSegment);
|
||||
break;
|
||||
case 'all':
|
||||
await this.enableVisuals();
|
||||
break;
|
||||
default:
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
if (params.view.name !== 'off' && !this.backgroundSegmentsInitialized) {
|
||||
this.guessBackgroundSegments();
|
||||
this.backgroundSegmentsInitialized = true;
|
||||
}
|
||||
if (params.view.name === 'select' && !this.focusSubscription) {
|
||||
this.focusSubscription = this.subscribeObservable(this.focusObservable, data => { this.selectSegment(data?.segmentId ?? NO_SEGMENT); });
|
||||
} else if (params.view.name !== 'select' && this.focusSubscription) {
|
||||
this.focusSubscription.unsubscribe();
|
||||
this.focusSubscription = undefined;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private getMetadataUrl() {
|
||||
return `${this.parentData.serverUrl}/${this.parentData.source}/${this.parentData.entryId}/metadata`;
|
||||
}
|
||||
|
||||
private getMeshUrl(segment: number, detail: number) {
|
||||
return `${this.parentData.serverUrl}/${this.parentData.source}/${this.parentData.entryId}/mesh_bcif/${segment}/${detail}`;
|
||||
}
|
||||
|
||||
private initVisualInfos() {
|
||||
const namesAndColors = Metadata.namesAndColorsBySegment(this.metadata!);
|
||||
|
||||
const visuals: { [tag: string]: VisualInfo } = {};
|
||||
for (const segid of Metadata.meshSegments(this.metadata!)) {
|
||||
if (DEBUG_IGNORED_SEGMENTS.has(segid)) continue;
|
||||
const name = namesAndColors[segid]?.name ?? DEFAULT_SEGMENT_NAME;
|
||||
const color = namesAndColors[segid]?.color ?? DEFAULT_SEGMENT_COLOR;
|
||||
for (const detailType of VisualInfo.DetailTypes) {
|
||||
const visual: VisualInfo = {
|
||||
tag: VisualInfo.tagFor(segid, detailType),
|
||||
segmentId: segid,
|
||||
segmentName: name,
|
||||
detailType: detailType,
|
||||
detail: -1, // to be set at the end
|
||||
color: color,
|
||||
visible: false,
|
||||
data: undefined,
|
||||
};
|
||||
visuals[visual.tag] = visual;
|
||||
}
|
||||
}
|
||||
this.visuals = visuals;
|
||||
this.updateVisualInfoDetails();
|
||||
}
|
||||
private updateVisualInfoDetails() {
|
||||
let highDetail: number | undefined;
|
||||
let lowDetail: number | undefined;
|
||||
switch (this.params.view.name) {
|
||||
case 'off':
|
||||
lowDetail = undefined;
|
||||
highDetail = undefined;
|
||||
break;
|
||||
case 'select':
|
||||
lowDetail = this.params.view.params.baseDetail;
|
||||
highDetail = this.params.view.params.focusDetail;
|
||||
break;
|
||||
case 'all':
|
||||
lowDetail = this.params.view.params.detail;
|
||||
highDetail = undefined;
|
||||
break;
|
||||
}
|
||||
for (const tag in this.visuals) {
|
||||
const visual = this.visuals[tag];
|
||||
const preferredDetail = (visual.detailType === 'high') ? highDetail : lowDetail;
|
||||
if (preferredDetail !== undefined) {
|
||||
visual.detail = Metadata.getSufficientDetail(this.metadata!, visual.segmentId, preferredDetail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enableVisuals(highDetailSegment?: number) {
|
||||
for (const tag in this.visuals) {
|
||||
const visual = this.visuals[tag];
|
||||
const requiredDetailType = visual.segmentId === highDetailSegment ? 'high' : 'low';
|
||||
if (visual.detailType === requiredDetailType) {
|
||||
visual.data = await this.getMeshData(visual);
|
||||
visual.visible = true;
|
||||
} else {
|
||||
visual.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async disableVisuals() {
|
||||
for (const tag in this.visuals) {
|
||||
const visual = this.visuals[tag];
|
||||
visual.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch data in current `visual.detail`, or return already fetched data (if available in the correct detail). */
|
||||
private async getMeshData(visual: VisualInfo): Promise<MeshlistData> {
|
||||
if (visual.data && visual.data.detail === visual.detail) {
|
||||
// Do not recreate
|
||||
return visual.data;
|
||||
}
|
||||
// TODO cache
|
||||
const url = this.getMeshUrl(visual.segmentId, visual.detail);
|
||||
const urlAsset = Asset.getUrlAsset(this.plugin.managers.asset, url);
|
||||
const asset = await this.plugin.runTask(this.plugin.managers.asset.resolve(urlAsset, 'binary'));
|
||||
const parsed = await this.plugin.runTask(CIF.parseBinary(asset.data));
|
||||
if (parsed.isError) {
|
||||
throw new Error(`Failed parsing CIF file from ${url}`);
|
||||
}
|
||||
const meshlistData = await MeshlistData.fromCIF(parsed.result, visual.segmentId, visual.segmentName, visual.detail);
|
||||
meshlistData.ownerId = this.id;
|
||||
// const bbox = MeshlistData.bbox(meshlistData);
|
||||
// const bboxVolume = bbox ? MS.Box3D.volume(bbox) : 0.0;
|
||||
// console.log(`BBox ${visual.segmentId}: ${Math.round(bboxVolume! / 1e6)} M`, bbox); // DEBUG
|
||||
return meshlistData;
|
||||
}
|
||||
|
||||
private async guessBackgroundSegments() {
|
||||
const bboxes: { [segid: number]: Box3D } = {};
|
||||
for (const tag in this.visuals) {
|
||||
const visual = this.visuals[tag];
|
||||
if (visual.detailType === 'low' && visual.data) {
|
||||
const bbox = MeshlistData.bbox(visual.data);
|
||||
if (bbox) {
|
||||
bboxes[visual.segmentId] = bbox;
|
||||
}
|
||||
}
|
||||
}
|
||||
const totalBbox = MeshlistData.combineBBoxes(Object.values(bboxes));
|
||||
const totalVolume = totalBbox ? Box3D.volume(totalBbox) : 0.0;
|
||||
// console.log(`BBox total: ${Math.round(totalVolume! / 1e6)} M`, totalBbox); // DEBUG
|
||||
|
||||
const isBgSegment: { [segid: number]: boolean } = {};
|
||||
for (const segid in bboxes) {
|
||||
const bbox = bboxes[segid];
|
||||
const bboxVolume = Box3D.volume(bbox);
|
||||
isBgSegment[segid] = (bboxVolume > totalVolume * BACKGROUND_SEGMENT_VOLUME_THRESHOLD);
|
||||
// console.log(`BBox ${segid}: ${Math.round(bboxVolume! / 1e6)} M, ${bboxVolume / totalVolume}`, bbox); // DEBUG
|
||||
}
|
||||
this.backgroundSegments = isBgSegment;
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
return Params.ViewTypeChoice.prettyName(this.params.view.name);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
28
src/extensions/meshes/mesh-streaming/server-info.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
import { Choice } from '../choice';
|
||||
|
||||
|
||||
export const DEFAULT_MESH_SERVER = 'http://localhost:9000/v2';
|
||||
|
||||
|
||||
export class MeshServerInfo extends PluginStateObject.Create<MeshServerInfo.Data>({ name: 'Volume Server', typeClass: 'Object' }) { }
|
||||
|
||||
export namespace MeshServerInfo {
|
||||
export const MeshSourceChoice = new Choice({ empiar: 'EMPIAR', emdb: 'EMDB' }, 'empiar');
|
||||
export type MeshSource = Choice.Values<typeof MeshSourceChoice>;
|
||||
|
||||
export const Params = {
|
||||
serverUrl: PD.Text(DEFAULT_MESH_SERVER),
|
||||
source: MeshSourceChoice.PDSelect(),
|
||||
entryId: PD.Text(''),
|
||||
};
|
||||
export type Data = PD.Values<typeof Params>;
|
||||
}
|
||||
218
src/extensions/meshes/mesh-streaming/transformers.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { ShapeRepresentation } from '../../../mol-repr/shape/representation';
|
||||
import { StateAction, StateTransformer } from '../../../mol-state';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { shallowEqualObjects } from '../../../mol-util';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
import { VolsegTransform, MeshlistData } from '../mesh-extension';
|
||||
import { MeshStreaming, NO_SEGMENT } from './behavior';
|
||||
import { MeshServerInfo } from './server-info';
|
||||
|
||||
|
||||
export const BACKGROUND_OPACITY = 0.2;
|
||||
export const FOREROUND_OPACITY = 1;
|
||||
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // //
|
||||
|
||||
export const MeshServerTransformer = VolsegTransform({
|
||||
name: 'mesh-server-info',
|
||||
from: PluginStateObject.Root,
|
||||
to: MeshServerInfo,
|
||||
params: MeshServerInfo.Params,
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) { // `a` is the parent node, `params` are 2nd argument to To.apply()
|
||||
params.serverUrl = params.serverUrl.replace(/\/*$/, ''); // trim trailing slash
|
||||
const description: string = params.entryId;
|
||||
return new MeshServerInfo({ ...params }, { label: 'Mesh Server', description: description });
|
||||
}
|
||||
});
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // //
|
||||
|
||||
export const MeshStreamingTransformer = VolsegTransform({
|
||||
name: 'mesh-streaming-from-server-info',
|
||||
display: { name: 'Mesh Streaming' },
|
||||
from: MeshServerInfo,
|
||||
to: MeshStreaming,
|
||||
params: a => MeshStreaming.Params.create(a!.data),
|
||||
})({
|
||||
canAutoUpdate() { return true; },
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Mesh Streaming', async ctx => {
|
||||
const behavior = new MeshStreaming.Behavior(plugin, a.data, params);
|
||||
await behavior.update(params);
|
||||
return new MeshStreaming(behavior, { label: 'Mesh Streaming', description: behavior.getDescription() });
|
||||
});
|
||||
},
|
||||
update({ a, b, oldParams, newParams }) {
|
||||
return Task.create('Update Mesh Streaming', async ctx => {
|
||||
if (a.data.source !== b.data.parentData.source || a.data.entryId !== b.data.parentData.entryId) {
|
||||
return StateTransformer.UpdateResult.Recreate;
|
||||
}
|
||||
b.data.parentData = a.data;
|
||||
await b.data.update(newParams);
|
||||
b.description = b.data.getDescription();
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // //
|
||||
|
||||
interface MeshVisualGroupData {
|
||||
opacity: number,
|
||||
}
|
||||
|
||||
// export type MeshVisualGroupTransformer = typeof MeshVisualGroupTransformer;
|
||||
export const MeshVisualGroupTransformer = VolsegTransform({
|
||||
name: 'mesh-visual-group-from-streaming',
|
||||
display: { name: 'Mesh Visuals for a Segment' },
|
||||
from: MeshStreaming,
|
||||
to: PluginStateObject.Group,
|
||||
params: {
|
||||
/** Shown on the node in GUI */
|
||||
label: PD.Text('', { isHidden: true }),
|
||||
/** Shown on the node in GUI (gray letters) */
|
||||
description: PD.Text(''),
|
||||
segmentId: PD.Numeric(NO_SEGMENT, {}, { isHidden: true }),
|
||||
opacity: PD.Numeric(-1, { min: 0, max: 1, step: 0.01 }),
|
||||
}
|
||||
})({
|
||||
apply({ a, params }, plugin) {
|
||||
trySetAutoOpacity(params, a);
|
||||
return new PluginStateObject.Group({ opacity: params.opacity }, params);
|
||||
},
|
||||
update({ a, b, oldParams, newParams }, plugin) {
|
||||
if (shallowEqualObjects(oldParams, newParams)) {
|
||||
return StateTransformer.UpdateResult.Unchanged;
|
||||
}
|
||||
newParams.label ||= oldParams.label; // Protect against resetting params to invalid defaults
|
||||
if (newParams.segmentId === NO_SEGMENT) newParams.segmentId = oldParams.segmentId; // Protect against resetting params to invalid defaults
|
||||
trySetAutoOpacity(newParams, a);
|
||||
b.label = newParams.label;
|
||||
b.description = newParams.description;
|
||||
(b.data as MeshVisualGroupData).opacity = newParams.opacity;
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
},
|
||||
canAutoUpdate({ oldParams, newParams }, plugin) {
|
||||
return newParams.description === oldParams.description;
|
||||
},
|
||||
});
|
||||
|
||||
function trySetAutoOpacity(params: StateTransformer.Params<typeof MeshVisualGroupTransformer>, parent: MeshStreaming) {
|
||||
if (params.opacity === -1) {
|
||||
const isBgSegment = parent.data.backgroundSegments[params.segmentId];
|
||||
if (isBgSegment !== undefined) {
|
||||
params.opacity = isBgSegment ? BACKGROUND_OPACITY : FOREROUND_OPACITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // //
|
||||
|
||||
export const MeshVisualTransformer = VolsegTransform({
|
||||
name: 'mesh-visual-from-streaming',
|
||||
display: { name: 'Mesh Visual from Streaming' },
|
||||
from: MeshStreaming,
|
||||
to: PluginStateObject.Shape.Representation3D,
|
||||
params: {
|
||||
/** Must be set to PluginStateObject reference to self */
|
||||
ref: PD.Text('', { isHidden: true, isEssential: true }), // QUESTION what is isEssential
|
||||
/** Identification of the mesh visual, e.g. 'low-2' */
|
||||
tag: PD.Text('', { isHidden: true, isEssential: true }),
|
||||
/** Opacity of the visual (not to be set directly, but controlled by the opacity of the parent Group, and by VisualInfo.visible) */
|
||||
opacity: PD.Numeric(-1, { min: 0, max: 1, step: 0.01 }, { isHidden: true }),
|
||||
}
|
||||
})({
|
||||
apply({ a, params, spine }, plugin: PluginContext) {
|
||||
return Task.create('Mesh Visual', async ctx => {
|
||||
const visualInfo: MeshStreaming.VisualInfo = a.data.visuals![params.tag];
|
||||
if (!visualInfo) throw new Error(`VisualInfo with tag '${params.tag}' is missing.`);
|
||||
const groupData = spine.getAncestorOfType(PluginStateObject.Group)?.data as MeshVisualGroupData | undefined;
|
||||
params.opacity = visualInfo.visible ? (groupData?.opacity ?? FOREROUND_OPACITY) : 0.0;
|
||||
const props = PD.getDefaultValues(Mesh.Params);
|
||||
props.flatShaded = true; // `flatShaded: true` is to see the real mesh vertices and triangles (default: false)
|
||||
props.alpha = params.opacity;
|
||||
const repr = ShapeRepresentation((ctx, meshlist: MeshlistData) => MeshlistData.getShape(meshlist, visualInfo.color), Mesh.Utils);
|
||||
await repr.createOrUpdate(props, visualInfo.data ?? MeshlistData.empty()).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Representation3D({ repr, sourceData: visualInfo.data }, { label: 'Mesh Visual', description: params.tag });
|
||||
});
|
||||
},
|
||||
update({ a, b, oldParams, newParams, spine }, plugin: PluginContext) {
|
||||
return Task.create('Update Mesh Visual', async ctx => {
|
||||
newParams.ref ||= oldParams.ref; // Protect against resetting params to invalid defaults
|
||||
newParams.tag ||= oldParams.tag; // Protect against resetting params to invalid defaults
|
||||
const visualInfo: MeshStreaming.VisualInfo = a.data.visuals![newParams.tag];
|
||||
if (!visualInfo) throw new Error(`VisualInfo with tag '${newParams.tag}' is missing.`);
|
||||
const oldData = b.data.sourceData as MeshlistData | undefined;
|
||||
if (visualInfo.data?.detail !== oldData?.detail) {
|
||||
return StateTransformer.UpdateResult.Recreate;
|
||||
}
|
||||
const groupData = spine.getAncestorOfType(PluginStateObject.Group)?.data as MeshVisualGroupData | undefined;
|
||||
const newOpacity = visualInfo.visible ? (groupData?.opacity ?? FOREROUND_OPACITY) : 0.0; // do not store to newParams directly, because oldParams and newParams might point to the same object!
|
||||
if (newOpacity !== oldParams.opacity) {
|
||||
newParams.opacity = newOpacity;
|
||||
await b.data.repr.createOrUpdate({ alpha: newParams.opacity }).runInContext(ctx);
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
} else {
|
||||
return StateTransformer.UpdateResult.Unchanged;
|
||||
}
|
||||
});
|
||||
},
|
||||
canAutoUpdate(params, globalCtx) {
|
||||
return true;
|
||||
},
|
||||
dispose({ b, params }, plugin) {
|
||||
b?.data.repr.destroy(); // QUESTION is this correct?
|
||||
},
|
||||
});
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // //
|
||||
|
||||
export const InitMeshStreaming = StateAction.build({
|
||||
display: { name: 'Mesh Streaming' },
|
||||
from: PluginStateObject.Root,
|
||||
params: MeshServerInfo.Params,
|
||||
isApplicable: (a, _, plugin: PluginContext) => true
|
||||
})(function (p, plugin: PluginContext) {
|
||||
return Task.create('Mesh Streaming', async ctx => {
|
||||
const { params } = p;
|
||||
// p.ref
|
||||
const serverNode = await plugin.build().to(p.ref).apply(MeshServerTransformer, params).commit();
|
||||
// const serverNode = await plugin.build().toRoot().apply(MeshServerTransformer, params).commit();
|
||||
const streamingNode = await plugin.build().to(serverNode).apply(MeshStreamingTransformer, {}).commit();
|
||||
const visuals = streamingNode.data?.visuals ?? {};
|
||||
const bgSegments = streamingNode.data?.backgroundSegments ?? {};
|
||||
|
||||
const segmentGroups: { [segid: number]: string } = {};
|
||||
for (const tag in visuals) {
|
||||
const segid = visuals[tag].segmentId;
|
||||
if (!segmentGroups[segid]) {
|
||||
let description = visuals[tag].segmentName;
|
||||
if (bgSegments[segid]) description += ' (background)';
|
||||
const group = await plugin.build().to(streamingNode).apply(MeshVisualGroupTransformer, { label: `Segment ${segid}`, description: description, segmentId: segid }, { state: { isCollapsed: true } }).commit();
|
||||
segmentGroups[segid] = group.ref;
|
||||
}
|
||||
}
|
||||
const visualsUpdate = plugin.build();
|
||||
for (const tag in visuals) {
|
||||
const ref = `${streamingNode.ref}-${tag}`;
|
||||
const segid = visuals[tag].segmentId;
|
||||
visualsUpdate.to(segmentGroups[segid]).apply(MeshVisualTransformer, { ref: ref, tag: tag }, { ref: ref }); // ref - hack to allow the node make itself invisible
|
||||
}
|
||||
await plugin.state.data.updateTree(visualsUpdate).runInContext(ctx); // QUESTION what is really the difference between this and `visualsUpdate.commit()`?
|
||||
});
|
||||
});
|
||||
|
||||
// TODO make available in GUI, in left panel or in right panel like Volume Streaming in src/mol-plugin-ui/structure/volume.tsx?
|
||||
340
src/extensions/meshes/mesh-utils.ts
Normal file
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
/** Helper functions for manipulation with mesh data. */
|
||||
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { CIF, CifFile } from '../../mol-io/reader/cif';
|
||||
import { Box3D } from '../../mol-math/geometry';
|
||||
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { volumeFromDensityServerData } from '../../mol-model-formats/volume/density-server';
|
||||
import { Grid } from '../../mol-model/volume';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { TypedArray } from '../../mol-util/type-helpers';
|
||||
|
||||
import { CIF_schema_mesh } from './mesh-cif-schema';
|
||||
|
||||
|
||||
type MeshModificationParams = {
|
||||
scale?: [number, number, number],
|
||||
shift?: [number, number, number],
|
||||
matrix?: Mat4,
|
||||
group?: number,
|
||||
invertSides?: boolean
|
||||
};
|
||||
|
||||
/** Modify mesh in-place */
|
||||
export function modify(m: Mesh, params: MeshModificationParams) {
|
||||
if (params.scale !== undefined) {
|
||||
const [qx, qy, qz] = params.scale;
|
||||
const vertices = m.vertexBuffer.ref.value;
|
||||
for (let i = 0; i < vertices.length; i += 3) {
|
||||
vertices[i] *= qx;
|
||||
vertices[i + 1] *= qy;
|
||||
vertices[i + 2] *= qz;
|
||||
}
|
||||
}
|
||||
if (params.shift !== undefined) {
|
||||
const [dx, dy, dz] = params.shift;
|
||||
const vertices = m.vertexBuffer.ref.value;
|
||||
for (let i = 0; i < vertices.length; i += 3) {
|
||||
vertices[i] += dx;
|
||||
vertices[i + 1] += dy;
|
||||
vertices[i + 2] += dz;
|
||||
}
|
||||
}
|
||||
if (params.matrix !== undefined) {
|
||||
const r = m.vertexBuffer.ref.value;
|
||||
const matrix = params.matrix;
|
||||
const size = 3 * m.vertexCount;
|
||||
for (let i = 0; i < size; i += 3) {
|
||||
Vec3.transformMat4Offset(r, r, matrix, i, i, 0);
|
||||
}
|
||||
}
|
||||
if (params.group !== undefined) {
|
||||
const groups = m.groupBuffer.ref.value;
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
groups[i] = params.group;
|
||||
}
|
||||
}
|
||||
if (params.invertSides) {
|
||||
const indices = m.indexBuffer.ref.value;
|
||||
let tmp;
|
||||
for (let i = 0; i < indices.length; i += 3) {
|
||||
tmp = indices[i];
|
||||
indices[i] = indices[i + 1];
|
||||
indices[i + 1] = tmp;
|
||||
}
|
||||
const normals = m.normalBuffer.ref.value;
|
||||
for (let i = 0; i < normals.length; i++) {
|
||||
normals[i] *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a copy a mesh, possibly modified */
|
||||
export function copy(m: Mesh, modification?: MeshModificationParams): Mesh {
|
||||
const nVertices = m.vertexCount;
|
||||
const nTriangles = m.triangleCount;
|
||||
const vertices = new Float32Array(m.vertexBuffer.ref.value);
|
||||
const indices = new Uint32Array(m.indexBuffer.ref.value);
|
||||
const normals = new Float32Array(m.normalBuffer.ref.value);
|
||||
const groups = new Float32Array(m.groupBuffer.ref.value);
|
||||
const result = Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
|
||||
if (modification) {
|
||||
modify(result, modification);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Join more meshes into one */
|
||||
export function concat(...meshes: Mesh[]): Mesh {
|
||||
const nVertices = sum(meshes.map(m => m.vertexCount));
|
||||
const nTriangles = sum(meshes.map(m => m.triangleCount));
|
||||
const vertices = concatArrays(Float32Array, meshes.map(m => m.vertexBuffer.ref.value));
|
||||
const normals = concatArrays(Float32Array, meshes.map(m => m.normalBuffer.ref.value));
|
||||
const groups = concatArrays(Float32Array, meshes.map(m => m.groupBuffer.ref.value));
|
||||
const newIndices = [];
|
||||
let offset = 0;
|
||||
for (const m of meshes) {
|
||||
newIndices.push(m.indexBuffer.ref.value.map(i => i + offset));
|
||||
offset += m.vertexCount;
|
||||
}
|
||||
const indices = concatArrays(Uint32Array, newIndices);
|
||||
return Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
|
||||
}
|
||||
|
||||
/** Return Mesh from CIF data and mesh IDs (group IDs).
|
||||
* Assume the CIF contains coords in grid space,
|
||||
* transform the output mesh to `space` */
|
||||
export async function meshFromCif(data: CifFile, invertSides: boolean | undefined = undefined, outSpace: 'grid' | 'fractional' | 'cartesian' = 'cartesian'): Promise<{ mesh: Mesh, meshIds: number[] }> {
|
||||
const volumeInfoBlock = data.blocks.find(b => b.header === 'VOLUME_INFO');
|
||||
const meshesBlock = data.blocks.find(b => b.header === 'MESHES');
|
||||
if (!volumeInfoBlock || !meshesBlock) throw new Error('Missing VOLUME_INFO or MESHES block in mesh CIF file');
|
||||
const volumeInfoCif = CIF.schema.densityServer(volumeInfoBlock);
|
||||
const meshCif = CIF_schema_mesh(meshesBlock);
|
||||
|
||||
const nVertices = meshCif.mesh_vertex._rowCount;
|
||||
const nTriangles = Math.floor(meshCif.mesh_triangle._rowCount / 3);
|
||||
|
||||
const mesh_id = meshCif.mesh.id.toArray();
|
||||
const vertex_meshId = meshCif.mesh_vertex.mesh_id.toArray();
|
||||
const x = meshCif.mesh_vertex.x.toArray();
|
||||
const y = meshCif.mesh_vertex.y.toArray();
|
||||
const z = meshCif.mesh_vertex.z.toArray();
|
||||
const triangle_meshId = meshCif.mesh_triangle.mesh_id.toArray();
|
||||
const triangle_vertexId = meshCif.mesh_triangle.vertex_id.toArray();
|
||||
|
||||
// Shift indices from within-mesh indices to overall indices
|
||||
const indices = new Uint32Array(3 * nTriangles);
|
||||
const offsets = offsetMap(vertex_meshId);
|
||||
for (let i = 0; i < 3 * nTriangles; i++) {
|
||||
const offset = offsets.get(triangle_meshId[i])!;
|
||||
indices[i] = offset + triangle_vertexId[i];
|
||||
}
|
||||
const vertices = flattenCoords(x, y, z);
|
||||
const normals = new Float32Array(3 * nVertices);
|
||||
const groups = new Float32Array(vertex_meshId);
|
||||
const mesh = Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
|
||||
|
||||
invertSides ??= isInverted(mesh);
|
||||
if (invertSides) {
|
||||
modify(mesh, { invertSides: true }); // Vertex orientation convention is opposite in Volseg API and in MolStar
|
||||
}
|
||||
|
||||
if (outSpace === 'cartesian') {
|
||||
const volume = await volumeFromDensityServerData(volumeInfoCif).run();
|
||||
const gridToCartesian = Grid.getGridToCartesianTransform(volume.grid);
|
||||
modify(mesh, { matrix: gridToCartesian });
|
||||
} else if (outSpace === 'fractional') {
|
||||
const gridSize = volumeInfoCif.volume_data_3d_info.sample_count.value(0);
|
||||
const originFract = volumeInfoCif.volume_data_3d_info.origin.value(0);
|
||||
const dimensionFract = volumeInfoCif.volume_data_3d_info.dimensions.value(0);
|
||||
if (dimensionFract[0] !== 1 || dimensionFract[1] !== 1 || dimensionFract[2] !== 1) throw new Error(`Asserted the fractional dimensions are [1,1,1], but are actually [${dimensionFract}]`);
|
||||
const scale: [number, number, number] = [1 / gridSize[0], 1 / gridSize[1], 1 / gridSize[2]];
|
||||
modify(mesh, { scale: scale, shift: Array.from(originFract) as any });
|
||||
}
|
||||
|
||||
Mesh.computeNormals(mesh); // normals only necessary if flatShaded==false
|
||||
|
||||
// const boxMesh = makeMeshFromBox([[0,0,0], [1,1,1]], 1);
|
||||
// const gridSize = volumeInfoCif.volume_data_3d_info.sample_count.value(0); const boxMesh = makeMeshFromBox([[0,0,0], Array.from(gridSize)] as any, 1);
|
||||
// const cellSize = volumeInfoCif.volume_data_3d_info.spacegroup_cell_size.value(0); const boxMesh = makeMeshFromBox([[0, 0, 0], Array.from(cellSize)] as any, 1);
|
||||
// mesh = concat(mesh, boxMesh); // debug
|
||||
return { mesh: mesh, meshIds: Array.from(mesh_id) };
|
||||
}
|
||||
|
||||
function isInverted(mesh: Mesh): boolean {
|
||||
const vertices = mesh.vertexBuffer.ref.value;
|
||||
const indices = mesh.indexBuffer.ref.value;
|
||||
const center = meshCenter(mesh);
|
||||
const center3 = Vec3.create(3 * center[0], 3 * center[1], 3 * center[2]);
|
||||
|
||||
let dirMetric = 0.0;
|
||||
const [a, b, c, u, v, normal, radius] = [Vec3(), Vec3(), Vec3(), Vec3(), Vec3(), Vec3(), Vec3()];
|
||||
for (let i = 0; i < indices.length; i += 3) {
|
||||
Vec3.fromArray(a, vertices, 3 * indices[i]);
|
||||
Vec3.fromArray(b, vertices, 3 * indices[i + 1]);
|
||||
Vec3.fromArray(c, vertices, 3 * indices[i + 2]);
|
||||
Vec3.sub(u, b, a);
|
||||
Vec3.sub(v, c, b);
|
||||
Vec3.cross(normal, u, v); // direction of the surface
|
||||
Vec3.add(radius, a, b);
|
||||
Vec3.add(radius, radius, c);
|
||||
Vec3.sub(radius, radius, center3); // direction center -> this triangle
|
||||
dirMetric += Vec3.dot(radius, normal);
|
||||
}
|
||||
return dirMetric < 0;
|
||||
}
|
||||
|
||||
function meshCenter(mesh: Mesh) {
|
||||
const vertices = mesh.vertexBuffer.ref.value;
|
||||
const n = vertices.length;
|
||||
let x = 0.0;
|
||||
let y = 0.0;
|
||||
let z = 0.0;
|
||||
for (let i = 0; i < vertices.length; i += 3) {
|
||||
x += vertices[i];
|
||||
y += vertices[i + 1];
|
||||
z += vertices[i + 2];
|
||||
}
|
||||
return Vec3.create(x / n, y / n, z / n);
|
||||
}
|
||||
|
||||
function flattenCoords(x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number>): Float32Array {
|
||||
const n = x.length;
|
||||
const out = new Float32Array(3 * n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
out[3 * i] = x[i];
|
||||
out[3 * i + 1] = y[i];
|
||||
out[3 * i + 2] = z[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Get mapping of unique values to the position of their first occurrence */
|
||||
function offsetMap(values: ArrayLike<number>) {
|
||||
const result = new Map<number, number>();
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (!result.has(values[i])) {
|
||||
result.set(values[i], i);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return bounding box */
|
||||
export function bbox(mesh: Mesh): Box3D | null { // Is there no function for this?
|
||||
const nVertices = mesh.vertexCount;
|
||||
const coords = mesh.vertexBuffer.ref.value;
|
||||
if (nVertices === 0) {
|
||||
return null;
|
||||
}
|
||||
let minX = coords[0], minY = coords[1], minZ = coords[2];
|
||||
let maxX = minX, maxY = minY, maxZ = minZ;
|
||||
for (let i = 0; i < 3 * nVertices; i += 3) {
|
||||
const x = coords[i], y = coords[i + 1], z = coords[i + 2];
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (z < minZ) minZ = z;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y > maxY) maxY = y;
|
||||
if (z > maxZ) maxZ = z;
|
||||
}
|
||||
return Box3D.create(Vec3.create(minX, minY, minZ), Vec3.create(maxX, maxY, maxZ));
|
||||
}
|
||||
|
||||
/** Example mesh - 1 triangle */
|
||||
export function fakeFakeMesh1(): Mesh {
|
||||
const nVertices = 3;
|
||||
const nTriangles = 1;
|
||||
const vertices = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
|
||||
const indices = new Uint32Array([0, 1, 2]);
|
||||
const normals = new Float32Array([0, 0, 1]);
|
||||
const groups = new Float32Array([0]);
|
||||
return Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
|
||||
}
|
||||
|
||||
/** Example mesh - irregular tetrahedron */
|
||||
export function fakeMesh4(): Mesh {
|
||||
const nVertices = 4;
|
||||
const nTriangles = 4;
|
||||
const vertices = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
||||
const indices = new Uint32Array([0, 2, 1, 0, 1, 3, 1, 2, 3, 2, 0, 3]);
|
||||
const normals = new Float32Array([-1, -1, -1, 1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
||||
const groups = new Float32Array([0, 1, 2, 3]);
|
||||
return Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
|
||||
}
|
||||
|
||||
/** Return a box-shaped mesh */
|
||||
export function meshFromBox(box: [[number, number, number], [number, number, number]], group: number = 0) {
|
||||
const [[x0, y0, z0], [x1, y1, z1]] = box;
|
||||
const vertices = new Float32Array([
|
||||
x0, y0, z0,
|
||||
x1, y0, z0,
|
||||
x0, y1, z0,
|
||||
x1, y1, z0,
|
||||
x0, y0, z1,
|
||||
x1, y0, z1,
|
||||
x0, y1, z1,
|
||||
x1, y1, z1,
|
||||
]);
|
||||
const indices = new Uint32Array([
|
||||
2, 1, 0, 1, 2, 3,
|
||||
1, 4, 0, 4, 1, 5,
|
||||
3, 5, 1, 5, 3, 7,
|
||||
2, 7, 3, 7, 2, 6,
|
||||
0, 6, 2, 6, 0, 4,
|
||||
4, 7, 6, 7, 4, 5,
|
||||
]);
|
||||
const groups = new Float32Array([group, group, group, group, group, group, group, group]);
|
||||
const normals = new Float32Array(8);
|
||||
const mesh = Mesh.create(vertices, indices, normals, groups, 8, 12);
|
||||
Mesh.computeNormals(mesh); // normals only necessary if flatShaded==false
|
||||
return mesh;
|
||||
}
|
||||
|
||||
function sum(array: number[]): number {
|
||||
return array.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
function concatArrays<T extends TypedArray>(t: new (len: number) => T, arrays: T[]): T {
|
||||
const totalLength = arrays.map(a => a.length).reduce((a, b) => a + b, 0);
|
||||
const result: T = new t(totalLength);
|
||||
let offset = 0;
|
||||
for (const array of arrays) {
|
||||
result.set(array, offset);
|
||||
offset += array.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Generate random colors (in a cycle) */
|
||||
export const ColorGenerator = function* () {
|
||||
const colors = shuffleArray(Object.values(ColorNames));
|
||||
let i = 0;
|
||||
while (true) {
|
||||
yield colors[i];
|
||||
i++;
|
||||
if (i >= colors.length) i = 0;
|
||||
}
|
||||
}();
|
||||
function shuffleArray<T>(array: T[]): T[] {
|
||||
// Stealed from https://www.w3docs.com/snippets/javascript/how-to-randomize-shuffle-a-javascript-array.html
|
||||
let curId = array.length;
|
||||
// There remain elements to shuffle
|
||||
while (0 !== curId) {
|
||||
// Pick a remaining element
|
||||
const randId = Math.floor(Math.random() * curId);
|
||||
curId -= 1;
|
||||
// Swap it with the current element.
|
||||
const tmp = array[curId];
|
||||
array[curId] = array[randId];
|
||||
array[randId] = tmp;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
135
src/extensions/meshes/metadata.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
import { Color } from '../../mol-util/color';
|
||||
|
||||
|
||||
// TODO unify with Metadata in Volseg
|
||||
|
||||
export interface Metadata {
|
||||
grid: {
|
||||
general: {
|
||||
details: string,
|
||||
},
|
||||
volumes: Volumes,
|
||||
segmentation_lattices: SegmentationLattices,
|
||||
segmentation_meshes: SegmentationMeshes,
|
||||
},
|
||||
annotation: Annotation,
|
||||
}
|
||||
|
||||
export interface Volumes {
|
||||
volume_downsamplings: number[],
|
||||
voxel_size: { [downsampling: number]: Vector3 },
|
||||
origin: Vector3,
|
||||
grid_dimensions: Vector3,
|
||||
sampled_grid_dimensions: { [downsampling: number]: Vector3 },
|
||||
mean: { [downsampling: number]: number },
|
||||
std: { [downsampling: number]: number },
|
||||
min: { [downsampling: number]: number },
|
||||
max: { [downsampling: number]: number },
|
||||
volume_force_dtype: string,
|
||||
}
|
||||
|
||||
export interface SegmentationLattices {
|
||||
segmentation_lattice_ids: number[],
|
||||
segmentation_downsamplings: { [lattice: number]: number[] },
|
||||
}
|
||||
|
||||
export interface Annotation {
|
||||
name: string,
|
||||
details: string,
|
||||
segment_list: Segment[],
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
id: number,
|
||||
colour: number[],
|
||||
biological_annotation: BiologicalAnnotation,
|
||||
}
|
||||
|
||||
export interface BiologicalAnnotation {
|
||||
name: string,
|
||||
external_references: { id: number, resource: string, accession: string, label: string, description: string }[]
|
||||
}
|
||||
|
||||
export interface SegmentationMeshes {
|
||||
mesh_component_numbers: {
|
||||
segment_ids?: {
|
||||
[segId: number]: {
|
||||
detail_lvls: {
|
||||
[detail: number]: {
|
||||
mesh_ids: {
|
||||
[meshId: number]: {
|
||||
num_triangles: number,
|
||||
num_vertices: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
detail_lvl_to_fraction: {
|
||||
[lvl: number]: number
|
||||
}
|
||||
}
|
||||
|
||||
type Vector3 = [number, number, number];
|
||||
|
||||
|
||||
|
||||
export namespace Metadata {
|
||||
export function meshSegments(metadata: Metadata): number[] {
|
||||
const segmentIds = metadata.grid.segmentation_meshes.mesh_component_numbers.segment_ids;
|
||||
if (segmentIds === undefined) return [];
|
||||
return Object.keys(segmentIds).map(s => parseInt(s));
|
||||
}
|
||||
export function meshSegmentDetails(metadata: Metadata, segmentId: number): number[] {
|
||||
const segmentIds = metadata.grid.segmentation_meshes.mesh_component_numbers.segment_ids;
|
||||
if (segmentIds === undefined) return [];
|
||||
const details = segmentIds[segmentId].detail_lvls;
|
||||
return Object.keys(details).map(s => parseInt(s));
|
||||
}
|
||||
/** Get the worst available detail level that is not worse than preferredDetail.
|
||||
* If preferredDetail is null, get the worst detail level overall.
|
||||
* (worse = greater number) */
|
||||
export function getSufficientDetail(metadata: Metadata, segmentId: number, preferredDetail: number | null) {
|
||||
let availDetails = meshSegmentDetails(metadata, segmentId);
|
||||
if (preferredDetail !== null) {
|
||||
availDetails = availDetails.filter(det => det <= preferredDetail);
|
||||
}
|
||||
return Math.max(...availDetails);
|
||||
}
|
||||
export function annotationsBySegment(metadata: Metadata): { [id: number]: Segment } {
|
||||
const result: { [id: number]: Segment } = {};
|
||||
for (const segment of metadata.annotation.segment_list) {
|
||||
if (segment.id in result) {
|
||||
throw new Error(`Duplicate segment annotation for segment ${segment.id}`);
|
||||
}
|
||||
result[segment.id] = segment;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function dropSegments(metadata: Metadata, segments: number[]): void {
|
||||
if (metadata.grid.segmentation_meshes.mesh_component_numbers.segment_ids === undefined) return;
|
||||
const dropSet = new Set(segments);
|
||||
metadata.annotation.segment_list = metadata.annotation.segment_list.filter(seg => !dropSet.has(seg.id));
|
||||
for (const seg of segments) {
|
||||
delete metadata.grid.segmentation_meshes.mesh_component_numbers.segment_ids[seg];
|
||||
}
|
||||
}
|
||||
export function namesAndColorsBySegment(metadata: Metadata) {
|
||||
const result: { [id: number]: { name: string, color: Color } } = {};
|
||||
for (const segment of metadata.annotation.segment_list) {
|
||||
if (segment.id in result) throw new Error(`Duplicate segment annotation for segment ${segment.id}`);
|
||||
result[segment.id] = { name: segment.biological_annotation.name, color: Color.fromNormalizedArray(segment.colour, 0) };
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean,
|
||||
description: 'Data included in Model Archive files.'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
|
||||
private provider = QualityAssessmentProvider
|
||||
private provider = QualityAssessmentProvider;
|
||||
|
||||
private labelProvider = {
|
||||
label: (loci: Loci): string | undefined => {
|
||||
@@ -36,7 +36,7 @@ export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean,
|
||||
qmeanLabel(loci),
|
||||
].filter(l => !!l).join('</br>');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
register(): void {
|
||||
DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -14,6 +15,7 @@ import { CustomPropSymbol } from '../../../mol-script/language/symbol';
|
||||
import { Type } from '../../../mol-script/language/type';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { AtomicIndex } from '../../../mol-model/structure/model/properties/atomic';
|
||||
|
||||
export { QualityAssessment };
|
||||
|
||||
@@ -71,14 +73,28 @@ namespace QualityAssessment {
|
||||
localNames.set(ma_qa_metric.id.value(i), name);
|
||||
}
|
||||
|
||||
const residueKey: AtomicIndex.ResidueLabelKey = {
|
||||
label_entity_id: '',
|
||||
label_asym_id: '',
|
||||
label_seq_id: 0,
|
||||
pdbx_PDB_ins_code: undefined,
|
||||
};
|
||||
|
||||
for (let i = 0, il = ma_qa_metric_local._rowCount; i < il; i++) {
|
||||
if (model_id.value(i) !== model.modelNum) continue;
|
||||
|
||||
const labelAsymId = label_asym_id.value(i);
|
||||
const entityIndex = index.findEntity(labelAsymId);
|
||||
const rI = index.findResidue(model.entities.data.id.value(entityIndex), labelAsymId, label_seq_id.value(i));
|
||||
const name = localNames.get(metric_id.value(i))!;
|
||||
localMetrics.get(name)!.set(rI, metric_value.value(i));
|
||||
|
||||
residueKey.label_entity_id = model.entities.data.id.value(entityIndex);
|
||||
residueKey.label_asym_id = labelAsymId;
|
||||
residueKey.label_seq_id = label_seq_id.value(i);
|
||||
|
||||
const rI = index.findResidueLabel(residueKey);
|
||||
if (rI >= 0) {
|
||||
const name = localNames.get(metric_id.value(i))!;
|
||||
localMetrics.get(name)!.set(rI, metric_value.value(i));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
87
src/extensions/model-export/export.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { utf8ByteCount, utf8Write } from '../../mol-io/common/utf8';
|
||||
import { to_mmCIF, Unit } from '../../mol-model/structure';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { getFormattedTime } from '../../mol-util/date';
|
||||
import { download } from '../../mol-util/download';
|
||||
import { zip } from '../../mol-util/zip/zip';
|
||||
|
||||
export async function exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'bcif' }) {
|
||||
try {
|
||||
await plugin.runTask(_exportHierarchy(plugin, options), { useOverlay: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
plugin.log.error(`Model export failed. See console for details.`);
|
||||
}
|
||||
}
|
||||
|
||||
function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'bcif' }) {
|
||||
return Task.create('Export', async ctx => {
|
||||
await ctx.update({ message: 'Exporting...', isIndeterminate: true, canAbort: false });
|
||||
|
||||
const format = options?.format ?? 'cif';
|
||||
const { structures } = plugin.managers.structure.hierarchy.current;
|
||||
|
||||
const files: [name: string, data: string | Uint8Array][] = [];
|
||||
const entryMap = new Map<string, number>();
|
||||
|
||||
for (const _s of structures) {
|
||||
const s = _s.transform?.cell.obj?.data ?? _s.cell.obj?.data;
|
||||
if (!s) continue;
|
||||
if (s.models.length > 1) {
|
||||
plugin.log.warn(`[Export] Skipping ${_s.cell.obj?.label}: Multimodel exports not supported.`);
|
||||
continue;
|
||||
}
|
||||
if (s.units.some(u => !Unit.isAtomic(u))) {
|
||||
plugin.log.warn(`[Export] Skipping ${_s.cell.obj?.label}: Non-atomic model exports not supported.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = entryMap.has(s.model.entryId)
|
||||
? `${s.model.entryId}_${entryMap.get(s.model.entryId)! + 1}.${format}`
|
||||
: `${s.model.entryId}.${format}`;
|
||||
entryMap.set(s.model.entryId, (entryMap.get(s.model.entryId) ?? 0) + 1);
|
||||
|
||||
await ctx.update({ message: `Exporting ${s.model.entryId}...`, isIndeterminate: true, canAbort: false });
|
||||
if (s.elementCount > 100000) {
|
||||
// Give UI chance to update, only needed for larger structures.
|
||||
await new Promise(res => setTimeout(res, 50));
|
||||
}
|
||||
|
||||
try {
|
||||
files.push([name, to_mmCIF(s.model.entryId, s, format === 'bcif', { copyAllCategories: true })]);
|
||||
} catch (e) {
|
||||
if (format === 'cif' && s.elementCount > 2000000) {
|
||||
plugin.log.warn(`[Export] The structure might be too big to be exported as Text CIF, consider using the BinaryCIF format instead.`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length === 1) {
|
||||
download(new Blob([files[0][1]]), files[0][0]);
|
||||
} else if (files.length > 1) {
|
||||
const zipData: Record<string, Uint8Array> = {};
|
||||
for (const [fn, data] of files) {
|
||||
if (data instanceof Uint8Array) {
|
||||
zipData[fn] = data;
|
||||
} else {
|
||||
const bytes = new Uint8Array(utf8ByteCount(data));
|
||||
utf8Write(bytes, 0, data);
|
||||
zipData[fn] = bytes;
|
||||
}
|
||||
}
|
||||
await ctx.update({ message: `Compressing Data...`, isIndeterminate: true, canAbort: false });
|
||||
const buffer = await zip(ctx, zipData);
|
||||
download(new Blob([new Uint8Array(buffer, 0, buffer.byteLength)]), `structures_${getFormattedTime()}.zip`);
|
||||
}
|
||||
|
||||
plugin.log.info(`[Export] Done.`);
|
||||
});
|
||||
}
|
||||
30
src/extensions/model-export/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { ModelExportUI } from './ui';
|
||||
|
||||
export const ModelExport = PluginBehavior.create<{}>({
|
||||
name: 'extension-model-export',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'Model Export'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{}> {
|
||||
register(): void {
|
||||
this.ctx.customStructureControls.set('model-export', ModelExportUI as any);
|
||||
}
|
||||
|
||||
update() {
|
||||
return false;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
this.ctx.customStructureControls.delete('model-export');
|
||||
}
|
||||
},
|
||||
params: () => ({})
|
||||
});
|
||||
69
src/extensions/model-export/ui.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
|
||||
import { Button } from '../../mol-plugin-ui/controls/common';
|
||||
import { GetAppSvg } from '../../mol-plugin-ui/controls/icons';
|
||||
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
|
||||
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { exportHierarchy } from './export';
|
||||
|
||||
export class ModelExportUI extends CollapsableControls<{}, {}> {
|
||||
protected defaultState(): CollapsableState {
|
||||
return {
|
||||
header: 'Export Models',
|
||||
isCollapsed: true,
|
||||
brand: { accent: 'cyan', svg: GetAppSvg }
|
||||
};
|
||||
}
|
||||
protected renderControls(): JSX.Element | null {
|
||||
return <ExportControls plugin={this.plugin} />;
|
||||
}
|
||||
}
|
||||
|
||||
const Params = {
|
||||
format: PD.Select<'cif' | 'bcif'>('cif', [['cif', 'mmCIF'], ['bcif', 'Binary mmCIF']])
|
||||
};
|
||||
const DefaultParams = PD.getDefaultValues(Params);
|
||||
|
||||
function ExportControls({ plugin }: { plugin: PluginContext }) {
|
||||
const [params, setParams] = useState(DefaultParams);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
useBehavior(plugin.managers.structure.hierarchy.behaviors.selection); // triggers UI update
|
||||
const isBusy = useBehavior(plugin.behaviors.state.isBusy);
|
||||
const hierarchy = plugin.managers.structure.hierarchy.current;
|
||||
|
||||
let label: string = 'Nothing to Export';
|
||||
if (hierarchy.structures.length === 1) {
|
||||
label = 'Export';
|
||||
} if (hierarchy.structures.length > 1) {
|
||||
label = 'Export (as ZIP)';
|
||||
}
|
||||
|
||||
const onExport = async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
await exportHierarchy(plugin, { format: params.format });
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
<ParameterControls params={Params} values={params} onChangeValues={setParams} isDisabled={isBusy || exporting} />
|
||||
<Button
|
||||
onClick={onExport}
|
||||
style={{ marginTop: 1 }}
|
||||
disabled={isBusy || hierarchy.structures.length === 0 || exporting}
|
||||
commit={hierarchy.structures.length ? 'on' : 'off'}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</>;
|
||||
}
|
||||
@@ -118,11 +118,13 @@ export class Mp4Controls extends PluginComponent {
|
||||
}
|
||||
|
||||
private init() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
|
||||
this.subscribe(this.plugin.managers.animation.events.updated.pipe(debounceTime(16)), () => {
|
||||
this.sync();
|
||||
});
|
||||
|
||||
this.subscribe(this.plugin.canvas3d?.resized!, () => this.syncInfo());
|
||||
this.subscribe(this.plugin.canvas3d.resized, () => this.syncInfo());
|
||||
this.subscribe(this.plugin.helpers.viewportScreenshot?.events.previewed!, () => this.syncInfo());
|
||||
|
||||
this.subscribe(this.plugin.behaviors.state.isBusy, b => this.updateCanApply(b));
|
||||
|
||||
@@ -69,11 +69,12 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
|
||||
const dt = durationMs / N;
|
||||
|
||||
await ctx.update({ message: 'Rendering...', isIndeterminate: false, current: 0, max: N + 1 });
|
||||
await params.pass.updateBackground();
|
||||
|
||||
await plugin.managers.animation.play(params.animation.definition, params.animation.params);
|
||||
stoppedAnimation = false;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
await loop.tick(i * dt, { isSynchronous: true, manualDraw: true });
|
||||
await loop.tick(i * dt, { isSynchronous: true, animation: { currentFrame: i, frameCount: N }, manualDraw: true });
|
||||
|
||||
const image = params.pass.getImageData(width, height, normalizedViewport);
|
||||
encoder.addFrameRgba(image.data);
|
||||
|
||||
@@ -102,6 +102,7 @@ export class Mp4EncoderUI extends CollapsableControls<{}, State> {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this._controls?.dispose();
|
||||
this._controls = void 0;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,16 @@ function createIntraUnitClashCylinderMesh(ctx: VisualContext, unit: Unit, struct
|
||||
radius: (edgeIndex: number) => magnitude[edgeIndex] * sizeFactor,
|
||||
};
|
||||
|
||||
return createLinkCylinderMesh(ctx, builderProps, props, mesh);
|
||||
const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
|
||||
|
||||
if (boundingSphere) {
|
||||
m.setBoundingSphere(boundingSphere);
|
||||
} else if (m.triangleCount > 0) {
|
||||
const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * sizeFactor);
|
||||
m.setBoundingSphere(sphere);
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
export const IntraUnitClashParams = {
|
||||
@@ -169,7 +178,16 @@ function createInterUnitClashCylinderMesh(ctx: VisualContext, structure: Structu
|
||||
radius: (edgeIndex: number) => edges[edgeIndex].props.magnitude * sizeFactor
|
||||
};
|
||||
|
||||
return createLinkCylinderMesh(ctx, builderProps, props, mesh);
|
||||
const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
|
||||
|
||||
if (boundingSphere) {
|
||||
m.setBoundingSphere(boundingSphere);
|
||||
} else {
|
||||
const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * sizeFactor);
|
||||
m.setBoundingSphere(sphere);
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
export const InterUnitClashParams = {
|
||||
|
||||
103
src/extensions/volumes-and-segmentations/entry-meshes.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
|
||||
import { ShapeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
|
||||
import { createMeshFromUrl } from '../meshes/examples';
|
||||
import { BACKGROUND_SEGMENT_VOLUME_THRESHOLD } from '../meshes/mesh-streaming/behavior';
|
||||
|
||||
import { Segment } from './volseg-api/data';
|
||||
import { VolsegEntryData } from './entry-root';
|
||||
|
||||
|
||||
const DEFAULT_MESH_DETAIL: number | null = 5; // null means worst
|
||||
|
||||
|
||||
export class VolsegMeshSegmentationData {
|
||||
private entryData: VolsegEntryData;
|
||||
|
||||
constructor(rootData: VolsegEntryData) {
|
||||
this.entryData = rootData;
|
||||
}
|
||||
|
||||
async loadSegmentation() {
|
||||
const hasMeshes = this.entryData.metadata.meshSegmentIds.length > 0;
|
||||
if (hasMeshes) {
|
||||
await this.showSegments(this.entryData.metadata.allSegmentIds);
|
||||
}
|
||||
}
|
||||
|
||||
updateOpacity(opacity: number) {
|
||||
const visuals = this.entryData.findNodesByTags('mesh-segment-visual');
|
||||
const update = this.entryData.newUpdate();
|
||||
for (const visual of visuals) {
|
||||
update.to(visual).update(ShapeRepresentation3D, p => { (p as any).alpha = opacity; });
|
||||
}
|
||||
return update.commit();
|
||||
}
|
||||
|
||||
async highlightSegment(segment: Segment) {
|
||||
const visuals = this.entryData.findNodesByTags('mesh-segment-visual', `segment-${segment.id}`);
|
||||
for (const visual of visuals) {
|
||||
await PluginCommands.Interactivity.Object.Highlight(this.entryData.plugin, { state: this.entryData.plugin.state.data, ref: visual.transform.ref });
|
||||
}
|
||||
}
|
||||
|
||||
async selectSegment(segment?: number) {
|
||||
if (segment === undefined || segment < 0) return;
|
||||
const visuals = this.entryData.findNodesByTags('mesh-segment-visual', `segment-${segment}`);
|
||||
const reprNode: PluginStateObject.Shape.Representation3D | undefined = visuals[0]?.obj;
|
||||
if (!reprNode) return;
|
||||
const loci = reprNode.data.repr.getAllLoci()[0];
|
||||
if (!loci) return;
|
||||
this.entryData.plugin.managers.interactivity.lociSelects.select({ loci: loci, repr: reprNode.data.repr }, false);
|
||||
}
|
||||
|
||||
/** Make visible the specified set of mesh segments */
|
||||
async showSegments(segments: number[]) {
|
||||
const segmentsToShow = new Set(segments);
|
||||
|
||||
const visuals = this.entryData.findNodesByTags('mesh-segment-visual');
|
||||
for (const visual of visuals) {
|
||||
const theTag = visual.obj?.tags?.find(tag => tag.startsWith('segment-'));
|
||||
if (!theTag) continue;
|
||||
const id = parseInt(theTag.split('-')[1]);
|
||||
const visibility = segmentsToShow.has(id);
|
||||
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, !visibility); // true means hide, ¯\_(ツ)_/¯
|
||||
segmentsToShow.delete(id);
|
||||
}
|
||||
|
||||
const segmentsToCreate = this.entryData.metadata.meshSegmentIds.filter(seg => segmentsToShow.has(seg));
|
||||
if (segmentsToCreate.length === 0) return;
|
||||
|
||||
let group = this.entryData.findNodesByTags('mesh-segmentation-group')[0]?.transform.ref;
|
||||
if (!group) {
|
||||
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup, { label: 'Segmentation', description: 'Mesh' }, { tags: ['mesh-segmentation-group'], state: { isCollapsed: true } }).commit();
|
||||
group = newGroupNode.ref;
|
||||
}
|
||||
const totalVolume = this.entryData.metadata.gridTotalVolume;
|
||||
|
||||
const awaiting = [];
|
||||
for (const seg of segmentsToCreate) {
|
||||
const segment = this.entryData.metadata.getSegment(seg);
|
||||
if (!segment) continue;
|
||||
const detail = this.entryData.metadata.getSufficientMeshDetail(seg, DEFAULT_MESH_DETAIL);
|
||||
const color = segment.colour.length >= 3 ? Color.fromNormalizedArray(segment.colour, 0) : ColorNames.gray;
|
||||
const url = this.entryData.api.meshUrl_Bcif(this.entryData.source, this.entryData.entryId, seg, detail);
|
||||
const label = segment.biological_annotation.name ?? `Segment ${seg}`;
|
||||
const meshPromise = createMeshFromUrl(this.entryData.plugin, url, seg, detail, true, color, group,
|
||||
BACKGROUND_SEGMENT_VOLUME_THRESHOLD * totalVolume, `<b>${label}</b>`, this.entryData.ref);
|
||||
awaiting.push(meshPromise);
|
||||
}
|
||||
for (const promise of awaiting) await promise;
|
||||
}
|
||||
}
|
||||
60
src/extensions/volumes-and-segmentations/entry-models.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
|
||||
import { TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
|
||||
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
|
||||
import { StateObjectRef, StateObjectSelector } from '../../mol-state';
|
||||
|
||||
import { VolsegEntryData } from './entry-root';
|
||||
|
||||
|
||||
export class VolsegModelData {
|
||||
private entryData: VolsegEntryData;
|
||||
|
||||
constructor(rootData: VolsegEntryData) {
|
||||
this.entryData = rootData;
|
||||
}
|
||||
|
||||
private async loadPdb(pdbId: string, parent: StateObjectSelector | StateObjectRef) {
|
||||
const url = `https://www.ebi.ac.uk/pdbe/entry-files/download/${pdbId}.bcif`;
|
||||
const dataNode = await this.entryData.plugin.build().to(parent).apply(Download, { url: url, isBinary: true }, { tags: ['fitted-model-data', `pdbid-${pdbId}`] }).commit();
|
||||
const cifNode = await this.entryData.plugin.build().to(dataNode).apply(ParseCif).commit();
|
||||
const trajectoryNode = await this.entryData.plugin.build().to(cifNode).apply(TrajectoryFromMmCif).commit();
|
||||
await this.entryData.plugin.builders.structure.hierarchy.applyPreset(trajectoryNode, 'default', { representationPreset: 'polymer-cartoon' });
|
||||
return dataNode;
|
||||
}
|
||||
|
||||
async showPdbs(pdbIds: string[]) {
|
||||
const segmentsToShow = new Set(pdbIds);
|
||||
|
||||
const visuals = this.entryData.findNodesByTags('fitted-model-data');
|
||||
for (const visual of visuals) {
|
||||
const theTag = visual.obj?.tags?.find(tag => tag.startsWith('pdbid-'));
|
||||
if (!theTag) continue;
|
||||
const id = theTag.split('-')[1];
|
||||
const visibility = segmentsToShow.has(id);
|
||||
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, !visibility); // true means hide, ¯\_(ツ)_/¯
|
||||
segmentsToShow.delete(id);
|
||||
}
|
||||
|
||||
const segmentsToCreate = Array.from(segmentsToShow);
|
||||
if (segmentsToCreate.length === 0) return;
|
||||
|
||||
let group = this.entryData.findNodesByTags('fitted-models-group')[0]?.transform.ref;
|
||||
if (!group) {
|
||||
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup, { label: 'Fitted Models' }, { tags: ['fitted-models-group'], state: { isCollapsed: true } }).commit();
|
||||
group = newGroupNode.ref;
|
||||
}
|
||||
|
||||
const awaiting = [];
|
||||
for (const pdbId of segmentsToCreate) {
|
||||
awaiting.push(this.loadPdb(pdbId, group));
|
||||
}
|
||||
for (const promise of awaiting) await promise;
|
||||
}
|
||||
}
|
||||
377
src/extensions/volumes-and-segmentations/entry-root.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, distinctUntilChanged, Subject, throttleTime } from 'rxjs';
|
||||
import { VolsegVolumeServerConfig } from '.';
|
||||
import { Loci } from '../../mol-model/loci';
|
||||
|
||||
import { ShapeGroup } from '../../mol-model/shape';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectCell, StateSelection, StateTransform } from '../../mol-state';
|
||||
import { shallowEqualObjects } from '../../mol-util';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { MeshlistData } from '../meshes/mesh-extension';
|
||||
|
||||
import { DEFAULT_VOLSEG_SERVER, VolumeApiV2 } from './volseg-api/api';
|
||||
import { Segment } from './volseg-api/data';
|
||||
import { MetadataWrapper } from './volseg-api/utils';
|
||||
import { VolsegMeshSegmentationData } from './entry-meshes';
|
||||
import { VolsegModelData } from './entry-models';
|
||||
import { VolsegLatticeSegmentationData } from './entry-segmentation';
|
||||
import { VolsegState, VolsegStateData, VolsegStateParams } from './entry-state';
|
||||
import { VolsegVolumeData, SimpleVolumeParamValues, VOLUME_VISUAL_TAG } from './entry-volume';
|
||||
import * as ExternalAPIs from './external-api';
|
||||
import { VolsegGlobalStateData } from './global-state';
|
||||
import { applyEllipsis, Choice, isDefined, lazyGetter, splitEntryId } from './helpers';
|
||||
import { type VolsegStateFromEntry } from './transformers';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
|
||||
|
||||
export const MAX_VOXELS = 10 ** 7;
|
||||
// export const MAX_VOXELS = 10 ** 2; // DEBUG
|
||||
export const BOX: [[number, number, number], [number, number, number]] | null = null;
|
||||
// export const BOX: [[number, number, number], [number, number, number]] | null = [[-90, -90, -90], [90, 90, 90]]; // DEBUG
|
||||
|
||||
const MAX_ANNOTATIONS_IN_LABEL = 6;
|
||||
|
||||
|
||||
const SourceChoice = new Choice({ emdb: 'EMDB', empiar: 'EMPIAR', idr: 'IDR' }, 'emdb');
|
||||
export type Source = Choice.Values<typeof SourceChoice>;
|
||||
|
||||
|
||||
export function createLoadVolsegParams(plugin?: PluginContext, entrylists: { [source: string]: string[] } = {}) {
|
||||
const defaultVolumeServer = plugin?.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLSEG_SERVER;
|
||||
return {
|
||||
serverUrl: ParamDefinition.Text(defaultVolumeServer),
|
||||
source: ParamDefinition.Mapped(SourceChoice.values[0], SourceChoice.options, src => entryParam(entrylists[src])),
|
||||
};
|
||||
}
|
||||
function entryParam(entries: string[] = []) {
|
||||
const options: [string, string][] = entries.map(e => [e, e]);
|
||||
options.push(['__custom__', 'Custom']);
|
||||
return ParamDefinition.Group({
|
||||
entryId: ParamDefinition.Select(options[0][0], options, { description: 'Choose an entry from the list, or choose "Custom" and type any entry ID (useful when using other than default server).' }),
|
||||
customEntryId: ParamDefinition.Text('', { hideIf: p => p.entryId !== '__custom__', description: 'Entry identifier, including the source prefix, e.g. "emd-1832"' }),
|
||||
}, { isFlat: true });
|
||||
}
|
||||
type LoadVolsegParamValues = ParamDefinition.Values<ReturnType<typeof createLoadVolsegParams>>;
|
||||
|
||||
export function createVolsegEntryParams(plugin?: PluginContext) {
|
||||
const defaultVolumeServer = plugin?.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLSEG_SERVER;
|
||||
return {
|
||||
serverUrl: ParamDefinition.Text(defaultVolumeServer),
|
||||
source: SourceChoice.PDSelect(),
|
||||
entryId: ParamDefinition.Text('emd-1832', { description: 'Entry identifier, including the source prefix, e.g. "emd-1832"' }),
|
||||
};
|
||||
}
|
||||
type VolsegEntryParamValues = ParamDefinition.Values<ReturnType<typeof createVolsegEntryParams>>;
|
||||
|
||||
export namespace VolsegEntryParamValues {
|
||||
export function fromLoadVolsegParamValues(params: LoadVolsegParamValues): VolsegEntryParamValues {
|
||||
let entryId = (params.source.params as any).entryId;
|
||||
if (entryId === '__custom__') {
|
||||
entryId = (params.source.params as any).customEntryId;
|
||||
}
|
||||
return {
|
||||
serverUrl: params.serverUrl,
|
||||
source: params.source.name as Source,
|
||||
entryId: entryId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class VolsegEntry extends PluginStateObject.CreateBehavior<VolsegEntryData>({ name: 'Vol & Seg Entry' }) { }
|
||||
|
||||
type VolRepr3DT = typeof StateTransforms.Representation.VolumeRepresentation3D
|
||||
|
||||
export class VolsegEntryData extends PluginBehavior.WithSubscribers<VolsegEntryParamValues> {
|
||||
plugin: PluginContext;
|
||||
ref: string = '';
|
||||
api: VolumeApiV2;
|
||||
source: Source;
|
||||
/** Number part of entry ID; e.g. '1832' */
|
||||
entryNumber: string;
|
||||
/** Full entry ID; e.g. 'emd-1832' */
|
||||
entryId: string;
|
||||
metadata: MetadataWrapper;
|
||||
pdbs: string[];
|
||||
|
||||
public readonly volumeData = new VolsegVolumeData(this);
|
||||
private readonly latticeSegmentationData = new VolsegLatticeSegmentationData(this);
|
||||
private readonly meshSegmentationData = new VolsegMeshSegmentationData(this);
|
||||
private readonly modelData = new VolsegModelData(this);
|
||||
private highlightRequest = new Subject<Segment | undefined>();
|
||||
|
||||
private getStateNode = lazyGetter(() => this.plugin.state.data.selectQ(q => q.byRef(this.ref).subtree().ofType(VolsegState))[0] as StateObjectCell<VolsegState, StateTransform<typeof VolsegStateFromEntry>>, 'Missing VolsegState node. Must first create VolsegState for this VolsegEntry.');
|
||||
public currentState = new BehaviorSubject(ParamDefinition.getDefaultValues(VolsegStateParams));
|
||||
public currentVolume = new BehaviorSubject<StateTransform<VolRepr3DT> | undefined>(undefined);
|
||||
|
||||
|
||||
private constructor(plugin: PluginContext, params: VolsegEntryParamValues) {
|
||||
super(plugin, params);
|
||||
this.plugin = plugin;
|
||||
this.api = new VolumeApiV2(params.serverUrl);
|
||||
this.source = params.source;
|
||||
this.entryId = params.entryId;
|
||||
this.entryNumber = splitEntryId(this.entryId).entryNumber;
|
||||
}
|
||||
|
||||
private async initialize() {
|
||||
const metadata = await this.api.getMetadata(this.source, this.entryId);
|
||||
this.metadata = new MetadataWrapper(metadata);
|
||||
this.pdbs = await ExternalAPIs.getPdbIdsForEmdbEntry(this.metadata.raw.grid.general.source_db_id ?? this.entryId);
|
||||
// TODO use Asset?
|
||||
}
|
||||
|
||||
static async create(plugin: PluginContext, params: VolsegEntryParamValues) {
|
||||
const result = new VolsegEntryData(plugin, params);
|
||||
await result.initialize();
|
||||
return result;
|
||||
}
|
||||
|
||||
async register(ref: string) {
|
||||
this.ref = ref;
|
||||
this.plugin.managers.lociLabels.addProvider(this.labelProvider);
|
||||
|
||||
try {
|
||||
const params = this.getStateNode().obj?.data;
|
||||
if (params) {
|
||||
this.currentState.next(params);
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
const volumeVisual = this.findNodesByTags(VOLUME_VISUAL_TAG)[0];
|
||||
if (volumeVisual) this.currentVolume.next(volumeVisual.transform);
|
||||
|
||||
let volumeRef: string | undefined;
|
||||
this.subscribeObservable(this.plugin.state.data.events.cell.stateUpdated, e => {
|
||||
try { (this.getStateNode()); } catch { return; } // if state not does not exist yet
|
||||
if (e.cell.transform.ref === this.getStateNode().transform.ref) {
|
||||
const newState = this.getStateNode().obj?.data;
|
||||
if (newState && !shallowEqualObjects(newState, this.currentState.value)) { // avoid repeated update
|
||||
this.currentState.next(newState);
|
||||
}
|
||||
} else if (e.cell.transform.tags?.includes(VOLUME_VISUAL_TAG)) {
|
||||
if (e.ref === volumeRef) {
|
||||
this.currentVolume.next(e.cell.transform);
|
||||
} else if (StateSelection.findAncestor(this.plugin.state.data.tree, this.plugin.state.data.cells, e.ref, a => a.transform.ref === ref)) {
|
||||
volumeRef = e.ref;
|
||||
this.currentVolume.next(e.cell.transform);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.subscribeObservable(this.plugin.state.data.events.cell.removed, e => {
|
||||
if (e.ref === volumeRef) {
|
||||
volumeRef = undefined;
|
||||
this.currentVolume.next(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
this.subscribeObservable(this.plugin.behaviors.interaction.click, async e => {
|
||||
const loci = e.current.loci;
|
||||
const clickedSegment = this.getSegmentIdFromLoci(loci);
|
||||
if (clickedSegment === undefined) return;
|
||||
if (clickedSegment === this.currentState.value.selectedSegment) {
|
||||
this.actionSelectSegment(undefined);
|
||||
} else {
|
||||
this.actionSelectSegment(clickedSegment);
|
||||
}
|
||||
});
|
||||
|
||||
this.subscribeObservable(
|
||||
this.highlightRequest.pipe(throttleTime(50, undefined, { leading: true, trailing: true })),
|
||||
async segment => await this.highlightSegment(segment)
|
||||
);
|
||||
|
||||
this.subscribeObservable(
|
||||
this.currentState.pipe(distinctUntilChanged((a, b) => a.selectedSegment === b.selectedSegment)),
|
||||
async state => {
|
||||
if (VolsegGlobalStateData.getGlobalState(this.plugin)?.selectionMode) await this.selectSegment(state.selectedSegment);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async unregister() {
|
||||
this.plugin.managers.lociLabels.removeProvider(this.labelProvider);
|
||||
}
|
||||
|
||||
async loadVolume() {
|
||||
const result = await this.volumeData.loadVolume();
|
||||
if (result) {
|
||||
const isovalue = result.isovalue.kind === 'relative' ? result.isovalue.relativeValue : result.isovalue.absoluteValue;
|
||||
await this.updateStateNode({ volumeIsovalueKind: result.isovalue.kind, volumeIsovalueValue: isovalue });
|
||||
}
|
||||
}
|
||||
|
||||
async loadSegmentations() {
|
||||
await this.latticeSegmentationData.loadSegmentation();
|
||||
await this.meshSegmentationData.loadSegmentation();
|
||||
await this.actionShowSegments(this.metadata.allSegmentIds);
|
||||
}
|
||||
|
||||
|
||||
actionHighlightSegment(segment?: Segment) {
|
||||
this.highlightRequest.next(segment);
|
||||
}
|
||||
|
||||
async actionToggleSegment(segment: number) {
|
||||
const current = this.currentState.value.visibleSegments.map(seg => seg.segmentId);
|
||||
if (current.includes(segment)) {
|
||||
await this.actionShowSegments(current.filter(s => s !== segment));
|
||||
} else {
|
||||
await this.actionShowSegments([...current, segment]);
|
||||
}
|
||||
}
|
||||
|
||||
async actionToggleAllSegments() {
|
||||
const current = this.currentState.value.visibleSegments.map(seg => seg.segmentId);
|
||||
if (current.length !== this.metadata.allSegments.length) {
|
||||
await this.actionShowSegments(this.metadata.allSegmentIds);
|
||||
} else {
|
||||
await this.actionShowSegments([]);
|
||||
}
|
||||
}
|
||||
|
||||
async actionSelectSegment(segment?: number) {
|
||||
if (segment !== undefined && this.currentState.value.visibleSegments.find(s => s.segmentId === segment) === undefined) {
|
||||
// first make the segment visible if it is not
|
||||
await this.actionToggleSegment(segment);
|
||||
}
|
||||
await this.updateStateNode({ selectedSegment: segment });
|
||||
}
|
||||
|
||||
async actionSetOpacity(opacity: number) {
|
||||
if (opacity === this.getStateNode().obj?.data.segmentOpacity) return;
|
||||
this.latticeSegmentationData.updateOpacity(opacity);
|
||||
this.meshSegmentationData.updateOpacity(opacity);
|
||||
|
||||
await this.updateStateNode({ segmentOpacity: opacity });
|
||||
}
|
||||
|
||||
async actionShowFittedModel(pdbIds: string[]) {
|
||||
await this.modelData.showPdbs(pdbIds);
|
||||
await this.updateStateNode({ visibleModels: pdbIds.map(pdbId => ({ pdbId: pdbId })) });
|
||||
}
|
||||
|
||||
async actionSetVolumeVisual(type: 'isosurface' | 'direct-volume' | 'off') {
|
||||
await this.volumeData.setVolumeVisual(type);
|
||||
await this.updateStateNode({ volumeType: type });
|
||||
}
|
||||
|
||||
async actionUpdateVolumeVisual(params: SimpleVolumeParamValues) {
|
||||
await this.volumeData.updateVolumeVisual(params);
|
||||
await this.updateStateNode({
|
||||
volumeType: params.volumeType,
|
||||
volumeOpacity: params.opacity,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private async actionShowSegments(segments: number[]) {
|
||||
await this.latticeSegmentationData.showSegments(segments);
|
||||
await this.meshSegmentationData.showSegments(segments);
|
||||
await this.updateStateNode({ visibleSegments: segments.map(s => ({ segmentId: s })) });
|
||||
}
|
||||
|
||||
private async highlightSegment(segment?: Segment) {
|
||||
await PluginCommands.Interactivity.ClearHighlights(this.plugin);
|
||||
if (segment) {
|
||||
await this.latticeSegmentationData.highlightSegment(segment);
|
||||
await this.meshSegmentationData.highlightSegment(segment);
|
||||
}
|
||||
}
|
||||
|
||||
private async selectSegment(segment: number) {
|
||||
this.plugin.managers.interactivity.lociSelects.deselectAll();
|
||||
await this.latticeSegmentationData.selectSegment(segment);
|
||||
await this.meshSegmentationData.selectSegment(segment);
|
||||
await this.highlightSegment();
|
||||
}
|
||||
|
||||
private async updateStateNode(params: Partial<VolsegStateData>) {
|
||||
const oldParams = this.getStateNode().transform.params;
|
||||
const newParams = { ...oldParams, ...params };
|
||||
const state = this.plugin.state.data;
|
||||
const update = state.build().to(this.getStateNode().transform.ref).update(newParams);
|
||||
await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
|
||||
}
|
||||
|
||||
|
||||
/** Find the nodes under this entry root which have all of the given tags. */
|
||||
findNodesByTags(...tags: string[]) {
|
||||
return this.plugin.state.data.selectQ(q => {
|
||||
let builder = q.byRef(this.ref).subtree();
|
||||
for (const tag of tags) builder = builder.withTag(tag);
|
||||
return builder;
|
||||
});
|
||||
}
|
||||
|
||||
newUpdate() {
|
||||
if (this.ref !== '') {
|
||||
return this.plugin.build().to(this.ref);
|
||||
} else {
|
||||
return this.plugin.build().toRoot();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly labelProvider: LociLabelProvider = {
|
||||
label: (loci: Loci): string | undefined => {
|
||||
const segmentId = this.getSegmentIdFromLoci(loci);
|
||||
if (segmentId === undefined) return;
|
||||
const segment = this.metadata.getSegment(segmentId);
|
||||
if (!segment) return;
|
||||
const annotLabels = segment.biological_annotation.external_references.map(annot => `${applyEllipsis(annot.label)} [${annot.resource}:${annot.accession}]`);
|
||||
if (annotLabels.length === 0) return;
|
||||
if (annotLabels.length > MAX_ANNOTATIONS_IN_LABEL + 1) {
|
||||
const nHidden = annotLabels.length - MAX_ANNOTATIONS_IN_LABEL;
|
||||
annotLabels.length = MAX_ANNOTATIONS_IN_LABEL;
|
||||
annotLabels.push(`(${nHidden} more annotations, click on the segment to see all)`);
|
||||
}
|
||||
return '<hr class="msp-highlight-info-hr"/>' + annotLabels.filter(isDefined).join('<br/>');
|
||||
}
|
||||
};
|
||||
|
||||
private getSegmentIdFromLoci(loci: Loci): number | undefined {
|
||||
if (Volume.Segment.isLoci(loci) && loci.volume._propertyData.ownerId === this.ref) {
|
||||
if (loci.segments.length === 1) {
|
||||
return loci.segments[0];
|
||||
}
|
||||
}
|
||||
if (ShapeGroup.isLoci(loci)) {
|
||||
const meshData = (loci.shape.sourceData ?? {}) as MeshlistData;
|
||||
if (meshData.ownerId === this.ref && meshData.segmentId !== undefined) {
|
||||
return meshData.segmentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setTryUseGpu(tryUseGpu: boolean) {
|
||||
await Promise.all([
|
||||
this.volumeData.setTryUseGpu(tryUseGpu),
|
||||
this.latticeSegmentationData.setTryUseGpu(tryUseGpu),
|
||||
]);
|
||||
}
|
||||
async setSelectionMode(selectSegments: boolean) {
|
||||
if (selectSegments) {
|
||||
await this.selectSegment(this.currentState.value.selectedSegment);
|
||||
} else {
|
||||
this.plugin.managers.interactivity.lociSelects.deselectAll();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
131
src/extensions/volumes-and-segmentations/entry-segmentation.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
|
||||
import { VolumeFromSegmentationCif } from '../../mol-plugin-state/transforms/volume';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { Color } from '../../mol-util/color';
|
||||
|
||||
import { Segment } from './volseg-api/data';
|
||||
import { BOX, VolsegEntryData, MAX_VOXELS } from './entry-root';
|
||||
import { VolumeVisualParams } from './entry-volume';
|
||||
import { VolsegGlobalStateData } from './global-state';
|
||||
|
||||
|
||||
const GROUP_TAG = 'lattice-segmentation-group';
|
||||
const SEGMENT_VISUAL_TAG = 'lattice-segment-visual';
|
||||
|
||||
const DEFAULT_SEGMENT_COLOR = Color.fromNormalizedRgb(0.8, 0.8, 0.8);
|
||||
|
||||
|
||||
export class VolsegLatticeSegmentationData {
|
||||
private entryData: VolsegEntryData;
|
||||
|
||||
constructor(rootData: VolsegEntryData) {
|
||||
this.entryData = rootData;
|
||||
}
|
||||
|
||||
async loadSegmentation() {
|
||||
const hasLattices = this.entryData.metadata.raw.grid.segmentation_lattices.segmentation_lattice_ids.length > 0;
|
||||
if (hasLattices) {
|
||||
const url = this.entryData.api.latticeUrl(this.entryData.source, this.entryData.entryId, 0, BOX, MAX_VOXELS);
|
||||
let group = this.entryData.findNodesByTags(GROUP_TAG)[0]?.transform.ref;
|
||||
if (!group) {
|
||||
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup,
|
||||
{ label: 'Segmentation', description: 'Lattice' }, { tags: [GROUP_TAG], state: { isCollapsed: true } }).commit();
|
||||
group = newGroupNode.ref;
|
||||
}
|
||||
const segmentLabels = this.entryData.metadata.allSegments.map(seg => ({ id: seg.id, label: seg.biological_annotation.name ? `<b>${seg.biological_annotation.name}</b>` : '' }));
|
||||
const volumeNode = await this.entryData.newUpdate().to(group)
|
||||
.apply(Download, { url, isBinary: true, label: `Segmentation Data: ${url}` })
|
||||
.apply(ParseCif)
|
||||
.apply(VolumeFromSegmentationCif, { blockHeader: 'SEGMENTATION_DATA', segmentLabels: segmentLabels, ownerId: this.entryData.ref })
|
||||
.commit();
|
||||
const volumeData = volumeNode.data as Volume;
|
||||
const segmentation = Volume.Segmentation.get(volumeData);
|
||||
const segmentIds: number[] = Array.from(segmentation?.segments.keys() ?? []);
|
||||
await this.entryData.newUpdate().to(volumeNode)
|
||||
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.entryData.plugin, volumeData, {
|
||||
type: 'segment',
|
||||
typeParams: { tryUseGpu: VolsegGlobalStateData.getGlobalState(this.entryData.plugin)?.tryUseGpu },
|
||||
color: 'volume-segment',
|
||||
colorParams: { palette: this.createPalette(segmentIds) },
|
||||
}), { tags: [SEGMENT_VISUAL_TAG] }).commit();
|
||||
}
|
||||
}
|
||||
|
||||
private createPalette(segmentIds: number[]) {
|
||||
const colorMap = new Map<number, Color>();
|
||||
for (const segment of this.entryData.metadata.allSegments) {
|
||||
const color = Color.fromNormalizedArray(segment.colour, 0);
|
||||
colorMap.set(segment.id, color);
|
||||
}
|
||||
if (colorMap.size === 0) return undefined;
|
||||
for (const segid of segmentIds) {
|
||||
colorMap.get(segid);
|
||||
}
|
||||
const colors = segmentIds.map(segid => colorMap.get(segid) ?? DEFAULT_SEGMENT_COLOR);
|
||||
return { name: 'colors' as const, params: { list: { kind: 'set' as const, colors: colors } } };
|
||||
}
|
||||
|
||||
async updateOpacity(opacity: number) {
|
||||
const reprs = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG);
|
||||
const update = this.entryData.newUpdate();
|
||||
for (const s of reprs) {
|
||||
update.to(s).update(StateTransforms.Representation.VolumeRepresentation3D, p => { p.type.params.alpha = opacity; });
|
||||
}
|
||||
return await update.commit();
|
||||
}
|
||||
private makeLoci(segments: number[]) {
|
||||
const vis = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG)[0];
|
||||
if (!vis) return undefined;
|
||||
const repr = vis.obj?.data.repr;
|
||||
const wholeLoci = repr.getAllLoci()[0];
|
||||
if (!wholeLoci || !Volume.Segment.isLoci(wholeLoci)) return undefined;
|
||||
return { loci: Volume.Segment.Loci(wholeLoci.volume, segments), repr: repr };
|
||||
}
|
||||
async highlightSegment(segment: Segment) {
|
||||
const segmentLoci = this.makeLoci([segment.id]);
|
||||
if (!segmentLoci) return;
|
||||
this.entryData.plugin.managers.interactivity.lociHighlights.highlight(segmentLoci, false);
|
||||
}
|
||||
async selectSegment(segment?: number) {
|
||||
if (segment === undefined || segment < 0) return;
|
||||
const segmentLoci = this.makeLoci([segment]);
|
||||
if (!segmentLoci) return;
|
||||
this.entryData.plugin.managers.interactivity.lociSelects.select(segmentLoci, false);
|
||||
}
|
||||
|
||||
/** Make visible the specified set of lattice segments */
|
||||
async showSegments(segments: number[]) {
|
||||
const repr = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG)[0];
|
||||
if (!repr) return;
|
||||
const selectedSegment = this.entryData.currentState.value.selectedSegment;
|
||||
const mustReselect = segments.includes(selectedSegment) && !repr.params?.values.type.params.segments.includes(selectedSegment);
|
||||
const update = this.entryData.newUpdate();
|
||||
update.to(repr).update(StateTransforms.Representation.VolumeRepresentation3D, p => { p.type.params.segments = segments; });
|
||||
await update.commit();
|
||||
if (mustReselect) {
|
||||
await this.selectSegment(this.entryData.currentState.value.selectedSegment);
|
||||
}
|
||||
}
|
||||
|
||||
async setTryUseGpu(tryUseGpu: boolean) {
|
||||
const visuals = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG);
|
||||
for (const visual of visuals) {
|
||||
const oldParams: VolumeVisualParams = visual.transform.params;
|
||||
if (oldParams.type.params.tryUseGpu === !tryUseGpu) {
|
||||
const newParams = { ...oldParams, type: { ...oldParams.type, params: { ...oldParams.type.params, tryUseGpu: tryUseGpu } } };
|
||||
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newParams);
|
||||
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/extensions/volumes-and-segmentations/entry-state.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
|
||||
import { Choice } from './helpers';
|
||||
|
||||
|
||||
export const VolumeTypeChoice = new Choice({ 'isosurface': 'Isosurface', 'direct-volume': 'Direct volume', 'off': 'Off' }, 'isosurface');
|
||||
export type VolumeType = Choice.Values<typeof VolumeTypeChoice>
|
||||
|
||||
|
||||
export const VolsegStateParams = {
|
||||
volumeType: VolumeTypeChoice.PDSelect(),
|
||||
volumeIsovalueKind: PD.Select('relative', [['relative', 'Relative'], ['absolute', 'Absolute']]),
|
||||
volumeIsovalueValue: PD.Numeric(1),
|
||||
volumeOpacity: PD.Numeric(0.2, { min: 0, max: 1, step: 0.05 }),
|
||||
segmentOpacity: PD.Numeric(1, { min: 0, max: 1, step: 0.05 }),
|
||||
selectedSegment: PD.Numeric(-1, { step: 1 }),
|
||||
visibleSegments: PD.ObjectList({ segmentId: PD.Numeric(0) }, s => s.segmentId.toString()),
|
||||
visibleModels: PD.ObjectList({ pdbId: PD.Text('') }, s => s.pdbId.toString()),
|
||||
};
|
||||
export type VolsegStateData = PD.Values<typeof VolsegStateParams>;
|
||||
|
||||
|
||||
export class VolsegState extends PluginStateObject.Create<VolsegStateData>({ name: 'Vol & Seg Entry State', typeClass: 'Data' }) { }
|
||||
|
||||
|
||||
export const VOLSEG_STATE_FROM_ENTRY_TRANSFORMER_NAME = 'volseg-state-from-entry'; // defined here to avoid cyclic dependency
|
||||
191
src/extensions/volumes-and-segmentations/entry-volume.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Vec2 } from '../../mol-math/linear-algebra';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { Download } from '../../mol-plugin-state/transforms/data';
|
||||
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
|
||||
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
|
||||
import { BOX, VolsegEntryData, MAX_VOXELS } from './entry-root';
|
||||
import { VolsegStateParams, VolumeTypeChoice } from './entry-state';
|
||||
import * as ExternalAPIs from './external-api';
|
||||
import { VolsegGlobalStateData } from './global-state';
|
||||
|
||||
|
||||
const GROUP_TAG = 'volume-group';
|
||||
export const VOLUME_VISUAL_TAG = 'volume-visual';
|
||||
|
||||
const DIRECT_VOLUME_RELATIVE_PEAK_HALFWIDTH = 0.5;
|
||||
|
||||
|
||||
export type VolumeVisualParams = ReturnType<typeof createVolumeRepresentationParams>;
|
||||
|
||||
interface VolumeStats { min: number, max: number, mean: number, sigma: number };
|
||||
|
||||
|
||||
export const SimpleVolumeParams = {
|
||||
volumeType: VolumeTypeChoice.PDSelect(),
|
||||
opacity: PD.Numeric(0.2, { min: 0, max: 1, step: 0.05 }, { hideIf: p => p.volumeType === 'off' }),
|
||||
};
|
||||
export type SimpleVolumeParamValues = PD.Values<typeof SimpleVolumeParams>;
|
||||
|
||||
|
||||
export class VolsegVolumeData {
|
||||
private entryData: VolsegEntryData;
|
||||
private visualTypeParamCache: { [type: string]: any } = {};
|
||||
|
||||
constructor(rootData: VolsegEntryData) {
|
||||
this.entryData = rootData;
|
||||
}
|
||||
|
||||
async loadVolume() {
|
||||
const hasVolumes = this.entryData.metadata.raw.grid.volumes.volume_downsamplings.length > 0;
|
||||
if (hasVolumes) {
|
||||
const isoLevelPromise = ExternalAPIs.tryGetIsovalue(this.entryData.metadata.raw.grid.general.source_db_id ?? this.entryData.entryId);
|
||||
let group = this.entryData.findNodesByTags(GROUP_TAG)[0]?.transform.ref;
|
||||
if (!group) {
|
||||
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup, { label: 'Volume' }, { tags: [GROUP_TAG], state: { isCollapsed: true } }).commit();
|
||||
group = newGroupNode.ref;
|
||||
}
|
||||
const url = this.entryData.api.volumeUrl(this.entryData.source, this.entryData.entryId, BOX, MAX_VOXELS);
|
||||
const data = await this.entryData.newUpdate().to(group).apply(Download, { url, isBinary: true, label: `Volume Data: ${url}` }).commit();
|
||||
const parsed = await this.entryData.plugin.dataFormats.get('dscif')!.parse(this.entryData.plugin, data);
|
||||
const volumeNode: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[0] ?? parsed.volume;
|
||||
const volumeData = volumeNode.cell!.obj!.data;
|
||||
|
||||
const volumeType = VolsegStateParams.volumeType.defaultValue;
|
||||
let isovalue = await isoLevelPromise;
|
||||
if (!isovalue) {
|
||||
const stats = volumeData.grid.stats;
|
||||
const maxRelative = (stats.max - stats.mean) / stats.sigma;
|
||||
if (maxRelative > 1) {
|
||||
isovalue = { kind: 'relative', value: 1.0 };
|
||||
} else {
|
||||
isovalue = { kind: 'relative', value: maxRelative * 0.5 };
|
||||
}
|
||||
}
|
||||
|
||||
const adjustedIsovalue = Volume.adjustedIsoValue(volumeData, isovalue.value, isovalue.kind);
|
||||
const visualParams = this.createVolumeVisualParams(volumeData, volumeType);
|
||||
this.changeIsovalueInVolumeVisualParams(visualParams, adjustedIsovalue, volumeData.grid.stats);
|
||||
|
||||
await this.entryData.newUpdate()
|
||||
.to(volumeNode)
|
||||
.apply(StateTransforms.Representation.VolumeRepresentation3D, visualParams, { tags: [VOLUME_VISUAL_TAG], state: { isHidden: volumeType === 'off' } })
|
||||
.commit();
|
||||
return { isovalue: adjustedIsovalue };
|
||||
}
|
||||
}
|
||||
|
||||
async setVolumeVisual(type: 'isosurface' | 'direct-volume' | 'off') {
|
||||
const visual = this.entryData.findNodesByTags(VOLUME_VISUAL_TAG)[0];
|
||||
if (!visual) return;
|
||||
const oldParams: VolumeVisualParams = visual.transform.params;
|
||||
this.visualTypeParamCache[oldParams.type.name] = oldParams.type.params;
|
||||
if (type === 'off') {
|
||||
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, true); // true means hide, ¯\_(ツ)_/¯
|
||||
} else {
|
||||
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, false); // true means hide, ¯\_(ツ)_/¯
|
||||
if (oldParams.type.name === type) return;
|
||||
const newParams: VolumeVisualParams = {
|
||||
...oldParams,
|
||||
type: {
|
||||
name: type,
|
||||
params: this.visualTypeParamCache[type] ?? oldParams.type.params,
|
||||
}
|
||||
};
|
||||
const volumeStats = visual.obj?.data.sourceData.grid.stats;
|
||||
if (!volumeStats) throw new Error(`Cannot get volume stats from volume visual ${visual.transform.ref}`);
|
||||
this.changeIsovalueInVolumeVisualParams(newParams, undefined, volumeStats);
|
||||
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newParams);
|
||||
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
|
||||
}
|
||||
}
|
||||
|
||||
async updateVolumeVisual(newParams: SimpleVolumeParamValues) {
|
||||
const { volumeType, opacity } = newParams;
|
||||
const visual = this.entryData.findNodesByTags(VOLUME_VISUAL_TAG)[0];
|
||||
if (!visual) return;
|
||||
const oldVisualParams: VolumeVisualParams = visual.transform.params;
|
||||
this.visualTypeParamCache[oldVisualParams.type.name] = oldVisualParams.type.params;
|
||||
|
||||
if (volumeType === 'off') {
|
||||
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, true); // true means hide, ¯\_(ツ)_/¯
|
||||
} else {
|
||||
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, false); // true means hide, ¯\_(ツ)_/¯
|
||||
const newVisualParams: VolumeVisualParams = {
|
||||
...oldVisualParams,
|
||||
type: {
|
||||
name: volumeType,
|
||||
params: this.visualTypeParamCache[volumeType] ?? oldVisualParams.type.params,
|
||||
}
|
||||
};
|
||||
newVisualParams.type.params.alpha = opacity;
|
||||
const volumeStats = visual.obj?.data.sourceData.grid.stats;
|
||||
if (!volumeStats) throw new Error(`Cannot get volume stats from volume visual ${visual.transform.ref}`);
|
||||
this.changeIsovalueInVolumeVisualParams(newVisualParams, undefined, volumeStats);
|
||||
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newVisualParams);
|
||||
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
|
||||
}
|
||||
}
|
||||
|
||||
async setTryUseGpu(tryUseGpu: boolean) {
|
||||
const visuals = this.entryData.findNodesByTags(VOLUME_VISUAL_TAG);
|
||||
for (const visual of visuals) {
|
||||
const oldParams: VolumeVisualParams = visual.transform.params;
|
||||
if (oldParams.type.params.tryUseGpu === !tryUseGpu) {
|
||||
const newParams = { ...oldParams, type: { ...oldParams.type, params: { ...oldParams.type.params, tryUseGpu: tryUseGpu } } };
|
||||
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newParams);
|
||||
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getIsovalueFromState(): Volume.IsoValue {
|
||||
const { volumeIsovalueKind, volumeIsovalueValue } = this.entryData.currentState.value;
|
||||
return volumeIsovalueKind === 'relative'
|
||||
? Volume.IsoValue.relative(volumeIsovalueValue)
|
||||
: Volume.IsoValue.absolute(volumeIsovalueValue);
|
||||
}
|
||||
|
||||
private createVolumeVisualParams(volume: Volume, type: 'isosurface' | 'direct-volume' | 'off'): VolumeVisualParams {
|
||||
if (type === 'off') type = 'isosurface';
|
||||
return createVolumeRepresentationParams(this.entryData.plugin, volume, {
|
||||
type: type,
|
||||
typeParams: { alpha: 0.2, tryUseGpu: VolsegGlobalStateData.getGlobalState(this.entryData.plugin)?.tryUseGpu },
|
||||
color: 'uniform',
|
||||
colorParams: { value: Color(0x121212) },
|
||||
});
|
||||
}
|
||||
|
||||
private changeIsovalueInVolumeVisualParams(params: VolumeVisualParams, isovalue: Volume.IsoValue | undefined, stats: VolumeStats) {
|
||||
isovalue ??= this.getIsovalueFromState();
|
||||
switch (params.type.name) {
|
||||
case 'isosurface':
|
||||
params.type.params.isoValue = isovalue;
|
||||
params.type.params.tryUseGpu = VolsegGlobalStateData.getGlobalState(this.entryData.plugin)?.tryUseGpu;
|
||||
break;
|
||||
case 'direct-volume':
|
||||
const absIso = Volume.IsoValue.toAbsolute(isovalue, stats).absoluteValue;
|
||||
const fractIso = (absIso - stats.min) / (stats.max - stats.min);
|
||||
const peakHalfwidth = DIRECT_VOLUME_RELATIVE_PEAK_HALFWIDTH * stats.sigma / (stats.max - stats.min);
|
||||
params.type.params.controlPoints = [
|
||||
Vec2.create(Math.max(fractIso - peakHalfwidth, 0), 0),
|
||||
Vec2.create(fractIso, 1),
|
||||
Vec2.create(Math.min(fractIso + peakHalfwidth, 1), 0),
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/extensions/volumes-and-segmentations/external-api.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { splitEntryId } from './helpers';
|
||||
|
||||
|
||||
/** Try to get author-defined contour value for isosurface from EMDB API. Return relative value 1.0, if not applicable or fails. */
|
||||
export async function tryGetIsovalue(entryId: string): Promise<{ kind: 'absolute' | 'relative', value: number } | undefined> {
|
||||
const split = splitEntryId(entryId);
|
||||
if (split.source === 'emdb') {
|
||||
try {
|
||||
const response = await fetch(`https://www.ebi.ac.uk/emdb/api/entry/map/${split.entryNumber}`);
|
||||
const json = await response.json();
|
||||
const contours: any[] = json?.map?.contour_list?.contour;
|
||||
if (contours && contours.length > 0) {
|
||||
const theContour = contours.find(c => c.primary) || contours[0];
|
||||
if (theContour.level === undefined) throw new Error('EMDB API response missing contour level.');
|
||||
return { kind: 'absolute', value: theContour.level };
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getPdbIdsForEmdbEntry(entryId: string): Promise<string[]> {
|
||||
const split = splitEntryId(entryId);
|
||||
const result = [];
|
||||
if (split.source === 'emdb') {
|
||||
entryId = entryId.toUpperCase();
|
||||
const apiUrl = `https://www.ebi.ac.uk/pdbe/api/emdb/entry/fitted/${entryId}`;
|
||||
try {
|
||||
const response = await fetch(apiUrl);
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
const jsonEntry = json[entryId] ?? [];
|
||||
for (const record of jsonEntry) {
|
||||
const pdbs = record?.fitted_emdb_id_list?.pdb_id ?? [];
|
||||
result.push(...pdbs);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
65
src/extensions/volumes-and-segmentations/global-state.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { VolsegEntry } from './entry-root';
|
||||
import { isDefined } from './helpers';
|
||||
|
||||
|
||||
export const VolsegGlobalStateParams = {
|
||||
tryUseGpu: PD.Boolean(true, { description: 'Attempt using GPU for faster rendering. \nCaution: with some hardware setups, this might render some objects incorrectly or not at all.' }),
|
||||
selectionMode: PD.Boolean(true, { description: 'Allow selecting/deselecting a segment by clicking on it.' }),
|
||||
};
|
||||
export type VolsegGlobalStateParamValues = PD.Values<typeof VolsegGlobalStateParams>;
|
||||
|
||||
|
||||
export class VolsegGlobalState extends PluginStateObject.CreateBehavior<VolsegGlobalStateData>({ name: 'Vol & Seg Global State' }) { }
|
||||
|
||||
export class VolsegGlobalStateData extends PluginBehavior.WithSubscribers<VolsegGlobalStateParamValues> {
|
||||
private ref: string;
|
||||
currentState = new BehaviorSubject(PD.getDefaultValues(VolsegGlobalStateParams));
|
||||
|
||||
constructor(plugin: PluginContext, params: VolsegGlobalStateParamValues) {
|
||||
super(plugin, params);
|
||||
this.currentState.next(params);
|
||||
}
|
||||
|
||||
register(ref: string) {
|
||||
this.ref = ref;
|
||||
}
|
||||
unregister() {
|
||||
this.ref = '';
|
||||
}
|
||||
isRegistered() {
|
||||
return this.ref !== '';
|
||||
}
|
||||
async updateState(plugin: PluginContext, state: Partial<VolsegGlobalStateParamValues>) {
|
||||
const oldState = this.currentState.value;
|
||||
|
||||
const promises = [];
|
||||
const allEntries = plugin.state.data.selectQ(q => q.ofType(VolsegEntry)).map(cell => cell.obj?.data).filter(isDefined);
|
||||
if (state.tryUseGpu !== undefined && state.tryUseGpu !== oldState.tryUseGpu) {
|
||||
for (const entry of allEntries) {
|
||||
promises.push(entry.setTryUseGpu(state.tryUseGpu));
|
||||
}
|
||||
}
|
||||
if (state.selectionMode !== undefined && state.selectionMode !== oldState.selectionMode) {
|
||||
for (const entry of allEntries) {
|
||||
promises.push(entry.setSelectionMode(state.selectionMode));
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
await plugin.build().to(this.ref).update(state).commit();
|
||||
}
|
||||
|
||||
static getGlobalState(plugin: PluginContext): VolsegGlobalStateParamValues | undefined {
|
||||
return plugin.state.data.selectQ(q => q.ofType(VolsegGlobalState))[0]?.obj?.data.currentState.value;
|
||||
}
|
||||
}
|
||||
163
src/extensions/volumes-and-segmentations/helpers.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
|
||||
import { StateBuilder, StateObjectSelector, StateTransformer } from '../../mol-state';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { Source } from './entry-root';
|
||||
|
||||
|
||||
/** Split entry ID (e.g. 'emd-1832') into source ('emdb') and number ('1832') */
|
||||
export function splitEntryId(entryId: string) {
|
||||
const PREFIX_TO_SOURCE: { [prefix: string]: Source } = { 'emd': 'emdb' };
|
||||
const [prefix, entry] = entryId.split('-');
|
||||
return {
|
||||
source: PREFIX_TO_SOURCE[prefix] ?? prefix,
|
||||
entryNumber: entry
|
||||
};
|
||||
}
|
||||
|
||||
/** Create entry ID (e.g. 'emd-1832') for a combination of source ('emdb') and number ('1832') */
|
||||
export function createEntryId(source: Source, entryNumber: string | number) {
|
||||
const SOURCE_TO_PREFIX: { [prefix: string]: string } = { 'emdb': 'emd' };
|
||||
const prefix = SOURCE_TO_PREFIX[source] ?? source;
|
||||
return `${prefix}-${entryNumber}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Represents a set of values to choose from, with a default value. Example:
|
||||
* ```
|
||||
* export const MyChoice = new Choice({ yes: 'I agree', no: 'Nope' }, 'yes');
|
||||
* export type MyChoiceType = Choice.Values<typeof MyChoice>; // 'yes'|'no'
|
||||
* ```
|
||||
*/
|
||||
export class Choice<T extends string, D extends T> {
|
||||
readonly defaultValue: D;
|
||||
readonly options: [T, string][];
|
||||
private readonly nameDict: { [value in T]: string };
|
||||
constructor(opts: { [value in T]: string }, defaultValue: D) {
|
||||
this.defaultValue = defaultValue;
|
||||
this.options = Object.keys(opts).map(k => [k as T, opts[k as T]]);
|
||||
this.nameDict = opts;
|
||||
}
|
||||
PDSelect(defaultValue?: T, info?: ParamDefinition.Info): ParamDefinition.Select<T> {
|
||||
return ParamDefinition.Select<T>(defaultValue ?? this.defaultValue, this.options, info);
|
||||
}
|
||||
prettyName(value: T): string {
|
||||
return this.nameDict[value];
|
||||
}
|
||||
get values(): T[] {
|
||||
return this.options.map(([value, pretty]) => value);
|
||||
}
|
||||
}
|
||||
export namespace Choice {
|
||||
export type Values<T extends Choice<any, any>> = T extends Choice<infer R, any> ? R : any;
|
||||
}
|
||||
|
||||
|
||||
export function isDefined<T>(x: T | undefined): x is T {
|
||||
return x !== undefined;
|
||||
}
|
||||
|
||||
|
||||
export class NodeManager {
|
||||
private nodes: { [key: string]: StateObjectSelector };
|
||||
|
||||
constructor() {
|
||||
this.nodes = {};
|
||||
}
|
||||
|
||||
private static nodeExists(node: StateObjectSelector): boolean {
|
||||
try {
|
||||
return node.checkValid();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public getNode(key: string): StateObjectSelector | undefined {
|
||||
const node = this.nodes[key];
|
||||
if (node && !NodeManager.nodeExists(node)) {
|
||||
delete this.nodes[key];
|
||||
return undefined;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
public getNodes(): StateObjectSelector[] {
|
||||
return Object.keys(this.nodes).map(key => this.getNode(key)).filter(node => node) as StateObjectSelector[];
|
||||
}
|
||||
|
||||
public deleteAllNodes(update: StateBuilder.Root) {
|
||||
for (const node of this.getNodes()) {
|
||||
update.delete(node);
|
||||
}
|
||||
this.nodes = {};
|
||||
}
|
||||
|
||||
public hideAllNodes() {
|
||||
for (const node of this.getNodes()) {
|
||||
setSubtreeVisibility(node.state!, node.ref, true); // hide
|
||||
}
|
||||
}
|
||||
|
||||
public async showNode(key: string, factory: () => StateObjectSelector | Promise<StateObjectSelector>, forceVisible: boolean = true) {
|
||||
let node = this.getNode(key);
|
||||
if (node) {
|
||||
if (forceVisible) {
|
||||
setSubtreeVisibility(node.state!, node.ref, false); // show
|
||||
}
|
||||
} else {
|
||||
node = await factory();
|
||||
this.nodes[key] = node;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CreateTransformer = StateTransformer.builderFactory('volseg');
|
||||
|
||||
export const CreateVolume = CreateTransformer({
|
||||
name: 'create-transformer',
|
||||
from: PluginStateObject.Root,
|
||||
to: PluginStateObject.Volume.Data,
|
||||
params: {
|
||||
label: ParamDefinition.Text('Volume', { isHidden: true }),
|
||||
description: ParamDefinition.Text('', { isHidden: true }),
|
||||
volume: ParamDefinition.Value<Volume>(undefined as any, { isHidden: true }),
|
||||
}
|
||||
})({
|
||||
apply({ params }) {
|
||||
return new PluginStateObject.Volume.Data(params.volume, { label: params.label, description: params.description });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
export function applyEllipsis(name: string, max_chars: number = 60) {
|
||||
if (name.length <= max_chars) return name;
|
||||
const beginning = name.substring(0, max_chars);
|
||||
let lastSpace = beginning.lastIndexOf(' ');
|
||||
if (lastSpace === -1) return beginning + '...';
|
||||
if (lastSpace > 0 && ',;.'.includes(name.charAt(lastSpace - 1))) lastSpace--;
|
||||
return name.substring(0, lastSpace) + '...';
|
||||
}
|
||||
|
||||
|
||||
export function lazyGetter<T>(getter: () => T, errorIfUndefined?: string) {
|
||||
let value: T | undefined = undefined;
|
||||
return () => {
|
||||
if (value === undefined) value = getter();
|
||||
if (errorIfUndefined && value === undefined) throw new Error(errorIfUndefined);
|
||||
return value;
|
||||
};
|
||||
}
|
||||
102
src/extensions/volumes-and-segmentations/index.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject as SO } from '../../mol-plugin-state/objects';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior';
|
||||
import { PluginConfigItem } from '../../mol-plugin/config';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateAction } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { DEFAULT_VOLSEG_SERVER, VolumeApiV2 } from './volseg-api/api';
|
||||
|
||||
import { VolsegEntryData, VolsegEntryParamValues, createLoadVolsegParams } from './entry-root';
|
||||
import { VolsegGlobalState } from './global-state';
|
||||
import { createEntryId } from './helpers';
|
||||
import { VolsegEntryFromRoot, VolsegGlobalStateFromRoot, VolsegStateFromEntry } from './transformers';
|
||||
import { VolsegUI } from './ui';
|
||||
|
||||
|
||||
const DEBUGGING = window.location.hostname === 'localhost';
|
||||
|
||||
export const VolsegVolumeServerConfig = {
|
||||
// DefaultServer: new PluginConfigItem('volseg-volume-server', DEFAULT_VOLUME_SERVER_V2),
|
||||
DefaultServer: new PluginConfigItem('volseg-volume-server', DEBUGGING ? 'http://localhost:9000/v2' : DEFAULT_VOLSEG_SERVER),
|
||||
};
|
||||
|
||||
|
||||
export const Volseg = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
|
||||
name: 'volseg',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'Volseg',
|
||||
description: 'Volseg'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
|
||||
register() {
|
||||
this.ctx.state.data.actions.add(LoadVolseg);
|
||||
this.ctx.customStructureControls.set('volseg', VolsegUI as any);
|
||||
this.initializeEntryLists(); // do not await
|
||||
|
||||
const entries = new Map<string, VolsegEntryData>();
|
||||
this.subscribeObservable(this.ctx.state.data.events.cell.created, o => {
|
||||
if (o.cell.obj instanceof VolsegEntryData) entries.set(o.ref, o.cell.obj);
|
||||
});
|
||||
|
||||
this.subscribeObservable(this.ctx.state.data.events.cell.removed, o => {
|
||||
if (entries.has(o.ref)) {
|
||||
entries.get(o.ref)!.dispose();
|
||||
entries.delete(o.ref);
|
||||
}
|
||||
});
|
||||
}
|
||||
unregister() {
|
||||
this.ctx.state.data.actions.remove(LoadVolseg);
|
||||
this.ctx.customStructureControls.delete('volseg');
|
||||
}
|
||||
private async initializeEntryLists() {
|
||||
const apiUrl = this.ctx.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLSEG_SERVER;
|
||||
const api = new VolumeApiV2(apiUrl);
|
||||
const entryLists = await api.getEntryList(10 ** 6);
|
||||
Object.values(entryLists).forEach(l => l.sort());
|
||||
(this.ctx.customState as any).volsegAvailableEntries = entryLists;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const LoadVolseg = StateAction.build({
|
||||
display: { name: 'Load Volume & Segmentation' },
|
||||
from: SO.Root,
|
||||
params: (a, plugin: PluginContext) => {
|
||||
const res = createLoadVolsegParams(plugin, (plugin.customState as any).volsegAvailableEntries);
|
||||
return res;
|
||||
},
|
||||
})(({ params, state }, ctx: PluginContext) => Task.create('Loading Volume & Segmentation', taskCtx => {
|
||||
return state.transaction(async () => {
|
||||
const entryParams = VolsegEntryParamValues.fromLoadVolsegParamValues(params);
|
||||
if (entryParams.entryId.trim().length === 0) {
|
||||
alert('Must specify Entry Id!');
|
||||
throw new Error('Specify Entry Id');
|
||||
}
|
||||
if (!entryParams.entryId.includes('-')) {
|
||||
// add source prefix if the user omitted it (e.g. 1832 -> emd-1832)
|
||||
entryParams.entryId = createEntryId(entryParams.source, entryParams.entryId);
|
||||
}
|
||||
ctx.behaviors.layout.leftPanelTabName.next('data');
|
||||
|
||||
const globalStateNode = ctx.state.data.selectQ(q => q.ofType(VolsegGlobalState))[0];
|
||||
if (!globalStateNode) {
|
||||
await state.build().toRoot().apply(VolsegGlobalStateFromRoot, {}, { state: { isGhost: !DEBUGGING } }).commit();
|
||||
}
|
||||
|
||||
const entryNode = await state.build().toRoot().apply(VolsegEntryFromRoot, entryParams).commit();
|
||||
await state.build().to(entryNode).apply(VolsegStateFromEntry, {}, { state: { isGhost: !DEBUGGING } }).commit();
|
||||
if (entryNode.data) {
|
||||
await entryNode.data.loadVolume();
|
||||
await entryNode.data.loadSegmentations();
|
||||
}
|
||||
}).runInContext(taskCtx);
|
||||
}));
|
||||
70
src/extensions/volumes-and-segmentations/transformers.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject, PluginStateTransform } from '../../mol-plugin-state/objects';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
|
||||
import { VolsegEntry, VolsegEntryData, createVolsegEntryParams } from './entry-root';
|
||||
import { VolsegState, VolsegStateParams, VOLSEG_STATE_FROM_ENTRY_TRANSFORMER_NAME } from './entry-state';
|
||||
import { VolsegGlobalState, VolsegGlobalStateData, VolsegGlobalStateParams } from './global-state';
|
||||
|
||||
|
||||
export const VolsegEntryFromRoot = PluginStateTransform.BuiltIn({
|
||||
name: 'volseg-entry-from-root',
|
||||
display: { name: 'Vol & Seg Entry', description: 'Vol & Seg Entry' },
|
||||
from: PluginStateObject.Root,
|
||||
to: VolsegEntry,
|
||||
params: (a, plugin: PluginContext) => createVolsegEntryParams(plugin),
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Load Vol & Seg Entry', async () => {
|
||||
const data = await VolsegEntryData.create(plugin, params);
|
||||
return new VolsegEntry(data, { label: data.entryId, description: 'Vol & Seg Entry' });
|
||||
});
|
||||
},
|
||||
update({ b, oldParams, newParams }) {
|
||||
Object.assign(newParams, oldParams);
|
||||
console.error('Changing params of existing VolsegEntry node is not allowed');
|
||||
return StateTransformer.UpdateResult.Unchanged;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const VolsegStateFromEntry = PluginStateTransform.BuiltIn({
|
||||
name: VOLSEG_STATE_FROM_ENTRY_TRANSFORMER_NAME,
|
||||
display: { name: 'Vol & Seg Entry State', description: 'Vol & Seg Entry State' },
|
||||
from: VolsegEntry,
|
||||
to: VolsegState,
|
||||
params: VolsegStateParams,
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Create Vol & Seg Entry State', async () => {
|
||||
return new VolsegState(params, { label: 'State' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const VolsegGlobalStateFromRoot = PluginStateTransform.BuiltIn({
|
||||
name: 'volseg-global-state-from-root',
|
||||
display: { name: 'Vol & Seg Global State', description: 'Vol & Seg Global State' },
|
||||
from: PluginStateObject.Root,
|
||||
to: VolsegGlobalState,
|
||||
params: VolsegGlobalStateParams,
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Create Vol & Seg Global State', async () => {
|
||||
const data = new VolsegGlobalStateData(plugin, params);
|
||||
return new VolsegGlobalState(data, { label: 'Global State', description: 'Vol & Seg Global State' });
|
||||
});
|
||||
},
|
||||
update({ b, oldParams, newParams }) {
|
||||
b.data.currentState.next(newParams);
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
}
|
||||
});
|
||||
264
src/extensions/volumes-and-segmentations/ui.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
|
||||
import { Button, ControlRow, ExpandGroup, IconButton } from '../../mol-plugin-ui/controls/common';
|
||||
import * as Icons from '../../mol-plugin-ui/controls/icons';
|
||||
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
|
||||
import { Slider } from '../../mol-plugin-ui/controls/slider';
|
||||
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { UpdateTransformControl } from '../../mol-plugin-ui/state/update-transform';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { shallowEqualArrays } from '../../mol-util';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { sleep } from '../../mol-util/sleep';
|
||||
|
||||
import { VolsegEntry, VolsegEntryData } from './entry-root';
|
||||
import { SimpleVolumeParams, SimpleVolumeParamValues } from './entry-volume';
|
||||
import { VolsegGlobalState, VolsegGlobalStateData, VolsegGlobalStateParams } from './global-state';
|
||||
import { isDefined } from './helpers';
|
||||
|
||||
|
||||
interface VolsegUIData {
|
||||
globalState?: VolsegGlobalStateData,
|
||||
availableNodes: VolsegEntry[],
|
||||
activeNode?: VolsegEntry,
|
||||
}
|
||||
namespace VolsegUIData {
|
||||
export function changeAvailableNodes(data: VolsegUIData, newNodes: VolsegEntry[]): VolsegUIData {
|
||||
const newActiveNode = newNodes.length > data.availableNodes.length ?
|
||||
newNodes[newNodes.length - 1]
|
||||
: newNodes.find(node => node.data.ref === data.activeNode?.data.ref) ?? newNodes[0];
|
||||
return { ...data, availableNodes: newNodes, activeNode: newActiveNode };
|
||||
}
|
||||
export function changeActiveNode(data: VolsegUIData, newActiveRef: string): VolsegUIData {
|
||||
const newActiveNode = data.availableNodes.find(node => node.data.ref === newActiveRef) ?? data.availableNodes[0];
|
||||
return { ...data, availableNodes: data.availableNodes, activeNode: newActiveNode };
|
||||
}
|
||||
export function equals(data1: VolsegUIData, data2: VolsegUIData) {
|
||||
return shallowEqualArrays(data1.availableNodes, data2.availableNodes) && data1.activeNode === data2.activeNode && data1.globalState === data2.globalState;
|
||||
}
|
||||
}
|
||||
|
||||
export class VolsegUI extends CollapsableControls<{}, { data: VolsegUIData }> {
|
||||
protected defaultState(): CollapsableState & { data: VolsegUIData } {
|
||||
return {
|
||||
header: 'Volume & Segmentation',
|
||||
isCollapsed: true,
|
||||
brand: { accent: 'orange', svg: Icons.ExtensionSvg },
|
||||
data: {
|
||||
globalState: undefined,
|
||||
availableNodes: [],
|
||||
activeNode: undefined,
|
||||
}
|
||||
};
|
||||
}
|
||||
protected renderControls(): JSX.Element | null {
|
||||
return <VolsegControls plugin={this.plugin} data={this.state.data} setData={d => this.setState({ data: d })} />;
|
||||
}
|
||||
componentDidMount(): void {
|
||||
this.setState({ isHidden: true, isCollapsed: false });
|
||||
this.subscribe(this.plugin.state.data.events.changed, e => {
|
||||
const nodes = e.state.selectQ(q => q.ofType(VolsegEntry)).map(cell => cell?.obj).filter(isDefined);
|
||||
const isHidden = nodes.length === 0;
|
||||
const newData = VolsegUIData.changeAvailableNodes(this.state.data, nodes);
|
||||
if (!this.state.data.globalState?.isRegistered()) {
|
||||
const globalState = e.state.selectQ(q => q.ofType(VolsegGlobalState))[0]?.obj?.data;
|
||||
if (globalState) newData.globalState = globalState;
|
||||
}
|
||||
if (!VolsegUIData.equals(this.state.data, newData) || this.state.isHidden !== isHidden) {
|
||||
this.setState({ data: newData, isHidden: isHidden });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function VolsegControls({ plugin, data, setData }: { plugin: PluginContext, data: VolsegUIData, setData: (d: VolsegUIData) => void }) {
|
||||
const entryData = data.activeNode?.data;
|
||||
if (!entryData) {
|
||||
return <p>No data!</p>;
|
||||
}
|
||||
if (!data.globalState) {
|
||||
return <p>No global state!</p>;
|
||||
}
|
||||
|
||||
const params = {
|
||||
/** Reference to the active VolsegEntry node */
|
||||
entry: PD.Select(data.activeNode!.data.ref, data.availableNodes.map(entry => [entry.data.ref, entry.data.entryId]))
|
||||
};
|
||||
const values: PD.Values<typeof params> = {
|
||||
entry: data.activeNode!.data.ref,
|
||||
};
|
||||
|
||||
const globalState = useBehavior(data.globalState.currentState);
|
||||
|
||||
return <>
|
||||
<ParameterControls params={params} values={values} onChangeValues={next => setData(VolsegUIData.changeActiveNode(data, next.entry))} />
|
||||
|
||||
<ExpandGroup header='Global options'>
|
||||
<WaitingParameterControls params={VolsegGlobalStateParams} values={globalState} onChangeValues={async next => await data.globalState?.updateState(plugin, next)} />
|
||||
</ExpandGroup>
|
||||
|
||||
<VolsegEntryControls entryData={entryData} key={entryData.ref} />
|
||||
</>;
|
||||
}
|
||||
|
||||
function VolsegEntryControls({ entryData }: { entryData: VolsegEntryData }) {
|
||||
const state = useBehavior(entryData.currentState);
|
||||
|
||||
const allSegments = entryData.metadata.allSegments;
|
||||
const selectedSegment = entryData.metadata.getSegment(state.selectedSegment);
|
||||
const visibleSegments = state.visibleSegments.map(seg => seg.segmentId);
|
||||
const visibleModels = state.visibleModels.map(model => model.pdbId);
|
||||
const allPdbs = entryData.pdbs;
|
||||
|
||||
return <>
|
||||
{/* Title */}
|
||||
<div style={{ fontWeight: 'bold', padding: 8, paddingTop: 6, paddingBottom: 4, overflow: 'hidden' }}>
|
||||
{entryData.metadata.raw.annotation?.name ?? 'Unnamed Annotation'}
|
||||
</div>
|
||||
|
||||
{/* Fitted models */}
|
||||
{allPdbs.length > 0 && <ExpandGroup header='Fitted models in PDB' initiallyExpanded>
|
||||
{allPdbs.map(pdb =>
|
||||
<WaitingButton key={pdb} onClick={() => entryData.actionShowFittedModel(visibleModels.includes(pdb) ? [] : [pdb])}
|
||||
style={{ fontWeight: visibleModels.includes(pdb) ? 'bold' : undefined, textAlign: 'left', marginTop: 1 }}>
|
||||
{pdb}
|
||||
</WaitingButton>
|
||||
)}
|
||||
</ExpandGroup>}
|
||||
|
||||
{/* Volume */}
|
||||
<VolumeControls entryData={entryData} />
|
||||
<ExpandGroup header='Segmentation data' initiallyExpanded>
|
||||
{/* Segment opacity slider */}
|
||||
<ControlRow label='Opacity' control={
|
||||
<WaitingSlider min={0} max={1} value={state.segmentOpacity} step={0.05} onChange={async v => await entryData.actionSetOpacity(v)} />
|
||||
} />
|
||||
|
||||
{/* Segment toggles */}
|
||||
{allSegments.length > 0 && <>
|
||||
<WaitingButton onClick={async () => { await sleep(20); await entryData.actionToggleAllSegments(); }} style={{ marginTop: 1 }}>
|
||||
Toggle All segments
|
||||
</WaitingButton>
|
||||
<div style={{ maxHeight: 200, overflow: 'hidden', overflowY: 'auto', marginBlock: 1 }}>
|
||||
{allSegments.map(segment =>
|
||||
<div style={{ display: 'flex', marginBottom: 1 }} key={segment.id}
|
||||
onMouseEnter={() => entryData.actionHighlightSegment(segment)}
|
||||
onMouseLeave={() => entryData.actionHighlightSegment()}>
|
||||
<Button onClick={() => entryData.actionSelectSegment(segment !== selectedSegment ? segment.id : undefined)}
|
||||
style={{ fontWeight: segment.id === selectedSegment?.id ? 'bold' : undefined, marginRight: 1, flexGrow: 1, textAlign: 'left' }}>
|
||||
<div title={segment.biological_annotation.name ?? 'Unnamed segment'} style={{ maxWidth: 240, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{segment.biological_annotation.name ?? 'Unnamed segment'} ({segment.id})
|
||||
</div>
|
||||
</Button>
|
||||
<IconButton svg={visibleSegments.includes(segment.id) ? Icons.VisibilityOutlinedSvg : Icons.VisibilityOffOutlinedSvg}
|
||||
onClick={() => entryData.actionToggleSegment(segment.id)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>}
|
||||
</ExpandGroup>
|
||||
|
||||
{/* Segment annotations */}
|
||||
<ExpandGroup header='Selected segment annotation' initiallyExpanded>
|
||||
<div style={{ paddingTop: 4, paddingRight: 8, maxHeight: 300, overflow: 'hidden', overflowY: 'auto' }}>
|
||||
{!selectedSegment && 'No segment selected'}
|
||||
{selectedSegment && <b>Segment {selectedSegment.id}:<br />{selectedSegment.biological_annotation.name ?? 'Unnamed segment'}</b>}
|
||||
{selectedSegment?.biological_annotation.external_references.map(ref =>
|
||||
<p key={ref.id} style={{ marginTop: 4 }}>
|
||||
<small>{ref.resource}:{ref.accession}</small><br />
|
||||
<b>{capitalize(ref.label)}</b><br />
|
||||
{ref.description}
|
||||
</p>)}
|
||||
</div>
|
||||
</ExpandGroup>
|
||||
</>;
|
||||
}
|
||||
|
||||
function VolumeControls({ entryData }: { entryData: VolsegEntryData }) {
|
||||
const vol = useBehavior(entryData.currentVolume);
|
||||
if (!vol) return null;
|
||||
|
||||
const volumeValues: SimpleVolumeParamValues = {
|
||||
volumeType: vol.state.isHidden ? 'off' : vol.params?.type.name as any,
|
||||
opacity: vol.params?.type.params.alpha,
|
||||
};
|
||||
|
||||
return <ExpandGroup header='Volume data' initiallyExpanded>
|
||||
<WaitingParameterControls params={SimpleVolumeParams} values={volumeValues} onChangeValues={async next => { await sleep(20); await entryData.actionUpdateVolumeVisual(next); }} />
|
||||
<ExpandGroup header='Detailed Volume Params' headerStyle={{ marginTop: 1 }}>
|
||||
<UpdateTransformControl state={entryData.plugin.state.data} transform={vol} customHeader='none' />
|
||||
</ExpandGroup>
|
||||
</ExpandGroup>;
|
||||
}
|
||||
|
||||
type ComponentParams<T extends React.Component<any, any, any> | ((props: any) => JSX.Element)> =
|
||||
T extends React.Component<infer P, any, any> ? P : T extends (props: infer P) => JSX.Element ? P : never;
|
||||
|
||||
function WaitingSlider({ value, onChange, ...etc }: { value: number, onChange: (value: number) => any } & ComponentParams<Slider>) {
|
||||
const [changing, sliderValue, execute] = useAsyncChange(value);
|
||||
|
||||
return <Slider value={sliderValue} disabled={changing} onChange={newValue => execute(onChange, newValue)} {...etc} />;
|
||||
}
|
||||
|
||||
function WaitingButton({ onClick, ...etc }: { onClick: () => any } & ComponentParams<typeof Button>) {
|
||||
const [changing, _, execute] = useAsyncChange(undefined);
|
||||
|
||||
return <Button disabled={changing} onClick={() => execute(onClick, undefined)} {...etc}>
|
||||
{etc.children}
|
||||
</Button>;
|
||||
}
|
||||
|
||||
function WaitingParameterControls<T extends PD.Params>({ values, onChangeValues, ...etc }: { values: PD.ValuesFor<T>, onChangeValues: (values: PD.ValuesFor<T>) => any } & ComponentParams<ParameterControls<T>>) {
|
||||
const [changing, currentValues, execute] = useAsyncChange(values);
|
||||
|
||||
return <ParameterControls isDisabled={changing} values={currentValues} onChangeValues={newValue => execute(onChangeValues, newValue)} {...etc} />;
|
||||
}
|
||||
|
||||
function capitalize(text: string) {
|
||||
const first = text.charAt(0);
|
||||
const rest = text.slice(1);
|
||||
return first.toUpperCase() + rest;
|
||||
}
|
||||
|
||||
function useAsyncChange<T>(initialValue: T) {
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const isMounted = useRef(false);
|
||||
|
||||
useEffect(() => setValue(initialValue), [initialValue]);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
return () => { isMounted.current = false; };
|
||||
}, []);
|
||||
|
||||
const execute = useCallback(
|
||||
async (func: (val: T) => Promise<any>, val: T) => {
|
||||
setIsExecuting(true);
|
||||
setValue(val);
|
||||
try {
|
||||
await func(val);
|
||||
} catch (err) {
|
||||
if (isMounted.current) {
|
||||
setValue(initialValue);
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
if (isMounted.current) {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return [isExecuting, value, execute] as const;
|
||||
}
|
||||
65
src/extensions/volumes-and-segmentations/volseg-api/api.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { type Metadata } from './data';
|
||||
|
||||
|
||||
export const DEFAULT_VOLSEG_SERVER = 'https://molstarvolseg.ncbr.muni.cz/v2';
|
||||
|
||||
|
||||
export class VolumeApiV2 {
|
||||
public volumeServerUrl: string;
|
||||
|
||||
public constructor(volumeServerUrl: string = DEFAULT_VOLSEG_SERVER) {
|
||||
this.volumeServerUrl = volumeServerUrl.replace(/\/$/, ''); // trim trailing slash
|
||||
}
|
||||
|
||||
public entryListUrl(maxEntries: number, keyword?: string): string {
|
||||
return `${this.volumeServerUrl}/list_entries/${maxEntries}/${keyword ?? ''}`;
|
||||
}
|
||||
|
||||
public metadataUrl(source: string, entryId: string): string {
|
||||
return `${this.volumeServerUrl}/${source}/${entryId}/metadata`;
|
||||
}
|
||||
public volumeUrl(source: string, entryId: string, box: [[number, number, number], [number, number, number]] | null, maxPoints: number): string {
|
||||
if (box) {
|
||||
const [[a1, a2, a3], [b1, b2, b3]] = box;
|
||||
return `${this.volumeServerUrl}/${source}/${entryId}/volume/box/${a1}/${a2}/${a3}/${b1}/${b2}/${b3}?max_points=${maxPoints}`;
|
||||
} else {
|
||||
return `${this.volumeServerUrl}/${source}/${entryId}/volume/cell?max_points=${maxPoints}`;
|
||||
}
|
||||
}
|
||||
public latticeUrl(source: string, entryId: string, segmentation: number, box: [[number, number, number], [number, number, number]] | null, maxPoints: number): string {
|
||||
if (box) {
|
||||
const [[a1, a2, a3], [b1, b2, b3]] = box;
|
||||
return `${this.volumeServerUrl}/${source}/${entryId}/segmentation/box/${segmentation}/${a1}/${a2}/${a3}/${b1}/${b2}/${b3}?max_points=${maxPoints}`;
|
||||
} else {
|
||||
return `${this.volumeServerUrl}/${source}/${entryId}/segmentation/cell/${segmentation}?max_points=${maxPoints}`;
|
||||
}
|
||||
}
|
||||
public meshUrl_Json(source: string, entryId: string, segment: number, detailLevel: number): string {
|
||||
return `${this.volumeServerUrl}/${source}/${entryId}/mesh/${segment}/${detailLevel}`;
|
||||
}
|
||||
|
||||
public meshUrl_Bcif(source: string, entryId: string, segment: number, detailLevel: number): string {
|
||||
return `${this.volumeServerUrl}/${source}/${entryId}/mesh_bcif/${segment}/${detailLevel}`;
|
||||
}
|
||||
public volumeInfoUrl(source: string, entryId: string): string {
|
||||
return `${this.volumeServerUrl}/${source}/${entryId}/volume_info`;
|
||||
}
|
||||
|
||||
public async getEntryList(maxEntries: number, keyword?: string): Promise<{ [source: string]: string[] }> {
|
||||
const response = await fetch(this.entryListUrl(maxEntries, keyword));
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
public async getMetadata(source: string, entryId: string): Promise<Metadata> {
|
||||
const url = this.metadataUrl(source, entryId);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`Failed to fetch metadata from ${url}`);
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
83
src/extensions/volumes-and-segmentations/volseg-api/data.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
export interface Metadata {
|
||||
grid: {
|
||||
general: {
|
||||
details: string,
|
||||
source_db_name: string,
|
||||
source_db_id: string,
|
||||
},
|
||||
volumes: Volumes,
|
||||
segmentation_lattices: SegmentationLattices,
|
||||
segmentation_meshes: SegmentationMeshes,
|
||||
},
|
||||
annotation: Annotation | null,
|
||||
}
|
||||
|
||||
export interface Volumes {
|
||||
volume_downsamplings: number[],
|
||||
voxel_size: { [downsampling: number]: Vector3 },
|
||||
origin: Vector3,
|
||||
grid_dimensions: Vector3,
|
||||
sampled_grid_dimensions: { [downsampling: number]: Vector3 },
|
||||
mean: { [downsampling: number]: number },
|
||||
std: { [downsampling: number]: number },
|
||||
min: { [downsampling: number]: number },
|
||||
max: { [downsampling: number]: number },
|
||||
volume_force_dtype: string,
|
||||
}
|
||||
|
||||
export interface SegmentationLattices {
|
||||
segmentation_lattice_ids: number[],
|
||||
segmentation_downsamplings: { [lattice: number]: number[] },
|
||||
}
|
||||
|
||||
export interface SegmentationMeshes {
|
||||
mesh_component_numbers: {
|
||||
segment_ids?: {
|
||||
[segId: number]: {
|
||||
detail_lvls: {
|
||||
[detail: number]: {
|
||||
mesh_ids: {
|
||||
[meshId: number]: {
|
||||
num_triangles: number,
|
||||
num_vertices: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
detail_lvl_to_fraction: {
|
||||
[lvl: number]: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface Annotation {
|
||||
name: string,
|
||||
details: string,
|
||||
segment_list: Segment[],
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
id: number,
|
||||
colour: number[],
|
||||
biological_annotation: BiologicalAnnotation,
|
||||
}
|
||||
|
||||
export interface BiologicalAnnotation {
|
||||
name: string,
|
||||
external_references: ExternalReference[]
|
||||
}
|
||||
|
||||
export interface ExternalReference {
|
||||
id: number, resource: string, accession: string, label: string,
|
||||
description: string
|
||||
}
|
||||
|
||||
type Vector3 = [number, number, number];
|
||||
68
src/extensions/volumes-and-segmentations/volseg-api/utils.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Metadata, Segment } from './data';
|
||||
|
||||
|
||||
export class MetadataWrapper {
|
||||
raw: Metadata;
|
||||
private segmentMap?: { [id: number]: Segment };
|
||||
|
||||
constructor(rawMetadata: Metadata) {
|
||||
this.raw = rawMetadata;
|
||||
}
|
||||
|
||||
get allSegments() {
|
||||
return this.raw.annotation?.segment_list ?? [];
|
||||
}
|
||||
|
||||
get allSegmentIds() {
|
||||
return this.allSegments.map(segment => segment.id);
|
||||
}
|
||||
|
||||
getSegment(segmentId: number): Segment | undefined {
|
||||
if (!this.segmentMap) {
|
||||
this.segmentMap = {};
|
||||
for (const segment of this.allSegments) {
|
||||
this.segmentMap[segment.id] = segment;
|
||||
}
|
||||
}
|
||||
return this.segmentMap[segmentId];
|
||||
}
|
||||
|
||||
/** Get the list of detail levels available for the given mesh segment. */
|
||||
getMeshDetailLevels(segmentId: number): number[] {
|
||||
const segmentIds = this.raw.grid.segmentation_meshes.mesh_component_numbers.segment_ids;
|
||||
if (!segmentIds) return [];
|
||||
const details = segmentIds[segmentId].detail_lvls;
|
||||
return Object.keys(details).map(s => parseInt(s));
|
||||
}
|
||||
|
||||
/** Get the worst available detail level that is not worse than preferredDetail.
|
||||
* If preferredDetail is null, get the worst detail level overall.
|
||||
* (worse = greater number) */
|
||||
getSufficientMeshDetail(segmentId: number, preferredDetail: number | null) {
|
||||
let availDetails = this.getMeshDetailLevels(segmentId);
|
||||
if (preferredDetail !== null) {
|
||||
availDetails = availDetails.filter(det => det <= preferredDetail);
|
||||
}
|
||||
return Math.max(...availDetails);
|
||||
}
|
||||
|
||||
/** IDs of all segments available as meshes */
|
||||
get meshSegmentIds() {
|
||||
const segmentIds = this.raw.grid.segmentation_meshes.mesh_component_numbers.segment_ids;
|
||||
if (!segmentIds) return [];
|
||||
return Object.keys(segmentIds).map(s => parseInt(s));
|
||||
}
|
||||
|
||||
get gridTotalVolume() {
|
||||
const [vx, vy, vz] = this.raw.grid.volumes.voxel_size[1];
|
||||
const [gx, gy, gz] = this.raw.grid.volumes.grid_dimensions;
|
||||
return vx * vy * vz * gx * gy * gz;
|
||||
}
|
||||
|
||||
}
|
||||
30
src/extensions/zenodo/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { ZenodoImportUI } from './ui';
|
||||
|
||||
export const ZenodoImport = PluginBehavior.create<{ }>({
|
||||
name: 'extension-zenodo-import',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'Zenodo Export'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ }> {
|
||||
register(): void {
|
||||
this.ctx.customImportControls.set('zenodo-import', ZenodoImportUI as any);
|
||||
}
|
||||
|
||||
update() {
|
||||
return false;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
this.ctx.customImportControls.delete('zenodo-import');
|
||||
}
|
||||
},
|
||||
params: () => ({ })
|
||||
});
|
||||
302
src/extensions/zenodo/ui.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { DownloadFile } from '../../mol-plugin-state/actions/file';
|
||||
import { DownloadStructure, LoadTrajectory } from '../../mol-plugin-state/actions/structure';
|
||||
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
|
||||
import { CoordinatesFormatCategory } from '../../mol-plugin-state/formats/coordinates';
|
||||
import { TopologyFormatCategory } from '../../mol-plugin-state/formats/topology';
|
||||
import { TrajectoryFormatCategory } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { VolumeFormatCategory } from '../../mol-plugin-state/formats/volume';
|
||||
import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
|
||||
import { Button } from '../../mol-plugin-ui/controls/common';
|
||||
import { OpenInBrowserSvg } from '../../mol-plugin-ui/controls/icons';
|
||||
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { formatBytes } from '../../mol-util';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
|
||||
type ZenodoFile = {
|
||||
bucket: string
|
||||
checksum: string
|
||||
key: string
|
||||
links: {
|
||||
[key: string]: string
|
||||
self: string
|
||||
}
|
||||
size: number
|
||||
type: string
|
||||
}
|
||||
|
||||
type ZenodoRecord = {
|
||||
id: number
|
||||
conceptdoi: string
|
||||
conceptrecid: string
|
||||
created: string
|
||||
doi: string
|
||||
files: ZenodoFile[]
|
||||
revision: number
|
||||
updated: string
|
||||
metadata: {
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
interface State {
|
||||
busy?: boolean
|
||||
recordValues: PD.Values<typeof ZenodoImportParams>
|
||||
importValues?: PD.Values<ImportParams>
|
||||
importParams?: ImportParams
|
||||
record?: ZenodoRecord
|
||||
files?: ZenodoFile[]
|
||||
}
|
||||
|
||||
const ZenodoImportParams = {
|
||||
record: PD.Text('', { description: 'Zenodo ID.' })
|
||||
};
|
||||
|
||||
function createImportParams(files: ZenodoFile[], plugin: PluginContext) {
|
||||
const modelOpts: [string, string][] = [];
|
||||
const topologyOpts: [string, string][] = [];
|
||||
const coordinatesOpts: [string, string][] = [];
|
||||
const volumeOpts: [string, string][] = [];
|
||||
const compressedOpts: [string, string][] = [];
|
||||
|
||||
const structureExts = new Map<string, { format: string, isBinary: boolean }>();
|
||||
const coordinatesExts = new Map<string, { format: string, isBinary: boolean }>();
|
||||
const topologyExts = new Map<string, { format: string, isBinary: boolean }>();
|
||||
const volumeExts = new Map<string, { format: string, isBinary: boolean }>();
|
||||
|
||||
for (const { provider: { category, binaryExtensions, stringExtensions }, name } of plugin.dataFormats.list) {
|
||||
if (category === TrajectoryFormatCategory) {
|
||||
if (binaryExtensions) for (const e of binaryExtensions) structureExts.set(e, { format: name, isBinary: true });
|
||||
if (stringExtensions) for (const e of stringExtensions) structureExts.set(e, { format: name, isBinary: false });
|
||||
} else if (category === VolumeFormatCategory) {
|
||||
if (binaryExtensions) for (const e of binaryExtensions) volumeExts.set(e, { format: name, isBinary: true });
|
||||
if (stringExtensions) for (const e of stringExtensions) volumeExts.set(e, { format: name, isBinary: false });
|
||||
} else if (category === CoordinatesFormatCategory) {
|
||||
if (binaryExtensions) for (const e of binaryExtensions) coordinatesExts.set(e, { format: name, isBinary: true });
|
||||
if (stringExtensions) for (const e of stringExtensions) coordinatesExts.set(e, { format: name, isBinary: false });
|
||||
} else if (category === TopologyFormatCategory) {
|
||||
if (binaryExtensions) for (const e of binaryExtensions) topologyExts.set(e, { format: name, isBinary: true });
|
||||
if (stringExtensions) for (const e of stringExtensions) topologyExts.set(e, { format: name, isBinary: false });
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const label = `${file.key} (${formatBytes(file.size)})`;
|
||||
if (structureExts.has(file.type)) {
|
||||
const { format, isBinary } = structureExts.get(file.type)!;
|
||||
modelOpts.push([`${file.links.self}|${format}|${isBinary}`, label]);
|
||||
topologyOpts.push([`${file.links.self}|${format}|${isBinary}`, label]);
|
||||
} else if (volumeExts.has(file.type)) {
|
||||
const { format, isBinary } = volumeExts.get(file.type)!;
|
||||
volumeOpts.push([`${file.links.self}|${format}|${isBinary}`, label]);
|
||||
} else if (topologyExts.has(file.type)) {
|
||||
const { format, isBinary } = topologyExts.get(file.type)!;
|
||||
topologyOpts.push([`${file.links.self}|${format}|${isBinary}`, label]);
|
||||
} else if (coordinatesExts.has(file.type)) {
|
||||
const { format, isBinary } = coordinatesExts.get(file.type)!;
|
||||
coordinatesOpts.push([`${file.links.self}|${format}|${isBinary}`, label]);
|
||||
} else if (file.type === 'zip') {
|
||||
compressedOpts.push([`${file.links.self}|${file.type}|true`, label]);
|
||||
}
|
||||
}
|
||||
|
||||
const params: PD.Params = {};
|
||||
let defaultType = '';
|
||||
|
||||
if (modelOpts.length) {
|
||||
defaultType = 'structure';
|
||||
params.structure = PD.Select(modelOpts[0][0], modelOpts);
|
||||
}
|
||||
|
||||
if (topologyOpts.length && coordinatesOpts.length) {
|
||||
if (!defaultType) defaultType = 'trajectory';
|
||||
params.trajectory = PD.Group({
|
||||
topology: PD.Select(topologyOpts[0][0], topologyOpts),
|
||||
coordinates: PD.Select(coordinatesOpts[0][0], coordinatesOpts),
|
||||
}, { isFlat: true });
|
||||
}
|
||||
|
||||
if (volumeOpts.length) {
|
||||
if (!defaultType) defaultType = 'volume';
|
||||
params.volume = PD.Select(volumeOpts[0][0], volumeOpts);
|
||||
}
|
||||
|
||||
if (compressedOpts.length) {
|
||||
if (!defaultType) defaultType = 'compressed';
|
||||
params.compressed = PD.Select(compressedOpts[0][0], compressedOpts);
|
||||
}
|
||||
|
||||
return {
|
||||
type: PD.MappedStatic(defaultType, Object.keys(params).length ? params : { '': PD.EmptyGroup() })
|
||||
};
|
||||
}
|
||||
type ImportParams = ReturnType<typeof createImportParams>
|
||||
|
||||
export class ZenodoImportUI extends CollapsableControls<{}, State> {
|
||||
protected defaultState(): State & CollapsableState {
|
||||
return {
|
||||
header: 'Zenodo Import',
|
||||
isCollapsed: true,
|
||||
brand: { accent: 'cyan', svg: OpenInBrowserSvg },
|
||||
recordValues: PD.getDefaultValues(ZenodoImportParams),
|
||||
importValues: undefined,
|
||||
importParams: undefined,
|
||||
record: undefined,
|
||||
files: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private recordParamsOnChange = (values: any) => {
|
||||
this.setState({ recordValues: values });
|
||||
};
|
||||
|
||||
private importParamsOnChange = (values: any) => {
|
||||
this.setState({ importValues: values });
|
||||
};
|
||||
|
||||
private loadRecord = async () => {
|
||||
try {
|
||||
this.setState({ busy: true });
|
||||
const record: ZenodoRecord = await this.plugin.runTask(this.plugin.fetch({ url: `https://zenodo.org/api/records/${this.state.recordValues.record}`, type: 'json' }));
|
||||
const importParams = createImportParams(record.files, this.plugin);
|
||||
this.setState({
|
||||
record,
|
||||
files: record.files,
|
||||
busy: false,
|
||||
importValues: PD.getDefaultValues(importParams),
|
||||
importParams
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.plugin.log.error(`Failed to load Zenodo record '${this.state.recordValues.record}'`);
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
};
|
||||
|
||||
private loadFile = async (values: PD.Values<ImportParams>) => {
|
||||
try {
|
||||
this.setState({ busy: true });
|
||||
|
||||
const t = values.type;
|
||||
if (t.name === 'structure') {
|
||||
const defaultParams = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
|
||||
const [url, format, isBinary] = t.params.split('|');
|
||||
|
||||
await this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'url',
|
||||
params: {
|
||||
url,
|
||||
format: format as any,
|
||||
isBinary: isBinary === 'true',
|
||||
options: defaultParams.source.params.options,
|
||||
}
|
||||
}
|
||||
}));
|
||||
} else if (t.name === 'trajectory') {
|
||||
const [topologyUrl, topologyFormat, topologyIsBinary] = t.params.topology.split('|');
|
||||
const [coordinatesUrl, coordinatesFormat, coordinatesIsBinary] = t.params.coordinates.split('|');
|
||||
|
||||
await this.plugin.runTask(this.plugin.state.data.applyAction(LoadTrajectory, {
|
||||
source: {
|
||||
name: 'url',
|
||||
params: {
|
||||
model: {
|
||||
url: topologyUrl,
|
||||
format: topologyFormat as any,
|
||||
isBinary: topologyIsBinary === 'true',
|
||||
},
|
||||
coordinates: {
|
||||
url: coordinatesUrl,
|
||||
format: coordinatesFormat as any,
|
||||
isBinary: coordinatesIsBinary === 'true',
|
||||
},
|
||||
}
|
||||
}
|
||||
}));
|
||||
} else if (t.name === 'volume') {
|
||||
const [url, format, isBinary] = t.params.split('|');
|
||||
|
||||
await this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, {
|
||||
source: {
|
||||
name: 'url',
|
||||
params: {
|
||||
url,
|
||||
format: format as any,
|
||||
isBinary: isBinary === 'true',
|
||||
}
|
||||
}
|
||||
}));
|
||||
} else if (t.name === 'compressed') {
|
||||
const [url, format, isBinary] = t.params.split('|');
|
||||
|
||||
await this.plugin.runTask(this.plugin.state.data.applyAction(DownloadFile, {
|
||||
url,
|
||||
format: format as any,
|
||||
isBinary: isBinary === 'true',
|
||||
visuals: true
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.plugin.log.error(`Failed to load Zenodo file`);
|
||||
} finally {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
};
|
||||
|
||||
private clearRecord = () => {
|
||||
this.setState({
|
||||
importValues: undefined,
|
||||
importParams: undefined,
|
||||
record: undefined,
|
||||
files: undefined
|
||||
});
|
||||
};
|
||||
|
||||
private renderLoadRecord() {
|
||||
return <div style={{ marginBottom: 10 }}>
|
||||
<ParameterControls params={ZenodoImportParams} values={this.state.recordValues} onChangeValues={this.recordParamsOnChange} isDisabled={this.state.busy} />
|
||||
<Button onClick={this.loadRecord} style={{ marginTop: 1 }} disabled={this.state.busy || !this.state.recordValues.record}>
|
||||
Load Record
|
||||
</Button>
|
||||
</div>;
|
||||
}
|
||||
|
||||
private renderRecordInfo(record: ZenodoRecord) {
|
||||
return <div style={{ marginBottom: 10 }}>
|
||||
<div className='msp-help-text'>
|
||||
<div>Record {`${record.id}`}: <i>{`${record.metadata.title}`}</i></div>
|
||||
</div>
|
||||
<Button onClick={this.clearRecord} style={{ marginTop: 1 }} disabled={this.state.busy}>
|
||||
Clear
|
||||
</Button>
|
||||
</div>;
|
||||
}
|
||||
|
||||
private renderImportFile(params: ImportParams, values: PD.Values<ImportParams>) {
|
||||
return values.type.name ? <div style={{ marginBottom: 10 }}>
|
||||
<ParameterControls params={params} values={this.state.importValues} onChangeValues={this.importParamsOnChange} isDisabled={this.state.busy} />
|
||||
<Button onClick={() => this.loadFile(values)} style={{ marginTop: 1 }} disabled={this.state.busy}>
|
||||
Import File
|
||||
</Button>
|
||||
</div> : <div className='msp-help-text' style={{ marginBottom: 10 }}>
|
||||
<div>No supported files</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected renderControls(): JSX.Element | null {
|
||||
return <>
|
||||
{!this.state.record ? this.renderLoadRecord() : null}
|
||||
{this.state.record ? this.renderRecordInfo(this.state.record) : null}
|
||||
{this.state.importParams && this.state.importValues ? this.renderImportFile(this.state.importParams, this.state.importValues) : null}
|
||||
</>;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -10,6 +10,7 @@ import { Viewport, cameraProject, cameraUnproject } from './camera/util';
|
||||
import { CameraTransitionManager } from './camera/transition';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Scene } from '../mol-gl/scene';
|
||||
import { assertUnreachable } from '../mol-util/type-helpers';
|
||||
|
||||
export { ICamera, Camera };
|
||||
|
||||
@@ -84,7 +85,7 @@ class Camera implements ICamera {
|
||||
switch (this.state.mode) {
|
||||
case 'orthographic': updateOrtho(this); break;
|
||||
case 'perspective': updatePers(this); break;
|
||||
default: throw new Error('unknown camera mode');
|
||||
default: assertUnreachable(this.state.mode);
|
||||
}
|
||||
|
||||
const changed = !Mat4.areEqual(this.projection, this.prevProjection, EPSILON) || !Mat4.areEqual(this.view, this.prevView, EPSILON);
|
||||
@@ -259,7 +260,8 @@ namespace Camera {
|
||||
radius: 0,
|
||||
radiusMax: 10,
|
||||
fog: 50,
|
||||
clipFar: true
|
||||
clipFar: true,
|
||||
minNear: 5,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -275,6 +277,7 @@ namespace Camera {
|
||||
radiusMax: number
|
||||
fog: number
|
||||
clipFar: boolean
|
||||
minNear: number
|
||||
}
|
||||
|
||||
export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) {
|
||||
@@ -291,6 +294,7 @@ namespace Camera {
|
||||
if (typeof source.radiusMax !== 'undefined') out.radiusMax = source.radiusMax;
|
||||
if (typeof source.fog !== 'undefined') out.fog = source.fog;
|
||||
if (typeof source.clipFar !== 'undefined') out.clipFar = source.clipFar;
|
||||
if (typeof source.minNear !== 'undefined') out.minNear = source.minNear;
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -302,6 +306,7 @@ namespace Camera {
|
||||
&& a.radiusMax === b.radiusMax
|
||||
&& a.fog === b.fog
|
||||
&& a.clipFar === b.clipFar
|
||||
&& a.minNear === b.minNear
|
||||
&& Vec3.exactEquals(a.position, b.position)
|
||||
&& Vec3.exactEquals(a.up, b.up)
|
||||
&& Vec3.exactEquals(a.target, b.target);
|
||||
@@ -369,7 +374,7 @@ function updatePers(camera: Camera) {
|
||||
}
|
||||
|
||||
function updateClip(camera: Camera) {
|
||||
let { radius, radiusMax, mode, fog, clipFar } = camera.state;
|
||||
let { radius, radiusMax, mode, fog, clipFar, minNear } = camera.state;
|
||||
if (radius < 0.01) radius = 0.01;
|
||||
|
||||
const normalizedFar = clipFar ? radius : radiusMax;
|
||||
@@ -383,12 +388,12 @@ function updateClip(camera: Camera) {
|
||||
|
||||
if (mode === 'perspective') {
|
||||
// set at least to 5 to avoid slow sphere impostor rendering
|
||||
near = Math.max(Math.min(radiusMax, 5), near);
|
||||
far = Math.max(5, far);
|
||||
near = Math.max(Math.min(radiusMax, minNear), near);
|
||||
far = Math.max(minNear, far);
|
||||
} else {
|
||||
// not too close to 0 as it causes issues with outline rendering
|
||||
near = Math.max(Math.min(radiusMax, 5), near);
|
||||
far = Math.max(5, far);
|
||||
near = Math.max(Math.min(radiusMax, minNear), near);
|
||||
far = Math.max(minNear, far);
|
||||
}
|
||||
|
||||
if (near === far) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -13,8 +13,8 @@ import { Camera, ICamera } from '../camera';
|
||||
import { Viewport } from './util';
|
||||
|
||||
export const StereoCameraParams = {
|
||||
eyeSeparation: PD.Numeric(0.064, { min: 0.01, max: 0.5, step: 0.001 }),
|
||||
focus: PD.Numeric(10, { min: 1, max: 100, step: 0.01 }),
|
||||
eyeSeparation: PD.Numeric(0.062, { min: 0.02, max: 0.1, step: 0.001 }, { description: 'Distance between left and right camera.' }),
|
||||
focus: PD.Numeric(10, { min: 1, max: 20, step: 0.1 }, { description: 'Apparent object distance.' }),
|
||||
};
|
||||
export const DefaultStereoCameraProps = PD.getDefaultValues(StereoCameraParams);
|
||||
export type StereoCameraProps = PD.Values<typeof StereoCameraParams>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
@@ -30,7 +31,7 @@ import { PickData } from './passes/pick';
|
||||
import { PickHelper } from './passes/pick';
|
||||
import { ImagePass, ImageProps } from './passes/image';
|
||||
import { Sphere3D } from '../mol-math/geometry';
|
||||
import { isDebugMode } from '../mol-util/debug';
|
||||
import { isDebugMode, isTimingMode } from '../mol-util/debug';
|
||||
import { CameraHelperParams } from './helper/camera-helper';
|
||||
import { produce } from 'immer';
|
||||
import { HandleHelperParams } from './helper/handle-helper';
|
||||
@@ -39,6 +40,10 @@ import { Helper } from './helper/helper';
|
||||
import { Passes } from './passes/passes';
|
||||
import { shallowEqual } from '../mol-util';
|
||||
import { MarkingParams } from './passes/marking';
|
||||
import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit, GraphicsRenderVariantsDpoit } from '../mol-gl/webgl/render-item';
|
||||
import { degToRad, radToDeg } from '../mol-math/misc';
|
||||
import { AssetManager } from '../mol-util/assets';
|
||||
import { deepClone } from '../mol-util/object';
|
||||
|
||||
export const Canvas3DParams = {
|
||||
camera: PD.Group({
|
||||
@@ -48,6 +53,7 @@ export const Canvas3DParams = {
|
||||
on: PD.Group(StereoCameraParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
|
||||
fov: PD.Numeric(45, { min: 10, max: 130, step: 1 }, { label: 'Field of View' }),
|
||||
manualReset: PD.Boolean(false, { isHidden: true }),
|
||||
}, { pivot: 'mode' }),
|
||||
cameraFog: PD.MappedStatic('on', {
|
||||
@@ -59,6 +65,7 @@ export const Canvas3DParams = {
|
||||
cameraClipping: PD.Group({
|
||||
radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
|
||||
far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
|
||||
minNear: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
|
||||
}, { pivot: 'radius' }),
|
||||
viewport: PD.MappedStatic('canvas', {
|
||||
canvas: PD.Group({}),
|
||||
@@ -77,7 +84,9 @@ export const Canvas3DParams = {
|
||||
}),
|
||||
|
||||
cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
|
||||
sceneRadiusFactor: PD.Numeric(1, { min: 1, max: 10, step: 0.1 }),
|
||||
transparentBackground: PD.Boolean(false),
|
||||
dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
|
||||
|
||||
multiSample: PD.Group(MultiSampleParams),
|
||||
postprocessing: PD.Group(PostprocessingParams),
|
||||
@@ -105,11 +114,14 @@ interface Canvas3DContext {
|
||||
readonly attribs: Readonly<Canvas3DContext.Attribs>
|
||||
readonly contextLost: BehaviorSubject<now.Timestamp>
|
||||
readonly contextRestored: BehaviorSubject<now.Timestamp>
|
||||
readonly assetManager: AssetManager
|
||||
dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
|
||||
}
|
||||
|
||||
namespace Canvas3DContext {
|
||||
export const DefaultAttribs = {
|
||||
powerPreference: 'high-performance' as WebGLContextAttributes['powerPreference'],
|
||||
failIfMajorPerformanceCaveat: false,
|
||||
/** true by default to avoid issues with Safari (Jan 2021) */
|
||||
antialias: true,
|
||||
/** true to support multiple Canvas3D objects with a single context */
|
||||
@@ -119,14 +131,20 @@ namespace Canvas3DContext {
|
||||
/** extra pixels to around target to check in case target is empty */
|
||||
pickPadding: 1,
|
||||
enableWboit: true,
|
||||
enableDpoit: false,
|
||||
preferWebGl1: false
|
||||
};
|
||||
export type Attribs = typeof DefaultAttribs
|
||||
|
||||
export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext {
|
||||
export function fromCanvas(canvas: HTMLCanvasElement, assetManager: AssetManager, attribs: Partial<Attribs> = {}): Canvas3DContext {
|
||||
const a = { ...DefaultAttribs, ...attribs };
|
||||
const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
|
||||
|
||||
if (a.enableWboit && a.enableDpoit) throw new Error('Multiple transparency methods not allowed.');
|
||||
|
||||
const { powerPreference, failIfMajorPerformanceCaveat, antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
|
||||
const gl = getGLContext(canvas, {
|
||||
powerPreference,
|
||||
failIfMajorPerformanceCaveat,
|
||||
antialias,
|
||||
preserveDrawingBuffer,
|
||||
alpha: true, // the renderer requires an alpha channel
|
||||
@@ -138,7 +156,7 @@ namespace Canvas3DContext {
|
||||
|
||||
const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true });
|
||||
const webgl = createContext(gl, { pixelScale });
|
||||
const passes = new Passes(webgl, attribs);
|
||||
const passes = new Passes(webgl, assetManager, a);
|
||||
|
||||
if (isDebugMode) {
|
||||
const loseContextExt = gl.getExtension('WEBGL_lose_context');
|
||||
@@ -191,6 +209,7 @@ namespace Canvas3DContext {
|
||||
attribs: a,
|
||||
contextLost,
|
||||
contextRestored: webgl.contextRestored,
|
||||
assetManager,
|
||||
dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => {
|
||||
input.dispose();
|
||||
|
||||
@@ -222,7 +241,7 @@ interface Canvas3D {
|
||||
clear(): void
|
||||
syncVisibility(): void
|
||||
|
||||
requestDraw(force?: boolean): void
|
||||
requestDraw(): void
|
||||
|
||||
/** Reset the timers, used by "animate" */
|
||||
resetTime(t: number): void
|
||||
@@ -235,7 +254,7 @@ interface Canvas3D {
|
||||
/** Sets drawPaused = false without starting the built in animation loop */
|
||||
resume(): void
|
||||
identify(x: number, y: number): PickData | undefined
|
||||
mark(loci: Representation.Loci, action: MarkerAction, noDraw?: boolean): void
|
||||
mark(loci: Representation.Loci, action: MarkerAction): void
|
||||
getLoci(pickingId: PickingId | undefined): Representation.Loci
|
||||
|
||||
notifyDidDraw: boolean,
|
||||
@@ -277,8 +296,8 @@ namespace Canvas3D {
|
||||
export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
|
||||
export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
|
||||
|
||||
export function create({ webgl, input, passes, attribs }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
|
||||
const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props };
|
||||
export function create({ webgl, input, passes, attribs, assetManager }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
|
||||
const p: Canvas3DProps = { ...deepClone(DefaultCanvas3DParams), ...deepClone(props) };
|
||||
|
||||
const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
|
||||
const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
|
||||
@@ -295,14 +314,19 @@ namespace Canvas3D {
|
||||
let width = 128;
|
||||
let height = 128;
|
||||
updateViewport();
|
||||
const scene = Scene.create(webgl, passes.draw.dpoitEnabled ? GraphicsRenderVariantsDpoit : (passes.draw.wboitEnabled ? GraphicsRenderVariantsWboit : GraphicsRenderVariantsBlended));
|
||||
|
||||
const scene = Scene.create(webgl);
|
||||
function getSceneRadius() {
|
||||
return scene.boundingSphere.radius * p.sceneRadiusFactor;
|
||||
}
|
||||
|
||||
const camera = new Camera({
|
||||
position: Vec3.create(0, 0, 100),
|
||||
mode: p.camera.mode,
|
||||
fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
|
||||
clipFar: p.cameraClipping.far
|
||||
clipFar: p.cameraClipping.far,
|
||||
minNear: p.cameraClipping.minNear,
|
||||
fov: degToRad(p.camera.fov),
|
||||
}, { x, y, width, height }, { pixelScale: attribs.pixelScale });
|
||||
const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
|
||||
|
||||
@@ -314,6 +338,10 @@ namespace Canvas3D {
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction);
|
||||
const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
|
||||
|
||||
passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
|
||||
if (changed) requestDraw();
|
||||
});
|
||||
|
||||
let cameraResetRequested = false;
|
||||
let nextCameraResetDuration: number | undefined = void 0;
|
||||
let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0;
|
||||
@@ -344,24 +372,40 @@ namespace Canvas3D {
|
||||
return { loci, repr };
|
||||
}
|
||||
|
||||
function mark(reprLoci: Representation.Loci, action: MarkerAction, noDraw = false) {
|
||||
const { repr, loci } = reprLoci;
|
||||
let markBuffer: [reprLoci: Representation.Loci, action: MarkerAction][] = [];
|
||||
|
||||
function mark(reprLoci: Representation.Loci, action: MarkerAction) {
|
||||
// NOTE: might try to optimize a case with opposite actions for the
|
||||
// same loci. Tho this might end up being more expensive (and error prone)
|
||||
// then just applying everything "naively".
|
||||
markBuffer.push([reprLoci, action]);
|
||||
}
|
||||
|
||||
function resolveMarking() {
|
||||
let changed = false;
|
||||
if (repr) {
|
||||
changed = repr.mark(loci, action);
|
||||
} else {
|
||||
changed = helper.handle.mark(loci, action);
|
||||
changed = helper.camera.mark(loci, action) || changed;
|
||||
reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
|
||||
for (const [r, l] of markBuffer) {
|
||||
changed = applyMark(r, l) || changed;
|
||||
}
|
||||
if (changed && !noDraw) {
|
||||
markBuffer = [];
|
||||
if (changed) {
|
||||
scene.update(void 0, true);
|
||||
helper.handle.scene.update(void 0, true);
|
||||
helper.camera.scene.update(void 0, true);
|
||||
const prevPickDirty = pickHelper.dirty;
|
||||
draw(true);
|
||||
pickHelper.dirty = prevPickDirty; // marking does not change picking buffers
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function applyMark(reprLoci: Representation.Loci, action: MarkerAction) {
|
||||
const { repr, loci } = reprLoci;
|
||||
let changed = false;
|
||||
if (repr) {
|
||||
changed = repr.mark(loci, action) || changed;
|
||||
} else {
|
||||
reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
|
||||
}
|
||||
changed = helper.handle.mark(loci, action) || changed;
|
||||
changed = helper.camera.mark(loci, action) || changed;
|
||||
return changed;
|
||||
}
|
||||
|
||||
function render(force: boolean) {
|
||||
@@ -378,6 +422,8 @@ namespace Canvas3D {
|
||||
y > gl.drawingBufferHeight || y + height < 0
|
||||
) return false;
|
||||
|
||||
const markingUpdated = resolveMarking() && (renderer.props.colorMarker || p.marking.enabled);
|
||||
|
||||
let didRender = false;
|
||||
controls.update(currentTime);
|
||||
const cameraChanged = camera.update();
|
||||
@@ -385,25 +431,27 @@ namespace Canvas3D {
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender;
|
||||
forceNextRender = false;
|
||||
|
||||
const multiSampleChanged = multiSampleHelper.update(shouldRender, p.multiSample);
|
||||
const multiSampleChanged = multiSampleHelper.update(markingUpdated || shouldRender, p.multiSample);
|
||||
|
||||
if (shouldRender || multiSampleChanged) {
|
||||
if (shouldRender || multiSampleChanged || markingUpdated) {
|
||||
let cam: Camera | StereoCamera = camera;
|
||||
if (p.camera.stereo.name === 'on') {
|
||||
stereoCamera.update();
|
||||
cam = stereoCamera;
|
||||
}
|
||||
|
||||
if (isTimingMode) webgl.timer.mark('Canvas3D.render');
|
||||
const ctx = { renderer, camera: cam, scene, helper };
|
||||
if (MultiSamplePass.isEnabled(p.multiSample)) {
|
||||
if (!cameraChanged) {
|
||||
while (!multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p));
|
||||
} else {
|
||||
multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
|
||||
}
|
||||
const forceOn = !cameraChanged && markingUpdated && !controls.isAnimating;
|
||||
multiSampleHelper.render(ctx, p, true, forceOn);
|
||||
} else {
|
||||
passes.draw.render(renderer, cam, scene, helper, true, p.transparentBackground, p.postprocessing, p.marking);
|
||||
passes.draw.render(ctx, p, true);
|
||||
}
|
||||
pickHelper.dirty = true;
|
||||
if (isTimingMode) webgl.timer.markEnd('Canvas3D.render');
|
||||
|
||||
// if only marking has updated, do not set the flag to dirty
|
||||
pickHelper.dirty = pickHelper.dirty || shouldRender;
|
||||
didRender = true;
|
||||
}
|
||||
|
||||
@@ -415,15 +463,15 @@ namespace Canvas3D {
|
||||
let currentTime = 0;
|
||||
let drawPaused = false;
|
||||
|
||||
function draw(force?: boolean) {
|
||||
function draw(options?: { force?: boolean }) {
|
||||
if (drawPaused) return;
|
||||
if (render(!!force) && notifyDidDraw) {
|
||||
if (render(!!options?.force) && notifyDidDraw) {
|
||||
didDraw.next(now() - startTime as now.Timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
function requestDraw(force?: boolean) {
|
||||
forceNextRender = forceNextRender || !!force;
|
||||
function requestDraw() {
|
||||
forceNextRender = true;
|
||||
}
|
||||
|
||||
let animationFrameHandle = 0;
|
||||
@@ -437,7 +485,7 @@ namespace Canvas3D {
|
||||
return;
|
||||
}
|
||||
|
||||
draw(false);
|
||||
draw();
|
||||
if (!camera.transition.inTransition && !webgl.isContextLost) {
|
||||
interactionHelper.tick(currentTime);
|
||||
}
|
||||
@@ -477,7 +525,7 @@ namespace Canvas3D {
|
||||
resolveCameraReset();
|
||||
if (forceDrawAfterAllCommited) {
|
||||
if (helper.debug.isEnabled) helper.debug.update();
|
||||
draw(true);
|
||||
draw({ force: true });
|
||||
forceDrawAfterAllCommited = false;
|
||||
}
|
||||
commited.next(now());
|
||||
@@ -502,7 +550,7 @@ namespace Canvas3D {
|
||||
const focus = camera.getFocus(center, radius);
|
||||
const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot;
|
||||
const snapshot = next ? { ...focus, ...next } : focus;
|
||||
camera.setState({ ...snapshot, radiusMax: scene.boundingSphere.radius }, duration);
|
||||
camera.setState({ ...snapshot, radiusMax: getSceneRadius() }, duration);
|
||||
}
|
||||
|
||||
nextCameraResetDuration = void 0;
|
||||
@@ -553,7 +601,7 @@ namespace Canvas3D {
|
||||
}
|
||||
if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
|
||||
|
||||
camera.setState({ radiusMax: scene.boundingSphere.radius }, 0);
|
||||
if (!p.camera.manualReset) camera.setState({ radiusMax: getSceneRadius() }, 0);
|
||||
reprCount.next(reprRenderObjects.size);
|
||||
if (isDebugMode) consoleStats();
|
||||
|
||||
@@ -629,7 +677,7 @@ namespace Canvas3D {
|
||||
|
||||
function getProps(): Canvas3DProps {
|
||||
const radius = scene.boundingSphere.radius > 0
|
||||
? 100 - Math.round((camera.transition.target.radius / scene.boundingSphere.radius) * 100)
|
||||
? 100 - Math.round((camera.transition.target.radius / getSceneRadius()) * 100)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
@@ -637,14 +685,17 @@ namespace Canvas3D {
|
||||
mode: camera.state.mode,
|
||||
helper: { ...helper.camera.props },
|
||||
stereo: { ...p.camera.stereo },
|
||||
fov: Math.round(radToDeg(camera.state.fov)),
|
||||
manualReset: !!p.camera.manualReset
|
||||
},
|
||||
cameraFog: camera.state.fog > 0
|
||||
? { name: 'on' as const, params: { intensity: camera.state.fog } }
|
||||
: { name: 'off' as const, params: {} },
|
||||
cameraClipping: { far: camera.state.clipFar, radius },
|
||||
cameraClipping: { far: camera.state.clipFar, radius, minNear: camera.state.minNear },
|
||||
cameraResetDurationMs: p.cameraResetDurationMs,
|
||||
sceneRadiusFactor: p.sceneRadiusFactor,
|
||||
transparentBackground: p.transparentBackground,
|
||||
dpoitIterations: p.dpoitIterations,
|
||||
viewport: p.viewport,
|
||||
|
||||
postprocessing: { ...p.postprocessing },
|
||||
@@ -660,11 +711,11 @@ namespace Canvas3D {
|
||||
|
||||
const contextRestoredSub = contextRestored.subscribe(() => {
|
||||
pickHelper.dirty = true;
|
||||
draw(true);
|
||||
draw({ force: true });
|
||||
// Unclear why, but in Chrome with wboit enabled the first `draw` only clears
|
||||
// the drawingBuffer. Note that in Firefox the drawingBuffer is preserved after
|
||||
// context loss so it is unclear if it behaves the same.
|
||||
draw(true);
|
||||
draw({ force: true });
|
||||
});
|
||||
|
||||
const resized = new BehaviorSubject<any>(0);
|
||||
@@ -673,7 +724,7 @@ namespace Canvas3D {
|
||||
passes.updateSize();
|
||||
updateViewport();
|
||||
syncViewport();
|
||||
if (draw) requestDraw(true);
|
||||
if (draw) requestDraw();
|
||||
resized.next(+new Date());
|
||||
}
|
||||
|
||||
@@ -698,7 +749,7 @@ namespace Canvas3D {
|
||||
reprRenderObjects.clear();
|
||||
scene.clear();
|
||||
helper.debug.clear();
|
||||
requestDraw(true);
|
||||
requestDraw();
|
||||
reprCount.next(reprRenderObjects.size);
|
||||
},
|
||||
syncVisibility: () => {
|
||||
@@ -710,7 +761,7 @@ namespace Canvas3D {
|
||||
if (scene.syncVisibility()) {
|
||||
if (helper.debug.isEnabled) helper.debug.update();
|
||||
}
|
||||
requestDraw(true);
|
||||
requestDraw();
|
||||
},
|
||||
|
||||
requestDraw,
|
||||
@@ -746,10 +797,19 @@ namespace Canvas3D {
|
||||
? produce(getProps(), properties as any)
|
||||
: properties;
|
||||
|
||||
if (props.sceneRadiusFactor !== undefined) {
|
||||
p.sceneRadiusFactor = props.sceneRadiusFactor;
|
||||
camera.setState({ radiusMax: getSceneRadius() }, 0);
|
||||
}
|
||||
|
||||
const cameraState: Partial<Camera.Snapshot> = Object.create(null);
|
||||
if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
|
||||
cameraState.mode = props.camera.mode;
|
||||
}
|
||||
const oldFov = Math.round(radToDeg(camera.state.fov));
|
||||
if (props.camera && props.camera.fov !== undefined && props.camera.fov !== oldFov) {
|
||||
cameraState.fov = degToRad(props.camera.fov);
|
||||
}
|
||||
if (props.cameraFog !== undefined && props.cameraFog.params) {
|
||||
const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0;
|
||||
if (newFog !== camera.state.fog) cameraState.fog = newFog;
|
||||
@@ -758,8 +818,11 @@ namespace Canvas3D {
|
||||
if (props.cameraClipping.far !== undefined && props.cameraClipping.far !== camera.state.clipFar) {
|
||||
cameraState.clipFar = props.cameraClipping.far;
|
||||
}
|
||||
if (props.cameraClipping.minNear !== undefined && props.cameraClipping.minNear !== camera.state.minNear) {
|
||||
cameraState.minNear = props.cameraClipping.minNear;
|
||||
}
|
||||
if (props.cameraClipping.radius !== undefined) {
|
||||
const radius = (scene.boundingSphere.radius / 100) * (100 - props.cameraClipping.radius);
|
||||
const radius = (getSceneRadius() / 100) * (100 - props.cameraClipping.radius);
|
||||
if (radius > 0 && radius !== cameraState.radius) {
|
||||
// if radius = 0, NaNs happen
|
||||
cameraState.radius = Math.max(radius, 0.01);
|
||||
@@ -770,9 +833,13 @@ namespace Canvas3D {
|
||||
|
||||
if (props.camera?.helper) helper.camera.setProps(props.camera.helper);
|
||||
if (props.camera?.manualReset !== undefined) p.camera.manualReset = props.camera.manualReset;
|
||||
if (props.camera?.stereo !== undefined) Object.assign(p.camera.stereo, props.camera.stereo);
|
||||
if (props.camera?.stereo !== undefined) {
|
||||
Object.assign(p.camera.stereo, props.camera.stereo);
|
||||
stereoCamera.setProps(p.camera.stereo.params);
|
||||
}
|
||||
if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
|
||||
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
|
||||
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
|
||||
if (props.viewport !== undefined) {
|
||||
const doNotUpdate = p.viewport === props.viewport ||
|
||||
(p.viewport.name === props.viewport.name && shallowEqual(p.viewport.params, props.viewport.params));
|
||||
@@ -784,6 +851,12 @@ namespace Canvas3D {
|
||||
}
|
||||
}
|
||||
|
||||
if (props.postprocessing?.background) {
|
||||
Object.assign(p.postprocessing.background, props.postprocessing.background);
|
||||
passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
|
||||
if (changed && !doNotRequestDraw) requestDraw();
|
||||
});
|
||||
}
|
||||
if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
|
||||
if (props.marking) Object.assign(p.marking, props.marking);
|
||||
if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
|
||||
@@ -798,11 +871,11 @@ namespace Canvas3D {
|
||||
}
|
||||
|
||||
if (!doNotRequestDraw) {
|
||||
requestDraw(true);
|
||||
requestDraw();
|
||||
}
|
||||
},
|
||||
getImagePass: (props: Partial<ImageProps> = {}) => {
|
||||
return new ImagePass(webgl, renderer, scene, camera, helper, passes.draw.wboitEnabled, props);
|
||||
return new ImagePass(webgl, assetManager, renderer, scene, camera, helper, passes.draw.wboitEnabled, passes.draw.dpoitEnabled, props);
|
||||
},
|
||||
getRenderObjects(): GraphicsRenderObject[] {
|
||||
const renderObjects: GraphicsRenderObject[] = [];
|
||||
@@ -825,6 +898,8 @@ namespace Canvas3D {
|
||||
dispose: () => {
|
||||
contextRestoredSub.unsubscribe();
|
||||
|
||||
markBuffer = [];
|
||||
|
||||
scene.clear();
|
||||
helper.debug.clear();
|
||||
controls.dispose();
|
||||
@@ -865,4 +940,4 @@ namespace Canvas3D {
|
||||
Viewport.set(controls.viewport, x, y, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -13,7 +13,7 @@ import { Viewport } from '../camera/util';
|
||||
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput } from '../../mol-util/input/input-observer';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Camera } from '../camera';
|
||||
import { absMax } from '../../mol-math/misc';
|
||||
import { absMax, degToRad } from '../../mol-math/misc';
|
||||
import { Binding } from '../../mol-util/binding';
|
||||
|
||||
const B = ButtonsType;
|
||||
@@ -40,8 +40,16 @@ export const TrackballControlsParams = {
|
||||
zoomSpeed: PD.Numeric(7.0, { min: 1, max: 15, step: 1 }),
|
||||
panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
|
||||
|
||||
spin: PD.Boolean(false, { description: 'Spin the 3D scene around the x-axis in view space' }),
|
||||
spinSpeed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
|
||||
animate: PD.MappedStatic('off', {
|
||||
off: PD.EmptyGroup(),
|
||||
spin: PD.Group({
|
||||
speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
|
||||
}, { description: 'Spin the 3D scene around the x-axis in view space' }),
|
||||
rock: PD.Group({
|
||||
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }),
|
||||
angle: PD.Numeric(10, { min: 0, max: 90, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
|
||||
}, { description: 'Rock the 3D scene around the x-axis in view space' })
|
||||
}),
|
||||
|
||||
staticMoving: PD.Boolean(true, { isHidden: true }),
|
||||
dynamicDampingFactor: PD.Numeric(0.2, {}, { isHidden: true }),
|
||||
@@ -72,7 +80,8 @@ export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
|
||||
|
||||
export { TrackballControls };
|
||||
interface TrackballControls {
|
||||
viewport: Viewport
|
||||
readonly viewport: Viewport
|
||||
readonly isAnimating: boolean
|
||||
|
||||
readonly props: Readonly<TrackballControlsProps>
|
||||
setProps: (props: Partial<TrackballControlsProps>) => void
|
||||
@@ -144,6 +153,11 @@ namespace TrackballControls {
|
||||
);
|
||||
}
|
||||
|
||||
function getRotateFactor() {
|
||||
const aspectRatio = input.width / input.height;
|
||||
return p.rotateSpeed * input.pixelRatio * aspectRatio;
|
||||
}
|
||||
|
||||
const rotAxis = Vec3();
|
||||
const rotQuat = Quat();
|
||||
const rotEyeDir = Vec3();
|
||||
@@ -156,8 +170,7 @@ namespace TrackballControls {
|
||||
const dy = _rotCurr[1] - _rotPrev[1];
|
||||
Vec3.set(rotMoveDir, dx, dy, 0);
|
||||
|
||||
const aspectRatio = input.width / input.height;
|
||||
const angle = Vec3.magnitude(rotMoveDir) * p.rotateSpeed * input.pixelRatio * aspectRatio;
|
||||
const angle = Vec3.magnitude(rotMoveDir) * getRotateFactor();
|
||||
|
||||
if (angle) {
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
@@ -306,7 +319,10 @@ namespace TrackballControls {
|
||||
/** Update the object's position, direction and up vectors */
|
||||
function update(t: number) {
|
||||
if (lastUpdated === t) return;
|
||||
if (p.spin && lastUpdated > 0) spin(t - lastUpdated);
|
||||
if (lastUpdated > 0) {
|
||||
if (p.animate.name === 'spin') spin(t - lastUpdated);
|
||||
else if (p.animate.name === 'rock') rock(t - lastUpdated);
|
||||
}
|
||||
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
|
||||
@@ -345,6 +361,7 @@ namespace TrackballControls {
|
||||
if (!isStart && !_isInteracting) return;
|
||||
|
||||
_isInteracting = true;
|
||||
resetRock(); // start rocking from the center after interactions
|
||||
|
||||
const dragRotate = Binding.match(p.bindings.dragRotate, buttons, modifiers);
|
||||
const dragRotateZ = Binding.match(p.bindings.dragRotateZ, buttons, modifiers);
|
||||
@@ -434,11 +451,34 @@ namespace TrackballControls {
|
||||
|
||||
const _spinSpeed = Vec2.create(0.005, 0);
|
||||
function spin(deltaT: number) {
|
||||
if (p.spinSpeed === 0) return;
|
||||
if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
|
||||
|
||||
const frameSpeed = (p.spinSpeed || 0) / 1000;
|
||||
const frameSpeed = p.animate.params.speed / 1000;
|
||||
_spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
|
||||
if (!_isInteracting) Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
|
||||
Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
|
||||
}
|
||||
|
||||
let _rockPhase = 0;
|
||||
const _rockSpeed = Vec2.create(0.005, 0);
|
||||
function rock(deltaT: number) {
|
||||
if (p.animate.name !== 'rock' || p.animate.params.speed === 0 || _isInteracting) return;
|
||||
|
||||
const dt = deltaT / 1000 * p.animate.params.speed;
|
||||
const maxAngle = degToRad(p.animate.params.angle) / getRotateFactor();
|
||||
const angleA = Math.sin(_rockPhase * Math.PI * 2) * maxAngle;
|
||||
const angleB = Math.sin((_rockPhase + dt) * Math.PI * 2) * maxAngle;
|
||||
|
||||
_rockSpeed[0] = angleB - angleA;
|
||||
Vec2.add(_rotCurr, _rotPrev, _rockSpeed);
|
||||
|
||||
_rockPhase += dt;
|
||||
if (_rockPhase >= 1) {
|
||||
_rockPhase = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function resetRock() {
|
||||
_rockPhase = 0;
|
||||
}
|
||||
|
||||
function start(t: number) {
|
||||
@@ -448,9 +488,13 @@ namespace TrackballControls {
|
||||
|
||||
return {
|
||||
viewport,
|
||||
get isAnimating() { return p.animate.name !== 'off'; },
|
||||
|
||||
get props() { return p as Readonly<TrackballControlsProps>; },
|
||||
setProps: (props: Partial<TrackballControlsProps>) => {
|
||||
if (props.animate?.name === 'rock' && p.animate.name !== 'rock') {
|
||||
resetRock(); // start rocking from the center
|
||||
}
|
||||
Object.assign(p, props);
|
||||
},
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { TransformData } from '../../mol-geo/geometry/transform-data';
|
||||
import { sphereVertexCount } from '../../mol-geo/primitive/sphere';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { Geometry } from '../../mol-geo/geometry/geometry';
|
||||
import { GraphicsRenderVariantsBlended } from '../../mol-gl/webgl/render-item';
|
||||
|
||||
export const DebugHelperParams = {
|
||||
sceneBoundingSpheres: PD.Boolean(false, { description: 'Show full scene bounding spheres.' }),
|
||||
@@ -41,7 +42,7 @@ export class BoundingSphereHelper {
|
||||
private visibleSceneData: BoundingSphereData | undefined;
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<DebugHelperProps>) {
|
||||
this.scene = Scene.create(ctx);
|
||||
this.scene = Scene.create(ctx, GraphicsRenderVariantsBlended);
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(DebugHelperParams), ...props };
|
||||
}
|
||||
@@ -160,5 +161,5 @@ const instanceMaterialId = getNextMaterialId();
|
||||
|
||||
function createBoundingSphereRenderObject(mesh: Mesh, color: Color, materialId: number, transform?: TransformData) {
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false }, color, 1, transform);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false, noClip: false }, materialId);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -14,9 +14,10 @@ import { PickingId } from '../../mol-geo/geometry/picking';
|
||||
import { GraphicsRenderObject } from '../../mol-gl/render-object';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { GraphicsRenderVariantsBlended } from '../../mol-gl/webgl/render-item';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { DataLoci, EmptyLoci, Loci } from '../../mol-model/loci';
|
||||
import { DataLoci, EmptyLoci, isEveryLoci, Loci } from '../../mol-model/loci';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { Visual } from '../../mol-repr/visual';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
@@ -58,7 +59,7 @@ export class CameraHelper {
|
||||
private renderObject: GraphicsRenderObject | undefined;
|
||||
|
||||
constructor(private webgl: WebGLContext, props: Partial<CameraHelperProps> = {}) {
|
||||
this.scene = Scene.create(webgl);
|
||||
this.scene = Scene.create(webgl, GraphicsRenderVariantsBlended);
|
||||
|
||||
this.camera = new Camera();
|
||||
Vec3.set(this.camera.up, 0, 1, 0);
|
||||
@@ -75,7 +76,6 @@ export class CameraHelper {
|
||||
this.scene.clear();
|
||||
const params = { ...props.axes.params, scale: props.axes.params.scale * this.webgl.pixelRatio };
|
||||
this.renderObject = createAxesRenderObject(params);
|
||||
this.renderObject.state.noClip = true;
|
||||
this.scene.add(this.renderObject);
|
||||
this.scene.commit();
|
||||
|
||||
@@ -113,8 +113,10 @@ export class CameraHelper {
|
||||
|
||||
mark(loci: Loci, action: MarkerAction) {
|
||||
if (!MarkerActions.is(MarkerActions.Highlighting, action)) return false;
|
||||
if (!isCameraAxesLoci(loci)) return false;
|
||||
if (loci.data !== this) return false;
|
||||
if (!isEveryLoci(loci)) {
|
||||
if (!isCameraAxesLoci(loci)) return false;
|
||||
if (loci.data !== this) return false;
|
||||
}
|
||||
return Visual.mark(this.renderObject, loci, action, this.eachGroup);
|
||||
}
|
||||
|
||||
@@ -201,7 +203,7 @@ function createAxesMesh(scale: number, mesh?: Mesh) {
|
||||
const x = Vec3.scale(Vec3(), Vec3.unitX, scale);
|
||||
const y = Vec3.scale(Vec3(), Vec3.unitY, scale);
|
||||
const z = Vec3.scale(Vec3(), Vec3.unitZ, scale);
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments: 32 };
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments: 32 };
|
||||
|
||||
state.currentGroup = CameraHelperAxis.None;
|
||||
addSphere(state, Vec3.origin, radius, 2);
|
||||
|
||||