mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 21:34:23 +08:00
Compare commits
792 Commits
v0.7.1-dev
...
v2.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ba13a2f2 | ||
|
|
3b25e037aa | ||
|
|
189fad3d84 | ||
|
|
c3c22ee3bc | ||
|
|
8a3222005c | ||
|
|
a17da36410 | ||
|
|
80323d8122 | ||
|
|
cbd6aa0b6b | ||
|
|
3831bd9941 | ||
|
|
3d3e2c3a86 | ||
|
|
bc5d796653 | ||
|
|
82dd0496c2 | ||
|
|
056742ac74 | ||
|
|
29d4cfbcca | ||
|
|
376449f7c8 | ||
|
|
bc37fad007 | ||
|
|
2e561a8de7 | ||
|
|
e6c8c69d0c | ||
|
|
d121a11e28 | ||
|
|
5484a2a72c | ||
|
|
d527609b6d | ||
|
|
e628f580a7 | ||
|
|
b662179b4d | ||
|
|
fa2b8542bf | ||
|
|
901522f500 | ||
|
|
62b63c1aa5 | ||
|
|
24b36f41da | ||
|
|
c9c890782c | ||
|
|
f2c539ebd8 | ||
|
|
feb922ca91 | ||
|
|
25127bb84b | ||
|
|
8fb01d2157 | ||
|
|
c09357ea75 | ||
|
|
9f2513dae0 | ||
|
|
11a52c0390 | ||
|
|
e955dc7e94 | ||
|
|
c8107272f6 | ||
|
|
fb08fe7545 | ||
|
|
b6f054ea28 | ||
|
|
dc7e85133c | ||
|
|
90cddf4e41 | ||
|
|
2cddbb72a6 | ||
|
|
a16faaac4e | ||
|
|
6c5224f33e | ||
|
|
77d013b775 | ||
|
|
02a466e8b9 | ||
|
|
3cb65cbe3d | ||
|
|
fe8838542c | ||
|
|
78b5c9aac4 | ||
|
|
021fa7b79b | ||
|
|
0443589b09 | ||
|
|
415288de9f | ||
|
|
ecbafb086a | ||
|
|
e5dae6c0dd | ||
|
|
16f4524bdb | ||
|
|
6b33021f43 | ||
|
|
fdf37100c2 | ||
|
|
e28674d0dc | ||
|
|
fb7456286a | ||
|
|
9d240f8928 | ||
|
|
48ef5efb21 | ||
|
|
52b2e7c144 | ||
|
|
f2d1d60f6b | ||
|
|
5a176a378a | ||
|
|
60151c2c24 | ||
|
|
a5db6350a2 | ||
|
|
0618eb18ba | ||
|
|
bffdff6aad | ||
|
|
7753a6ec56 | ||
|
|
b8aafa1d78 | ||
|
|
672875187b | ||
|
|
547d60d573 | ||
|
|
99471d2a7b | ||
|
|
45d249b71a | ||
|
|
1382edd81c | ||
|
|
89a6102f8d | ||
|
|
163929477e | ||
|
|
c10a8369e8 | ||
|
|
8fbba52de8 | ||
|
|
ca3174b2c3 | ||
|
|
b9864fba80 | ||
|
|
f8e9bc1e7f | ||
|
|
f79f1507f7 | ||
|
|
61ab205a5d | ||
|
|
2c65260a4f | ||
|
|
0597a1ef24 | ||
|
|
8d6557e51c | ||
|
|
5cff0dff3d | ||
|
|
93206e76d7 | ||
|
|
40933a8539 | ||
|
|
989800783b | ||
|
|
d83b0d2c4d | ||
|
|
5e5d5a63dc | ||
|
|
b1755604e2 | ||
|
|
e58da9b574 | ||
|
|
f5d6498601 | ||
|
|
07f351888f | ||
|
|
4588fdd5d5 | ||
|
|
c3b32baf6a | ||
|
|
b8d60cea9b | ||
|
|
25b8956712 | ||
|
|
7015309db6 | ||
|
|
aad861db37 | ||
|
|
ae7811705d | ||
|
|
7e26dac50b | ||
|
|
75f43d038c | ||
|
|
b9ba940510 | ||
|
|
35603baaaa | ||
|
|
19dc32c491 | ||
|
|
95997e6a61 | ||
|
|
03e19a2ad7 | ||
|
|
765b133369 | ||
|
|
703e729514 | ||
|
|
b0216c4ce6 | ||
|
|
6796fc1cd4 | ||
|
|
87c504f9a8 | ||
|
|
2e770cb733 | ||
|
|
9f440f68e0 | ||
|
|
40028b27ba | ||
|
|
4676ad8738 | ||
|
|
e1c7833826 | ||
|
|
dd1bca0fee | ||
|
|
c38ab2c638 | ||
|
|
459c5aa5a7 | ||
|
|
b8bf07d393 | ||
|
|
ea87ac2094 | ||
|
|
e1b830a59d | ||
|
|
41e1ac76c0 | ||
|
|
98b118fd1e | ||
|
|
5f691913e4 | ||
|
|
26e2516097 | ||
|
|
3d2e4115ed | ||
|
|
dbce1ccb3d | ||
|
|
03aa2be978 | ||
|
|
8dfc52e1ab | ||
|
|
6058179f10 | ||
|
|
ea9e25b03c | ||
|
|
d60c3ddce3 | ||
|
|
724e79bddf | ||
|
|
2de61215c4 | ||
|
|
e783d9a9f1 | ||
|
|
e9e971d4f3 | ||
|
|
96dea14cb1 | ||
|
|
04fc157340 | ||
|
|
cfc24fa99e | ||
|
|
19c1088209 | ||
|
|
ee6c2e0841 | ||
|
|
20ee659b00 | ||
|
|
b6514a4a50 | ||
|
|
07b8bdb951 | ||
|
|
afd18cabd4 | ||
|
|
1117ce05d5 | ||
|
|
fc15e952bf | ||
|
|
249e5a3e0b | ||
|
|
4bfe3f6bde | ||
|
|
75b7e0b4d9 | ||
|
|
ee4ce2fd7a | ||
|
|
db0aa12e75 | ||
|
|
6d2578d3d0 | ||
|
|
99d61f48b4 | ||
|
|
146022dc12 | ||
|
|
92730cad01 | ||
|
|
d6b68b06da | ||
|
|
b174fbf0c6 | ||
|
|
fde1557955 | ||
|
|
24a0753881 | ||
|
|
5664e1d8be | ||
|
|
4881a41256 | ||
|
|
235e41ee03 | ||
|
|
94d293a4d3 | ||
|
|
40f1ca207f | ||
|
|
926fb38c1e | ||
|
|
5a14fcabc5 | ||
|
|
560e40773f | ||
|
|
6561732f57 | ||
|
|
b45cf206fd | ||
|
|
70e07be64d | ||
|
|
f3013f0e46 | ||
|
|
2e7041bd78 | ||
|
|
5d0447c9bb | ||
|
|
9eba0b91a8 | ||
|
|
58bc6722a9 | ||
|
|
1acfed3233 | ||
|
|
8147b3aa34 | ||
|
|
b21552ff36 | ||
|
|
c683cbe962 | ||
|
|
bd270e4428 | ||
|
|
23d942d8a5 | ||
|
|
cbcd6b99d2 | ||
|
|
ee5c098a9f | ||
|
|
070a15d679 | ||
|
|
befa5174f8 | ||
|
|
d6c4366f40 | ||
|
|
181cfefa63 | ||
|
|
0e7c885961 | ||
|
|
d58e90d93f | ||
|
|
cd872b47e6 | ||
|
|
2683c5b318 | ||
|
|
c71f60a164 | ||
|
|
881cbc1947 | ||
|
|
f3e7febbd1 | ||
|
|
e68ad13031 | ||
|
|
7fbbe1e63a | ||
|
|
a5ca72af3c | ||
|
|
1ce6641eb3 | ||
|
|
5dc413ab8c | ||
|
|
50b615e86c | ||
|
|
5b4c6743e7 | ||
|
|
99a3906978 | ||
|
|
981db34736 | ||
|
|
c079a8c5a8 | ||
|
|
ad70adf6ce | ||
|
|
89110b52bd | ||
|
|
8a69f050a6 | ||
|
|
9e38a44406 | ||
|
|
3514ab23c3 | ||
|
|
b59e3c383d | ||
|
|
eeba565d78 | ||
|
|
687e54cc87 | ||
|
|
ac73939440 | ||
|
|
7a3eb8d03f | ||
|
|
3d26904e0b | ||
|
|
468e14bc35 | ||
|
|
e2dc61212e | ||
|
|
aa911ad4bc | ||
|
|
bb5494264c | ||
|
|
c0116a3baa | ||
|
|
9c7497b447 | ||
|
|
fa3a79fdeb | ||
|
|
2987240df4 | ||
|
|
17a1640da5 | ||
|
|
a86da8ee11 | ||
|
|
20e373115d | ||
|
|
531260fbc5 | ||
|
|
47a3dfcef9 | ||
|
|
c5ca51fd80 | ||
|
|
2f2e44c032 | ||
|
|
26acb37098 | ||
|
|
466308cde8 | ||
|
|
dc9af9d8b0 | ||
|
|
c17bfd65e7 | ||
|
|
6de07ab8c2 | ||
|
|
0b8aab802c | ||
|
|
13f28fbe33 | ||
|
|
2eda679966 | ||
|
|
35c778b644 | ||
|
|
96f8ba5a80 | ||
|
|
0daaa94958 | ||
|
|
e672503fda | ||
|
|
4d86c9e0ae | ||
|
|
80fbc474f6 | ||
|
|
dacdc6abfc | ||
|
|
58bc8b58de | ||
|
|
07ead670dd | ||
|
|
00fd760f71 | ||
|
|
a71186905d | ||
|
|
a7e0524d01 | ||
|
|
7d7c1241d4 | ||
|
|
1f6e928d78 | ||
|
|
9bc256bdab | ||
|
|
734096260d | ||
|
|
1b4b6f9435 | ||
|
|
54fe5c85d6 | ||
|
|
f336891bf3 | ||
|
|
d2a3c9c61f | ||
|
|
6968959fe2 | ||
|
|
7749fe5000 | ||
|
|
bf45d2df5d | ||
|
|
b2222844ae | ||
|
|
3d21f1ecc6 | ||
|
|
cde280de60 | ||
|
|
9b415ddff2 | ||
|
|
906c3ac2b6 | ||
|
|
498611d4d4 | ||
|
|
a11bc73d68 | ||
|
|
9616ae5d63 | ||
|
|
c81476d2a7 | ||
|
|
397f001352 | ||
|
|
7edf274477 | ||
|
|
3c1a26c4f5 | ||
|
|
1c695846d5 | ||
|
|
a4c6d1e0e6 | ||
|
|
e51fe83800 | ||
|
|
316076d81e | ||
|
|
4073055d8d | ||
|
|
c6e0ec1c06 | ||
|
|
49aaa48e6e | ||
|
|
0eb882883e | ||
|
|
a6c25551dd | ||
|
|
0a3f73860a | ||
|
|
1de159d65c | ||
|
|
e2c411fefe | ||
|
|
3cf1c64e12 | ||
|
|
b159752b72 | ||
|
|
0d7db59c9e | ||
|
|
a8bf90a68b | ||
|
|
96aff39272 | ||
|
|
a9ae08fc1f | ||
|
|
a24f989c01 | ||
|
|
41ff45d14c | ||
|
|
6ad80bf66b | ||
|
|
eeed48a1f7 | ||
|
|
232bc0d076 | ||
|
|
ac6b87add4 | ||
|
|
2e3bff7d48 | ||
|
|
bd223b4c39 | ||
|
|
a75dc11427 | ||
|
|
30acaffb72 | ||
|
|
2818102b8b | ||
|
|
519e5a6f92 | ||
|
|
071740e7c1 | ||
|
|
7aafb2f4c3 | ||
|
|
3c72988d77 | ||
|
|
3c01dfbd42 | ||
|
|
3a5829aa3e | ||
|
|
ffeeddb37a | ||
|
|
50945493c1 | ||
|
|
fa80c4797a | ||
|
|
650e8bf703 | ||
|
|
13d57737ae | ||
|
|
a6d1a3dfdd | ||
|
|
afffdc06e5 | ||
|
|
80f1b1c795 | ||
|
|
06111e2731 | ||
|
|
adb49371bb | ||
|
|
7b726ded20 | ||
|
|
9f85a0c840 | ||
|
|
f92755c920 | ||
|
|
d141c27765 | ||
|
|
062ac65f0f | ||
|
|
bb420d0806 | ||
|
|
0018032423 | ||
|
|
3dd48ac73c | ||
|
|
4632a6f305 | ||
|
|
eda570d4f1 | ||
|
|
b0127d746d | ||
|
|
5a66ca69c4 | ||
|
|
1c17277f03 | ||
|
|
d771bdc8ff | ||
|
|
eace3f4259 | ||
|
|
8f88da70a6 | ||
|
|
b797be9642 | ||
|
|
2395b7a10a | ||
|
|
0764795c08 | ||
|
|
ea8b7a1d56 | ||
|
|
7c6827f5f5 | ||
|
|
5a6f16ef8d | ||
|
|
8dfdcdd0b7 | ||
|
|
67a2594108 | ||
|
|
871f9635e3 | ||
|
|
25251f3546 | ||
|
|
98824f477e | ||
|
|
aae9a117e8 | ||
|
|
452639c3ce | ||
|
|
6bd45e0a9b | ||
|
|
be100a3ac6 | ||
|
|
96a8cd789c | ||
|
|
d195e1dbf5 | ||
|
|
e6a8e788f5 | ||
|
|
a755ed441e | ||
|
|
de8f294329 | ||
|
|
021171c07d | ||
|
|
013ddb72ed | ||
|
|
207c226f66 | ||
|
|
b4ff98499b | ||
|
|
8471d337a2 | ||
|
|
2f84b94227 | ||
|
|
8dcd6063b7 | ||
|
|
caefe7ba67 | ||
|
|
ad6cebc59b | ||
|
|
e8d2e6d806 | ||
|
|
39352c40d1 | ||
|
|
9994262abc | ||
|
|
5bcf923381 | ||
|
|
ab5dd0b733 | ||
|
|
5a8a6310f8 | ||
|
|
a634c7a587 | ||
|
|
353c5d6d95 | ||
|
|
92698c486c | ||
|
|
898dd1161d | ||
|
|
361f289d0e | ||
|
|
b49d036fcd | ||
|
|
cfee9d86c0 | ||
|
|
92622dfbd7 | ||
|
|
80c2876350 | ||
|
|
fb99f6db8a | ||
|
|
862f8193ef | ||
|
|
490c6679eb | ||
|
|
dd278ca964 | ||
|
|
0892bb24d0 | ||
|
|
83ce5e9422 | ||
|
|
c4ba92c7cb | ||
|
|
846bdf10b0 | ||
|
|
91a46ea7df | ||
|
|
e4ec68a86c | ||
|
|
410655052f | ||
|
|
17162e967a | ||
|
|
c119a1bc21 | ||
|
|
7f2e98f714 | ||
|
|
82f5d8be21 | ||
|
|
73fa675346 | ||
|
|
1f2812b2e3 | ||
|
|
6f05179db8 | ||
|
|
fc8848e97c | ||
|
|
b991102bfa | ||
|
|
e3768805a6 | ||
|
|
cdb65665a6 | ||
|
|
3765bc410c | ||
|
|
69bbd76f33 | ||
|
|
b61b3e1115 | ||
|
|
835f717e47 | ||
|
|
6a35a3ece0 | ||
|
|
518621a1bd | ||
|
|
51acfa1dce | ||
|
|
5be6c9176a | ||
|
|
dfa83c94f7 | ||
|
|
67aedd4770 | ||
|
|
346eb59da9 | ||
|
|
3195594ef3 | ||
|
|
489b412308 | ||
|
|
2d0e8d4ca0 | ||
|
|
27f94c81a2 | ||
|
|
1e865ecacc | ||
|
|
f293a02485 | ||
|
|
ddf00600c6 | ||
|
|
88cd639493 | ||
|
|
0a30ed45f9 | ||
|
|
b5b282c141 | ||
|
|
c4c60cb263 | ||
|
|
e3cf4e928e | ||
|
|
d8a08ef900 | ||
|
|
8b8f3bf492 | ||
|
|
3983073d6c | ||
|
|
82b22fa3f2 | ||
|
|
8a38f73771 | ||
|
|
37da82b138 | ||
|
|
dd17cb23d9 | ||
|
|
8f3afd9f7c | ||
|
|
6e39188f0b | ||
|
|
667cacea12 | ||
|
|
49f0ec981c | ||
|
|
a60d6e9223 | ||
|
|
2d111c1e25 | ||
|
|
874cde4f72 | ||
|
|
826318760e | ||
|
|
de790051aa | ||
|
|
00bf75839e | ||
|
|
b9d4501dcc | ||
|
|
46fb1789b0 | ||
|
|
a1e8bf841b | ||
|
|
6662dbfdd6 | ||
|
|
39b9710d16 | ||
|
|
4fe303da72 | ||
|
|
0662506d35 | ||
|
|
ea987f5601 | ||
|
|
bcae586122 | ||
|
|
e56f188a12 | ||
|
|
8fda9beb7b | ||
|
|
0f50a6682b | ||
|
|
524d38c450 | ||
|
|
68a2e52355 | ||
|
|
447d1f940f | ||
|
|
1cbb59b5d0 | ||
|
|
6f5bcdef90 | ||
|
|
f6f1c5a350 | ||
|
|
f6545c38be | ||
|
|
53fe73d3ee | ||
|
|
3bf5ab1ef7 | ||
|
|
b4813ff866 | ||
|
|
845269e9a5 | ||
|
|
59968d92ab | ||
|
|
d5b7cd370b | ||
|
|
b4434cce17 | ||
|
|
e73227519b | ||
|
|
f8e6d5cbfb | ||
|
|
70bde8c899 | ||
|
|
361dce2b96 | ||
|
|
c83ce28bf4 | ||
|
|
b9a3620a4c | ||
|
|
a939a57811 | ||
|
|
8388ee8f1e | ||
|
|
befa40f5a2 | ||
|
|
36257e2b0f | ||
|
|
769022e88c | ||
|
|
51c180a8f4 | ||
|
|
2ffc5dc5c0 | ||
|
|
431ba01117 | ||
|
|
c6a4350b81 | ||
|
|
9ef8d0c9f8 | ||
|
|
e7606477c2 | ||
|
|
de093b5472 | ||
|
|
88cd9184d8 | ||
|
|
055c169c1f | ||
|
|
1988275695 | ||
|
|
82451bff00 | ||
|
|
e6fd0202a6 | ||
|
|
c21d84dd62 | ||
|
|
f392ac21cd | ||
|
|
d4e5473b86 | ||
|
|
760298c6bf | ||
|
|
d949b99629 | ||
|
|
fff9719e48 | ||
|
|
5858a6eb19 | ||
|
|
46acc1b95e | ||
|
|
da4654b859 | ||
|
|
32869a9a45 | ||
|
|
4cfbccc5d6 | ||
|
|
6301196e67 | ||
|
|
0c61c2badd | ||
|
|
1207526161 | ||
|
|
405d9d524f | ||
|
|
f724717821 | ||
|
|
6247efa8b6 | ||
|
|
7045545419 | ||
|
|
17a0a2be6f | ||
|
|
0e92cfa007 | ||
|
|
04e17872d0 | ||
|
|
59255c720d | ||
|
|
eb71e2c606 | ||
|
|
db2905ba9f | ||
|
|
d097a4abd2 | ||
|
|
459cfd7ab8 | ||
|
|
317229afee | ||
|
|
1e2f16d6b3 | ||
|
|
be07c1668f | ||
|
|
90ddb3dc34 | ||
|
|
f968e86387 | ||
|
|
56639f0bda | ||
|
|
8ae40bfd7c | ||
|
|
a7901c53ce | ||
|
|
76f856fa4f | ||
|
|
63a4cda442 | ||
|
|
603aa89609 | ||
|
|
51dd388912 | ||
|
|
c4708f0260 | ||
|
|
dcfe2e3072 | ||
|
|
9d536fefff | ||
|
|
ed69d15ee1 | ||
|
|
4cd7f0575e | ||
|
|
ad1507dadf | ||
|
|
c358259437 | ||
|
|
6b961c532f | ||
|
|
bb8f872a13 | ||
|
|
93df8a65cd | ||
|
|
de9fd2fcd7 | ||
|
|
24ae8dfda8 | ||
|
|
909e4b3a9f | ||
|
|
09c46447d9 | ||
|
|
239a7cc072 | ||
|
|
f7ccff61e0 | ||
|
|
f8a7483467 | ||
|
|
0a7b3fa396 | ||
|
|
e5cf8bcc04 | ||
|
|
f93230ce44 | ||
|
|
bdde2cea31 | ||
|
|
d44bb6c908 | ||
|
|
052e30b739 | ||
|
|
48a7ac80f5 | ||
|
|
c9717c2332 | ||
|
|
aaed0a9a63 | ||
|
|
c29648ac9b | ||
|
|
cb1f52a8f4 | ||
|
|
5f759edc1b | ||
|
|
8897261836 | ||
|
|
23b1426567 | ||
|
|
329658ff54 | ||
|
|
482059cc9b | ||
|
|
4e509fc479 | ||
|
|
6b1edd9d10 | ||
|
|
d0c692fe03 | ||
|
|
36ee1089a8 | ||
|
|
723bf3e657 | ||
|
|
dc08e524b3 | ||
|
|
3ecd305adf | ||
|
|
60eb42391e | ||
|
|
26f5210518 | ||
|
|
7b41d800c0 | ||
|
|
3aa9ef7595 | ||
|
|
fad5a40ec4 | ||
|
|
58e0ba016e | ||
|
|
b8f168ebf5 | ||
|
|
560c26c73f | ||
|
|
73ada6b1f1 | ||
|
|
4f2cee3b56 | ||
|
|
ad6975c99d | ||
|
|
bea462b8b2 | ||
|
|
5652fa55db | ||
|
|
d2208a0814 | ||
|
|
62a456ce82 | ||
|
|
bef1142a31 | ||
|
|
8e07de62dc | ||
|
|
347b9ead6b | ||
|
|
6d9a09620c | ||
|
|
17349b8529 | ||
|
|
3a200fe2d7 | ||
|
|
10bd513680 | ||
|
|
9214c54e7d | ||
|
|
64da492d63 | ||
|
|
cbb65aaaac | ||
|
|
78c9dda257 | ||
|
|
20450e352f | ||
|
|
9559c22858 | ||
|
|
0d39dc69f1 | ||
|
|
1a29159dfd | ||
|
|
8c21d3b9d9 | ||
|
|
24143d7078 | ||
|
|
22ead527f2 | ||
|
|
104666a13e | ||
|
|
f9ea48fd7b | ||
|
|
80598dc102 | ||
|
|
fbaaa57ca2 | ||
|
|
b0113d6189 | ||
|
|
da1fa03a5f | ||
|
|
48d0418f0e | ||
|
|
cf0122ce23 | ||
|
|
7216a25877 | ||
|
|
89eae0807e | ||
|
|
bfc52fbc6b | ||
|
|
94cd5d3395 | ||
|
|
30d34584bf | ||
|
|
c81166d04b | ||
|
|
3384a8630b | ||
|
|
e39c7c4e98 | ||
|
|
38e838a352 | ||
|
|
cea22c0ea1 | ||
|
|
6b73361963 | ||
|
|
096a4ee63e | ||
|
|
28c8d6bef9 | ||
|
|
37fdbfe12a | ||
|
|
295cb84cc8 | ||
|
|
b5252516e3 | ||
|
|
8e2c0327d6 | ||
|
|
eaa92b75a3 | ||
|
|
ab81c89a9a | ||
|
|
e49af151c1 | ||
|
|
78ca5cbb43 | ||
|
|
48c242d59d | ||
|
|
8b4603d5a1 | ||
|
|
cc6dce8845 | ||
|
|
b2eac8092c | ||
|
|
a22362bac8 | ||
|
|
298b283686 | ||
|
|
14f4de2e3f | ||
|
|
9bde4c40b0 | ||
|
|
61390cb64f | ||
|
|
1b67bc41f5 | ||
|
|
5b698b816e | ||
|
|
bf9303ea80 | ||
|
|
017676c148 | ||
|
|
526f7700b2 | ||
|
|
220e01106f | ||
|
|
240de5b24d | ||
|
|
17a001427b | ||
|
|
a423970b9c | ||
|
|
56f4c8775f | ||
|
|
5ed4aa0fae | ||
|
|
4f4245b895 | ||
|
|
bc6d2112e2 | ||
|
|
3b2b87d264 | ||
|
|
181646f052 | ||
|
|
ea5a945810 | ||
|
|
7cfdc8ab1b | ||
|
|
52e011ade9 | ||
|
|
895e2ede2d | ||
|
|
3a25769a94 | ||
|
|
5db4b48a86 | ||
|
|
aa2899bfbd | ||
|
|
40bbd87c4e | ||
|
|
fb7dd66600 | ||
|
|
e12dc2b089 | ||
|
|
78242b18c3 | ||
|
|
9043e4c8e1 | ||
|
|
54b5e3a0cc | ||
|
|
b6719a2f57 | ||
|
|
087d5fbb68 | ||
|
|
4f60c91256 | ||
|
|
99415ef290 | ||
|
|
d446a2d047 | ||
|
|
b476f738bc | ||
|
|
4c0d1383b2 | ||
|
|
6a924bf732 | ||
|
|
5e1f1220af | ||
|
|
d13ee0a2cc | ||
|
|
826127672a | ||
|
|
df8dd7278a | ||
|
|
cbdc4a3e9d | ||
|
|
75266ad257 | ||
|
|
0e93374b2d | ||
|
|
8e350617f2 | ||
|
|
f6acf0a60a | ||
|
|
7982f25a45 | ||
|
|
ddafa7aac1 | ||
|
|
65946c3045 | ||
|
|
63d699d620 | ||
|
|
e1170c0552 | ||
|
|
97612bf044 | ||
|
|
012ac33bd5 | ||
|
|
272e208fd4 | ||
|
|
714f0623bb | ||
|
|
5327962409 | ||
|
|
7c3529ae30 | ||
|
|
61c63df9e9 | ||
|
|
9b6fcaeb79 | ||
|
|
ef3f035f27 | ||
|
|
e9bc67fbf4 | ||
|
|
745f2aecf8 | ||
|
|
eff80ad5ff | ||
|
|
27ef44b833 | ||
|
|
9d0190c11c | ||
|
|
0069687233 | ||
|
|
478033d405 | ||
|
|
58f57d5ad2 | ||
|
|
3f80230f4f | ||
|
|
da35f0ea16 | ||
|
|
72bdd5bc05 | ||
|
|
255cc620a5 | ||
|
|
4753271a6d | ||
|
|
11c7024edd | ||
|
|
47ba54199f | ||
|
|
cd7643e79b | ||
|
|
9a797e39e5 | ||
|
|
7898840003 | ||
|
|
8e91cb6d54 | ||
|
|
893401f7c4 | ||
|
|
021e6ffeb5 | ||
|
|
4080c1e005 | ||
|
|
5063e99761 | ||
|
|
a59f6546c5 | ||
|
|
3a737099ad | ||
|
|
d1f76fd48e | ||
|
|
437d52e484 | ||
|
|
610977cc08 | ||
|
|
6c78adb353 | ||
|
|
abef75bfa2 | ||
|
|
8137d4acdb | ||
|
|
899a186808 | ||
|
|
bb7d3e075c | ||
|
|
78ba9df263 | ||
|
|
9126416389 | ||
|
|
eef944b617 | ||
|
|
208cc2e48e | ||
|
|
1522bf4ae4 | ||
|
|
422f4567f1 | ||
|
|
6c8ae32ff9 | ||
|
|
e67c610b84 | ||
|
|
96aa003702 | ||
|
|
f6262f4be5 | ||
|
|
efc5bf6a45 | ||
|
|
cbc9801477 | ||
|
|
94e21e8a3a | ||
|
|
2ed118604c | ||
|
|
219d4f4d33 | ||
|
|
a937e3c57d | ||
|
|
9fe16e321e | ||
|
|
697e9986b4 | ||
|
|
613cdc3145 | ||
|
|
a2bf489017 | ||
|
|
aca49b9ba5 | ||
|
|
11d5e301f3 | ||
|
|
2797b451fb | ||
|
|
e82918db6a | ||
|
|
e0b98f70f0 | ||
|
|
297b9bd3ff | ||
|
|
013a59857d | ||
|
|
0b14381255 | ||
|
|
e1e0b0f2da | ||
|
|
2142290300 | ||
|
|
ee886244fc | ||
|
|
f2557fe80a | ||
|
|
bf904a5b32 | ||
|
|
1ff83d9648 | ||
|
|
6c27deed74 | ||
|
|
7e2d15f329 | ||
|
|
10bd7853f3 | ||
|
|
4a7bfe953c | ||
|
|
3598d13a3d | ||
|
|
96ac561279 | ||
|
|
2c81267ca7 | ||
|
|
395fa5dad1 | ||
|
|
d15340e62e | ||
|
|
eab4c08836 | ||
|
|
69c5bf0094 | ||
|
|
6c112f83e8 | ||
|
|
f49c34c551 | ||
|
|
ad3c07c634 | ||
|
|
68525c2109 | ||
|
|
9e9851472d | ||
|
|
c0edb27323 | ||
|
|
0a70783b5e | ||
|
|
5fcb495d24 | ||
|
|
5e6eb7ed49 | ||
|
|
2a9f6c88a0 |
113
.eslintrc.json
113
.eslintrc.json
@@ -3,60 +3,21 @@
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["tsconfig.json", "tsconfig.servers.json"],
|
||||
"sourceType": "module"
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-types": "warn",
|
||||
"@typescript-eslint/class-name-casing": "off",
|
||||
"indent": "off",
|
||||
"@typescript-eslint/indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"off",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": [
|
||||
"off",
|
||||
null
|
||||
],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"arrow-parens": [
|
||||
"off",
|
||||
"as-needed"
|
||||
],
|
||||
"brace-style": "off",
|
||||
"@typescript-eslint/brace-style": [
|
||||
"error",
|
||||
"1tbs", { "allowSingleLine": true }
|
||||
],
|
||||
"comma-spacing": "off",
|
||||
"@typescript-eslint/comma-spacing": "error",
|
||||
"space-infix-ops": "error",
|
||||
"comma-dangle": "off",
|
||||
"eqeqeq": [
|
||||
@@ -70,6 +31,66 @@
|
||||
"no-unsafe-finally": "warn",
|
||||
"no-var": "error",
|
||||
"spaced-comment": "error",
|
||||
"semi": "warn"
|
||||
}
|
||||
"semi": "warn",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "ExportDefaultDeclaration",
|
||||
"message": "Default exports are not allowed"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["tsconfig.json", "tsconfig.commonjs.json"],
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/class-name-casing": "off",
|
||||
"@typescript-eslint/indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"off",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": [
|
||||
"off",
|
||||
null
|
||||
],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/brace-style": [
|
||||
"error",
|
||||
"1tbs", { "allowSingleLine": true }
|
||||
],
|
||||
"@typescript-eslint/comma-spacing": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ node_modules/
|
||||
debug.log
|
||||
npm-debug.log
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.commonjs.tsbuildinfo
|
||||
|
||||
*.sublime-workspace
|
||||
.idea
|
||||
|
||||
@@ -1 +1 @@
|
||||
tsconfig.servers.buildinfo
|
||||
tsconfig.commonjs.buildinfo
|
||||
@@ -14,6 +14,5 @@ before_install:
|
||||
node_js:
|
||||
- "12"
|
||||
- "10"
|
||||
- "8"
|
||||
before_script:
|
||||
- export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start
|
||||
26
README.md
26
README.md
@@ -41,7 +41,7 @@ Moreover, the project contains the implementation of `servers`, including
|
||||
- `servers/volume` A tool for accessing volumetric experimental data related to molecular structures.
|
||||
- `servers/plugin-state` A basic server to store Mol* Plugin states.
|
||||
|
||||
The project also contains performance tests (`perf-tests`), `examples`, and basic proof of concept `apps` (CIF to BinaryCIF converter and JSON domain annotation to CIF converter).
|
||||
The project also contains performance tests (`perf-tests`), `examples`, and basic proof of concept `cli` apps (CIF to BinaryCIF converter and JSON domain annotation to CIF converter).
|
||||
|
||||
## Previous Work
|
||||
This project builds on experience from previous solutions:
|
||||
@@ -90,19 +90,28 @@ and navigate to `build/viewer`
|
||||
### Code generation
|
||||
**CIF schemas**
|
||||
|
||||
node ./lib/apps/cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/mmcif.ts -p mmCIF
|
||||
node ./lib/apps/cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/ccd.ts -p CCD
|
||||
node ./lib/apps/cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/bird.ts -p BIRD
|
||||
node ./lib/apps/cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/cif-core.ts -p CifCore -aa
|
||||
node ./lib/commonjs/cli/cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/mmcif.ts -p mmCIF
|
||||
node ./lib/commonjs/cli/cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/ccd.ts -p CCD
|
||||
node ./lib/commonjs/cli/cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/bird.ts -p BIRD
|
||||
node ./lib/commonjs/cli/cifschema -mip ../../../../mol-data -o src/mol-io/reader/cif/schema/cif-core.ts -p CifCore -aa
|
||||
|
||||
**Lipid names**
|
||||
|
||||
node lib/commonjs/cli/lipid-params -o src/mol-model/structure/model/types/lipids.ts
|
||||
|
||||
**Ion names**
|
||||
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-ions.js src/mol-model/structure/model/types/ions.ts
|
||||
|
||||
|
||||
**GraphQL schemas**
|
||||
|
||||
./node_modules/.bin/graphql-codegen -c ./src/extensions/rcsb/graphql/codegen.yml
|
||||
node node_modules//@graphql-codegen/cli/bin -c src/extensions/rcsb/graphql/codegen.yml
|
||||
|
||||
### Other scripts
|
||||
**Create chem comp bond table**
|
||||
|
||||
node --max-old-space-size=4096 lib/apps/chem-comp-bond/create-table.js build/data/ccb.bcif -b
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-table.js build/data/ccb.bcif -b
|
||||
|
||||
**Test model server**
|
||||
|
||||
@@ -120,7 +129,7 @@ To see all available commands, use ``node lib/servers/model/preprocess -h``.
|
||||
|
||||
Or
|
||||
|
||||
node ./lib/apps/cif2bcif
|
||||
node lib/commonjs/cli/cif2bcif
|
||||
|
||||
## Development
|
||||
|
||||
@@ -168,3 +177,4 @@ Funding sources include but are not limited to:
|
||||
* [RCSB PDB](https://www.rcsb.org) funding by a grant [DBI-1338415; PI: SK Burley] from the NSF, the NIH, and the US DoE
|
||||
* [PDBe, EMBL-EBI](https://pdbe.org)
|
||||
* [CEITEC](https://www.ceitec.eu/)
|
||||
* [EntosAI](https://www.entos.ai)
|
||||
|
||||
29
docs/interesting-pdb-entries.md
Normal file
29
docs/interesting-pdb-entries.md
Normal file
@@ -0,0 +1,29 @@
|
||||
* Cyclic polymers (1sfi, 6dny, 1HVZ)
|
||||
* B-DNA (1bna)
|
||||
* Missing carbonyl oxygen (1gfl)
|
||||
* Mono-saccharides with alt locs (1B5F)
|
||||
* Microheterogeneity
|
||||
* Protein (1EJG, 3NIR)
|
||||
* DNA (3VOK)
|
||||
* PNA: peptide nucleic acid (5eme, 1xj9)
|
||||
* Peptide derived residues
|
||||
* GFP chromophores (5Z6Y)
|
||||
* Nucleotides that don’t have a parent base set, i.e. detect purine/pyrimidine from geometry (THX in 1AUL, OMC in e.g. 5D3G)
|
||||
* Bases with modified ring atoms
|
||||
* 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)
|
||||
* Mixed (heterogeneous) all-atom/trace-only RNA model (1JGQ)
|
||||
* Polymers with residues with missing trace atoms (e.g. 2QFJ)
|
||||
* Modified RNA bases (1y26, 5L4O)
|
||||
* Discontinuous chains, i.e. gaps in the sequence (3sn6)
|
||||
* Lots of sheets (1cbs)
|
||||
* DNA (2np2, 1d66)
|
||||
* C-alpha only (2rcj)
|
||||
* Not cyclic, but termini are backbone-only and within distance but seqIds are not compatible (6SW3)
|
||||
* Close backbone atoms but not linked (e.g. 4HIV)
|
||||
* Non-standard residues
|
||||
* Protein (1BRR, 5Z6Y)
|
||||
* DNA (5D3G)
|
||||
* Multiple models with different sets of ligands or missing ligands (1J6T, 1VRC, 2ICY, 1O2F)
|
||||
* Long linear sugar chain (4HG6)
|
||||
@@ -24,7 +24,7 @@ npm run build-tsc
|
||||
and run the server by
|
||||
|
||||
```
|
||||
node lib/servers/model/server/server
|
||||
node lib/commonjs/servers/model/server/server
|
||||
```
|
||||
|
||||
## From NPM
|
||||
@@ -54,12 +54,12 @@ Sometimes nodejs might run into problems with memory. This is usually resolved b
|
||||
|
||||
## Preprocessor
|
||||
|
||||
The preprocessor application allows to add custom data to CIF files and/or convert CIF to BinaryCIF. ``node lib/servers/servers/model/preprocess`` or ``model-server-preprocess`` binary from the NPM package.
|
||||
The preprocessor application allows to add custom data to CIF files and/or convert CIF to BinaryCIF. ``node lib/commonjs/servers/model/preprocess`` or ``model-server-preprocess`` binary from the NPM package.
|
||||
|
||||
|
||||
## Local Mode
|
||||
|
||||
The server can be run in local/file based mode using ``node lib/servers/servers/model/query`` (``model-server-query`` binary from the NPM package).
|
||||
The server can be run in local/file based mode using ``node lib/commonjs/servers/model/query`` (``model-server-query`` binary from the NPM package).
|
||||
|
||||
Custom Properties
|
||||
=================
|
||||
|
||||
@@ -28,7 +28,7 @@ npm run build-tsc
|
||||
and run the server by
|
||||
|
||||
```
|
||||
node lib/servers/servers/volume/server
|
||||
node lib/commonjs/servers/volume/server
|
||||
```
|
||||
|
||||
## From NPM
|
||||
@@ -60,11 +60,11 @@ Sometimes nodejs might run into problems with memory. This is usually resolved b
|
||||
## Preparing the Data
|
||||
|
||||
For the server to work, CCP4/MAP (models 0, 1, 2 are supported) input data need to be converted into a custom block format.
|
||||
To achieve this, use the ``pack`` application (``node lib/servers/servers/volume/pack`` or ``volume-server-pack`` binary from the NPM package).
|
||||
To achieve this, use the ``pack`` application (``node lib/commonjs/servers/volume/pack`` or ``volume-server-pack`` binary from the NPM package).
|
||||
|
||||
## Local Mode
|
||||
|
||||
The program ``lib/servers/servers/volume/pack`` (``volume-server-query`` in NPM package) can be used to query the data without running a http server.
|
||||
The program ``lib/commonjs/servers/volume/pack`` (``volume-server-query`` in NPM package) can be used to query the data without running a http server.
|
||||
|
||||
## Navigating the Source Code
|
||||
|
||||
|
||||
47372
package-lock.json
generated
47372
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
126
package.json
126
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "0.7.1-dev.13",
|
||||
"version": "2.0.0-dev.6",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -11,12 +11,12 @@
|
||||
"url": "https://github.com/molstar/molstar/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint ./**/*.{ts,tsx}",
|
||||
"lint-fix": "eslint ./**/*.{ts,tsx} --fix",
|
||||
"lint": "eslint .",
|
||||
"lint-fix": "eslint . --fix",
|
||||
"test": "npm run lint && jest",
|
||||
"build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
|
||||
"build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer",
|
||||
"build-tsc": "tsc --incremental && tsc --build tsconfig.servers.json --incremental",
|
||||
"build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"",
|
||||
"build-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/",
|
||||
"build-webpack": "webpack --mode production --config ./webpack.config.production.js",
|
||||
"build-webpack-viewer": "webpack --mode production --config ./webpack.config.viewer.js",
|
||||
@@ -24,16 +24,16 @@
|
||||
"watch-viewer": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer\"",
|
||||
"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.servers.json --watch --incremental",
|
||||
"watch-servers": "tsc --build tsconfig.commonjs.json --watch --incremental",
|
||||
"watch-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/ --watch",
|
||||
"watch-webpack": "webpack -w --mode development --display minimal",
|
||||
"watch-webpack-viewer": "webpack -w --mode development --display errors-only --info-verbosity verbose --config ./webpack.config.viewer.js",
|
||||
"watch-webpack-viewer-debug": "webpack -w --mode development --display errors-only --info-verbosity verbose --config ./webpack.config.viewer.debug.js",
|
||||
"serve": "http-server -p 1338",
|
||||
"model-server": "node lib/servers/servers/model/server.js",
|
||||
"model-server-watch": "nodemon --watch lib lib/servers/servers/model/server.js",
|
||||
"volume-server-test": "node lib/servers/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort 1336",
|
||||
"plugin-state": "node lib/servers/servers/plugin-state/index.js",
|
||||
"serve": "http-server -p 1338 -g",
|
||||
"model-server": "node lib/commonjs/servers/model/server.js",
|
||||
"model-server-watch": "nodemon --watch lib lib/commonjs/servers/model/server.js",
|
||||
"volume-server-test": "node lib/commonjs/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort 1336",
|
||||
"plugin-state": "node lib/commonjs/servers/plugin-state/index.js --working-folder ./build/state --port 1339",
|
||||
"preversion": "npm run test",
|
||||
"version": "npm run build",
|
||||
"postversion": "git push && git push --tags"
|
||||
@@ -43,14 +43,14 @@
|
||||
"build/viewer/"
|
||||
],
|
||||
"bin": {
|
||||
"cif2bcif": "lib/apps/cif2bcif/index.js",
|
||||
"cifschema": "lib/apps/cifschema/index.js",
|
||||
"model-server": "lib/servers/servers/model/server.js",
|
||||
"model-server-query": "lib/servers/servers/model/query.js",
|
||||
"model-server-preprocess": "lib/servers/servers/model/preprocess.js",
|
||||
"volume-server": "lib/servers/servers/volume/server.js",
|
||||
"volume-server-query": "lib/servers/servers/volume/query.js",
|
||||
"volume-server-pack": "lib/servers/servers/volume/pack.js"
|
||||
"cif2bcif": "lib/commonjs/cli/cif2bcif/index.js",
|
||||
"cifschema": "lib/commonjs/cli/cifschema/index.js",
|
||||
"model-server": "lib/commonjs/servers/model/server.js",
|
||||
"model-server-query": "lib/commonjs/servers/model/query.js",
|
||||
"model-server-preprocess": "lib/commonjs/servers/model/preprocess.js",
|
||||
"volume-server": "lib/commonjs/servers/volume/server.js",
|
||||
"volume-server-query": "lib/commonjs/servers/volume/query.js",
|
||||
"volume-server-pack": "lib/commonjs/servers/volume/pack.js"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignoreRoot": [
|
||||
@@ -80,67 +80,71 @@
|
||||
"Alexander Rose <alexander.rose@weirdbyte.de>",
|
||||
"David Sehnal <david.sehnal@gmail.com>",
|
||||
"Sebastian Bittrich <sebastian.bittrich@rcsb.org>",
|
||||
"Ludovic Autin <autin@scripps.edu>"
|
||||
"Á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>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/add": "^1.13.5",
|
||||
"@graphql-codegen/cli": "^1.13.5",
|
||||
"@graphql-codegen/time": "^1.13.5",
|
||||
"@graphql-codegen/typescript": "^1.13.5",
|
||||
"@graphql-codegen/typescript-graphql-files-modules": "^1.13.5",
|
||||
"@graphql-codegen/typescript-graphql-request": "^1.13.5",
|
||||
"@graphql-codegen/typescript-operations": "^1.13.5",
|
||||
"@types/cors": "^2.8.6",
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
"@graphql-codegen/add": "^2.0.2",
|
||||
"@graphql-codegen/cli": "^1.19.4",
|
||||
"@graphql-codegen/time": "^2.0.2",
|
||||
"@graphql-codegen/typescript": "^1.19.0",
|
||||
"@graphql-codegen/typescript-graphql-files-modules": "^1.18.1",
|
||||
"@graphql-codegen/typescript-graphql-request": "^2.0.3",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.12",
|
||||
"@types/cors": "^2.8.8",
|
||||
"@typescript-eslint/eslint-plugin": "^4.9.1",
|
||||
"@typescript-eslint/parser": "^4.9.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^5.2.0",
|
||||
"cpx2": "^2.0.0",
|
||||
"css-loader": "^3.5.3",
|
||||
"eslint": "^7.0.0",
|
||||
"concurrently": "^5.3.0",
|
||||
"cpx2": "^3.0.0",
|
||||
"css-loader": "^5.0.1",
|
||||
"eslint": "^7.15.0",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"file-loader": "^6.0.0",
|
||||
"fs-extra": "^9.0.0",
|
||||
"graphql": "^15.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"graphql": "^15.4.0",
|
||||
"http-server": "^0.12.3",
|
||||
"jest": "^26.0.1",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"raw-loader": "^4.0.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"simple-git": "^2.4.0",
|
||||
"style-loader": "^1.2.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^3.9.2",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"jest": "^26.6.3",
|
||||
"mini-css-extract-plugin": "^1.3.2",
|
||||
"node-sass": "^5.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass-loader": "^10.1.0",
|
||||
"simple-git": "^2.25.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"ts-jest": "^26.4.4",
|
||||
"typescript": "^4.2.3",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-version-file-plugin": "^0.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^1.0.38",
|
||||
"@types/benchmark": "^1.0.33",
|
||||
"@types/benchmark": "^2.1.0",
|
||||
"@types/compression": "1.7.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/node": "^14.0.1",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/jest": "^26.0.18",
|
||||
"@types/node": "^14.14.11",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/swagger-ui-dist": "3.0.5",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/swagger-ui-dist": "3.30.0",
|
||||
"argparse": "^1.0.10",
|
||||
"body-parser": "^1.19.0",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"immer": "^6.0.5",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^8.0.1",
|
||||
"immutable": "^3.8.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"rxjs": "^6.5.5",
|
||||
"swagger-ui-dist": "^3.25.3",
|
||||
"tslib": "^2.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"rxjs": "^6.6.6",
|
||||
"swagger-ui-dist": "^3.37.2",
|
||||
"tslib": "^2.1.0",
|
||||
"util.promisify": "^1.0.1",
|
||||
"xhr2": "^0.2.0"
|
||||
}
|
||||
|
||||
@@ -4,40 +4,40 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
const git = require('simple-git')
|
||||
const path = require('path')
|
||||
const fs = require("fs")
|
||||
const fse = require("fs-extra")
|
||||
const git = require('simple-git');
|
||||
const path = require('path');
|
||||
const fs = require("fs");
|
||||
const fse = require("fs-extra");
|
||||
|
||||
const remoteUrl = "https://github.com/molstar/molstar.github.io.git"
|
||||
const buildDir = path.resolve(__dirname, '../build/')
|
||||
const deployDir = path.resolve(buildDir, 'deploy/')
|
||||
const localPath = path.resolve(deployDir, 'molstar.github.io/')
|
||||
const remoteUrl = "https://github.com/molstar/molstar.github.io.git";
|
||||
const buildDir = path.resolve(__dirname, '../build/');
|
||||
const deployDir = path.resolve(buildDir, 'deploy/');
|
||||
const localPath = path.resolve(deployDir, 'molstar.github.io/');
|
||||
|
||||
function log(command, stdout, stderr) {
|
||||
if (command) {
|
||||
console.log('\n###', command)
|
||||
stdout.pipe(process.stdout)
|
||||
stderr.pipe(process.stderr)
|
||||
console.log('\n###', command);
|
||||
stdout.pipe(process.stdout);
|
||||
stderr.pipe(process.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
function copyViewer() {
|
||||
console.log('\n###', 'copy viewer files')
|
||||
const viewerBuildPath = path.resolve(buildDir, '../build/viewer/')
|
||||
const viewerDeployPath = path.resolve(localPath, 'viewer/')
|
||||
fse.copySync(viewerBuildPath, viewerDeployPath, { overwrite: true })
|
||||
console.log('\n###', 'copy viewer files');
|
||||
const viewerBuildPath = path.resolve(buildDir, '../build/viewer/');
|
||||
const viewerDeployPath = path.resolve(localPath, 'viewer/');
|
||||
fse.copySync(viewerBuildPath, viewerDeployPath, { overwrite: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(localPath)) {
|
||||
console.log('\n###', 'create localPath')
|
||||
fs.mkdirSync(localPath, { recursive: true })
|
||||
console.log('\n###', 'create localPath');
|
||||
fs.mkdirSync(localPath, { recursive: true });
|
||||
}
|
||||
|
||||
process.chdir(localPath);
|
||||
|
||||
if (!fs.existsSync(path.resolve(localPath, '.git/'))) {
|
||||
console.log('\n###', 'clone repository')
|
||||
console.log('\n###', 'clone repository');
|
||||
git()
|
||||
.outputHandler(log)
|
||||
.clone(remoteUrl, localPath)
|
||||
@@ -45,9 +45,9 @@ if (!fs.existsSync(path.resolve(localPath, '.git/'))) {
|
||||
.exec(copyViewer)
|
||||
.add(['-A'])
|
||||
.commit('updated viewer')
|
||||
.push()
|
||||
.push();
|
||||
} else {
|
||||
console.log('\n###', 'update repository')
|
||||
console.log('\n###', 'update repository');
|
||||
git()
|
||||
.outputHandler(log)
|
||||
.fetch(['--all'])
|
||||
@@ -55,5 +55,5 @@ if (!fs.existsSync(path.resolve(localPath, '.git/'))) {
|
||||
.exec(copyViewer)
|
||||
.add(['-A'])
|
||||
.commit('updated viewer')
|
||||
.push()
|
||||
.push();
|
||||
}
|
||||
37
src/apps/docking-viewer/index.html
Normal file
37
src/apps/docking-viewer/index.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* Docking Viewer</title>
|
||||
<style>
|
||||
#app {
|
||||
position: absolute;
|
||||
left: 100px;
|
||||
top: 100px;
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="./molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
var viewer = new DockingViewer('app', [0x33DD22, 0x1133EE], true);
|
||||
|
||||
function getParam(name, regex) {
|
||||
var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
|
||||
return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
|
||||
}
|
||||
var pdbqt = getParam('pdbqt', '[^&]+').trim();
|
||||
var mol2 = getParam('mol2', '[^&]+').trim();
|
||||
|
||||
viewer.loadStructuresFromUrlsAndMerge([
|
||||
{ url: pdbqt, format: 'pdbqt' },
|
||||
{ url: mol2, format: 'mol2' }
|
||||
]);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
213
src/apps/docking-viewer/index.ts
Normal file
213
src/apps/docking-viewer/index.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 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 '../../mol-util/polyfill';
|
||||
import { createPlugin } from '../../mol-plugin';
|
||||
import { DefaultPluginSpec } from '../../mol-plugin/spec';
|
||||
import './index.html';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { PluginStateTransform, PluginStateObject as PSO } from '../../mol-plugin-state/objects';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Task } from '../../mol-task';
|
||||
import { StateObject } from '../../mol-state';
|
||||
import { ViewportComponent, StructurePreset, ShowButtons } from './viewport';
|
||||
import { PluginBehaviors } from '../../mol-plugin/behavior';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Color } from '../../mol-util/color';
|
||||
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { setProductionMode, setDebugMode } from '../../mol-util/debug';
|
||||
|
||||
const DefaultViewerOptions = {
|
||||
extensions: ObjectKeys({}),
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: true,
|
||||
layoutShowRemoteState: true,
|
||||
layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: true,
|
||||
layoutShowLeftPanel: true,
|
||||
|
||||
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,
|
||||
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
};
|
||||
|
||||
class Viewer {
|
||||
plugin: PluginContext
|
||||
|
||||
constructor(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,
|
||||
|
||||
viewportShowExpand: true,
|
||||
viewportShowControls: false,
|
||||
viewportShowSettings: false,
|
||||
viewportShowSelectionMode: false,
|
||||
viewportShowAnimation: false,
|
||||
} };
|
||||
const defaultSpec = DefaultPluginSpec();
|
||||
|
||||
const spec: PluginSpec = {
|
||||
actions: [...defaultSpec.actions],
|
||||
behaviors: [
|
||||
PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci, { mark: false }),
|
||||
PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
|
||||
PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci),
|
||||
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.StructureInfo),
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.Interactions),
|
||||
PluginSpec.Behavior(PluginBehaviors.CustomProps.SecondaryStructure),
|
||||
],
|
||||
animations: [...defaultSpec.animations || []],
|
||||
customParamEditors: defaultSpec.customParamEditors,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: o.layoutIsExpanded,
|
||||
showControls: o.layoutShowControls,
|
||||
controlsDisplay: o.layoutControlsDisplay,
|
||||
},
|
||||
controls: {
|
||||
...defaultSpec.layout && defaultSpec.layout.controls,
|
||||
top: o.layoutShowSequence ? undefined : 'none',
|
||||
bottom: o.layoutShowLog ? undefined : 'none',
|
||||
left: o.layoutShowLeftPanel ? undefined : 'none',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
...defaultSpec.components,
|
||||
remoteState: o.layoutShowRemoteState ? 'default' : 'none',
|
||||
viewport: {
|
||||
view: ViewportComponent
|
||||
}
|
||||
},
|
||||
config: [
|
||||
[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.Download.DefaultPdbProvider, o.pdbProvider],
|
||||
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
|
||||
[ShowButtons, showButtons]
|
||||
]
|
||||
};
|
||||
|
||||
const element = typeof elementOrId === 'string'
|
||||
? document.getElementById(elementOrId)
|
||||
: elementOrId;
|
||||
if (!element) throw new Error(`Could not get element with id '${elementOrId}'`);
|
||||
this.plugin = createPlugin(element, spec);
|
||||
|
||||
(this.plugin.customState as any) = {
|
||||
colorPalette: {
|
||||
name: 'colors',
|
||||
params: { list: { colors } }
|
||||
}
|
||||
};
|
||||
|
||||
this.plugin.behaviors.canvas3d.initialized.subscribe(v => {
|
||||
if (v) {
|
||||
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: {
|
||||
renderer: {
|
||||
...this.plugin.canvas3d!.props.renderer,
|
||||
backgroundColor: ColorNames.white,
|
||||
},
|
||||
camera: {
|
||||
...this.plugin.canvas3d!.props.camera,
|
||||
helper: { axes: { name: 'off', params: {} } }
|
||||
}
|
||||
} });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadStructuresFromUrlsAndMerge(sources: { url: string, format: BuiltInTrajectoryFormat, isBinary?: boolean }[]) {
|
||||
const structures: { ref: string }[] = [];
|
||||
for (const { url, format, isBinary } of sources) {
|
||||
const data = await this.plugin.builders.data.download({ url, isBinary });
|
||||
const trajectory = await this.plugin.builders.structure.parseTrajectory(data, format);
|
||||
const model = await this.plugin.builders.structure.createModel(trajectory);
|
||||
const modelProperties = await this.plugin.builders.structure.insertModelProperties(model);
|
||||
const structure = await this.plugin.builders.structure.createStructure(modelProperties || model);
|
||||
const structureProperties = await this.plugin.builders.structure.insertStructureProperties(structure);
|
||||
|
||||
structures.push({ ref: structureProperties?.ref || structure.ref });
|
||||
}
|
||||
|
||||
// remove current structuresfrom 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');
|
||||
|
||||
const dependsOn = structures.map(({ ref }) => ref);
|
||||
const data = this.plugin.state.data.build().toRoot().apply(MergeStructures, { structures }, { dependsOn });
|
||||
const structure = await data.commit();
|
||||
const structureProperties = await this.plugin.builders.structure.insertStructureProperties(structure);
|
||||
this.plugin.behaviors.canvas3d.initialized.subscribe(async v => {
|
||||
await this.plugin.builders.structure.representation.applyPreset(structureProperties || structure, StructurePreset);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type MergeStructures = typeof MergeStructures
|
||||
const MergeStructures = PluginStateTransform.BuiltIn({
|
||||
name: 'merge-structures',
|
||||
display: { name: 'Merge Structures', description: 'Merge Structure' },
|
||||
from: PSO.Root,
|
||||
to: PSO.Molecule.Structure,
|
||||
params: {
|
||||
structures: PD.ObjectList({
|
||||
ref: PD.Text('')
|
||||
}, ({ ref }) => ref, { isHidden: true })
|
||||
}
|
||||
})({
|
||||
apply({ params, dependencies }) {
|
||||
return Task.create('Merge Structures', async ctx => {
|
||||
if (params.structures.length === 0) return StateObject.Null;
|
||||
|
||||
const first = dependencies![params.structures[0].ref].data as Structure;
|
||||
const builder = Structure.Builder({ masterModel: first.models[0] });
|
||||
for (const { ref } of params.structures) {
|
||||
const s = dependencies![ref].data as Structure;
|
||||
for (const unit of s.units) {
|
||||
// TODO invariantId
|
||||
builder.addUnit(unit.kind, unit.model, unit.conformation.operator, unit.elements, unit.traits);
|
||||
}
|
||||
}
|
||||
|
||||
const structure = builder.getStructure();
|
||||
return new PSO.Molecule.Structure(structure, { label: 'Merged Structure' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
(window as any).DockingViewer = Viewer;
|
||||
export { Viewer as DockingViewer };
|
||||
276
src/apps/docking-viewer/viewport.tsx
Normal file
276
src/apps/docking-viewer/viewport.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { PluginUIComponent } from '../../mol-plugin-ui/base';
|
||||
import { Viewport, ViewportControls } from '../../mol-plugin-ui/viewport';
|
||||
import { BackgroundTaskProgress } from '../../mol-plugin-ui/task';
|
||||
import { LociLabels } from '../../mol-plugin-ui/controls';
|
||||
import { Toasts } from '../../mol-plugin-ui/toast';
|
||||
import { Button } from '../../mol-plugin-ui/controls/common';
|
||||
import { StructureRepresentationPresetProvider, presetStaticComponent } from '../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { StateObjectRef } from '../../mol-state';
|
||||
import { StructureSelectionQueries, StructureSelectionQuery } from '../../mol-plugin-state/helpers/structure-selection-query';
|
||||
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
|
||||
import { InteractionsRepresentationProvider } from '../../mol-model-props/computed/representations/interactions';
|
||||
import { InteractionTypeColorThemeProvider } from '../../mol-model-props/computed/themes/interaction-type';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StructureRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
|
||||
function shinyStyle(plugin: PluginContext) {
|
||||
return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
|
||||
renderer: {
|
||||
...plugin.canvas3d!.props.renderer,
|
||||
style: { name: 'plastic', params: {} },
|
||||
},
|
||||
postprocessing: {
|
||||
...plugin.canvas3d!.props.postprocessing,
|
||||
occlusion: { name: 'off', params: {} },
|
||||
outline: { name: 'off', params: {} }
|
||||
}
|
||||
} });
|
||||
}
|
||||
|
||||
function occlusionStyle(plugin: PluginContext) {
|
||||
return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
|
||||
renderer: {
|
||||
...plugin.canvas3d!.props.renderer,
|
||||
style: { name: 'flat', params: {} }
|
||||
},
|
||||
postprocessing: {
|
||||
...plugin.canvas3d!.props.postprocessing,
|
||||
occlusion: { name: 'on', params: {
|
||||
samples: 64,
|
||||
radius: 8,
|
||||
bias: 1.0,
|
||||
blurKernelSize: 13
|
||||
} },
|
||||
outline: { name: 'on', params: {
|
||||
scale: 1.0,
|
||||
threshold: 0.33
|
||||
} }
|
||||
}
|
||||
} });
|
||||
}
|
||||
|
||||
const ligandPlusSurroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of Ligand plus Ligand itself', MS.struct.modifier.union([
|
||||
MS.struct.modifier.includeSurroundings({
|
||||
0: StructureSelectionQueries.ligand.expression,
|
||||
radius: 5,
|
||||
'as-whole-residues': true
|
||||
})
|
||||
]));
|
||||
|
||||
const ligandSurroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of Ligand', MS.struct.modifier.union([
|
||||
MS.struct.modifier.exceptBy({
|
||||
0: ligandPlusSurroundings.expression,
|
||||
by: StructureSelectionQueries.ligand.expression
|
||||
})
|
||||
]));
|
||||
|
||||
const PresetParams = {
|
||||
...StructureRepresentationPresetProvider.CommonParams,
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const StructurePreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure',
|
||||
display: { name: 'Structure' },
|
||||
params: () => PresetParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
if (!structureCell) return {};
|
||||
|
||||
const components = {
|
||||
ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
|
||||
polymer: await presetStaticComponent(plugin, structureCell, 'polymer'),
|
||||
};
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.35 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams }, color: 'chain-id', colorParams: { palette: (plugin.customState as any).colorPalette } }, { tag: 'polymer' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
await shinyStyle(plugin);
|
||||
plugin.managers.interactivity.setProps({ granularity: 'residue' });
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
|
||||
export const IllustrativePreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-illustrative',
|
||||
display: { name: 'Illustrative' },
|
||||
params: () => PresetParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
if (!structureCell) return {};
|
||||
|
||||
const components = {
|
||||
ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
|
||||
polymer: await presetStaticComponent(plugin, structureCell, 'polymer'),
|
||||
};
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'spacefill', typeParams: { ...typeParams }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'spacefill', typeParams: { ...typeParams }, color: 'illustrative', colorParams: { palette: (plugin.customState as any).colorPalette } }, { tag: 'polymer' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
await occlusionStyle(plugin);
|
||||
plugin.managers.interactivity.setProps({ granularity: 'residue' });
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
|
||||
const SurfacePreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-surface',
|
||||
display: { name: 'Surface' },
|
||||
params: () => PresetParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
const components = {
|
||||
ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
|
||||
polymer: await presetStaticComponent(plugin, structureCell, 'polymer'),
|
||||
};
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'molecular-surface', typeParams: { ...typeParams, quality: 'custom', resolution: 0.5, doubleSided: true }, color: 'partial-charge' }, { tag: 'polymer' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
await shinyStyle(plugin);
|
||||
plugin.managers.interactivity.setProps({ granularity: 'residue' });
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
|
||||
const PocketPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-pocket',
|
||||
display: { name: 'Pocket' },
|
||||
params: () => PresetParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
const components = {
|
||||
ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
|
||||
surroundings: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandSurroundings, `surroundings`),
|
||||
};
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
|
||||
surroundings: builder.buildRepresentation(update, components.surroundings, { type: 'molecular-surface', typeParams: { ...typeParams, includeParent: true, quality: 'custom', resolution: 0.2, doubleSided: true }, color: 'partial-charge' }, { tag: 'surroundings' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
await shinyStyle(plugin);
|
||||
plugin.managers.interactivity.setProps({ granularity: 'element' });
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
|
||||
const InteractionsPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-interactions',
|
||||
display: { name: 'Interactions' },
|
||||
params: () => PresetParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
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`)
|
||||
};
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, 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, 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 }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
|
||||
label: builder.buildRepresentation(update, components.surroundings, { type: 'label', typeParams: { ...typeParams, background: false, borderWidth: 0.1 }, color: 'uniform', colorParams: { value: Color(0x000000) } }, { tag: 'label' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
await shinyStyle(plugin);
|
||||
plugin.managers.interactivity.setProps({ granularity: 'element' });
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
|
||||
export const ShowButtons = PluginConfig.item('showButtons', true);
|
||||
|
||||
export class ViewportComponent extends PluginUIComponent {
|
||||
async _set(structures: readonly StructureRef[], preset: StructureRepresentationPresetProvider) {
|
||||
await this.plugin.managers.structure.component.clear(structures);
|
||||
await this.plugin.managers.structure.component.applyPreset(structures, preset);
|
||||
}
|
||||
|
||||
set = async (preset: StructureRepresentationPresetProvider) => {
|
||||
await this._set(this.plugin.managers.structure.hierarchy.selection.structures, preset);
|
||||
}
|
||||
|
||||
structurePreset = () => this.set(StructurePreset);
|
||||
illustrativePreset = () => this.set(IllustrativePreset);
|
||||
surfacePreset = () => this.set(SurfacePreset);
|
||||
pocketPreset = () => this.set(PocketPreset);
|
||||
interactionsPreset = () => this.set(InteractionsPreset);
|
||||
|
||||
get showButtons () {
|
||||
return this.plugin.config.get(ShowButtons);
|
||||
}
|
||||
|
||||
render() {
|
||||
const VPControls = this.plugin.spec.components?.viewport?.controls || ViewportControls;
|
||||
|
||||
return <>
|
||||
<Viewport />
|
||||
{this.showButtons && <div className='msp-viewport-top-left-controls'>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<Button onClick={this.structurePreset} >Structure</Button>
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<Button onClick={this.illustrativePreset}>Illustrative</Button>
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<Button onClick={this.surfacePreset}>Surface</Button>
|
||||
</div>
|
||||
{/* <div style={{ marginBottom: '4px' }}>
|
||||
<Button onClick={this.pocketPreset}>Pocket</Button>
|
||||
</div> */}
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<Button onClick={this.interactionsPreset}>Interactions</Button>
|
||||
</div>
|
||||
</div>}
|
||||
<VPControls />
|
||||
<BackgroundTaskProgress />
|
||||
<div className='msp-highlight-toast-wrapper'>
|
||||
<LociLabels />
|
||||
<Toasts />
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
@@ -36,28 +36,8 @@
|
||||
emdbProvider: 'rcsb',
|
||||
});
|
||||
viewer.loadPdb('7bv2');
|
||||
viewer.loadEmdb('EMD-30210');
|
||||
|
||||
// TODO add Volume.customProperty and load suggested isoValue via custom property
|
||||
var sub = viewer.plugin.managers.volume.hierarchy.behaviors.selection.subscribe(function (value) {
|
||||
if (value.volume?.representations[0]) {
|
||||
var ref = value.volume.representations[0].cell;
|
||||
var tree = viewer.plugin.state.data.build().to(ref).update({
|
||||
type: {
|
||||
name: 'isosurface',
|
||||
params: {
|
||||
isoValue: {
|
||||
kind: 'relative',
|
||||
relativeValue: 6
|
||||
}
|
||||
}
|
||||
},
|
||||
colorTheme: ref.transform.params?.colorTheme
|
||||
});
|
||||
viewer.plugin.runTask(viewer.plugin.state.data.updateTree(tree));
|
||||
if (typeof sub !== 'undefined') sub.unsubscribe();
|
||||
}
|
||||
});
|
||||
viewer.loadEmdb('EMD-30210', { detail: 6 });
|
||||
// viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,12 +46,15 @@
|
||||
}
|
||||
|
||||
var debugMode = getParam('debug-mode', '[^&]+').trim() === '1';
|
||||
if (debugMode) molstar.setDebugMode(debugMode);
|
||||
if (debugMode) molstar.setDebugMode(debugMode, debugMode);
|
||||
|
||||
var hideControls = getParam('hide-controls', '[^&]+').trim() === '1';
|
||||
var pdbProvider = getParam('pdb-provider', '[^&]+').trim().toLowerCase();
|
||||
var emdbProvider = getParam('emdb-provider', '[^&]+').trim().toLowerCase();
|
||||
var viewer = new molstar.Viewer('app', {
|
||||
disableAntialiasing: disableAntialiasing,
|
||||
pixelScale: pixelScale,
|
||||
enableWboit: !disableWboit,
|
||||
layoutShowControls: !hideControls,
|
||||
viewportShowExpand: false,
|
||||
pdbProvider: pdbProvider || 'pdbe',
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
*/
|
||||
|
||||
import '../../mol-util/polyfill';
|
||||
import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
|
||||
import { createPlugin } from '../../mol-plugin';
|
||||
import { DefaultPluginSpec } from '../../mol-plugin/spec';
|
||||
import './index.html';
|
||||
import './embedded.html';
|
||||
import './favicon.ico';
|
||||
@@ -24,20 +25,41 @@ import { PluginState } from '../../mol-plugin/state';
|
||||
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
|
||||
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
|
||||
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
|
||||
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
|
||||
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
|
||||
import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
|
||||
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { setProductionMode, setDebugMode } 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)
|
||||
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
|
||||
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
|
||||
'g3d': PluginSpec.Behavior(G3DFormat),
|
||||
'mp4-export': PluginSpec.Behavior(Mp4Export)
|
||||
};
|
||||
|
||||
const DefaultViewerOptions = {
|
||||
customFormats: CustomFormats as [string, DataFormatProvider][],
|
||||
extensions: ObjectKeys(Extensions),
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: true,
|
||||
@@ -46,12 +68,18 @@ const DefaultViewerOptions = {
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: true,
|
||||
layoutShowLeftPanel: true,
|
||||
disableAntialiasing: false,
|
||||
pixelScale: 1,
|
||||
enableWboit: true,
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -62,15 +90,17 @@ export class Viewer {
|
||||
|
||||
constructor(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
|
||||
const o = { ...DefaultViewerOptions, ...options };
|
||||
const defaultSpec = DefaultPluginSpec();
|
||||
|
||||
const spec: PluginSpec = {
|
||||
actions: [...DefaultPluginSpec.actions],
|
||||
actions: [...defaultSpec.actions],
|
||||
behaviors: [
|
||||
...DefaultPluginSpec.behaviors,
|
||||
...defaultSpec.behaviors,
|
||||
...o.extensions.map(e => Extensions[e]),
|
||||
],
|
||||
animations: [...DefaultPluginSpec.animations || []],
|
||||
customParamEditors: DefaultPluginSpec.customParamEditors,
|
||||
animations: [...defaultSpec.animations || []],
|
||||
customParamEditors: defaultSpec.customParamEditors,
|
||||
customFormats: o?.customFormats,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: o.layoutIsExpanded,
|
||||
@@ -78,23 +108,29 @@ export class Viewer {
|
||||
controlsDisplay: o.layoutControlsDisplay,
|
||||
},
|
||||
controls: {
|
||||
...DefaultPluginSpec.layout && DefaultPluginSpec.layout.controls,
|
||||
...defaultSpec.layout && defaultSpec.layout.controls,
|
||||
top: o.layoutShowSequence ? undefined : 'none',
|
||||
bottom: o.layoutShowLog ? undefined : 'none',
|
||||
left: o.layoutShowLeftPanel ? undefined : 'none',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
...DefaultPluginSpec.components,
|
||||
...defaultSpec.components,
|
||||
remoteState: o.layoutShowRemoteState ? 'default' : 'none',
|
||||
},
|
||||
config: [
|
||||
[PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
|
||||
[PluginConfig.General.PixelScale, o.pixelScale],
|
||||
[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]
|
||||
]
|
||||
@@ -116,7 +152,7 @@ export class Viewer {
|
||||
return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
|
||||
}
|
||||
|
||||
loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false) {
|
||||
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: {
|
||||
@@ -125,19 +161,28 @@ export class Viewer {
|
||||
url: Asset.Url(url),
|
||||
format: format as any,
|
||||
isBinary,
|
||||
options: params.source.params.options,
|
||||
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) {
|
||||
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, {
|
||||
@@ -151,7 +196,7 @@ export class Viewer {
|
||||
params: PdbDownloadProvider[provider].defaultValue as any
|
||||
}
|
||||
},
|
||||
options: params.source.params.options,
|
||||
options: { ...params.source.params.options, representationParams: options?.representationParams as any },
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -173,7 +218,7 @@ export class Viewer {
|
||||
}));
|
||||
}
|
||||
|
||||
loadEmdb(emdb: string) {
|
||||
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: {
|
||||
@@ -183,9 +228,52 @@ export class Viewer {
|
||||
id: emdb,
|
||||
server: provider,
|
||||
},
|
||||
detail: 3,
|
||||
detail: options?.detail ?? 3,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async loadVolumeFromUrl(url: string, format: BuildInVolumeFormat, isBinary: boolean, isovalues: VolumeIsovalueInfo[], entryId?: string) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
if (!plugin.dataFormats.get(format)) {
|
||||
throw new Error(`Unknown density format: ${format}`);
|
||||
}
|
||||
|
||||
return plugin.dataTransaction(async () => {
|
||||
const data = await plugin.builders.data.download({ url, isBinary, label: entryId }, { state: { isGhost: true } });
|
||||
|
||||
const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId });
|
||||
const volume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
|
||||
if (!volume?.isOk) throw new Error('Failed to parse any volume.');
|
||||
|
||||
const repr = plugin.build().to(volume);
|
||||
for (const iso of isovalues) {
|
||||
repr.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, volume.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();
|
||||
});
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.plugin.layout.events.updated.next();
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadStructureOptions {
|
||||
representationParams?: StructureRepresentationPresetProvider.CommonParams
|
||||
}
|
||||
|
||||
export interface VolumeIsovalueInfo {
|
||||
type: 'absolute' | 'relative',
|
||||
value: number,
|
||||
color: Color,
|
||||
alpha?: number
|
||||
}
|
||||
73
src/cli/chem-comp-dict/create-ions.ts
Normal file
73
src/cli/chem-comp-dict/create-ions.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Josh McMenemy <josh.mcmenemy@gmail.com>
|
||||
*/
|
||||
|
||||
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 { ensureDataAvailable, readCCD } from './util';
|
||||
|
||||
function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
|
||||
const ionNames: string[] = [];
|
||||
for (const k in ccd) {
|
||||
const {chem_comp} = ccd[k];
|
||||
if (chem_comp.name.value(0).toUpperCase().includes(' ION')) {
|
||||
ionNames.push(chem_comp.id.value(0));
|
||||
}
|
||||
}
|
||||
// these are extra ions that don't have ION in their name
|
||||
ionNames.push('NCO', 'OHX');
|
||||
return ionNames;
|
||||
}
|
||||
|
||||
function writeIonNamesFile(filePath: string, ionNames: string[]) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2020 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
|
||||
*/
|
||||
|
||||
export const IonNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
writeFile(filePath, output);
|
||||
}
|
||||
|
||||
async function run(out: string, forceDownload = false) {
|
||||
await ensureDataAvailable(forceDownload);
|
||||
const ccd = await readCCD();
|
||||
const ionNames = extractIonNames(ccd);
|
||||
if (!fs.existsSync(path.dirname(out))) {
|
||||
fs.mkdirSync(path.dirname(out));
|
||||
}
|
||||
writeIonNamesFile(out, ionNames);
|
||||
}
|
||||
|
||||
const parser = new argparse.ArgumentParser({
|
||||
addHelp: true,
|
||||
description: 'Extract and save IonNames from CCD.'
|
||||
});
|
||||
parser.addArgument('out', {
|
||||
help: 'Generated file output path.'
|
||||
});
|
||||
parser.addArgument([ '--forceDownload', '-f' ], {
|
||||
action: 'storeTrue',
|
||||
help: 'Force download of CCD and PVCD.'
|
||||
});
|
||||
interface Args {
|
||||
out: string,
|
||||
forceDownload?: boolean,
|
||||
}
|
||||
const args: Args = parser.parseArgs();
|
||||
|
||||
run(args.out, args.forceDownload);
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -8,69 +9,16 @@ import * as argparse from 'argparse';
|
||||
import * as util from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import fetch from 'node-fetch';
|
||||
require('util.promisify').shim();
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
|
||||
import { Progress } from '../../mol-task';
|
||||
import { Database, Table, DatabaseCollection } from '../../mol-data/db';
|
||||
import { CIF } from '../../mol-io/reader/cif';
|
||||
import { CifWriter } from '../../mol-io/writer/cif';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
import { SetUtils } from '../../mol-util/set';
|
||||
import { DefaultMap } from '../../mol-util/map';
|
||||
import { mmCIF_chemCompBond_schema } from '../../mol-io/reader/cif/schema/mmcif-extras';
|
||||
|
||||
export async function ensureAvailable(path: string, url: string) {
|
||||
if (FORCE_DOWNLOAD || !fs.existsSync(path)) {
|
||||
console.log(`downloading ${url}...`);
|
||||
const data = await fetch(url);
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR);
|
||||
}
|
||||
if (url.endsWith('.gz')) {
|
||||
await writeFile(path, zlib.gunzipSync(await data.buffer()));
|
||||
} else {
|
||||
await writeFile(path, await data.text());
|
||||
}
|
||||
console.log(`done downloading ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureDataAvailable() {
|
||||
await ensureAvailable(CCD_PATH, CCD_URL);
|
||||
await ensureAvailable(PVCD_PATH, PVCD_URL);
|
||||
}
|
||||
|
||||
export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
|
||||
const parsed = await parseCif(await readFile(path, 'utf8'));
|
||||
return CIF.toDatabaseCollection(schema, parsed.result);
|
||||
}
|
||||
|
||||
export async function readCCD() {
|
||||
return readFileAsCollection(CCD_PATH, CCD_Schema);
|
||||
}
|
||||
|
||||
export async function readPVCD() {
|
||||
return readFileAsCollection(PVCD_PATH, CCD_Schema);
|
||||
}
|
||||
|
||||
async function parseCif(data: string | Uint8Array) {
|
||||
const comp = CIF.parse(data);
|
||||
console.time('parse cif');
|
||||
const parsed = await comp.run(p => console.log(Progress.format(p)), 250);
|
||||
console.timeEnd('parse cif');
|
||||
if (parsed.isError) throw parsed;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function getEncodedCif(name: string, database: Database<Database.Schema>, binary = false) {
|
||||
const encoder = CifWriter.createEncoder({ binary, encoderName: 'mol*' });
|
||||
CifWriter.Encoder.writeDatabase(encoder, name, database);
|
||||
return encoder.getData();
|
||||
}
|
||||
import { ccd_chemCompAtom_schema } from '../../mol-io/reader/cif/schema/ccd-extras';
|
||||
import { ensureDataAvailable, getEncodedCif, readCCD, readPVCD } from './util';
|
||||
|
||||
type CCB = Table<CCD_Schema['chem_comp_bond']>
|
||||
type CCA = Table<CCD_Schema['chem_comp_atom']>
|
||||
@@ -79,6 +27,10 @@ function ccbKey(compId: string, atomId1: string, atomId2: string) {
|
||||
return atomId1 < atomId2 ? `${compId}:${atomId1}-${atomId2}` : `${compId}:${atomId2}-${atomId1}`;
|
||||
}
|
||||
|
||||
function ccaKey(compId: string, atomId: string) {
|
||||
return `${compId}:${atomId}`;
|
||||
}
|
||||
|
||||
function addChemCompBondToSet(set: Set<string>, ccb: CCB) {
|
||||
for (let i = 0, il = ccb._rowCount; i < il; ++i) {
|
||||
set.add(ccbKey(ccb.comp_id.value(i), ccb.atom_id_1.value(i), ccb.atom_id_2.value(i)));
|
||||
@@ -88,7 +40,7 @@ function addChemCompBondToSet(set: Set<string>, ccb: CCB) {
|
||||
|
||||
function addChemCompAtomToSet(set: Set<string>, cca: CCA) {
|
||||
for (let i = 0, il = cca._rowCount; i < il; ++i) {
|
||||
set.add(cca.atom_id.value(i));
|
||||
set.add(ccaKey(cca.comp_id.value(i), cca.atom_id.value(i)));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
@@ -134,11 +86,32 @@ function checkAddingBondsFromPVCD(pvcd: DatabaseCollection<CCD_Schema>) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createBonds() {
|
||||
await ensureDataAvailable();
|
||||
const ccd = await readCCD();
|
||||
const pvcd = await readPVCD();
|
||||
function checkAddingAtomsFromPVCD(pvcd: DatabaseCollection<CCD_Schema>) {
|
||||
const ccaSetByParent = DefaultMap<string, Set<string>>(() => new Set());
|
||||
|
||||
for (const k in pvcd) {
|
||||
const { chem_comp, chem_comp_atom } = pvcd[k];
|
||||
if (chem_comp_atom._rowCount) {
|
||||
const parentIds = chem_comp.mon_nstd_parent_comp_id.value(0);
|
||||
if (parentIds.length === 0) {
|
||||
const set = ccaSetByParent.getDefault(chem_comp.id.value(0));
|
||||
addChemCompAtomToSet(set, chem_comp_atom);
|
||||
} else {
|
||||
for (let i = 0, il = parentIds.length; i < il; ++i) {
|
||||
const parentId = parentIds[i];
|
||||
const set = ccaSetByParent.getDefault(parentId);
|
||||
addChemCompAtomToSet(set, chem_comp_atom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createBonds(
|
||||
ccd: DatabaseCollection<CCD_Schema>,
|
||||
pvcd: DatabaseCollection<CCD_Schema>,
|
||||
atomsRequested: boolean
|
||||
) {
|
||||
const ccbSet = new Set<string>();
|
||||
|
||||
const comp_id: string[] = [];
|
||||
@@ -199,31 +172,97 @@ async function createBonds() {
|
||||
});
|
||||
|
||||
const bondDatabase = Database.ofTables(
|
||||
TABLE_NAME,
|
||||
CCB_TABLE_NAME,
|
||||
{ chem_comp_bond: mmCIF_chemCompBond_schema },
|
||||
{ chem_comp_bond: bondTable }
|
||||
);
|
||||
|
||||
return bondDatabase;
|
||||
return { bonds: bondDatabase, atoms: atomsRequested ? createAtoms(ccd, pvcd) : void 0 };
|
||||
}
|
||||
|
||||
async function run(out: string, binary = false) {
|
||||
const bonds = await createBonds();
|
||||
function createAtoms(ccd: DatabaseCollection<CCD_Schema>, pvcd: DatabaseCollection<CCD_Schema>) {
|
||||
const ccaSet = new Set<string>();
|
||||
|
||||
const cif = getEncodedCif(TABLE_NAME, bonds, binary);
|
||||
const comp_id: string[] = [];
|
||||
const atom_id: string[] = [];
|
||||
const charge: number[] = [];
|
||||
const pdbx_stereo_config: typeof CCD_Schema.chem_comp_atom['pdbx_stereo_config']['T'][] = [];
|
||||
|
||||
function addAtoms(compId: string, cca: CCA) {
|
||||
for (let i = 0, il = cca._rowCount; i < il; ++i) {
|
||||
const atomId = cca.atom_id.value(i);
|
||||
const k = ccaKey(compId, atomId);
|
||||
if (!ccaSet.has(k)) {
|
||||
atom_id.push(atomId);
|
||||
comp_id.push(compId);
|
||||
charge.push(cca.charge.value(i));
|
||||
pdbx_stereo_config.push(cca.pdbx_stereo_config.value(i));
|
||||
ccaSet.add(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check adding atoms from PVCD
|
||||
checkAddingAtomsFromPVCD(pvcd);
|
||||
|
||||
// add atoms from PVCD
|
||||
for (const k in pvcd) {
|
||||
const { chem_comp, chem_comp_atom } = pvcd[k];
|
||||
if (chem_comp_atom._rowCount) {
|
||||
const parentIds = chem_comp.mon_nstd_parent_comp_id.value(0);
|
||||
if (parentIds.length === 0) {
|
||||
addAtoms(chem_comp.id.value(0), chem_comp_atom);
|
||||
} else {
|
||||
for (let i = 0, il = parentIds.length; i < il; ++i) {
|
||||
addAtoms(parentIds[i], chem_comp_atom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add atoms from CCD
|
||||
for (const k in ccd) {
|
||||
const { chem_comp, chem_comp_atom } = ccd[k];
|
||||
if (chem_comp_atom._rowCount) {
|
||||
addAtoms(chem_comp.id.value(0), chem_comp_atom);
|
||||
}
|
||||
}
|
||||
|
||||
const atomTable = Table.ofArrays(ccd_chemCompAtom_schema, {
|
||||
comp_id, atom_id, charge, pdbx_stereo_config
|
||||
});
|
||||
|
||||
return Database.ofTables(
|
||||
CCA_TABLE_NAME,
|
||||
{ chem_comp_atom: ccd_chemCompAtom_schema },
|
||||
{ chem_comp_atom: atomTable }
|
||||
);
|
||||
}
|
||||
|
||||
async function run(out: string, binary = false, forceDownload = false, ccaOut?: string) {
|
||||
await ensureDataAvailable(forceDownload);
|
||||
const ccd = await readCCD();
|
||||
const pvcd = await readPVCD();
|
||||
|
||||
const { bonds, atoms } = await createBonds(ccd, pvcd, !!ccaOut);
|
||||
|
||||
const ccbCif = getEncodedCif(CCB_TABLE_NAME, bonds, binary);
|
||||
if (!fs.existsSync(path.dirname(out))) {
|
||||
fs.mkdirSync(path.dirname(out));
|
||||
}
|
||||
writeFile(out, cif);
|
||||
writeFile(out, ccbCif);
|
||||
|
||||
if (!!ccaOut) {
|
||||
const ccaCif = getEncodedCif(CCA_TABLE_NAME, atoms, binary);
|
||||
if (!fs.existsSync(path.dirname(ccaOut))) {
|
||||
fs.mkdirSync(path.dirname(ccaOut));
|
||||
}
|
||||
writeFile(ccaOut, ccaCif);
|
||||
}
|
||||
}
|
||||
|
||||
const TABLE_NAME = 'CHEM_COMP_BONDS';
|
||||
|
||||
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');
|
||||
const CCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/components.cif';
|
||||
const PVCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';
|
||||
const CCB_TABLE_NAME = 'CHEM_COMP_BONDS';
|
||||
const CCA_TABLE_NAME = 'CHEM_COMP_ATOMS';
|
||||
|
||||
const parser = new argparse.ArgumentParser({
|
||||
addHelp: true,
|
||||
@@ -240,13 +279,16 @@ parser.addArgument([ '--binary', '-b' ], {
|
||||
action: 'storeTrue',
|
||||
help: 'Output as BinaryCIF.'
|
||||
});
|
||||
parser.addArgument(['--ccaOut', '-a'], {
|
||||
help: 'Optional generated file output path for chem_comp_atom data.',
|
||||
required: false
|
||||
});
|
||||
interface Args {
|
||||
out: string
|
||||
forceDownload?: boolean
|
||||
binary?: boolean
|
||||
out: string,
|
||||
forceDownload?: boolean,
|
||||
binary?: boolean,
|
||||
ccaOut?: string
|
||||
}
|
||||
const args: Args = parser.parseArgs();
|
||||
|
||||
const FORCE_DOWNLOAD = args.forceDownload;
|
||||
|
||||
run(args.out, args.binary);
|
||||
run(args.out, args.binary, args.forceDownload, args.ccaOut);
|
||||
75
src/cli/chem-comp-dict/util.ts
Normal file
75
src/cli/chem-comp-dict/util.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import fetch from 'node-fetch';
|
||||
require('util.promisify').shim();
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
|
||||
import { Progress } from '../../mol-task';
|
||||
import { Database } from '../../mol-data/db';
|
||||
import { CIF } from '../../mol-io/reader/cif';
|
||||
import { CifWriter } from '../../mol-io/writer/cif';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
|
||||
export async function ensureAvailable(path: string, url: string, forceDownload = false) {
|
||||
if (forceDownload || !fs.existsSync(path)) {
|
||||
console.log(`downloading ${url}...`);
|
||||
const data = await fetch(url);
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR);
|
||||
}
|
||||
if (url.endsWith('.gz')) {
|
||||
await writeFile(path, zlib.gunzipSync(await data.buffer()));
|
||||
} else {
|
||||
await writeFile(path, await data.text());
|
||||
}
|
||||
console.log(`done downloading ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureDataAvailable(forceDownload = false) {
|
||||
await ensureAvailable(CCD_PATH, CCD_URL, forceDownload);
|
||||
await ensureAvailable(PVCD_PATH, PVCD_URL, forceDownload);
|
||||
}
|
||||
|
||||
export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
|
||||
const parsed = await parseCif(await readFile(path, 'utf8'));
|
||||
return CIF.toDatabaseCollection(schema, parsed.result);
|
||||
}
|
||||
|
||||
export async function readCCD() {
|
||||
return readFileAsCollection(CCD_PATH, CCD_Schema);
|
||||
}
|
||||
|
||||
export async function readPVCD() {
|
||||
return readFileAsCollection(PVCD_PATH, CCD_Schema);
|
||||
}
|
||||
|
||||
async function parseCif(data: string | Uint8Array) {
|
||||
const comp = CIF.parse(data);
|
||||
console.time('parse cif');
|
||||
const parsed = await comp.run(p => console.log(Progress.format(p)), 250);
|
||||
console.timeEnd('parse cif');
|
||||
if (parsed.isError) throw parsed;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function getEncodedCif(name: string, database: Database<Database.Schema>, binary = false) {
|
||||
const encoder = CifWriter.createEncoder({ binary, encoderName: 'mol*' });
|
||||
CifWriter.Encoder.writeDatabase(encoder, name, database);
|
||||
return encoder.getData();
|
||||
}
|
||||
|
||||
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');
|
||||
const CCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/components.cif';
|
||||
const PVCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';
|
||||
@@ -70,7 +70,7 @@ function classify(name: string, field: CifField): CifWriter.Field {
|
||||
}
|
||||
}
|
||||
|
||||
export default function convert(path: string, asText = false, hints?: EncodingStrategyHint[], filter?: string) {
|
||||
export function convert(path: string, asText = false, hints?: EncodingStrategyHint[], filter?: string) {
|
||||
return Task.create<Uint8Array>('BinaryCIF', async ctx => {
|
||||
const encodingProvider: BinaryEncodingProvider = hints
|
||||
? CifWriter.createEncodingProviderFromJsonConfig(hints)
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
@@ -9,7 +10,7 @@ import * as argparse from 'argparse';
|
||||
import * as util from 'util';
|
||||
import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import convert from './converter';
|
||||
import { convert } from './converter';
|
||||
|
||||
require('util.promisify').shim();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
@@ -11,7 +12,7 @@ import fetch from 'node-fetch';
|
||||
|
||||
import { parseCsv } from '../../mol-io/reader/csv/parser';
|
||||
import { CifFrame, CifBlock } from '../../mol-io/reader/cif';
|
||||
import parseText from '../../mol-io/reader/cif/text/parser';
|
||||
import { parseCifText } from '../../mol-io/reader/cif/text/parser';
|
||||
import { generateSchema } from './util/cif-dic';
|
||||
import { generate } from './util/generate';
|
||||
import { Filter, Database } from './util/schema';
|
||||
@@ -27,19 +28,19 @@ function getDicNamespace(block: CifBlock) {
|
||||
|
||||
async function runGenerateSchemaMmcif(name: string, fieldNamesPath: string, typescript = false, out: string, moldbImportPath: string, addAliases: boolean) {
|
||||
await ensureMmcifDicAvailable();
|
||||
const mmcifDic = await parseText(fs.readFileSync(MMCIF_DIC_PATH, 'utf8')).run();
|
||||
const mmcifDic = await parseCifText(fs.readFileSync(MMCIF_DIC_PATH, 'utf8')).run();
|
||||
if (mmcifDic.isError) throw mmcifDic;
|
||||
|
||||
await ensureIhmDicAvailable();
|
||||
const ihmDic = await parseText(fs.readFileSync(IHM_DIC_PATH, 'utf8')).run();
|
||||
const ihmDic = await parseCifText(fs.readFileSync(IHM_DIC_PATH, 'utf8')).run();
|
||||
if (ihmDic.isError) throw ihmDic;
|
||||
|
||||
await ensureCarbBranchDicAvailable();
|
||||
const carbBranchDic = await parseText(fs.readFileSync(CARB_BRANCH_DIC_PATH, 'utf8')).run();
|
||||
const carbBranchDic = await parseCifText(fs.readFileSync(CARB_BRANCH_DIC_PATH, 'utf8')).run();
|
||||
if (carbBranchDic.isError) throw carbBranchDic;
|
||||
|
||||
await ensureCarbCompDicAvailable();
|
||||
const carbCompDic = await parseText(fs.readFileSync(CARB_COMP_DIC_PATH, 'utf8')).run();
|
||||
const carbCompDic = await parseCifText(fs.readFileSync(CARB_COMP_DIC_PATH, 'utf8')).run();
|
||||
if (carbCompDic.isError) throw carbCompDic;
|
||||
|
||||
const mmcifDicVersion = getDicVersion(mmcifDic.result.blocks[0]);
|
||||
@@ -55,7 +56,7 @@ async function runGenerateSchemaMmcif(name: string, fieldNamesPath: string, type
|
||||
|
||||
async function runGenerateSchemaCifCore(name: string, fieldNamesPath: string, typescript = false, out: string, moldbImportPath: string, addAliases: boolean) {
|
||||
await ensureCifCoreDicAvailable();
|
||||
const cifCoreDic = await parseText(fs.readFileSync(CIF_CORE_DIC_PATH, 'utf8')).run();
|
||||
const cifCoreDic = await parseCifText(fs.readFileSync(CIF_CORE_DIC_PATH, 'utf8')).run();
|
||||
if (cifCoreDic.isError) throw cifCoreDic;
|
||||
|
||||
const cifCoreDicVersion = getDicVersion(cifCoreDic.result.blocks[0]);
|
||||
@@ -79,7 +80,7 @@ async function resolveImports(frames: CifFrame[], baseDir: string): Promise<Map<
|
||||
if (!file) continue;
|
||||
if (imports.has(file)) continue;
|
||||
|
||||
const dic = await parseText(fs.readFileSync(path.join(baseDir, file), 'utf8')).run();
|
||||
const dic = await parseCifText(fs.readFileSync(path.join(baseDir, file), 'utf8')).run();
|
||||
if (dic.isError) throw dic;
|
||||
|
||||
imports.set(file, [...dic.result.blocks[0].saveFrames]);
|
||||
@@ -91,7 +92,7 @@ async function resolveImports(frames: CifFrame[], baseDir: string): Promise<Map<
|
||||
}
|
||||
|
||||
async function runGenerateSchemaDic(name: string, dicPath: string, fieldNamesPath: string, typescript = false, out: string, moldbImportPath: string, addAliases: boolean) {
|
||||
const dic = await parseText(fs.readFileSync(dicPath, 'utf8')).run();
|
||||
const dic = await parseCifText(fs.readFileSync(dicPath, 'utf8')).run();
|
||||
if (dic.isError) throw dic;
|
||||
|
||||
const dicVersion = getDicVersion(dic.result.blocks[0]);
|
||||
@@ -159,7 +160,7 @@ async function ensureDicAvailable(dicPath: string, dicUrl: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const DIC_DIR = path.resolve(__dirname, '../../../build/dics/');
|
||||
const DIC_DIR = path.resolve(__dirname, '../../../../build/dics/');
|
||||
const MMCIF_DIC_PATH = `${DIC_DIR}/mmcif_pdbx_v50.dic`;
|
||||
const MMCIF_DIC_URL = 'http://mmcif.wwpdb.org/dictionaries/ascii/mmcif_pdbx_v50.dic';
|
||||
const IHM_DIC_PATH = `${DIC_DIR}/ihm-extension.dic`;
|
||||
@@ -237,22 +238,22 @@ switch (args.preset) {
|
||||
case 'mmCIF':
|
||||
args.name = 'mmCIF';
|
||||
args.dic = 'mmCIF';
|
||||
args.fieldNamesPath = path.resolve(__dirname, '../../../data/cif-field-names/mmcif-field-names.csv');
|
||||
args.fieldNamesPath = path.resolve(__dirname, '../../../../data/cif-field-names/mmcif-field-names.csv');
|
||||
break;
|
||||
case 'CCD':
|
||||
args.name = 'CCD';
|
||||
args.dic = 'mmCIF';
|
||||
args.fieldNamesPath = path.resolve(__dirname, '../../../data/cif-field-names/ccd-field-names.csv');
|
||||
args.fieldNamesPath = path.resolve(__dirname, '../../../../data/cif-field-names/ccd-field-names.csv');
|
||||
break;
|
||||
case 'BIRD':
|
||||
args.name = 'BIRD';
|
||||
args.dic = 'mmCIF';
|
||||
args.fieldNamesPath = path.resolve(__dirname, '../../../data/cif-field-names/bird-field-names.csv');
|
||||
args.fieldNamesPath = path.resolve(__dirname, '../../../../data/cif-field-names/bird-field-names.csv');
|
||||
break;
|
||||
case 'CifCore':
|
||||
args.name = 'CifCore';
|
||||
args.dic = 'CifCore';
|
||||
args.fieldNamesPath = path.resolve(__dirname, '../../../data/cif-field-names/cif-core-field-names.csv');
|
||||
args.fieldNamesPath = path.resolve(__dirname, '../../../../data/cif-field-names/cif-core-field-names.csv');
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@ function header (name: string, info: string, moldataImportPath: string) {
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
|
||||
import { Database, Column } from '${moldataImportPath}/db'
|
||||
import { Database, Column } from '${moldataImportPath}/db';
|
||||
|
||||
import Schema = Column.Schema`;
|
||||
import Schema = Column.Schema;`;
|
||||
}
|
||||
|
||||
function footer (name: string) {
|
||||
return `
|
||||
export type ${name}_Schema = typeof ${name}_Schema;
|
||||
export interface ${name}_Database extends Database<${name}_Schema> {}`;
|
||||
export interface ${name}_Database extends Database<${name}_Schema> {};`;
|
||||
}
|
||||
|
||||
function getTypeShorthands(schema: Database, fields?: Filter) {
|
||||
@@ -122,7 +122,7 @@ export function generate (name: string, info: string, schema: Database, fields:
|
||||
});
|
||||
codeLines.push(' },');
|
||||
});
|
||||
codeLines.push('}');
|
||||
codeLines.push('};');
|
||||
|
||||
if (addAliases) {
|
||||
codeLines.push('');
|
||||
@@ -144,7 +144,7 @@ export function generate (name: string, info: string, schema: Database, fields:
|
||||
});
|
||||
codeLines.push(' ],');
|
||||
});
|
||||
codeLines.push('}');
|
||||
codeLines.push('};');
|
||||
}
|
||||
|
||||
return `${header(name, info, moldataImportPath)}\n\n${getTypeShorthands(schema, fields)}\n\n${codeLines.join('\n')}\n${footer(name)}`;
|
||||
93
src/cli/lipid-params/index.ts
Normal file
93
src/cli/lipid-params/index.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2020 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 fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
import { UniqueArray } from '../../mol-data/generic';
|
||||
|
||||
const LIPIDS_DIR = path.resolve(__dirname, '../../../../build/lipids/');
|
||||
|
||||
const MARTINI_LIPIDS_PATH = path.resolve(LIPIDS_DIR, 'martini_lipids.itp');
|
||||
const MARTINI_LIPIDS_URL = 'http://www.cgmartini.nl/images/parameters/lipids/Collections/martini_v2.0_lipids_all_201506.itp';
|
||||
|
||||
async function ensureAvailable(path: string, url: string) {
|
||||
if (FORCE_DOWNLOAD || !fs.existsSync(path)) {
|
||||
const name = url.substr(url.lastIndexOf('/') + 1);
|
||||
console.log(`downloading ${name}...`);
|
||||
const data = await fetch(url);
|
||||
if (!fs.existsSync(LIPIDS_DIR)) {
|
||||
fs.mkdirSync(LIPIDS_DIR);
|
||||
}
|
||||
fs.writeFileSync(path, await data.text());
|
||||
console.log(`done downloading ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureLipidsAvailable() { await ensureAvailable(MARTINI_LIPIDS_PATH, MARTINI_LIPIDS_URL); }
|
||||
|
||||
const extraLipids = ['DMPC'];
|
||||
|
||||
async function run(out: string) {
|
||||
await ensureLipidsAvailable();
|
||||
const lipidsItpStr = fs.readFileSync(MARTINI_LIPIDS_PATH, 'utf8');
|
||||
|
||||
const lipids = UniqueArray.create<string>();
|
||||
const reLipid = /\[moleculetype\]\n; molname nrexcl\n +([a-zA-Z]{3,5})/g;
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
while ((m = reLipid.exec(lipidsItpStr)) !== null) {
|
||||
const v = m[0].substr(m[0].lastIndexOf(' ') + 1);
|
||||
UniqueArray.add(lipids, v, v);
|
||||
}
|
||||
|
||||
for (const v of extraLipids) {
|
||||
UniqueArray.add(lipids, v, v);
|
||||
}
|
||||
|
||||
const lipidNames = JSON.stringify(lipids.array);
|
||||
|
||||
if (out) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated lipid params file. Names extracted from Martini FF lipids itp.
|
||||
*
|
||||
* @author molstar/lipid-params cli
|
||||
*/
|
||||
|
||||
export const LipidNames = new Set(${lipidNames.replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
fs.writeFileSync(out, output);
|
||||
} else {
|
||||
console.log(lipidNames);
|
||||
}
|
||||
}
|
||||
|
||||
const parser = new argparse.ArgumentParser({
|
||||
addHelp: true,
|
||||
description: 'Create lipid params (from martini lipids itp)'
|
||||
});
|
||||
parser.addArgument([ '--out', '-o' ], {
|
||||
help: 'Generated lipid params output path, if not given printed to stdout'
|
||||
});
|
||||
parser.addArgument([ '--forceDownload', '-f' ], {
|
||||
action: 'storeTrue',
|
||||
help: 'Force download of martini lipids itp'
|
||||
});
|
||||
interface Args {
|
||||
out: string
|
||||
forceDownload: boolean
|
||||
}
|
||||
const args: Args = parser.parseArgs();
|
||||
|
||||
const FORCE_DOWNLOAD = args.forceDownload;
|
||||
|
||||
run(args.out || '').catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
@@ -26,6 +26,8 @@ function paramInfo(param: PD.Any, offset: number): string {
|
||||
case 'file': return `JavaScript File Handle`;
|
||||
case 'file-list': return `JavaScript FileList Handle`;
|
||||
case 'select': return `One of ${oToS(param.options)}`;
|
||||
case 'value-ref': return `Reference to a runtime defined value.`;
|
||||
case 'data-ref': return `Reference to a computed data value.`;
|
||||
case 'text': return 'String';
|
||||
case 'interval': return `Interval [min, max]`;
|
||||
case 'group': return `Object with:\n${getParams(param.params, offset + 2)}`;
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
@@ -9,7 +10,7 @@ import * as argparse from 'argparse';
|
||||
require('util.promisify').shim();
|
||||
|
||||
import { CifFrame } from '../../mol-io/reader/cif';
|
||||
import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing } from '../../mol-model/structure';
|
||||
import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing, Trajectory } from '../../mol-model/structure';
|
||||
// import { Run, Progress } from '../../mol-task'
|
||||
import { OrderedSet } from '../../mol-data/int';
|
||||
import { openCif, downloadCif } from './helpers';
|
||||
@@ -18,6 +19,7 @@ import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
|
||||
import { Sequence } from '../../mol-model/sequence';
|
||||
import { ModelSecondaryStructure } from '../../mol-model-formats/structure/property/secondary-structure';
|
||||
import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
|
||||
import { Task } from '../../mol-task';
|
||||
|
||||
|
||||
async function downloadFromPdb(pdb: string) {
|
||||
@@ -33,20 +35,22 @@ export async function readCifFile(path: string) {
|
||||
|
||||
export function atomLabel(model: Model, aI: number) {
|
||||
const { atoms, residues, chains, residueAtomSegments, chainAtomSegments } = model.atomicHierarchy;
|
||||
const { label_atom_id } = atoms;
|
||||
const { label_comp_id, label_seq_id } = residues;
|
||||
const { label_atom_id, label_comp_id } = atoms;
|
||||
const { label_seq_id } = residues;
|
||||
const { label_asym_id } = chains;
|
||||
const rI = residueAtomSegments.index[aI];
|
||||
const cI = chainAtomSegments.index[aI];
|
||||
return `${label_asym_id.value(cI)} ${label_comp_id.value(rI)} ${label_seq_id.value(rI)} ${label_atom_id.value(aI)}`;
|
||||
return `${label_asym_id.value(cI)} ${label_comp_id.value(aI)} ${label_seq_id.value(rI)} ${label_atom_id.value(aI)}`;
|
||||
}
|
||||
|
||||
export function residueLabel(model: Model, rI: number) {
|
||||
const { residues, chains, residueAtomSegments, chainAtomSegments } = model.atomicHierarchy;
|
||||
const { label_comp_id, label_seq_id } = residues;
|
||||
const { atoms, residues, chains, residueAtomSegments, chainAtomSegments } = model.atomicHierarchy;
|
||||
const { label_comp_id } = atoms;
|
||||
const { label_seq_id } = residues;
|
||||
const { label_asym_id } = chains;
|
||||
const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]];
|
||||
return `${label_asym_id.value(cI)} ${label_comp_id.value(rI)} ${label_seq_id.value(rI)}`;
|
||||
const aI = residueAtomSegments.offsets[rI];
|
||||
const cI = chainAtomSegments.index[aI];
|
||||
return `${label_asym_id.value(cI)} ${label_comp_id.value(aI)} ${label_seq_id.value(rI)}`;
|
||||
}
|
||||
|
||||
export function printSecStructure(model: Model) {
|
||||
@@ -95,15 +99,17 @@ export function printBonds(structure: Structure, showIntra: boolean, showInter:
|
||||
for (const unit of structure.units) {
|
||||
if (!Unit.isAtomic(unit)) continue;
|
||||
|
||||
for (const pairBonds of bonds.getConnectedUnits(unit)) {
|
||||
for (const pairBonds of bonds.getConnectedUnits(unit.id)) {
|
||||
if (!pairBonds.areUnitsOrdered || pairBonds.edgeCount === 0) continue;
|
||||
|
||||
const { unitA, unitB } = pairBonds;
|
||||
console.log(`${pairBonds.unitA.id} - ${pairBonds.unitB.id}: ${pairBonds.edgeCount} bond(s)`);
|
||||
const { unitA, unitB, edgeCount } = pairBonds;
|
||||
const uA = structure.unitMap.get(unitA);
|
||||
const uB = structure.unitMap.get(unitB);
|
||||
console.log(`${unitA} - ${unitB}: ${edgeCount} bond(s)`);
|
||||
|
||||
for (const aI of pairBonds.connectedIndices) {
|
||||
for (const bond of pairBonds.getEdges(aI)) {
|
||||
console.log(`${atomLabel(unitA.model, unitA.elements[aI])} -- ${atomLabel(unitB.model, unitB.elements[bond.indexB])}`);
|
||||
console.log(`${atomLabel(uA.model, uA.elements[aI])} -- ${atomLabel(uB.model, uB.elements[bond.indexB])}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,10 +186,11 @@ export function printSymmetryInfo(model: Model) {
|
||||
console.log(`NCS operators: ${symmetry.ncsOperators && symmetry.ncsOperators.map(a => a.name).join(', ')}`);
|
||||
}
|
||||
|
||||
export function printModelStats(models: ReadonlyArray<Model>) {
|
||||
export async function printModelStats(models: Trajectory) {
|
||||
console.log('\nModels\n=============');
|
||||
|
||||
for (const m of models) {
|
||||
for (let i = 0; i < models.frameCount; i++) {
|
||||
const m = await Task.resolveInContext(models.getFrameAtIndex(i));
|
||||
if (m.coarseHierarchy.isDefined) {
|
||||
console.log(`${m.label} ${m.modelNum}: ${m.atomicHierarchy.atoms._rowCount} atom(s), ${m.coarseHierarchy.spheres.count} sphere(s), ${m.coarseHierarchy.gaussians.count} gaussian(s)`);
|
||||
} else {
|
||||
@@ -195,7 +202,7 @@ export function printModelStats(models: ReadonlyArray<Model>) {
|
||||
|
||||
export async function getModelsAndStructure(frame: CifFrame) {
|
||||
const models = await trajectoryFromMmCIF(frame).run();
|
||||
const structure = Structure.ofModel(models[0]);
|
||||
const structure = Structure.ofModel(models.representative);
|
||||
return { models, structure };
|
||||
}
|
||||
|
||||
@@ -203,13 +210,13 @@ async function run(frame: CifFrame, args: Args) {
|
||||
const { models, structure } = await getModelsAndStructure(frame);
|
||||
|
||||
if (args.models) printModelStats(models);
|
||||
if (args.seq) printSequence(models[0]);
|
||||
if (args.seq) printSequence(models.representative);
|
||||
if (args.units) printUnits(structure);
|
||||
if (args.sym) printSymmetryInfo(models[0]);
|
||||
if (args.sym) printSymmetryInfo(models.representative);
|
||||
if (args.rings) printRings(structure);
|
||||
if (args.intraBonds) printBonds(structure, true, false);
|
||||
if (args.interBonds) printBonds(structure, false, true);
|
||||
if (args.sec) printSecStructure(models[0]);
|
||||
if (args.sec) printSecStructure(models.representative);
|
||||
}
|
||||
|
||||
async function runDL(pdb: string, args: Args) {
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
25
src/examples/alpha-orbitals/controls.tsx
Normal file
25
src/examples/alpha-orbitals/controls.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
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}>
|
||||
<Controls orbitals={orbitals} />
|
||||
</PluginContextContainer>, parent);
|
||||
}
|
||||
|
||||
function Controls({ orbitals }: { orbitals: AlphaOrbitalsExample }) {
|
||||
const params = useBehavior(orbitals.params);
|
||||
const values = useBehavior(orbitals.state);
|
||||
|
||||
return <ParameterControls params={params as any} values={values} onChangeValues={(vs: any) => orbitals.state.next(vs)} />;
|
||||
}
|
||||
60420
src/examples/alpha-orbitals/example-data.ts
Normal file
60420
src/examples/alpha-orbitals/example-data.ts
Normal file
File diff suppressed because it is too large
Load Diff
61
src/examples/alpha-orbitals/index.html
Normal file
61
src/examples/alpha-orbitals/index.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* Alpha Orbitals Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#app {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
width: 300px;
|
||||
}
|
||||
#sponsor {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
bottom: 8px;
|
||||
font-family: "Helvetica Neue", "Segoe UI", Helvetica, "Source Sans Pro", Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
#sponsor svg {
|
||||
fill: #128EA4;
|
||||
width: 100px;
|
||||
}
|
||||
#sponsor a {
|
||||
text-decoration: none;
|
||||
color: #128EA4;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id='controls'></div>
|
||||
<div id='sponsor'>
|
||||
<a href='https://www.entos.ai/envision' target="_blank" rel="noopener">
|
||||
<svg class="makeStyles-root-46" viewBox="0 0 190 36" xmlns="http://www.w3.org/2000/svg"><path d="M32.2591 28.6707C32.2591 32.3914 29.2421 35.407 25.5214 35.407C22.0752 35.407 19.2338 32.8206 18.8325 29.4831V29.4775C18.8143 29.3312 18.8018 29.1835 18.7934 29.0344C18.7934 29.0316 18.7921 29.0274 18.7921 29.0246V29.0177C18.7865 28.902 18.7837 28.7864 18.7837 28.6707C18.7837 26.2557 20.0532 24.1389 21.9609 22.9503C21.9623 22.9489 21.9651 22.9489 21.9665 22.9475C22.0933 22.8666 22.2243 22.7914 22.3581 22.7203C22.3581 22.7203 22.3595 22.7203 22.3595 22.7189C23.3029 22.2173 24.3787 21.933 25.5214 21.933C29.2421 21.933 32.2591 24.9486 32.2591 28.6707Z"></path><path d="M25.5214 14.0692C29.2421 14.0692 32.2591 11.0522 32.2591 7.33146C32.2591 3.61074 29.2421 0.59375 25.5214 0.59375C22.0529 0.59375 19.1962 3.21637 18.8255 6.58592C18.8185 6.67092 18.8116 6.75454 18.8018 6.83815C18.7893 7.00119 18.7837 7.16563 18.7837 7.33146C18.7837 9.73669 20.0434 11.8465 21.94 13.038C22.0891 13.116 22.2355 13.201 22.3776 13.2916C22.3783 13.2923 22.379 13.2926 22.3797 13.293C22.3804 13.2933 22.3811 13.2937 22.3818 13.2944C23.3196 13.7891 24.3871 14.0692 25.5214 14.0692Z"></path><path d="M19.3645 12.4113C20.2926 12.4113 21.1694 12.638 21.94 13.038C20.0434 11.8465 18.7837 9.73669 18.7837 7.33146C18.7837 7.16563 18.7893 7.00119 18.8018 6.83815C18.4688 9.76455 16.1385 12.0866 13.2065 12.4044C13.8545 13.1193 14.3785 13.9484 14.745 14.857C15.7497 13.3798 17.4443 12.4113 19.3645 12.4113Z"></path><path d="M14.7312 21.1249V21.1236C14.1279 20.2331 13.7767 19.1587 13.7767 18.0007V17.9728C13.7767 15.3084 12.2285 13.0035 9.9835 11.911C9.98141 11.9103 9.97967 11.9096 9.97793 11.9089C9.97619 11.9082 9.97444 11.9075 9.97235 11.9068C9.96817 11.904 9.96538 11.9026 9.9612 11.9012C9.95981 11.9012 9.95981 11.8998 9.95981 11.8998C9.9417 11.8915 9.92394 11.8831 9.90618 11.8747C9.8884 11.8664 9.87063 11.858 9.85251 11.8497C9.82046 11.8343 9.78701 11.819 9.75357 11.8051L9.74521 11.8009C8.91745 11.4372 8.0019 11.2351 7.03898 11.2351C3.31826 11.2351 0.30127 14.2521 0.30127 17.9728C0.30127 21.6935 3.31826 24.7105 7.03898 24.7105C7.98797 24.7105 8.89098 24.514 9.71037 24.1601C9.71246 24.1594 9.7142 24.1583 9.71594 24.1573C9.71768 24.1562 9.71943 24.1552 9.72152 24.1545C9.8107 24.1169 9.8985 24.0765 9.9849 24.0333L9.98629 24.0319C10.7625 23.6919 11.6181 23.5037 12.5197 23.5037C12.7524 23.5037 12.9824 23.5163 13.2081 23.54C13.2082 23.5399 13.2081 23.54 13.2081 23.54C15.0168 23.7365 16.5971 24.695 17.6185 26.0885C17.9195 25.1688 18.3752 24.3201 18.9563 23.5732C17.1964 23.4464 15.6635 22.5058 14.7312 21.1249Z"></path><g clip-path="url(#clip0)"><path d="M106.391 18.0021C106.391 11.3724 101.039 6 94.4389 6H88.4585C81.8581 6 76.5061 11.3724 76.5061 18.0021V30.0042H81.2845V18.0021C81.2845 14.0268 84.4941 10.8008 88.4585 10.8008H94.4347C98.395 10.8008 101.609 14.0226 101.609 18.0021V30.0042H106.391V18.0021Z"></path><path d="M149.432 6H142.258C135.653 6 130.301 11.3724 130.301 18.0021C130.301 24.6319 135.653 30.0042 142.258 30.0042H149.432C156.036 30.0042 161.388 24.6319 161.388 18.0021C161.388 11.3724 156.032 6 149.432 6ZM149.432 25.1992H142.258C138.297 25.1992 135.084 21.9774 135.084 17.9979C135.084 14.0183 138.293 10.7966 142.258 10.7966H149.432C153.392 10.7966 156.606 14.0183 156.606 17.9979C156.606 21.9774 153.392 25.1992 149.432 25.1992Z"></path><path d="M74.1151 25.1992H58.5736C55.4526 25.1992 52.804 23.1924 51.8171 20.3983H74.1151V17.9979C74.1151 17.1808 74.1868 16.3807 74.3175 15.5975H51.8171C52.804 12.8033 55.4526 10.7966 58.5736 10.7966H76.0383C77.1475 8.87458 78.6911 7.22773 80.5299 6H58.5736C51.969 6 46.6169 11.3724 46.6169 18.0021C46.6169 24.6276 51.969 30 58.5736 30H74.1151V25.1992Z"></path><path d="M120.74 6H115.958H102.369C104.212 7.22773 105.751 8.87458 106.861 10.8008H115.958V30H120.74V10.8008H129.838C130.947 8.87458 132.486 7.22773 134.329 6H120.74Z"></path><path d="M182.906 15.6017H169.756C168.436 15.6017 167.365 14.5264 167.365 13.2013C167.365 11.8762 168.436 10.8008 169.756 10.8008H188.882V6H169.756C165.796 6 162.582 9.22173 162.582 13.2013C162.582 17.1808 165.791 20.4025 169.756 20.4025H182.906C184.226 20.4025 185.297 21.4779 185.297 22.803C185.297 24.1281 184.226 25.2034 182.906 25.2034H161.852C160.743 27.1297 159.199 28.7765 157.361 30.0042H182.906C186.866 30.0042 190.08 26.7825 190.08 22.803C190.08 18.8234 186.866 15.6017 182.906 15.6017Z"></path></g><defs><clipPath id="clip0"><rect width="190" height="24" fill="white" transform="translate(0 6)"></rect></clipPath></defs></svg>
|
||||
<div>
|
||||
Entos Envision
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<script>
|
||||
AlphaOrbitalsExample.init('app')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
223
src/examples/alpha-orbitals/index.ts
Normal file
223
src/examples/alpha-orbitals/index.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { SphericalBasisOrder } from '../../extensions/alpha-orbitals/spherical-functions';
|
||||
import { BasisAndOrbitals, CreateOrbitalDensityVolume, CreateOrbitalRepresentation3D, CreateOrbitalVolume, StaticBasisAndOrbitals } from '../../extensions/alpha-orbitals/transforms';
|
||||
import { createPluginAsync } from '../../mol-plugin';
|
||||
import { DefaultPluginSpec } from '../../mol-plugin/spec';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectSelector, StateTransformer } from '../../mol-state';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { mountControls } from './controls';
|
||||
import { DemoMoleculeSDF, DemoOrbitals } from './example-data';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, skip } from 'rxjs/operators';
|
||||
import './index.html';
|
||||
import { Basis, AlphaOrbital } from '../../extensions/alpha-orbitals/data-model';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
|
||||
interface DemoInput {
|
||||
moleculeSdf: string,
|
||||
basis: Basis,
|
||||
order: SphericalBasisOrder,
|
||||
orbitals: AlphaOrbital[]
|
||||
}
|
||||
|
||||
interface Params {
|
||||
show: { name: 'orbital', params: { index: number } } | { name: 'density', params: {} },
|
||||
isoValue: number,
|
||||
gpuSurface: boolean
|
||||
}
|
||||
|
||||
type Selectors = {
|
||||
type: 'orbital',
|
||||
volume: StateObjectSelector<PluginStateObject.Volume.Data, typeof CreateOrbitalVolume>,
|
||||
positive: StateObjectSelector<PluginStateObject.Volume.Representation3D, typeof CreateOrbitalRepresentation3D>
|
||||
negative: StateObjectSelector<PluginStateObject.Volume.Representation3D, typeof CreateOrbitalRepresentation3D>
|
||||
} | {
|
||||
type: 'density',
|
||||
volume: StateObjectSelector<PluginStateObject.Volume.Data, typeof CreateOrbitalDensityVolume>,
|
||||
positive: StateObjectSelector<PluginStateObject.Volume.Representation3D, typeof CreateOrbitalRepresentation3D>
|
||||
}
|
||||
|
||||
export class AlphaOrbitalsExample {
|
||||
plugin: PluginContext;
|
||||
|
||||
async init(target: string | HTMLElement) {
|
||||
const defaultSpec = DefaultPluginSpec();
|
||||
this.plugin = await createPluginAsync(typeof target === 'string' ? document.getElementById(target)! : target, {
|
||||
...defaultSpec,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: false,
|
||||
showControls: false
|
||||
},
|
||||
controls: { left: 'none', right: 'none', top: 'none', bottom: 'none' },
|
||||
},
|
||||
components: {
|
||||
viewport: {
|
||||
canvas3d: {
|
||||
camera: {
|
||||
helper: { axes: { name: 'off', params: { } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
config: [
|
||||
[PluginConfig.Viewport.ShowExpand, false],
|
||||
[PluginConfig.Viewport.ShowControls, false],
|
||||
[PluginConfig.Viewport.ShowSelectionMode, false],
|
||||
[PluginConfig.Viewport.ShowAnimation, false],
|
||||
]
|
||||
});
|
||||
|
||||
this.plugin.managers.interactivity.setProps({ granularity: 'element' });
|
||||
|
||||
if (!canComputeGrid3dOnGPU(this.plugin.canvas3d?.webgl)) {
|
||||
PluginCommands.Toast.Show(this.plugin, {
|
||||
title: 'Error',
|
||||
message: `Browser/device does not support required WebGL extension (OES_texture_float).`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.load({
|
||||
moleculeSdf: DemoMoleculeSDF,
|
||||
...DemoOrbitals
|
||||
});
|
||||
|
||||
mountControls(this, document.getElementById('controls')!);
|
||||
}
|
||||
|
||||
readonly params = new BehaviorSubject<ParamDefinition.For<Params>>({} as any);
|
||||
readonly state = new BehaviorSubject<Params>({ show: { name: 'orbital', params: { index: 32 } }, isoValue: 1, gpuSurface: false });
|
||||
|
||||
private selectors?: Selectors = void 0;
|
||||
private basis?: StateObjectSelector<BasisAndOrbitals> = void 0;
|
||||
|
||||
private currentParams: Params = { ...this.state.value };
|
||||
|
||||
private clearVolume() {
|
||||
if (!this.selectors) return;
|
||||
const v = this.selectors.volume;
|
||||
this.selectors = void 0;
|
||||
return this.plugin.build().delete(v).commit();
|
||||
}
|
||||
|
||||
private async syncVolume() {
|
||||
if (!this.basis?.isOk) return;
|
||||
|
||||
const state = this.state.value;
|
||||
|
||||
if (state.show.name !== this.selectors?.type) {
|
||||
await this.clearVolume();
|
||||
}
|
||||
|
||||
const update = this.plugin.build();
|
||||
if (state.show.name === 'orbital') {
|
||||
if (!this.selectors) {
|
||||
const volume = update
|
||||
.to(this.basis)
|
||||
.apply(CreateOrbitalVolume, { index: state.show.params.index });
|
||||
|
||||
const positive = volume.apply(CreateOrbitalRepresentation3D, this.volumeParams('positive', ColorNames.blue)).selector;
|
||||
const negative = volume.apply(CreateOrbitalRepresentation3D, this.volumeParams('negative', ColorNames.red)).selector;
|
||||
|
||||
this.selectors = { type: 'orbital', volume: volume.selector, positive, negative };
|
||||
} else {
|
||||
const index = state.show.params.index;
|
||||
update.to(this.selectors.volume).update(CreateOrbitalVolume, () => ({ index }));
|
||||
}
|
||||
} else {
|
||||
if (!this.selectors) {
|
||||
const volume = update
|
||||
.to(this.basis)
|
||||
.apply(CreateOrbitalDensityVolume);
|
||||
const positive = volume.apply(CreateOrbitalRepresentation3D, this.volumeParams('positive', ColorNames.blue)).selector;
|
||||
this.selectors = { type: 'density', volume: volume.selector, positive };
|
||||
}
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
|
||||
if (this.currentParams.gpuSurface !== this.state.value.gpuSurface) {
|
||||
await this.setIsovalue();
|
||||
}
|
||||
|
||||
this.currentParams = this.state.value;
|
||||
}
|
||||
|
||||
private setIsovalue() {
|
||||
if (!this.selectors) return;
|
||||
|
||||
this.currentParams = this.state.value;
|
||||
const update = this.plugin.build();
|
||||
update.to(this.selectors.positive).update(this.volumeParams('positive', ColorNames.blue));
|
||||
if (this.selectors?.type === 'orbital') {
|
||||
update.to(this.selectors.negative).update(this.volumeParams('negative', ColorNames.red));
|
||||
}
|
||||
return update.commit();
|
||||
}
|
||||
|
||||
private volumeParams(kind: 'positive' | 'negative', color: Color): StateTransformer.Params<typeof CreateOrbitalRepresentation3D> {
|
||||
return {
|
||||
alpha: 0.85,
|
||||
color,
|
||||
directVolume: this.state.value.gpuSurface,
|
||||
kind,
|
||||
relativeIsovalue: this.state.value.isoValue,
|
||||
pickable: false,
|
||||
xrayShaded: true
|
||||
};
|
||||
}
|
||||
|
||||
async load(input: DemoInput) {
|
||||
await this.plugin.clear();
|
||||
|
||||
const data = await this.plugin.builders.data.rawData({ data: input.moleculeSdf }, { state: { isGhost: true } });
|
||||
const trajectory = await this.plugin.builders.structure.parseTrajectory(data, 'mol');
|
||||
const model = await this.plugin.builders.structure.createModel(trajectory);
|
||||
const structure = await this.plugin.builders.structure.createStructure(model);
|
||||
|
||||
const all = await this.plugin.builders.structure.tryCreateComponentStatic(structure, 'all');
|
||||
if (all) await this.plugin.builders.structure.representation.addRepresentation(all, { type: 'ball-and-stick', color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } });
|
||||
|
||||
|
||||
this.basis = await this.plugin.build().toRoot()
|
||||
.apply(StaticBasisAndOrbitals, { basis: input.basis, order: input.order, orbitals: input.orbitals })
|
||||
.commit();
|
||||
|
||||
await this.syncVolume();
|
||||
|
||||
this.params.next({
|
||||
show: ParamDefinition.MappedStatic('orbital', {
|
||||
'orbital': ParamDefinition.Group({
|
||||
index: ParamDefinition.Numeric(32, { min: 0, max: input.orbitals.length - 1 }, { immediateUpdate: true, isEssential: true }),
|
||||
}),
|
||||
'density': ParamDefinition.EmptyGroup()
|
||||
}, { cycle: true }),
|
||||
isoValue: ParamDefinition.Numeric(this.currentParams.isoValue, { min: 0.5, max: 3, step: 0.1 }, { immediateUpdate: true, isEssential: false }),
|
||||
gpuSurface: ParamDefinition.Boolean(this.currentParams.gpuSurface, { isHidden: true })
|
||||
});
|
||||
|
||||
this.state.pipe(skip(1), debounceTime(1000 / 24)).subscribe(async params => {
|
||||
if (params.show.name !== this.currentParams.show.name
|
||||
|| (params.show.name === 'orbital' && this.currentParams.show.name === 'orbital' && params.show.params.index !== this.currentParams.show.params.index)) {
|
||||
this.syncVolume();
|
||||
} else if (params.isoValue !== this.currentParams.isoValue || params.gpuSurface !== this.currentParams.gpuSurface) {
|
||||
this.setIsovalue();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).AlphaOrbitalsExample = new AlphaOrbitalsExample();
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
// adjust this number to make the animation faster or slower
|
||||
// requires to "restart" the animation if changed
|
||||
BasicMolStarWrapper.animate.modelIndex.maxFPS = 30;
|
||||
BasicMolStarWrapper.animate.modelIndex.targetFps = 30;
|
||||
|
||||
addControl('Play To End', () => BasicMolStarWrapper.animate.modelIndex.onceForward());
|
||||
addControl('Play To Start', () => BasicMolStarWrapper.animate.modelIndex.onceBackward());
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
import { EmptyLoci } from '../../mol-model/loci';
|
||||
import { StructureSelection } from '../../mol-model/structure';
|
||||
import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
|
||||
import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in';
|
||||
import { createPlugin } from '../../mol-plugin';
|
||||
import { DefaultPluginSpec } from '../../mol-plugin/spec';
|
||||
import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in/model-index';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Script } from '../../mol-script/script';
|
||||
@@ -29,7 +29,7 @@ class BasicWrapper {
|
||||
|
||||
init(target: string | HTMLElement) {
|
||||
this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, {
|
||||
...DefaultPluginSpec,
|
||||
...DefaultPluginSpec(),
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: false,
|
||||
@@ -83,13 +83,17 @@ class BasicWrapper {
|
||||
if (!this.plugin.canvas3d.props.trackball.spin) PluginCommands.Camera.Reset(this.plugin, {});
|
||||
}
|
||||
|
||||
private animateModelIndexTargetFps() {
|
||||
return Math.max(1, this.animate.modelIndex.targetFps | 0);
|
||||
}
|
||||
|
||||
animate = {
|
||||
modelIndex: {
|
||||
maxFPS: 8,
|
||||
onceForward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }); },
|
||||
onceBackward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }); },
|
||||
palindrome: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }); },
|
||||
loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }); },
|
||||
targetFps: 8,
|
||||
onceForward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'once', params: { direction: 'forward' } } }); },
|
||||
onceBackward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'once', params: { direction: 'backward' } } }); },
|
||||
palindrome: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'palindrome', params: {} } }); },
|
||||
loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'loop', params: { direction: 'forward' } } }); },
|
||||
stop: () => this.plugin.managers.animation.stop()
|
||||
}
|
||||
}
|
||||
@@ -113,8 +117,10 @@ class BasicWrapper {
|
||||
|
||||
interactivity = {
|
||||
highlightOn: () => {
|
||||
const data = this.plugin.managers.structure.hierarchy.current.structures[0]?.cell.obj?.data;
|
||||
if (!data) return;
|
||||
|
||||
const seq_id = 7;
|
||||
const data = (this.plugin.state.data.select('asm')[0].obj as PluginStateObject.Molecule.Structure).data;
|
||||
const sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
|
||||
'residue-test': Q.core.rel.eq([Q.struct.atomProperty.macromolecular.label_seq_id(), seq_id]),
|
||||
'group-by': Q.struct.atomProperty.macromolecular.residueKey()
|
||||
|
||||
@@ -10,7 +10,7 @@ import { superpose } from '../../mol-model/structure/structure/util/superpositio
|
||||
import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
|
||||
import Expression from '../../mol-script/language/expression';
|
||||
import { Expression } from '../../mol-script/language/expression';
|
||||
import { compile } from '../../mol-script/runtime/query/compiler';
|
||||
import { StateObjectRef } from '../../mol-state';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CifWriter } from '../../mol-io/writer/cif';
|
||||
import * as S from './schemas';
|
||||
// import { getCategoryInstanceProvider } from './utils'
|
||||
|
||||
export default function create(allData: any) {
|
||||
export function createMapping(allData: any) {
|
||||
const mols = Object.keys(allData);
|
||||
const enc = CifWriter.createEncoder();
|
||||
enc.startDataBlock(mols[0]);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import express from 'express';
|
||||
import fetch from 'node-fetch';
|
||||
import createMapping from './mapping';
|
||||
import { createMapping } from './mapping';
|
||||
|
||||
async function getMappings(id: string) {
|
||||
const data = await fetch(`https://www.ebi.ac.uk/pdbe/api/mappings/${id}`);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import createMapping from './mapping';
|
||||
import { createMapping } from './mapping';
|
||||
|
||||
(async function () {
|
||||
const data = await fetch('https://www.ebi.ac.uk/pdbe/api/mappings/1tqn?pretty=true');
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
}
|
||||
#app {
|
||||
position: absolute;
|
||||
left: 160px;
|
||||
top: 100px;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
border: 1px solid #ccc;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
top: 100px;
|
||||
left: 780px;
|
||||
bottom: 100px;
|
||||
right: 50px;
|
||||
z-index: 10;
|
||||
font-family: sans-serif;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#controls > button {
|
||||
@@ -46,13 +46,13 @@
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
LightingDemo.init('app')
|
||||
LightingDemo.load({ url: 'https://files.rcsb.org/download/1M07.cif', assemblyId: '1' })
|
||||
LightingDemo.load({ url: 'https://models.rcsb.org/4KTC.bcif', assemblyId: '1' }, 5, 1.3)
|
||||
|
||||
addHeader('Example PDB IDs');
|
||||
addControl('1M07', () => LightingDemo.load({ url: 'https://files.rcsb.org/download/1M07.cif', assemblyId: '1' }));
|
||||
addControl('6HY0', () => LightingDemo.load({ url: 'https://files.rcsb.org/download/6HY0.cif', assemblyId: '1' }));
|
||||
addControl('6QVK', () => LightingDemo.load({ url: 'https://files.rcsb.org/download/6QVK.cif', assemblyId: '1' }));
|
||||
addControl('1RB8', () => LightingDemo.load({ url: 'https://files.rcsb.org/download/1RB8.cif', assemblyId: '1' }));
|
||||
addControl('4KTC', () => LightingDemo.load({ url: 'https://models.rcsb.org/4KTC.bcif', assemblyId: '1' }, 5, 1.3));
|
||||
addControl('5FJ5', () => LightingDemo.load({ url: 'https://models.rcsb.org/5FJ5.bcif', assemblyId: '1' }, 8, 1.8));
|
||||
addControl('1UPN', () => LightingDemo.load({ url: 'https://models.rcsb.org/1UPN.bcif', assemblyId: '1' }, 7, 1.6));
|
||||
addControl('1RB8', () => LightingDemo.load({ url: 'https://models.rcsb.org/1RB8.bcif', assemblyId: '1' }, 6, 1.3));
|
||||
|
||||
addSeparator()
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import { Canvas3DProps } from '../../mol-canvas3d/canvas3d';
|
||||
import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
|
||||
import { createPlugin } from '../../mol-plugin';
|
||||
import { DefaultPluginSpec } from '../../mol-plugin/spec';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
@@ -24,12 +25,11 @@ const Canvas3DPresets = {
|
||||
mode: 'temporal' as Canvas3DProps['multiSample']['mode']
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: { name: 'on', params: { bias: 0.8, kernelSize: 6, radius: 64 } },
|
||||
outline: { name: 'on', params: { scale: 1, threshold: 0.8 } }
|
||||
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15 } },
|
||||
outline: { name: 'on', params: { scale: 1, threshold: 0.1 } }
|
||||
},
|
||||
renderer: {
|
||||
ambientIntensity: 1,
|
||||
lightIntensity: 0,
|
||||
style: { name: 'flat', params: {} }
|
||||
}
|
||||
},
|
||||
occlusion: <Preset> {
|
||||
@@ -37,12 +37,11 @@ const Canvas3DPresets = {
|
||||
mode: 'temporal' as Canvas3DProps['multiSample']['mode']
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: { name: 'on', params: { bias: 0.8, kernelSize: 6, radius: 64 } },
|
||||
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15 } },
|
||||
outline: { name: 'off', params: { } }
|
||||
},
|
||||
renderer: {
|
||||
ambientIntensity: 0.4,
|
||||
lightIntensity: 0.6,
|
||||
style: { name: 'matte', params: {} }
|
||||
}
|
||||
},
|
||||
standard: <Preset> {
|
||||
@@ -54,20 +53,24 @@ const Canvas3DPresets = {
|
||||
outline: { name: 'off', params: { } }
|
||||
},
|
||||
renderer: {
|
||||
ambientIntensity: 0.4,
|
||||
lightIntensity: 0.6,
|
||||
style: { name: 'matte', params: {} }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
type Canvas3DPreset = keyof typeof Canvas3DPresets
|
||||
|
||||
class LightingDemo {
|
||||
plugin: PluginContext;
|
||||
|
||||
private radius = 5;
|
||||
private bias = 1.1;
|
||||
private preset: Canvas3DPreset = 'illustrative';
|
||||
|
||||
init(target: string | HTMLElement) {
|
||||
this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, {
|
||||
...DefaultPluginSpec,
|
||||
...DefaultPluginSpec(),
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: false,
|
||||
@@ -82,6 +85,10 @@ class LightingDemo {
|
||||
|
||||
setPreset(preset: Canvas3DPreset) {
|
||||
const props = Canvas3DPresets[preset];
|
||||
if (props.postprocessing.occlusion?.name === 'on') {
|
||||
props.postprocessing.occlusion.params.radius = this.radius;
|
||||
props.postprocessing.occlusion.params.bias = this.bias;
|
||||
}
|
||||
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: {
|
||||
...props,
|
||||
multiSample: {
|
||||
@@ -99,7 +106,7 @@ class LightingDemo {
|
||||
}});
|
||||
}
|
||||
|
||||
async load({ url, format = 'mmcif', isBinary = false, assemblyId = '' }: LoadParams) {
|
||||
async load({ url, format = 'mmcif', isBinary = true, assemblyId = '' }: LoadParams, radius: number, bias: number) {
|
||||
await this.plugin.clear();
|
||||
|
||||
const data = await this.plugin.builders.data.download({ url: Asset.Url(url), isBinary }, { state: { isGhost: true } });
|
||||
@@ -111,7 +118,11 @@ class LightingDemo {
|
||||
if (polymer) await this.plugin.builders.structure.representation.addRepresentation(polymer, { type: 'spacefill', color: 'illustrative' });
|
||||
|
||||
const ligand = await this.plugin.builders.structure.tryCreateComponentStatic(structure, 'ligand');
|
||||
if (ligand) await this.plugin.builders.structure.representation.addRepresentation(ligand, { type: 'ball-and-stick' });
|
||||
if (ligand) await this.plugin.builders.structure.representation.addRepresentation(ligand, { type: 'ball-and-stick', color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } });
|
||||
|
||||
this.radius = radius;
|
||||
this.bias = bias;
|
||||
this.setPreset(this.preset);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export namespace ModelInfo {
|
||||
const entityType = model.entities.data.type.value(eI);
|
||||
if (entityType !== 'non-polymer' && entityType !== 'branched') continue;
|
||||
|
||||
const comp_id = model.atomicHierarchy.residues.label_comp_id.value(rI);
|
||||
const comp_id = model.atomicHierarchy.atoms.label_comp_id.value(residueOffsets[rI]);
|
||||
|
||||
let lig = hetMap.get(comp_id);
|
||||
if (!lig) {
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
// adjust this number to make the animation faster or slower
|
||||
// requires to "restart" the animation if changed
|
||||
PluginWrapper.animate.modelIndex.maxFPS = 30;
|
||||
PluginWrapper.animate.modelIndex.targetFps = 30;
|
||||
|
||||
addControl('Play To End', () => PluginWrapper.animate.modelIndex.onceForward());
|
||||
addControl('Play To Start', () => PluginWrapper.animate.modelIndex.onceBackward());
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
|
||||
import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
|
||||
import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in';
|
||||
import { createPlugin } from '../../mol-plugin';
|
||||
import { DefaultPluginSpec } from '../../mol-plugin/spec';
|
||||
import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in/model-index';
|
||||
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';
|
||||
@@ -46,7 +47,7 @@ class MolStarProteopediaWrapper {
|
||||
customColorList?: number[]
|
||||
}) {
|
||||
this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, {
|
||||
...DefaultPluginSpec,
|
||||
...DefaultPluginSpec(),
|
||||
animations: [
|
||||
AnimateModelIndex
|
||||
],
|
||||
@@ -271,13 +272,17 @@ class MolStarProteopediaWrapper {
|
||||
resetPosition: () => PluginCommands.Camera.Reset(this.plugin, { })
|
||||
}
|
||||
|
||||
private animateModelIndexTargetFps() {
|
||||
return Math.max(1, this.animate.modelIndex.targetFps | 0);
|
||||
}
|
||||
|
||||
animate = {
|
||||
modelIndex: {
|
||||
maxFPS: 8,
|
||||
onceForward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }); },
|
||||
onceBackward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }); },
|
||||
palindrome: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }); },
|
||||
loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }); },
|
||||
targetFps: 8,
|
||||
onceForward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'once', params: { direction: 'forward' } } }); },
|
||||
onceBackward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'once', params: { direction: 'backward' } } }); },
|
||||
palindrome: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'palindrome', params: {} } }); },
|
||||
loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'loop', params: { direction: 'forward' } } }); },
|
||||
stop: () => this.plugin.managers.animation.stop()
|
||||
}
|
||||
}
|
||||
@@ -403,7 +408,7 @@ class MolStarProteopediaWrapper {
|
||||
},
|
||||
download: async (type: 'molj' | 'molx' = 'molj', params?: PluginState.SnapshotParams) => {
|
||||
const data = await this.plugin.managers.snapshot.serialize({ type, params });
|
||||
download(data, `mol-star_state_${(name || getFormattedTime())}.${type}`);
|
||||
download(data, `mol-star_state_${getFormattedTime()}.${type}`);
|
||||
},
|
||||
fetch: async (url: string, type: 'molj' | 'molx' = 'molj') => {
|
||||
try {
|
||||
|
||||
214
src/extensions/alpha-orbitals/_spec/collocation.spec.ts
Normal file
214
src/extensions/alpha-orbitals/_spec/collocation.spec.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Box3D } from '../../../mol-math/geometry';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { sphericalCollocation } from '../collocation';
|
||||
import { Basis, CubeGridInfo } from '../data-model';
|
||||
|
||||
describe('alpha-orbitals-cubes', () => {
|
||||
it('water', async () => {
|
||||
const grid: CubeGridInfo = {
|
||||
params: {
|
||||
basis: _testBasis,
|
||||
cutoffThreshold: 0,
|
||||
sphericalOrder: 'cca-reverse',
|
||||
boxExpand: 0,
|
||||
gridSpacing: []
|
||||
},
|
||||
box: Box3D.create(Vec3.create(-1, -1, -1), Vec3.create(1, 1, 1)),
|
||||
delta: Vec3.create(2, 2, 2),
|
||||
dimensions: Vec3.create(2, 2, 2),
|
||||
npoints: 8,
|
||||
size: Vec3.create(2, 2, 2)
|
||||
};
|
||||
|
||||
const matrix = await sphericalCollocation(grid, {
|
||||
energy: 0,
|
||||
occupancy: 0,
|
||||
alpha: [-2.2623991420609075e-16, 0.6360205395000592, 0.6672122399886391, -0.3876927909355508, -1.6780131293332933e-16, 2.844782862661151e-16, 4.977960694176068e-19, -2.3945919908996803e-16]
|
||||
}, RuntimeContext.Synchronous);
|
||||
|
||||
const expected = [-0.1451730622877498, 0.06479453956039086, -0.2777738736440713, -0.057116584776260436, 0.05929916178822645, 0.2742903371231049, -0.07221698722165386, 0.15389180241391376];
|
||||
|
||||
expect(matrix.length).toBe(expected.length);
|
||||
|
||||
for (let i = 0; i < matrix.length; i++) {
|
||||
expect(Math.abs(matrix[i] - expected[i]) < 1e-6).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const _testBasis: Basis = {
|
||||
'atoms': [
|
||||
{
|
||||
'center': [
|
||||
0.025886090588624934,
|
||||
0.019164790004065606,
|
||||
-0.013539970104105408
|
||||
] as Vec3,
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
'coefficients': [
|
||||
[
|
||||
-0.004151277818987536,
|
||||
-0.02067024147993795,
|
||||
-0.05150303336984537,
|
||||
0.33462711739899537,
|
||||
0.5621061300983125,
|
||||
0.17129946969948573
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
152.28769660788095,
|
||||
27.928015215973073,
|
||||
7.848374792384515,
|
||||
1.1223350202705642,
|
||||
0.5093846587907856,
|
||||
0.24292266532510307
|
||||
]
|
||||
},
|
||||
{
|
||||
'angularMomentum': [1],
|
||||
'coefficients': [
|
||||
[
|
||||
0.007924233646294425,
|
||||
0.051441048251911314,
|
||||
0.18984000600705359,
|
||||
0.4049863191150474,
|
||||
0.40123628611490797,
|
||||
0.1051855189039082
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
27.203421487167727,
|
||||
7.09409912597673,
|
||||
2.5383362605345954,
|
||||
1.0610730767843852,
|
||||
0.4851948916410433,
|
||||
0.22938302550642545
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'center': [
|
||||
0.5082729578468134,
|
||||
1.6880351220025265,
|
||||
0.4963443067810461
|
||||
] as Vec3,
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
'coefficients': [
|
||||
[
|
||||
0.009163596280542963,
|
||||
0.04936149294292479,
|
||||
0.16853830490998634,
|
||||
0.37056279972195677,
|
||||
0.4164915298246781,
|
||||
0.13033408410772263
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
33.710073211949485,
|
||||
6.180705022740464,
|
||||
1.7291385346152253,
|
||||
0.5940057549921978,
|
||||
0.2306698170449518,
|
||||
0.09500256906284119
|
||||
]
|
||||
},
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
'coefficients': [
|
||||
[
|
||||
-0.32279868167000036,
|
||||
3.209629817295221,
|
||||
2.4672629224617935,
|
||||
-0.048487066612842224,
|
||||
-0.2611850111200143,
|
||||
-0.8917817597810863,
|
||||
-1.9607480081275706,
|
||||
-2.203769342520311,
|
||||
-0.6896328935259993
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
10.256286070314905,
|
||||
0.6227965325875392,
|
||||
0.2391007667853915,
|
||||
33.710073211949485,
|
||||
6.180705022740464,
|
||||
1.7291385346152253,
|
||||
0.5940057549921978,
|
||||
0.2306698170449518,
|
||||
0.09500256906284119
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'center': [
|
||||
1.1367367844436005,
|
||||
-0.47018519422670163,
|
||||
-1.356802622574504
|
||||
] as Vec3,
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
'coefficients': [
|
||||
[
|
||||
0.009163596280542963,
|
||||
0.04936149294292479,
|
||||
0.16853830490998634,
|
||||
0.37056279972195677,
|
||||
0.4164915298246781,
|
||||
0.13033408410772263
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
33.710073211949485,
|
||||
6.180705022740464,
|
||||
1.7291385346152253,
|
||||
0.5940057549921978,
|
||||
0.2306698170449518,
|
||||
0.09500256906284119
|
||||
]
|
||||
},
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
'coefficients': [
|
||||
[
|
||||
-0.32279868167000036,
|
||||
3.209629817295221,
|
||||
2.4672629224617935,
|
||||
-0.048487066612842224,
|
||||
-0.2611850111200143,
|
||||
-0.8917817597810863,
|
||||
-1.9607480081275706,
|
||||
-2.203769342520311,
|
||||
-0.6896328935259993
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
10.256286070314905,
|
||||
0.6227965325875392,
|
||||
0.2391007667853915,
|
||||
33.710073211949485,
|
||||
6.180705022740464,
|
||||
1.7291385346152253,
|
||||
0.5940057549921978,
|
||||
0.2306698170449518,
|
||||
0.09500256906284119
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
162
src/extensions/alpha-orbitals/collocation.ts
Normal file
162
src/extensions/alpha-orbitals/collocation.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Inspired by https://github.com/dgasmith/gau2grid.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { arrayMin } from '../../mol-util/array';
|
||||
import { AlphaOrbital, CubeGridInfo } from './data-model';
|
||||
import { normalizeBasicOrder, SphericalFunctions } from './spherical-functions';
|
||||
|
||||
export async function sphericalCollocation(
|
||||
grid: CubeGridInfo,
|
||||
orbital: AlphaOrbital,
|
||||
taskCtx: RuntimeContext
|
||||
) {
|
||||
const { basis, sphericalOrder, cutoffThreshold } = grid.params;
|
||||
let baseCount = 0;
|
||||
|
||||
for (const atom of basis.atoms) {
|
||||
for (const shell of atom.shells) {
|
||||
for (const L of shell.angularMomentum) {
|
||||
if (L > 4) {
|
||||
// TODO: will L > 4 be required? Would need to precompute more functions in that case.
|
||||
throw new Error('Angular momentum L > 4 not supported.');
|
||||
}
|
||||
|
||||
baseCount += 2 * L + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const matrix = new Float32Array(grid.npoints);
|
||||
|
||||
let baseIndex = 0;
|
||||
for (const atom of basis.atoms) {
|
||||
for (const shell of atom.shells) {
|
||||
let amIndex = 0;
|
||||
for (const L of shell.angularMomentum) {
|
||||
const alpha = normalizeBasicOrder(
|
||||
L,
|
||||
orbital.alpha.slice(baseIndex, baseIndex + 2 * L + 1),
|
||||
sphericalOrder
|
||||
);
|
||||
baseIndex += 2 * L + 1;
|
||||
|
||||
collocationBasis(
|
||||
matrix,
|
||||
grid,
|
||||
L,
|
||||
shell.coefficients[amIndex++],
|
||||
shell.exponents,
|
||||
atom.center,
|
||||
cutoffThreshold,
|
||||
alpha
|
||||
);
|
||||
|
||||
if (taskCtx.shouldUpdate) {
|
||||
await taskCtx.update({
|
||||
message: 'Computing...',
|
||||
current: baseIndex,
|
||||
max: baseCount,
|
||||
isIndeterminate: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
function collocationBasis(
|
||||
matrix: Float32Array,
|
||||
grid: CubeGridInfo,
|
||||
L: number,
|
||||
coefficients: number[],
|
||||
exponents: number[],
|
||||
center: Vec3,
|
||||
cutoffThreshold: number,
|
||||
alpha: number[]
|
||||
) {
|
||||
const ncoeff = exponents.length;
|
||||
const sphericalFunc = SphericalFunctions[L];
|
||||
|
||||
const cx = center[0],
|
||||
cy = center[1],
|
||||
cz = center[2];
|
||||
const ny = grid.dimensions[1],
|
||||
nz = grid.dimensions[2];
|
||||
const gdx = grid.delta[0],
|
||||
gdy = grid.delta[1],
|
||||
gdz = grid.delta[2];
|
||||
const sx = grid.box.min[0],
|
||||
sy = grid.box.min[1],
|
||||
sz = grid.box.min[2];
|
||||
|
||||
const cutoffRadius =
|
||||
cutoffThreshold > 0
|
||||
? Math.sqrt(-Math.log(cutoffThreshold) / arrayMin(exponents))
|
||||
: 10000;
|
||||
const cutoffSquared = cutoffRadius * cutoffRadius;
|
||||
|
||||
const radiusBox = getRadiusBox(grid, center, cutoffRadius);
|
||||
const iMin = radiusBox[0][0],
|
||||
jMin = radiusBox[0][1],
|
||||
kMin = radiusBox[0][2];
|
||||
const iMax = radiusBox[1][0],
|
||||
jMax = radiusBox[1][1],
|
||||
kMax = radiusBox[1][2];
|
||||
|
||||
for (let i = iMin; i <= iMax; i++) {
|
||||
const x = sx + gdx * i - cx;
|
||||
const oX = i * ny * nz;
|
||||
|
||||
for (let j = jMin; j <= jMax; j++) {
|
||||
const y = sy + gdy * j - cy;
|
||||
const oY = oX + j * nz;
|
||||
for (let k = kMin; k <= kMax; k++) {
|
||||
const z = sz + gdz * k - cz;
|
||||
const R2 = x * x + y * y + z * z;
|
||||
|
||||
if (R2 > cutoffSquared) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let gaussianSum = 0;
|
||||
for (let c = 0; c < ncoeff; c++) {
|
||||
gaussianSum +=
|
||||
coefficients[c] * Math.exp(-exponents[c] * R2);
|
||||
}
|
||||
|
||||
const sphericalSum = L === 0 ? alpha[0] : sphericalFunc(alpha, x, y, z);
|
||||
|
||||
|
||||
matrix[k + oY] += gaussianSum * sphericalSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getRadiusBox(grid: CubeGridInfo, center: Vec3, radius: number) {
|
||||
const r = Vec3.create(radius, radius, radius);
|
||||
const min = Vec3.scaleAndAdd(Vec3(), center, r, -1);
|
||||
const max = Vec3.add(Vec3(), center, r);
|
||||
|
||||
Vec3.sub(min, min, grid.box.min);
|
||||
Vec3.sub(max, max, grid.box.min);
|
||||
|
||||
Vec3.div(min, min, grid.delta);
|
||||
Vec3.floor(min, min);
|
||||
Vec3.max(min, min, Vec3());
|
||||
|
||||
Vec3.div(max, max, grid.delta);
|
||||
Vec3.ceil(max, max);
|
||||
Vec3.min(max, max, Vec3.subScalar(Vec3(), grid.dimensions, 1));
|
||||
|
||||
return [min, max];
|
||||
}
|
||||
131
src/extensions/alpha-orbitals/data-model.ts
Normal file
131
src/extensions/alpha-orbitals/data-model.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Mat4, Tensor, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Grid } from '../../mol-model/volume';
|
||||
import { SphericalBasisOrder } from './spherical-functions';
|
||||
import { Box3D, RegularGrid3d } from '../../mol-math/geometry';
|
||||
import { arrayMin, arrayMax, arrayRms, arrayMean } from '../../mol-util/array';
|
||||
|
||||
// Note: generally contracted gaussians are currently not supported.
|
||||
export interface SphericalElectronShell {
|
||||
exponents: number[];
|
||||
angularMomentum: number[];
|
||||
// number[] for each angular momentum
|
||||
coefficients: number[][];
|
||||
}
|
||||
|
||||
export interface Basis {
|
||||
atoms: {
|
||||
// in Bohr units!
|
||||
center: Vec3;
|
||||
shells: SphericalElectronShell[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface AlphaOrbital {
|
||||
energy: number;
|
||||
occupancy: number;
|
||||
alpha: number[];
|
||||
}
|
||||
|
||||
export interface CubeGridComputationParams {
|
||||
basis: Basis;
|
||||
/**
|
||||
* for each electron shell compute a cutoff radius as
|
||||
* const cutoffRadius = Math.sqrt(-Math.log(cutoffThreshold) / arrayMin(exponents));
|
||||
*/
|
||||
cutoffThreshold: number;
|
||||
sphericalOrder: SphericalBasisOrder;
|
||||
boxExpand: number;
|
||||
gridSpacing: number | [atomCountThreshold: number, spacing: number][];
|
||||
doNotComputeIsovalues?: boolean;
|
||||
}
|
||||
|
||||
export interface CubeGridInfo {
|
||||
params: CubeGridComputationParams;
|
||||
dimensions: Vec3;
|
||||
box: Box3D;
|
||||
size: Vec3;
|
||||
npoints: number;
|
||||
delta: Vec3;
|
||||
}
|
||||
|
||||
export interface CubeGrid {
|
||||
grid: Grid;
|
||||
isovalues?: { negative?: number; positive?: number };
|
||||
}
|
||||
|
||||
export function initCubeGrid(params: CubeGridComputationParams): CubeGridInfo {
|
||||
const geometry = params.basis.atoms.map(a => a.center);
|
||||
const { gridSpacing: spacing, boxExpand: expand } = params;
|
||||
|
||||
const count = geometry.length;
|
||||
const box = Box3D.expand(
|
||||
Box3D(),
|
||||
Box3D.fromVec3Array(Box3D(), geometry),
|
||||
Vec3.create(expand, expand, expand)
|
||||
);
|
||||
const size = Box3D.size(Vec3(), box);
|
||||
|
||||
const spacingThresholds =
|
||||
typeof spacing === 'number' ? [[0, spacing]] : [...spacing];
|
||||
spacingThresholds.sort((a, b) => b[0] - a[0]);
|
||||
|
||||
let s = 0.4;
|
||||
for (let i = 0; i <= spacingThresholds.length; i++) {
|
||||
s = spacingThresholds[i][1];
|
||||
if (spacingThresholds[i][0] <= count) break;
|
||||
}
|
||||
|
||||
const dimensions = Vec3.ceil(Vec3(), Vec3.scale(Vec3(), size, 1 / s));
|
||||
|
||||
return {
|
||||
params,
|
||||
box,
|
||||
dimensions,
|
||||
size,
|
||||
npoints: dimensions[0] * dimensions[1] * dimensions[2],
|
||||
delta: Vec3.div(Vec3(), size, Vec3.subScalar(Vec3(), dimensions, 1)),
|
||||
};
|
||||
}
|
||||
|
||||
const BohrToAngstromFactor = 0.529177210859;
|
||||
|
||||
export function createGrid(gridInfo: RegularGrid3d, values: Float32Array, axisOrder: number[]) {
|
||||
const boxSize = Box3D.size(Vec3(), gridInfo.box);
|
||||
const boxOrigin = Vec3.clone(gridInfo.box.min);
|
||||
|
||||
Vec3.scale(boxSize, boxSize, BohrToAngstromFactor);
|
||||
Vec3.scale(boxOrigin, boxOrigin, BohrToAngstromFactor);
|
||||
|
||||
const scale = Mat4.fromScaling(
|
||||
Mat4(),
|
||||
Vec3.div(
|
||||
Vec3(),
|
||||
boxSize,
|
||||
Vec3.sub(Vec3(), gridInfo.dimensions, Vec3.create(1, 1, 1))
|
||||
)
|
||||
);
|
||||
const translate = Mat4.fromTranslation(Mat4(), boxOrigin);
|
||||
const matrix = Mat4.mul(Mat4(), translate, scale);
|
||||
|
||||
const grid: Grid = {
|
||||
transform: { kind: 'matrix', matrix },
|
||||
cells: Tensor.create(
|
||||
Tensor.Space(gridInfo.dimensions, axisOrder, Float32Array),
|
||||
(values as any) as Tensor.Data
|
||||
),
|
||||
stats: {
|
||||
min: arrayMin(values),
|
||||
max: arrayMax(values),
|
||||
mean: arrayMean(values),
|
||||
sigma: arrayRms(values),
|
||||
},
|
||||
};
|
||||
|
||||
return grid;
|
||||
}
|
||||
124
src/extensions/alpha-orbitals/density.ts
Normal file
124
src/extensions/alpha-orbitals/density.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
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 { AlphaOrbital, createGrid, CubeGrid, CubeGridComputationParams, initCubeGrid } from './data-model';
|
||||
import { gpuComputeAlphaOrbitalsDensityGridValues } from './gpu/compute';
|
||||
|
||||
export function createSphericalCollocationDensityGrid(
|
||||
params: CubeGridComputationParams, orbitals: AlphaOrbital[], webgl?: WebGLContext
|
||||
): Task<CubeGrid> {
|
||||
return Task.create('Spherical Collocation Grid', async (ctx) => {
|
||||
const cubeGrid = initCubeGrid(params);
|
||||
|
||||
let matrix: Float32Array;
|
||||
if (canComputeGrid3dOnGPU(webgl)) {
|
||||
// console.time('gpu');
|
||||
matrix = await gpuComputeAlphaOrbitalsDensityGridValues(ctx, webgl!, cubeGrid, orbitals);
|
||||
// console.timeEnd('gpu');
|
||||
} else {
|
||||
throw new Error('Missing OES_texture_float WebGL extension.');
|
||||
}
|
||||
|
||||
const grid = createGrid(cubeGrid, matrix, [0, 1, 2]);
|
||||
let isovalues: { negative?: number, positive?: number } | undefined;
|
||||
|
||||
if (!params.doNotComputeIsovalues) {
|
||||
isovalues = computeDensityIsocontourValues(matrix, 0.85);
|
||||
}
|
||||
|
||||
return { grid, isovalues };
|
||||
});
|
||||
}
|
||||
|
||||
export function computeDensityIsocontourValues(input: Float32Array, cumulativeThreshold: number) {
|
||||
let weightSum = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = Math.abs(v);
|
||||
weightSum += w;
|
||||
}
|
||||
const avgWeight = weightSum / input.length;
|
||||
let minWeight = 3 * avgWeight;
|
||||
|
||||
// do not try to identify isovalues for degenerate data
|
||||
// e.g. all values are almost same
|
||||
if (Math.abs(avgWeight - input[0] * input[0]) < 1e-5) {
|
||||
return { negative: void 0, positive: void 0 };
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
while (true) {
|
||||
let csum = 0;
|
||||
size = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = Math.abs(v);
|
||||
if (w >= minWeight) {
|
||||
csum += w;
|
||||
size++;
|
||||
}
|
||||
}
|
||||
|
||||
if (csum / weightSum > cumulativeThreshold) {
|
||||
break;
|
||||
}
|
||||
|
||||
minWeight -= avgWeight;
|
||||
}
|
||||
|
||||
const values = new Float32Array(size);
|
||||
const weights = new Float32Array(size);
|
||||
const indices = new Int32Array(size);
|
||||
|
||||
let o = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = Math.abs(v);
|
||||
if (w >= minWeight) {
|
||||
values[o] = v;
|
||||
weights[o] = w;
|
||||
indices[o] = o;
|
||||
o++;
|
||||
}
|
||||
}
|
||||
|
||||
sortArray(
|
||||
indices,
|
||||
(indices, i, j) => weights[indices[j]] - weights[indices[i]]
|
||||
);
|
||||
|
||||
let cweight = 0,
|
||||
cutoffIndex = 0;
|
||||
for (let i = 0; i < size; i++) {
|
||||
cweight += weights[indices[i]];
|
||||
|
||||
if (cweight / weightSum >= cumulativeThreshold) {
|
||||
cutoffIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let positive = Number.POSITIVE_INFINITY,
|
||||
negative = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (let i = 0; i < cutoffIndex; i++) {
|
||||
const v = values[indices[i]];
|
||||
if (v > 0) {
|
||||
if (v < positive) positive = v;
|
||||
} else if (v < 0) {
|
||||
if (v > negative) negative = v;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
negative: negative !== Number.NEGATIVE_INFINITY ? negative : void 0,
|
||||
positive: positive !== Number.POSITIVE_INFINITY ? positive : void 0,
|
||||
};
|
||||
}
|
||||
170
src/extensions/alpha-orbitals/gpu/compute.ts
Normal file
170
src/extensions/alpha-orbitals/gpu/compute.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { createGrid3dComputeRenderable } from '../../../mol-gl/compute/grid3d';
|
||||
import { TextureSpec, UnboxedValues, UniformSpec } from '../../../mol-gl/renderable/schema';
|
||||
import { WebGLContext } from '../../../mol-gl/webgl/context';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { arrayMin } from '../../../mol-util/array';
|
||||
import { AlphaOrbital, Basis, CubeGridInfo } from '../data-model';
|
||||
import { normalizeBasicOrder, SphericalBasisOrder } from '../spherical-functions';
|
||||
import { MAIN, UTILS } from './shader.frag';
|
||||
|
||||
const Schema = {
|
||||
tCenters: TextureSpec('image-float32', 'rgba', 'float', 'nearest'),
|
||||
tInfo: TextureSpec('image-float32', 'rgba', 'float', 'nearest'),
|
||||
tCoeff: TextureSpec('image-float32', 'rgb', 'float', 'nearest'),
|
||||
tAlpha: TextureSpec('image-float32', 'alpha', 'float', 'nearest'),
|
||||
uNCenters: UniformSpec('i'),
|
||||
uNAlpha: UniformSpec('i'),
|
||||
uNCoeff: UniformSpec('i'),
|
||||
uMaxCoeffs: UniformSpec('i'),
|
||||
};
|
||||
|
||||
const Orbitals = createGrid3dComputeRenderable({
|
||||
schema: Schema,
|
||||
loopBounds: ['uNCenters', 'uMaxCoeffs'],
|
||||
mainCode: MAIN,
|
||||
utilCode: UTILS,
|
||||
returnCode: 'v',
|
||||
values(params: { grid: CubeGridInfo, orbital: AlphaOrbital }) {
|
||||
return createTextureData(params.grid, params.orbital);
|
||||
}
|
||||
});
|
||||
|
||||
const Density = createGrid3dComputeRenderable({
|
||||
schema: {
|
||||
...Schema,
|
||||
uOccupancy: UniformSpec('f'),
|
||||
},
|
||||
loopBounds: ['uNCenters', 'uMaxCoeffs'],
|
||||
mainCode: MAIN,
|
||||
utilCode: UTILS,
|
||||
returnCode: 'current + uOccupancy * v * v',
|
||||
values(params: { grid: CubeGridInfo, orbitals: AlphaOrbital[] }) {
|
||||
return {
|
||||
...createTextureData(params.grid, params.orbitals[0]),
|
||||
uOccupancy: 0
|
||||
};
|
||||
},
|
||||
cumulative: {
|
||||
states(params: { grid: CubeGridInfo, orbitals: AlphaOrbital[] }) {
|
||||
return params.orbitals.filter(o => o.occupancy !== 0);
|
||||
},
|
||||
update({ grid }, state: AlphaOrbital, values) {
|
||||
const alpha = getNormalizedAlpha(grid.params.basis, state.alpha, grid.params.sphericalOrder);
|
||||
ValueCell.updateIfChanged(values.uOccupancy, state.occupancy);
|
||||
ValueCell.update(values.tAlpha, { width: alpha.length, height: 1, array: alpha });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function gpuComputeAlphaOrbitalsGridValues(ctx: RuntimeContext, webgl: WebGLContext, grid: CubeGridInfo, orbital: AlphaOrbital) {
|
||||
return Orbitals(ctx, webgl, grid, { grid, orbital });
|
||||
}
|
||||
|
||||
export function gpuComputeAlphaOrbitalsDensityGridValues(ctx: RuntimeContext, webgl: WebGLContext, grid: CubeGridInfo, orbitals: AlphaOrbital[]) {
|
||||
return Density(ctx, webgl, grid, { grid, orbitals });
|
||||
}
|
||||
|
||||
function getNormalizedAlpha(basis: Basis, alphaOrbitals: number[], sphericalOrder: SphericalBasisOrder) {
|
||||
const alpha = new Float32Array(alphaOrbitals.length);
|
||||
|
||||
let aO = 0;
|
||||
for (const atom of basis.atoms) {
|
||||
for (const shell of atom.shells) {
|
||||
for (const L of shell.angularMomentum) {
|
||||
const a0 = normalizeBasicOrder(L, alphaOrbitals.slice(aO, aO + 2 * L + 1), sphericalOrder);
|
||||
for (let i = 0; i < a0.length; i++) alpha[aO + i] = a0[i];
|
||||
aO += 2 * L + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return alpha;
|
||||
}
|
||||
|
||||
function createTextureData(grid: CubeGridInfo, orbital: AlphaOrbital): UnboxedValues<typeof Schema> {
|
||||
const { basis, sphericalOrder, cutoffThreshold } = grid.params;
|
||||
|
||||
let centerCount = 0;
|
||||
let baseCount = 0;
|
||||
let coeffCount = 0;
|
||||
for (const atom of basis.atoms) {
|
||||
for (const shell of atom.shells) {
|
||||
for (const L of shell.angularMomentum) {
|
||||
if (L > 4) {
|
||||
// TODO: will L > 4 be required? Would need to precompute more functions in that case.
|
||||
throw new Error('Angular momentum L > 4 not supported.');
|
||||
}
|
||||
|
||||
centerCount++;
|
||||
baseCount += 2 * L + 1;
|
||||
coeffCount += shell.exponents.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const centers = new Float32Array(4 * centerCount);
|
||||
// L, alpha_offset, coeff_offset_start, coeff_offset_end
|
||||
const info = new Float32Array(4 * centerCount);
|
||||
const alpha = new Float32Array(baseCount);
|
||||
const coeff = new Float32Array(3 * coeffCount);
|
||||
|
||||
let maxCoeffs = 0;
|
||||
let cO = 0, aO = 0, coeffO = 0;
|
||||
for (const atom of basis.atoms) {
|
||||
for (const shell of atom.shells) {
|
||||
|
||||
let amIndex = 0;
|
||||
for (const L of shell.angularMomentum) {
|
||||
const a0 = normalizeBasicOrder(L, orbital.alpha.slice(aO, aO + 2 * L + 1), sphericalOrder);
|
||||
|
||||
const cutoffRadius = cutoffThreshold > 0
|
||||
? Math.sqrt(-Math.log(cutoffThreshold) / arrayMin(shell.exponents))
|
||||
: 10000;
|
||||
|
||||
centers[4 * cO + 0] = atom.center[0];
|
||||
centers[4 * cO + 1] = atom.center[1];
|
||||
centers[4 * cO + 2] = atom.center[2];
|
||||
centers[4 * cO + 3] = cutoffRadius * cutoffRadius;
|
||||
|
||||
info[4 * cO + 0] = L;
|
||||
info[4 * cO + 1] = aO;
|
||||
info[4 * cO + 2] = coeffO;
|
||||
info[4 * cO + 3] = coeffO + shell.exponents.length;
|
||||
|
||||
for (let i = 0; i < a0.length; i++) alpha[aO + i] = a0[i];
|
||||
|
||||
const c0 = shell.coefficients[amIndex++];
|
||||
for (let i = 0; i < shell.exponents.length; i++) {
|
||||
coeff[3 * (coeffO + i) + 0] = c0[i];
|
||||
coeff[3 * (coeffO + i) + 1] = shell.exponents[i];
|
||||
}
|
||||
|
||||
if (c0.length > maxCoeffs) {
|
||||
maxCoeffs = c0.length;
|
||||
}
|
||||
|
||||
cO++;
|
||||
aO += 2 * L + 1;
|
||||
coeffO += shell.exponents.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
uNCenters: centerCount,
|
||||
uNAlpha: baseCount,
|
||||
uNCoeff: coeffCount,
|
||||
uMaxCoeffs: maxCoeffs,
|
||||
tCenters: { width: centerCount, height: 1, array: centers },
|
||||
tInfo: { width: centerCount, height: 1, array: info },
|
||||
tCoeff: { width: coeffCount, height: 1, array: coeff },
|
||||
tAlpha: { width: baseCount, height: 1, array: alpha },
|
||||
};
|
||||
}
|
||||
145
src/extensions/alpha-orbitals/gpu/shader.frag.ts
Normal file
145
src/extensions/alpha-orbitals/gpu/shader.frag.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export const UTILS = `
|
||||
float L1(vec3 p, float a0, float a1, float a2) {
|
||||
return a0 * p.z + a1 * p.x + a2 * p.y;
|
||||
}
|
||||
|
||||
float L2(vec3 p, float a0, float a1, float a2, float a3, float a4) {
|
||||
float x = p.x, y = p.y, z = p.z;
|
||||
float xx = x * x, yy = y * y, zz = z * z;
|
||||
return (
|
||||
a0 * (-0.5 * xx - 0.5 * yy + zz) +
|
||||
a1 * (1.7320508075688772 * x * z) +
|
||||
a2 * (1.7320508075688772 * y * z) +
|
||||
a3 * (0.8660254037844386 * xx - 0.8660254037844386 * yy) +
|
||||
a4 * (1.7320508075688772 * x * y)
|
||||
);
|
||||
}
|
||||
|
||||
float L3(vec3 p, float a0, float a1, float a2, float a3, float a4, float a5, float a6) {
|
||||
float x = p.x, y = p.y, z = p.z;
|
||||
float xx = x * x, yy = y * y, zz = z * z;
|
||||
float xxx = xx * x, yyy = yy * y, zzz = zz * z;
|
||||
return (
|
||||
a0 * (-1.5 * xx * z - 1.5 * yy * z + zzz) +
|
||||
a1 * (-0.6123724356957945 * xxx - 0.6123724356957945 * x * yy + 2.449489742783178 * x * zz) +
|
||||
a2 * (-0.6123724356957945 * xx * y - 0.6123724356957945 * yyy + 2.449489742783178 * y * zz) +
|
||||
a3 * (1.9364916731037085 * xx * z - 1.9364916731037085 * yy * z) +
|
||||
a4 * (3.872983346207417 * x * y * z) +
|
||||
a5 * (0.7905694150420949 * xxx - 2.3717082451262845 * x * yy) +
|
||||
a6 * (2.3717082451262845 * xx * y - 0.7905694150420949 * yyy)
|
||||
);
|
||||
}
|
||||
|
||||
float L4(vec3 p, float a0, float a1, float a2, float a3, float a4, float a5, float a6, float a7, float a8) {
|
||||
float x = p.x, y = p.y, z = p.z;
|
||||
float xx = x * x, yy = y * y, zz = z * z;
|
||||
float xxx = xx * x, yyy = yy * y, zzz = zz * z;
|
||||
float xxxx = xxx * x, yyyy = yyy * y, zzzz = zzz * z;
|
||||
return (
|
||||
a0 * (0.375 * xxxx + 0.75 * xx * yy + 0.375 * yyyy - 3.0 * xx * zz - 3.0 * yy * zz + zzzz) +
|
||||
a1 * (-2.3717082451262845 * xxx * z - 2.3717082451262845 * x * yy * z + 3.1622776601683795 * x * zzz) +
|
||||
a2 * (-2.3717082451262845 * xx * y * z - 2.3717082451262845 * yyy * z + 3.1622776601683795 * y * zzz) +
|
||||
a3 * (-0.5590169943749475 * xxxx + 0.5590169943749475 * yyyy + 3.3541019662496847 * xx * zz - 3.3541019662496847 * yy * zz) +
|
||||
a4 * (-1.118033988749895 * xxx * y - 1.118033988749895 * x * yyy + 6.708203932499369 * x * y * zz) +
|
||||
a5 * (2.091650066335189 * xxx * z + -6.274950199005566 * x * yy * z) +
|
||||
a6 * (6.274950199005566 * xx * y * z + -2.091650066335189 * yyy * z) +
|
||||
a7 * (0.739509972887452 * xxxx - 4.437059837324712 * xx * yy + 0.739509972887452 * yyyy) +
|
||||
a8 * (2.958039891549808 * xxx * y + -2.958039891549808 * x * yyy)
|
||||
);
|
||||
}
|
||||
|
||||
float alpha(float offset, float f) {
|
||||
#ifdef WEBGL1
|
||||
// in webgl1, the value is in the alpha channel!
|
||||
return texture2D(tAlpha, vec2(offset * f, 0.5)).a;
|
||||
#else
|
||||
return texture2D(tAlpha, vec2(offset * f, 0.5)).x;
|
||||
#endif
|
||||
}
|
||||
|
||||
float Y(int L, vec3 X, float aO, float fA) {
|
||||
if (L == 0) {
|
||||
return alpha(aO, fA);
|
||||
} else if (L == 1) {
|
||||
return L1(X,
|
||||
alpha(aO, fA), alpha(aO + 1.0, fA), alpha(aO + 2.0, fA)
|
||||
);
|
||||
} else if (L == 2) {
|
||||
return L2(X,
|
||||
alpha(aO, fA), alpha(aO + 1.0, fA), alpha(aO + 2.0, fA), alpha(aO + 3.0, fA), alpha(aO + 4.0, fA)
|
||||
);
|
||||
} else if (L == 3) {
|
||||
return L3(X,
|
||||
alpha(aO, fA), alpha(aO + 1.0, fA), alpha(aO + 2.0, fA), alpha(aO + 3.0, fA), alpha(aO + 4.0, fA),
|
||||
alpha(aO + 5.0, fA), alpha(aO + 6.0, fA)
|
||||
);
|
||||
} else if (L == 4) {
|
||||
return L4(X,
|
||||
alpha(aO, fA), alpha(aO + 1.0, fA), alpha(aO + 2.0, fA), alpha(aO + 3.0, fA), alpha(aO + 4.0, fA),
|
||||
alpha(aO + 5.0, fA), alpha(aO + 6.0, fA), alpha(aO + 7.0, fA), alpha(aO + 8.0, fA)
|
||||
);
|
||||
}
|
||||
// TODO: do we need L > 4?
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
#ifndef WEBGL1
|
||||
float R(float R2, int start, int end, float fCoeff) {
|
||||
float gauss = 0.0;
|
||||
for (int i = start; i < end; i++) {
|
||||
vec2 c = texture2D(tCoeff, vec2(float(i) * fCoeff, 0.5)).xy;
|
||||
gauss += c.x * exp(-c.y * R2);
|
||||
}
|
||||
return gauss;
|
||||
}
|
||||
#else
|
||||
float R(float R2, int start, int end, float fCoeff) {
|
||||
float gauss = 0.0;
|
||||
int o = start;
|
||||
for (int i = 0; i < uMaxCoeffs; i++) {
|
||||
if (o >= end) break;
|
||||
|
||||
vec2 c = texture2D(tCoeff, vec2(float(o) * fCoeff, 0.5)).xy;
|
||||
gauss += c.x * exp(-c.y * R2);
|
||||
o++;
|
||||
}
|
||||
return gauss;
|
||||
}
|
||||
#endif
|
||||
`;
|
||||
|
||||
export const MAIN = `
|
||||
float fCenter = 1.0 / float(uNCenters - 1);
|
||||
float fCoeff = 1.0 / float(uNCoeff - 1);
|
||||
float fA = 1.0 / float(uNAlpha - 1);
|
||||
|
||||
float v = 0.0;
|
||||
|
||||
for (int i = 0; i < uNCenters; i++) {
|
||||
vec2 cCoord = vec2(float(i) * fCenter, 0.5);
|
||||
|
||||
vec4 center = texture2D(tCenters, cCoord);
|
||||
vec3 X = xyz - center.xyz;
|
||||
float R2 = dot(X, X);
|
||||
|
||||
// center.w is squared cutoff radius
|
||||
if (R2 > center.w) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vec4 info = texture2D(tInfo, cCoord);
|
||||
|
||||
int L = int(info.x);
|
||||
float aO = info.y;
|
||||
int coeffStart = int(info.z);
|
||||
int coeffEnd = int(info.w);
|
||||
|
||||
v += R(R2, coeffStart, coeffEnd, fCoeff) * Y(L, X, aO, fA);
|
||||
}
|
||||
`;
|
||||
131
src/extensions/alpha-orbitals/orbitals.ts
Normal file
131
src/extensions/alpha-orbitals/orbitals.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Inspired by https://github.com/dgasmith/gau2grid.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
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 { 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> {
|
||||
return Task.create('Spherical Collocation Grid', async (ctx) => {
|
||||
const cubeGrid = initCubeGrid(params);
|
||||
|
||||
let matrix: Float32Array;
|
||||
if (canComputeGrid3dOnGPU(webgl)) {
|
||||
// console.time('gpu');
|
||||
matrix = await gpuComputeAlphaOrbitalsGridValues(ctx, webgl!, cubeGrid, orbital);
|
||||
// console.timeEnd('gpu');
|
||||
} else {
|
||||
// console.time('cpu');
|
||||
matrix = await sphericalCollocation(cubeGrid, orbital, ctx);
|
||||
// console.timeEnd('cpu');
|
||||
}
|
||||
|
||||
const grid = createGrid(cubeGrid, matrix, [0, 1, 2]);
|
||||
let isovalues: { negative?: number, positive?: number } | undefined;
|
||||
|
||||
if (!params.doNotComputeIsovalues) {
|
||||
isovalues = computeOrbitalIsocontourValues(matrix, 0.85);
|
||||
}
|
||||
|
||||
return { grid, isovalues };
|
||||
});
|
||||
}
|
||||
|
||||
export function computeOrbitalIsocontourValues(input: Float32Array, cumulativeThreshold: number) {
|
||||
let weightSum = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = v * v;
|
||||
weightSum += w;
|
||||
}
|
||||
const avgWeight = weightSum / input.length;
|
||||
let minWeight = 3 * avgWeight;
|
||||
|
||||
// do not try to identify isovalues for degenerate data
|
||||
// e.g. all values are almost same
|
||||
if (Math.abs(avgWeight - input[0] * input[0]) < 1e-5) {
|
||||
return { negative: void 0, positive: void 0 };
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
while (true) {
|
||||
let csum = 0;
|
||||
size = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = v * v;
|
||||
if (w >= minWeight) {
|
||||
csum += w;
|
||||
size++;
|
||||
}
|
||||
}
|
||||
|
||||
if (csum / weightSum > cumulativeThreshold) {
|
||||
break;
|
||||
}
|
||||
|
||||
minWeight -= avgWeight;
|
||||
}
|
||||
|
||||
const values = new Float32Array(size);
|
||||
const weights = new Float32Array(size);
|
||||
const indices = new Int32Array(size);
|
||||
|
||||
let o = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = v * v;
|
||||
if (w >= minWeight) {
|
||||
values[o] = v;
|
||||
weights[o] = w;
|
||||
indices[o] = o;
|
||||
o++;
|
||||
}
|
||||
}
|
||||
|
||||
sortArray(
|
||||
indices,
|
||||
(indices, i, j) => weights[indices[j]] - weights[indices[i]]
|
||||
);
|
||||
|
||||
let cweight = 0,
|
||||
cutoffIndex = 0;
|
||||
for (let i = 0; i < size; i++) {
|
||||
cweight += weights[indices[i]];
|
||||
|
||||
if (cweight / weightSum >= cumulativeThreshold) {
|
||||
cutoffIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let positive = Number.POSITIVE_INFINITY,
|
||||
negative = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (let i = 0; i < cutoffIndex; i++) {
|
||||
const v = values[indices[i]];
|
||||
if (v > 0) {
|
||||
if (v < positive) positive = v;
|
||||
} else if (v < 0) {
|
||||
if (v > negative) negative = v;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
negative: negative !== Number.NEGATIVE_INFINITY ? negative : void 0,
|
||||
positive: positive !== Number.POSITIVE_INFINITY ? positive : void 0,
|
||||
};
|
||||
}
|
||||
93
src/extensions/alpha-orbitals/spherical-functions.ts
Normal file
93
src/extensions/alpha-orbitals/spherical-functions.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Inspired by https://github.com/dgasmith/gau2grid.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
// gaussian:
|
||||
// R_0, R^+_1, R^-_1, ..., R^+_l, R^-_l
|
||||
// cca:
|
||||
// R^-_(l), R^-_(l-1), ..., R_0, ..., R^+_(l-1), R^+_l
|
||||
// cca-reverse:
|
||||
// R^+_(l), R^+_(l-1), ..., R_0, ..., R^-_(l-1), R^-_l
|
||||
export type SphericalBasisOrder = 'gaussian' | 'cca' | 'cca-reverse';
|
||||
|
||||
export function normalizeBasicOrder(
|
||||
L: number,
|
||||
alpha: number[],
|
||||
order: SphericalBasisOrder
|
||||
) {
|
||||
if (order === 'gaussian' || L === 0) return alpha;
|
||||
|
||||
const ret: number[] = [alpha[L]];
|
||||
for (let l = 0; l < L; l++) {
|
||||
const a = alpha[L - l - 1],
|
||||
b = alpha[L + l + 1];
|
||||
if (order === 'cca') ret.push(b, a);
|
||||
else ret.push(a, b);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export type SphericalFunc = (
|
||||
alpha: number[],
|
||||
x: number,
|
||||
y: number,
|
||||
z: number
|
||||
) => number;
|
||||
|
||||
export const SphericalFunctions: SphericalFunc[] = [L0, L1, L2, L3, L4];
|
||||
|
||||
// L_i functions were auto-generated.
|
||||
|
||||
function L0(alpha: number[], x: number, y: number, z: number) {
|
||||
return alpha[0];
|
||||
}
|
||||
|
||||
function L1(alpha: number[], x: number, y: number, z: number) {
|
||||
return alpha[0] * z + alpha[1] * x + alpha[2] * y;
|
||||
}
|
||||
|
||||
function L2(alpha: number[], x: number, y: number, z: number) {
|
||||
const xx = x * x, yy = y * y, zz = z * z;
|
||||
return (
|
||||
alpha[0] * (-0.5 * xx - 0.5 * yy + zz) +
|
||||
alpha[1] * (1.7320508075688772 * x * z) +
|
||||
alpha[2] * (1.7320508075688772 * y * z) +
|
||||
alpha[3] * (0.8660254037844386 * xx - 0.8660254037844386 * yy) +
|
||||
alpha[4] * (1.7320508075688772 * x * y)
|
||||
);
|
||||
}
|
||||
|
||||
function L3(alpha: number[], x: number, y: number, z: number) {
|
||||
const xx = x * x, yy = y * y, zz = z * z;
|
||||
const xxx = xx * x, yyy = yy * y, zzz = zz * z;
|
||||
return (
|
||||
alpha[0] * (-1.5 * xx * z - 1.5 * yy * z + zzz) +
|
||||
alpha[1] * (-0.6123724356957945 * xxx - 0.6123724356957945 * x * yy + 2.449489742783178 * x * zz) +
|
||||
alpha[2] * (-0.6123724356957945 * xx * y - 0.6123724356957945 * yyy + 2.449489742783178 * y * zz) +
|
||||
alpha[3] * (1.9364916731037085 * xx * z - 1.9364916731037085 * yy * z) +
|
||||
alpha[4] * (3.872983346207417 * x * y * z) +
|
||||
alpha[5] * (0.7905694150420949 * xxx - 2.3717082451262845 * x * yy) +
|
||||
alpha[6] * (2.3717082451262845 * xx * y - 0.7905694150420949 * yyy)
|
||||
);
|
||||
}
|
||||
|
||||
function L4(alpha: number[], x: number, y: number, z: number) {
|
||||
const xx = x * x, yy = y * y, zz = z * z;
|
||||
const xxx = xx * x, yyy = yy * y, zzz = zz * z;
|
||||
const xxxx = xxx * x, yyyy = yyy * y, zzzz = zzz * z;
|
||||
return (
|
||||
alpha[0] * (0.375 * xxxx + 0.75 * xx * yy + 0.375 * yyyy - 3.0 * xx * zz - 3.0 * yy * zz + zzzz) +
|
||||
alpha[1] * (-2.3717082451262845 * xxx * z - 2.3717082451262845 * x * yy * z + 3.1622776601683795 * x * zzz) +
|
||||
alpha[2] * (-2.3717082451262845 * xx * y * z - 2.3717082451262845 * yyy * z + 3.1622776601683795 * y * zzz) +
|
||||
alpha[3] * (-0.5590169943749475 * xxxx + 0.5590169943749475 * yyyy + 3.3541019662496847 * xx * zz - 3.3541019662496847 * yy * zz) +
|
||||
alpha[4] * (-1.118033988749895 * xxx * y - 1.118033988749895 * x * yyy + 6.708203932499369 * x * y * zz) +
|
||||
alpha[5] * (2.091650066335189 * xxx * z + -6.274950199005566 * x * yy * z) +
|
||||
alpha[6] * (6.274950199005566 * xx * y * z + -2.091650066335189 * yyy * z) +
|
||||
alpha[7] * (0.739509972887452 * xxxx - 4.437059837324712 * xx * yy + 0.739509972887452 * yyyy) +
|
||||
alpha[8] * (2.958039891549808 * xxx * y + -2.958039891549808 * x * yyy)
|
||||
);
|
||||
}
|
||||
236
src/extensions/alpha-orbitals/transforms.ts
Normal file
236
src/extensions/alpha-orbitals/transforms.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject, PluginStateTransform } from '../../mol-plugin-state/objects';
|
||||
import { createSphericalCollocationGrid } from './orbitals';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Task } from '../../mol-task';
|
||||
import { CustomProperties } from '../../mol-model/custom-property';
|
||||
import { SphericalBasisOrder } from './spherical-functions';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { Theme } from '../../mol-theme/theme';
|
||||
import { VolumeRepresentation3DHelpers } from '../../mol-plugin-state/transforms/representation';
|
||||
import { AlphaOrbital, Basis, CubeGrid } from './data-model';
|
||||
import { createSphericalCollocationDensityGrid } from './density';
|
||||
import { Tensor } from '../../mol-math/linear-algebra';
|
||||
|
||||
export class BasisAndOrbitals extends PluginStateObject.Create<{ basis: Basis, order: SphericalBasisOrder, orbitals: AlphaOrbital[] }>({ name: 'Basis', typeClass: 'Object' }) { }
|
||||
|
||||
export const StaticBasisAndOrbitals = PluginStateTransform.BuiltIn({
|
||||
name: 'static-basis-and-orbitals',
|
||||
display: 'Basis and Orbitals',
|
||||
from: PluginStateObject.Root,
|
||||
to: BasisAndOrbitals,
|
||||
params: {
|
||||
label: PD.Text('Orbital Data', { isHidden: true }),
|
||||
basis: PD.Value<Basis>(void 0 as any, { isHidden: true }),
|
||||
order: PD.Text<SphericalBasisOrder>('gaussian' as SphericalBasisOrder, { isHidden: true }),
|
||||
orbitals: PD.Value<AlphaOrbital[]>([], { isHidden: true })
|
||||
},
|
||||
})({
|
||||
apply({ params }) {
|
||||
return new BasisAndOrbitals({ basis: params.basis, order: params.order, orbitals: params.orbitals }, { label: params.label });
|
||||
}
|
||||
});
|
||||
|
||||
const CreateOrbitalVolumeParamBase = {
|
||||
cutoffThreshold: PD.Numeric(0.0015, { min: 0, max: 0.1, step: 0.0001 }),
|
||||
boxExpand: PD.Numeric(4.5, { min: 0, max: 7, step: 0.1 }),
|
||||
gridSpacing: PD.ObjectList({ atomCount: PD.Numeric(0), spacing: PD.Numeric(0.35, { min: 0.1, max: 2, step: 0.01 }) }, e => `Atoms ${e.atomCount}: ${e.spacing}`, {
|
||||
defaultValue: [
|
||||
{ atomCount: 55, spacing: 0.5 },
|
||||
{ atomCount: 40, spacing: 0.45 },
|
||||
{ atomCount: 25, spacing: 0.4 },
|
||||
{ atomCount: 0, spacing: 0.35 },
|
||||
]
|
||||
}),
|
||||
clampValues: PD.MappedStatic('off', {
|
||||
off: PD.EmptyGroup(),
|
||||
on: PD.Group({
|
||||
sigma: PD.Numeric(8, { min: 1, max: 20 }, { description: 'Clamp values to range [sigma * negIsoValue, sigma * posIsoValue].' })
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
function clampData(matrix: Tensor.Data, min: number, max: number) {
|
||||
for (let i = 0, _i = matrix.length; i < _i; i++) {
|
||||
const v = matrix[i];
|
||||
if (v < min) matrix[i] = min;
|
||||
else if (v > max) matrix[i] = max;
|
||||
}
|
||||
}
|
||||
|
||||
function clampGrid(data: CubeGrid, v: number) {
|
||||
const grid = data.grid;
|
||||
const min = (data.isovalues?.negative ?? data.grid.stats.min) * v;
|
||||
const max = (data.isovalues?.positive ?? data.grid.stats.max) * v;
|
||||
|
||||
// clamp values for better direct volume resolution
|
||||
// current implementation uses Byte array for values
|
||||
// if this is not enough, update mol* to use float
|
||||
// textures instead
|
||||
if (grid.stats.min < min || grid.stats.max > max) {
|
||||
clampData(data.grid.cells.data, min, max);
|
||||
if (grid.stats.min < min) {
|
||||
(grid.stats.min as number) = min;
|
||||
}
|
||||
if (grid.stats.max > max) {
|
||||
(grid.stats.max as number) = max;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
|
||||
name: 'create-orbital-volume',
|
||||
display: 'Orbital Volume',
|
||||
from: BasisAndOrbitals,
|
||||
to: PluginStateObject.Volume.Data,
|
||||
params: (a) => {
|
||||
if (!a) {
|
||||
return { index: PD.Numeric(0), ...CreateOrbitalVolumeParamBase };
|
||||
}
|
||||
|
||||
return {
|
||||
index: PD.Select(0, a.data.orbitals.map((o, i) => [i, `[${i + 1}] ${o.energy.toFixed(4)}`])),
|
||||
...CreateOrbitalVolumeParamBase
|
||||
};
|
||||
}
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Orbital Volume', async ctx => {
|
||||
const data = await createSphericalCollocationGrid({
|
||||
basis: a.data.basis,
|
||||
cutoffThreshold: params.cutoffThreshold,
|
||||
sphericalOrder: a.data.order,
|
||||
boxExpand: params.boxExpand,
|
||||
gridSpacing: params.gridSpacing.map(e => [e.atomCount, e.spacing] as [number, number])
|
||||
}, a.data.orbitals[params.index], plugin.canvas3d?.webgl).runInContext(ctx);
|
||||
const volume: Volume = {
|
||||
grid: data.grid,
|
||||
sourceData: { name: 'custom grid', kind: 'alpha-orbitals', data },
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
};
|
||||
|
||||
if (params.clampValues?.name === 'on') {
|
||||
clampGrid(data, params.clampValues?.params?.sigma ?? 8);
|
||||
}
|
||||
|
||||
return new PluginStateObject.Volume.Data(volume, { label: 'Orbital Volume' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const CreateOrbitalDensityVolume = PluginStateTransform.BuiltIn({
|
||||
name: 'create-orbital-density-volume',
|
||||
display: 'Orbital Density Volume',
|
||||
from: BasisAndOrbitals,
|
||||
to: PluginStateObject.Volume.Data,
|
||||
params: CreateOrbitalVolumeParamBase
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Orbital Volume', async ctx => {
|
||||
const data = await createSphericalCollocationDensityGrid({
|
||||
basis: a.data.basis,
|
||||
cutoffThreshold: params.cutoffThreshold,
|
||||
sphericalOrder: a.data.order,
|
||||
boxExpand: params.boxExpand,
|
||||
gridSpacing: params.gridSpacing.map(e => [e.atomCount, e.spacing] as [number, number])
|
||||
}, a.data.orbitals, plugin.canvas3d?.webgl).runInContext(ctx);
|
||||
const volume: Volume = {
|
||||
grid: data.grid,
|
||||
sourceData: { name: 'custom grid', kind: 'alpha-orbitals', data },
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
};
|
||||
|
||||
if (params.clampValues?.name === 'on') {
|
||||
clampGrid(data, params.clampValues?.params?.sigma ?? 8);
|
||||
}
|
||||
|
||||
return new PluginStateObject.Volume.Data(volume, { label: 'Orbital Volume' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const CreateOrbitalRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
name: 'create-orbital-representation-3d',
|
||||
display: 'Orbital Representation 3D',
|
||||
from: PluginStateObject.Volume.Data,
|
||||
to: PluginStateObject.Volume.Representation3D,
|
||||
params: {
|
||||
directVolume: PD.Boolean(false),
|
||||
relativeIsovalue: PD.Numeric(1, { min: 0.01, max: 5, step: 0.01 }),
|
||||
kind: PD.Select<'positive' | 'negative'>('positive', [['positive', 'Positive'], ['negative', 'Negative']]),
|
||||
color: PD.Color(ColorNames.blue),
|
||||
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
xrayShaded: PD.Boolean(false),
|
||||
pickable: PD.Boolean(true)
|
||||
}
|
||||
})({
|
||||
canAutoUpdate() {
|
||||
return true;
|
||||
},
|
||||
apply({ a, params: srcParams }, plugin: PluginContext) {
|
||||
return Task.create('Orbitals Representation 3D', async ctx => {
|
||||
const params = volumeParams(plugin, a, srcParams);
|
||||
|
||||
const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
|
||||
const provider = plugin.representation.volume.registry.get(params.type.name);
|
||||
if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
|
||||
const props = params.type.params || {};
|
||||
const repr = provider.factory({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.volume.themes }, provider.getParams);
|
||||
repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: a.data }, params));
|
||||
await repr.createOrUpdate(props, a.data).runInContext(ctx);
|
||||
repr.setState({ pickable: srcParams.pickable });
|
||||
return new PluginStateObject.Volume.Representation3D({ repr, source: a }, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) });
|
||||
});
|
||||
},
|
||||
update({ a, b, newParams: srcParams }, plugin: PluginContext) {
|
||||
return Task.create('Orbitals Representation 3D', async ctx => {
|
||||
const newParams = volumeParams(plugin, a, srcParams);
|
||||
|
||||
const props = { ...b.data.repr.props, ...newParams.type.params };
|
||||
b.data.repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: a.data }, newParams));
|
||||
await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
|
||||
b.data.repr.setState({ pickable: srcParams.pickable });
|
||||
b.description = VolumeRepresentation3DHelpers.getDescription(props);
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function volumeParams(plugin: PluginContext, volume: PluginStateObject.Volume.Data, params: StateTransformer.Params<typeof CreateOrbitalRepresentation3D>) {
|
||||
if (volume.data.sourceData.kind !== 'alpha-orbitals') throw new Error('Invalid data source kind.');
|
||||
|
||||
const { isovalues } = volume.data.sourceData.data as CubeGrid;
|
||||
if (!isovalues) throw new Error('Isovalues are not computed.');
|
||||
|
||||
const value = isovalues[params.kind];
|
||||
|
||||
return createVolumeRepresentationParams(plugin, volume.data, params.directVolume ? {
|
||||
type: 'direct-volume',
|
||||
typeParams: {
|
||||
alpha: params.alpha,
|
||||
renderMode: {
|
||||
name: 'isosurface',
|
||||
params: { isoValue: { kind: 'absolute', absoluteValue: (value ?? 1000) * params.relativeIsovalue }, singleLayer: false }
|
||||
},
|
||||
xrayShaded: params.xrayShaded
|
||||
},
|
||||
color: 'uniform',
|
||||
colorParams: { value: params.color }
|
||||
} : {
|
||||
type: 'isosurface',
|
||||
typeParams: { isoValue: { kind: 'absolute', absoluteValue: (value ?? 1000) * params.relativeIsovalue }, alpha: params.alpha, xrayShaded: params.xrayShaded },
|
||||
color: 'uniform',
|
||||
colorParams: { value: params.color }
|
||||
});
|
||||
}
|
||||
370
src/extensions/anvil/algorithm.ts
Normal file
370
src/extensions/anvil/algorithm.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Structure, StructureElement, StructureProperties } from '../../mol-model/structure';
|
||||
import { Task, RuntimeContext } from '../../mol-task';
|
||||
import { CentroidHelper } from '../../mol-math/geometry/centroid-helper';
|
||||
import { AccessibleSurfaceAreaProvider } from '../../mol-model-props/computed/accessible-surface-area';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { getElementMoleculeType } from '../../mol-model/structure/util';
|
||||
import { MoleculeType } from '../../mol-model/structure/model/types';
|
||||
import { AccessibleSurfaceArea } from '../../mol-model-props/computed/accessible-surface-area/shrake-rupley';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { MembraneOrientation } from './prop';
|
||||
|
||||
interface ANVILContext {
|
||||
structure: Structure,
|
||||
|
||||
numberOfSpherePoints: number,
|
||||
stepSize: number,
|
||||
minThickness: number,
|
||||
maxThickness: number,
|
||||
asaCutoff: number,
|
||||
|
||||
offsets: ArrayLike<number>,
|
||||
exposed: ArrayLike<boolean>,
|
||||
centroid: Vec3,
|
||||
extent: number
|
||||
};
|
||||
|
||||
export const ANVILParams = {
|
||||
numberOfSpherePoints: PD.Numeric(120, { min: 35, max: 700, step: 1 }, { description: 'Number of spheres/directions to test for membrane placement. Original value is 350.' }),
|
||||
stepSize: PD.Numeric(1, { min: 0.25, max: 4, step: 0.25 }, { description: 'Thickness of membrane slices that will be tested' }),
|
||||
minThickness: PD.Numeric(20, { min: 10, max: 30, step: 1}, { description: 'Minimum membrane thickness used during refinement' }),
|
||||
maxThickness: PD.Numeric(40, { min: 30, max: 50, step: 1}, { description: 'Maximum membrane thickness used during refinement' }),
|
||||
asaCutoff: PD.Numeric(40, { min: 10, max: 100, step: 1 }, { description: 'Absolute ASA cutoff above which residues will be considered' })
|
||||
};
|
||||
export type ANVILParams = typeof ANVILParams
|
||||
export type ANVILProps = PD.Values<ANVILParams>
|
||||
|
||||
/**
|
||||
* Implements:
|
||||
* Membrane positioning for high- and low-resolution protein structures through a binary classification approach
|
||||
* Guillaume Postic, Yassine Ghouzam, Vincent Guiraud, and Jean-Christophe Gelly
|
||||
* Protein Engineering, Design & Selection, 2015, 1–5
|
||||
* doi: 10.1093/protein/gzv063
|
||||
*/
|
||||
export function computeANVIL(structure: Structure, props: ANVILProps) {
|
||||
return Task.create('Compute Membrane Orientation', async runtime => {
|
||||
return await calculate(runtime, structure, props);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const centroidHelper = new CentroidHelper();
|
||||
function initialize(structure: Structure, props: ANVILProps): ANVILContext {
|
||||
const l = StructureElement.Location.create(structure);
|
||||
const { label_atom_id, x, y, z } = StructureProperties.atom;
|
||||
const elementCount = structure.polymerResidueCount;
|
||||
centroidHelper.reset();
|
||||
|
||||
let offsets = new Int32Array(elementCount);
|
||||
let exposed = new Array<boolean>(elementCount);
|
||||
|
||||
const accessibleSurfaceArea = structure && AccessibleSurfaceAreaProvider.get(structure);
|
||||
const asa = accessibleSurfaceArea.value!;
|
||||
|
||||
const vec = Vec3();
|
||||
let m = 0;
|
||||
for (let i = 0, il = structure.units.length; i < il; ++i) {
|
||||
const unit = structure.units[i];
|
||||
const { elements } = unit;
|
||||
l.unit = unit;
|
||||
|
||||
for (let j = 0, jl = elements.length; j < jl; ++j) {
|
||||
const eI = elements[j];
|
||||
l.element = eI;
|
||||
|
||||
// consider only amino acids
|
||||
if (getElementMoleculeType(unit, eI) !== MoleculeType.Protein) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// only CA is considered for downstream operations
|
||||
if (label_atom_id(l) !== 'CA') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// while iterating use first pass to compute centroid
|
||||
Vec3.set(vec, x(l), y(l), z(l));
|
||||
centroidHelper.includeStep(vec);
|
||||
|
||||
// keep track of offsets and exposed state to reuse
|
||||
offsets[m] = structure.serialMapping.getSerialIndex(l.unit, l.element);
|
||||
exposed[m] = AccessibleSurfaceArea.getValue(l, asa) > props.asaCutoff;
|
||||
|
||||
m++;
|
||||
}
|
||||
}
|
||||
|
||||
// omit potentially empty tail1
|
||||
offsets = offsets.slice(0, m);
|
||||
exposed = exposed.slice(0, m);
|
||||
|
||||
// calculate centroid and extent
|
||||
centroidHelper.finishedIncludeStep();
|
||||
const centroid = centroidHelper.center;
|
||||
for (let k = 0, kl = offsets.length; k < kl; k++) {
|
||||
setLocation(l, structure, offsets[k]);
|
||||
Vec3.set(vec, x(l), y(l), z(l));
|
||||
centroidHelper.radiusStep(vec);
|
||||
}
|
||||
const extent = 1.2 * Math.sqrt(centroidHelper.radiusSq);
|
||||
|
||||
return {
|
||||
...props,
|
||||
structure: structure,
|
||||
|
||||
offsets: offsets,
|
||||
exposed: exposed,
|
||||
centroid: centroid,
|
||||
extent: extent
|
||||
};
|
||||
}
|
||||
|
||||
export async function calculate(runtime: RuntimeContext, structure: Structure, params: ANVILProps): Promise<MembraneOrientation> {
|
||||
const { label_comp_id } = StructureProperties.atom;
|
||||
|
||||
const ctx = initialize(structure, params);
|
||||
const initialHphobHphil = HphobHphil.filtered(ctx, label_comp_id);
|
||||
|
||||
const initialMembrane = findMembrane(ctx, generateSpherePoints(ctx, ctx.numberOfSpherePoints), initialHphobHphil, label_comp_id);
|
||||
const alternativeMembrane = findMembrane(ctx, findProximateAxes(ctx, initialMembrane), initialHphobHphil, label_comp_id);
|
||||
|
||||
const membrane = initialMembrane.qmax! > alternativeMembrane.qmax! ? initialMembrane : alternativeMembrane;
|
||||
|
||||
return {
|
||||
planePoint1: membrane.planePoint1,
|
||||
planePoint2: membrane.planePoint2,
|
||||
normalVector: membrane.normalVector!,
|
||||
radius: ctx.extent,
|
||||
centroid: ctx.centroid
|
||||
};
|
||||
}
|
||||
|
||||
interface MembraneCandidate {
|
||||
planePoint1: Vec3,
|
||||
planePoint2: Vec3,
|
||||
stats: HphobHphil,
|
||||
normalVector?: Vec3,
|
||||
spherePoint?: Vec3,
|
||||
qmax?: number
|
||||
}
|
||||
|
||||
namespace MembraneCandidate {
|
||||
export function initial(c1: Vec3, c2: Vec3, stats: HphobHphil): MembraneCandidate {
|
||||
return {
|
||||
planePoint1: c1,
|
||||
planePoint2: c2,
|
||||
stats: stats
|
||||
};
|
||||
}
|
||||
|
||||
export function scored(spherePoint: Vec3, c1: Vec3, c2: Vec3, stats: HphobHphil, qmax: number, centroid: Vec3): MembraneCandidate {
|
||||
const diam_vect = Vec3();
|
||||
Vec3.sub(diam_vect, centroid, spherePoint);
|
||||
return {
|
||||
planePoint1: c1,
|
||||
planePoint2: c2,
|
||||
stats: stats,
|
||||
normalVector: diam_vect,
|
||||
spherePoint: spherePoint,
|
||||
qmax: qmax
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function findMembrane(ctx: ANVILContext, spherePoints: Vec3[], initialStats: HphobHphil, label_comp_id: StructureElement.Property<string>): MembraneCandidate {
|
||||
const { centroid, stepSize, minThickness, maxThickness } = ctx;
|
||||
// best performing membrane
|
||||
let membrane: MembraneCandidate;
|
||||
// score of the best performing membrane
|
||||
let qmax = 0;
|
||||
|
||||
// construct slices of thickness 1.0 along the axis connecting the centroid and the spherePoint
|
||||
const diam = Vec3();
|
||||
for (let i = 0, il = spherePoints.length; i < il; i++) {
|
||||
const spherePoint = spherePoints[i];
|
||||
Vec3.sub(diam, centroid, spherePoint);
|
||||
Vec3.scale(diam, diam, 2);
|
||||
const diamNorm = Vec3.magnitude(diam);
|
||||
const qvartemp = [];
|
||||
|
||||
for (let i = 0, il = diamNorm - stepSize; i < il; i += stepSize) {
|
||||
const c1 = Vec3();
|
||||
const c2 = Vec3();
|
||||
Vec3.scaleAndAdd(c1, spherePoint, diam, i / diamNorm);
|
||||
Vec3.scaleAndAdd(c2, spherePoint, diam, (i + stepSize) / diamNorm);
|
||||
|
||||
// evaluate how well this membrane slice embeddeds the peculiar residues
|
||||
const stats = HphobHphil.filtered(ctx, label_comp_id, (testPoint: Vec3) => isInMembranePlane(testPoint, diam, c1, c2));
|
||||
qvartemp.push(MembraneCandidate.initial(c1, c2, stats));
|
||||
}
|
||||
|
||||
let jmax = (minThickness / stepSize) - 1;
|
||||
|
||||
for (let width = 0, widthl = maxThickness; width < widthl;) {
|
||||
const imax = qvartemp.length - 1 - jmax;
|
||||
|
||||
for (let i = 0, il = imax; i < il; i++) {
|
||||
const c1 = qvartemp[i].planePoint1;
|
||||
const c2 = qvartemp[i + jmax].planePoint2;
|
||||
|
||||
let hphob = 0;
|
||||
let hphil = 0;
|
||||
let total = 0;
|
||||
for (let j = 0; j < jmax; j++) {
|
||||
const ij = qvartemp[i + j];
|
||||
if (j === 0 || j === jmax - 1) {
|
||||
hphob += 0.5 * ij.stats.hphob;
|
||||
hphil += 0.5 * ij.stats.hphil;
|
||||
} else {
|
||||
hphob += ij.stats.hphob;
|
||||
hphil += ij.stats.hphil;
|
||||
}
|
||||
total += ij.stats.total;
|
||||
}
|
||||
|
||||
const stats = HphobHphil.of(hphob, hphil, total);
|
||||
|
||||
if (hphob !== 0) {
|
||||
const qvaltest = qValue(stats, initialStats);
|
||||
if (qvaltest > qmax) {
|
||||
qmax = qvaltest;
|
||||
membrane = MembraneCandidate.scored(spherePoint, c1, c2, HphobHphil.of(hphob, hphil, total), qmax, centroid);
|
||||
}
|
||||
}
|
||||
}
|
||||
jmax++;
|
||||
width = (jmax + 1) * stepSize;
|
||||
}
|
||||
}
|
||||
|
||||
return membrane!;
|
||||
}
|
||||
|
||||
function qValue(currentStats: HphobHphil, initialStats: HphobHphil): number {
|
||||
if(initialStats.hphob < 1) {
|
||||
initialStats.hphob = 0.1;
|
||||
}
|
||||
|
||||
if(initialStats.hphil < 1) {
|
||||
initialStats.hphil += 1;
|
||||
}
|
||||
|
||||
const part_tot = currentStats.hphob + currentStats.hphil;
|
||||
return (currentStats.hphob * (initialStats.hphil - currentStats.hphil) - currentStats.hphil * (initialStats.hphob - currentStats.hphob)) /
|
||||
Math.sqrt(part_tot * initialStats.hphob * initialStats.hphil * (initialStats.hphob + initialStats.hphil - part_tot));
|
||||
}
|
||||
|
||||
export function isInMembranePlane(testPoint: Vec3, normalVector: Vec3, planePoint1: Vec3, planePoint2: Vec3): boolean {
|
||||
const d1 = -Vec3.dot(normalVector, planePoint1);
|
||||
const d2 = -Vec3.dot(normalVector, planePoint2);
|
||||
const d = -Vec3.dot(normalVector, testPoint);
|
||||
return d > Math.min(d1, d2) && d < Math.max(d1, d2);
|
||||
}
|
||||
|
||||
// generates a defined number of points on a sphere with radius = extent around the specified centroid
|
||||
function generateSpherePoints(ctx: ANVILContext, numberOfSpherePoints: number): Vec3[] {
|
||||
const { centroid, extent } = ctx;
|
||||
const points = [];
|
||||
let oldPhi = 0, h, theta, phi;
|
||||
for(let k = 1, kl = numberOfSpherePoints + 1; k < kl; k++) {
|
||||
h = -1 + 2 * (k - 1) / (numberOfSpherePoints - 1);
|
||||
theta = Math.acos(h);
|
||||
phi = (k === 1 || k === numberOfSpherePoints) ? 0 : (oldPhi + 3.6 / Math.sqrt(numberOfSpherePoints * (1 - h * h))) % (2 * Math.PI);
|
||||
|
||||
const point = Vec3.create(
|
||||
extent * Math.sin(phi) * Math.sin(theta) + centroid[0],
|
||||
extent * Math.cos(theta) + centroid[1],
|
||||
extent * Math.cos(phi) * Math.sin(theta) + centroid[2]
|
||||
);
|
||||
points[k - 1] = point;
|
||||
oldPhi = phi;
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
// generates sphere points close to that of the initial membrane
|
||||
function findProximateAxes(ctx: ANVILContext, membrane: MembraneCandidate): Vec3[] {
|
||||
const { numberOfSpherePoints, extent } = ctx;
|
||||
const points = generateSpherePoints(ctx, 30000);
|
||||
let j = 4;
|
||||
let sphere_pts2: Vec3[] = [];
|
||||
while (sphere_pts2.length < numberOfSpherePoints) {
|
||||
const d = 2 * extent / numberOfSpherePoints + j;
|
||||
const dsq = d * d;
|
||||
sphere_pts2 = [];
|
||||
for (let i = 0, il = points.length; i < il; i++) {
|
||||
if (Vec3.squaredDistance(points[i], membrane.spherePoint!) < dsq) {
|
||||
sphere_pts2.push(points[i]);
|
||||
}
|
||||
}
|
||||
j += 0.2;
|
||||
}
|
||||
return sphere_pts2;
|
||||
}
|
||||
|
||||
interface HphobHphil {
|
||||
hphob: number,
|
||||
hphil: number,
|
||||
total: number
|
||||
}
|
||||
|
||||
namespace HphobHphil {
|
||||
export function of(hphob: number, hphil: number, total?: number) {
|
||||
return {
|
||||
hphob: hphob,
|
||||
hphil: hphil,
|
||||
total: !!total ? total : hphob + hphil
|
||||
};
|
||||
}
|
||||
|
||||
const testPoint = Vec3();
|
||||
export function filtered(ctx: ANVILContext, label_comp_id: StructureElement.Property<string>, filter?: (test: Vec3) => boolean): HphobHphil {
|
||||
const { offsets, exposed, structure } = ctx;
|
||||
const l = StructureElement.Location.create(structure);
|
||||
const { x, y, z } = StructureProperties.atom;
|
||||
let hphob = 0;
|
||||
let hphil = 0;
|
||||
for (let k = 0, kl = offsets.length; k < kl; k++) {
|
||||
// ignore buried residues
|
||||
if (!exposed[k]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setLocation(l, structure, offsets[k]);
|
||||
Vec3.set(testPoint, x(l), y(l), z(l));
|
||||
|
||||
// testPoints have to be in putative membrane layer
|
||||
if (filter && !filter(testPoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isHydrophobic(label_comp_id(l))) {
|
||||
hphob++;
|
||||
} else {
|
||||
hphil++;
|
||||
}
|
||||
}
|
||||
return of(hphob, hphil);
|
||||
}
|
||||
}
|
||||
|
||||
// ANVIL-specific (not general) definition of membrane-favoring amino acids
|
||||
const HYDROPHOBIC_AMINO_ACIDS = new Set(['ALA', 'CYS', 'GLY', 'HIS', 'ILE', 'LEU', 'MET', 'PHE', 'SER', 'THR', 'VAL']);
|
||||
export function isHydrophobic(label_comp_id: string): boolean {
|
||||
return HYDROPHOBIC_AMINO_ACIDS.has(label_comp_id);
|
||||
}
|
||||
|
||||
function setLocation(l: StructureElement.Location, structure: Structure, serialIndex: number) {
|
||||
l.structure = structure;
|
||||
l.unit = structure.units[structure.serialMapping.unitIndices[serialIndex]];
|
||||
l.element = structure.serialMapping.elementIndices[serialIndex];
|
||||
return l;
|
||||
}
|
||||
174
src/extensions/anvil/behavior.ts
Normal file
174
src/extensions/anvil/behavior.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { MembraneOrientationProvider, MembraneOrientation } from './prop';
|
||||
import { StateObjectRef, StateTransformer, StateTransform } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior';
|
||||
import { MembraneOrientationRepresentationProvider, MembraneOrientationParams, MembraneOrientationRepresentation } from './representation';
|
||||
import { HydrophobicityColorThemeProvider } from '../../mol-theme/color/hydrophobicity';
|
||||
import { PluginStateObject, PluginStateTransform } from '../../mol-plugin-state/objects';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { DefaultQueryRuntimeTable } from '../../mol-script/runtime/query/compiler';
|
||||
import { StructureSelectionQuery, StructureSelectionCategory } from '../../mol-plugin-state/helpers/structure-selection-query';
|
||||
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
|
||||
import { GenericRepresentationRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
|
||||
|
||||
const Tag = MembraneOrientation.Tag;
|
||||
|
||||
export const ANVILMembraneOrientation = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
name: 'anvil-membrane-orientation-prop',
|
||||
category: 'custom-props',
|
||||
display: {
|
||||
name: 'Membrane Orientation',
|
||||
description: 'Data calculated with ANVIL algorithm.'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
|
||||
private provider = MembraneOrientationProvider
|
||||
|
||||
register(): void {
|
||||
DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);
|
||||
|
||||
this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach);
|
||||
|
||||
this.ctx.representation.structure.registry.add(MembraneOrientationRepresentationProvider);
|
||||
this.ctx.query.structure.registry.add(isTransmembrane);
|
||||
|
||||
this.ctx.genericRepresentationControls.set(Tag.Representation, selection => {
|
||||
const refs: GenericRepresentationRef[] = [];
|
||||
selection.structures.forEach(structure => {
|
||||
const memRepr = structure.genericRepresentations?.filter(r => r.cell.transform.transformer.id === MembraneOrientation3D.id)[0];
|
||||
if (memRepr) refs.push(memRepr);
|
||||
});
|
||||
return [refs, 'Membrane Orientation'];
|
||||
});
|
||||
this.ctx.builders.structure.representation.registerPreset(MembraneOrientationPreset);
|
||||
}
|
||||
|
||||
update(p: { autoAttach: boolean }) {
|
||||
let updated = this.params.autoAttach !== p.autoAttach;
|
||||
this.params.autoAttach = p.autoAttach;
|
||||
this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
|
||||
return updated;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor);
|
||||
|
||||
this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
|
||||
|
||||
this.ctx.representation.structure.registry.remove(MembraneOrientationRepresentationProvider);
|
||||
this.ctx.query.structure.registry.remove(isTransmembrane);
|
||||
|
||||
this.ctx.genericRepresentationControls.delete(Tag.Representation);
|
||||
this.ctx.builders.structure.representation.unregisterPreset(MembraneOrientationPreset);
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(false)
|
||||
})
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
export const isTransmembrane = StructureSelectionQuery('Residues Embedded in Membrane', MS.struct.modifier.union([
|
||||
MS.struct.modifier.wholeResidues([
|
||||
MS.struct.modifier.union([
|
||||
MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
|
||||
'atom-test': MembraneOrientation.symbols.isTransmembrane.symbol(),
|
||||
})
|
||||
])
|
||||
])
|
||||
]), {
|
||||
description: 'Select residues that are embedded between the membrane layers.',
|
||||
category: StructureSelectionCategory.Residue,
|
||||
ensureCustomProperties: (ctx, structure) => {
|
||||
return MembraneOrientationProvider.attach(ctx, structure);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
export { MembraneOrientation3D };
|
||||
|
||||
type MembraneOrientation3D = typeof MembraneOrientation3D
|
||||
const MembraneOrientation3D = PluginStateTransform.BuiltIn({
|
||||
name: 'membrane-orientation-3d',
|
||||
display: {
|
||||
name: 'Membrane Orientation',
|
||||
description: 'Membrane Orientation planes and rims. Data calculated with ANVIL algorithm.'
|
||||
},
|
||||
from: PluginStateObject.Molecule.Structure,
|
||||
to: PluginStateObject.Shape.Representation3D,
|
||||
params: (a) => {
|
||||
return {
|
||||
...MembraneOrientationParams,
|
||||
};
|
||||
}
|
||||
})({
|
||||
canAutoUpdate({ oldParams, newParams }) {
|
||||
return true;
|
||||
},
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Membrane Orientation', async ctx => {
|
||||
await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
|
||||
const repr = MembraneOrientationRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => MembraneOrientationParams);
|
||||
await repr.createOrUpdate(params, a.data).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Representation3D({ repr, source: a }, { label: 'Membrane Orientation' });
|
||||
});
|
||||
},
|
||||
update({ a, b, newParams }, plugin: PluginContext) {
|
||||
return Task.create('Membrane Orientation', async ctx => {
|
||||
await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
|
||||
const props = { ...b.data.repr.props, ...newParams };
|
||||
await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
});
|
||||
},
|
||||
isApplicable(a) {
|
||||
return MembraneOrientationProvider.isApplicable(a.data);
|
||||
}
|
||||
});
|
||||
|
||||
export const MembraneOrientationPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-membrane-orientation',
|
||||
display: {
|
||||
name: 'Membrane Orientation', group: 'Annotation',
|
||||
description: 'Shows orientation of membrane layers. Data calculated with ANVIL algorithm.' // TODO add ' or obtained via RCSB PDB'
|
||||
},
|
||||
isApplicable(a) {
|
||||
return MembraneOrientationProvider.isApplicable(a.data);
|
||||
},
|
||||
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 (!MembraneOrientationProvider.get(structure).value) {
|
||||
await plugin.runTask(Task.create('Membrane Orientation', async runtime => {
|
||||
await MembraneOrientationProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure);
|
||||
}));
|
||||
}
|
||||
|
||||
const membraneOrientation = await tryCreateMembraneOrientation(plugin, structureCell);
|
||||
const colorTheme = HydrophobicityColorThemeProvider.name as any;
|
||||
const preset = await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin);
|
||||
|
||||
return { components: preset.components, representations: { ...preset.representations, membraneOrientation } };
|
||||
}
|
||||
});
|
||||
|
||||
export function tryCreateMembraneOrientation(plugin: PluginContext, structure: StateObjectRef<PluginStateObject.Molecule.Structure>, params?: StateTransformer.Params<MembraneOrientation3D>, initialState?: Partial<StateTransform.State>) {
|
||||
const state = plugin.state.data;
|
||||
const membraneOrientation = state.build().to(structure)
|
||||
.applyOrUpdateTagged('membrane-orientation-3d', MembraneOrientation3D, params, { state: initialState });
|
||||
return membraneOrientation.commit({ revertOnError: true });
|
||||
}
|
||||
82
src/extensions/anvil/prop.ts
Normal file
82
src/extensions/anvil/prop.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Structure, StructureProperties, Unit } from '../../mol-model/structure';
|
||||
import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
|
||||
import { ANVILParams, ANVILProps, computeANVIL, isInMembranePlane } from './algorithm';
|
||||
import { CustomStructureProperty } from '../../mol-model-props/common/custom-structure-property';
|
||||
import { CustomProperty } from '../../mol-model-props/common/custom-property';
|
||||
import { AccessibleSurfaceAreaProvider } from '../../mol-model-props/computed/accessible-surface-area';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { QuerySymbolRuntime } from '../../mol-script/runtime/query/base';
|
||||
import { CustomPropSymbol } from '../../mol-script/language/symbol';
|
||||
import { Type } from '../../mol-script/language/type';
|
||||
|
||||
export const MembraneOrientationParams = {
|
||||
...ANVILParams
|
||||
};
|
||||
export type MembraneOrientationParams = typeof MembraneOrientationParams
|
||||
export type MembraneOrientationProps = PD.Values<MembraneOrientationParams>
|
||||
|
||||
export { MembraneOrientation };
|
||||
|
||||
interface MembraneOrientation {
|
||||
// point in membrane boundary
|
||||
readonly planePoint1: Vec3,
|
||||
// point in opposite side of membrane boundary
|
||||
readonly planePoint2: Vec3,
|
||||
// normal vector of membrane layer
|
||||
readonly normalVector: Vec3,
|
||||
// the radius of the membrane layer
|
||||
readonly radius: number,
|
||||
readonly centroid: Vec3
|
||||
}
|
||||
|
||||
namespace MembraneOrientation {
|
||||
export enum Tag {
|
||||
Representation = 'membrane-orientation-3d'
|
||||
}
|
||||
|
||||
const pos = Vec3();
|
||||
export const symbols = {
|
||||
isTransmembrane: QuerySymbolRuntime.Dynamic(CustomPropSymbol('computed', 'membrane-orientation.is-transmembrane', Type.Bool),
|
||||
ctx => {
|
||||
const { unit, structure } = ctx.element;
|
||||
const { x, y, z } = StructureProperties.atom;
|
||||
if (!Unit.isAtomic(unit)) return 0;
|
||||
const membraneOrientation = MembraneOrientationProvider.get(structure).value;
|
||||
if (!membraneOrientation) return 0;
|
||||
Vec3.set(pos, x(ctx.element), y(ctx.element), z(ctx.element));
|
||||
const { normalVector, planePoint1, planePoint2 } = membraneOrientation!;
|
||||
return isInMembranePlane(pos, normalVector, planePoint1, planePoint2);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
export const MembraneOrientationProvider: CustomStructureProperty.Provider<MembraneOrientationParams, MembraneOrientation> = CustomStructureProperty.createProvider({
|
||||
label: 'Membrane Orientation',
|
||||
descriptor: CustomPropertyDescriptor({
|
||||
name: 'anvil_computed_membrane_orientation',
|
||||
symbols: MembraneOrientation.symbols,
|
||||
// TODO `cifExport`
|
||||
}),
|
||||
type: 'root',
|
||||
defaultParams: MembraneOrientationParams,
|
||||
getParams: (data: Structure) => MembraneOrientationParams,
|
||||
isApplicable: (data: Structure) => true,
|
||||
obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<MembraneOrientationProps>) => {
|
||||
const p = { ...PD.getDefaultValues(MembraneOrientationParams), ...props };
|
||||
return { value: await computeAnvil(ctx, data, p) };
|
||||
}
|
||||
});
|
||||
|
||||
async function computeAnvil(ctx: CustomProperty.Context, data: Structure, props: Partial<ANVILProps>): Promise<MembraneOrientation> {
|
||||
await AccessibleSurfaceAreaProvider.attach(ctx, data);
|
||||
const p = { ...PD.getDefaultValues(ANVILParams), ...props };
|
||||
return await computeANVIL(data, p).runInContext(ctx.runtime);
|
||||
}
|
||||
156
src/extensions/anvil/representation.ts
Normal file
156
src/extensions/anvil/representation.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../mol-repr/representation';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
|
||||
import { StructureRepresentationProvider, StructureRepresentation, StructureRepresentationStateBuilder } from '../../mol-repr/structure/representation';
|
||||
import { MembraneOrientation } from './prop';
|
||||
import { ThemeRegistryContext } from '../../mol-theme/theme';
|
||||
import { ShapeRepresentation } from '../../mol-repr/shape/representation';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Circle } from '../../mol-geo/primitive/circle';
|
||||
import { transformPrimitive } from '../../mol-geo/primitive/primitive';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { MembraneOrientationProvider } from './prop';
|
||||
import { MarkerActions } from '../../mol-util/marker-action';
|
||||
import { lociLabel } from '../../mol-theme/label';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { CustomProperty } from '../../mol-model-props/common/custom-property';
|
||||
|
||||
const SharedParams = {
|
||||
color: PD.Color(ColorNames.lightgrey),
|
||||
radiusFactor: PD.Numeric(0.8333, { min: 0.1, max: 3.0, step: 0.01 }, { description: 'Scale the radius of the membrane layer' })
|
||||
};
|
||||
|
||||
const BilayerSpheresParams = {
|
||||
...Spheres.Params,
|
||||
...SharedParams,
|
||||
sphereSize: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }, { description: 'Size of spheres that represent membrane planes' }),
|
||||
density: PD.Numeric(1, { min: 0.25, max: 10, step: 0.25 }, { description: 'Distance between spheres'})
|
||||
};
|
||||
export type BilayerSpheresParams = typeof BilayerSpheresParams
|
||||
export type BilayerSpheresProps = PD.Values<BilayerSpheresParams>
|
||||
|
||||
const BilayerPlanesParams = {
|
||||
...Mesh.Params,
|
||||
...SharedParams,
|
||||
sectorOpacity: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
|
||||
};
|
||||
export type BilayerPlanesParams = typeof BilayerPlanesParams
|
||||
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),
|
||||
};
|
||||
export type BilayerRimsParams = typeof BilayerRimsParams
|
||||
export type BilayerRimsProps = PD.Values<BilayerRimsParams>
|
||||
|
||||
const MembraneOrientationVisuals = {
|
||||
'bilayer-planes': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneOrientation, BilayerPlanesParams>) => ShapeRepresentation(getBilayerPlanes, Mesh.Utils, { modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }), modifyProps: p => ({ ...p, alpha: p.sectorOpacity, ignoreLight: true, doubleSided: false }) }),
|
||||
'bilayer-rims': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneOrientation, BilayerRimsParams>) => ShapeRepresentation(getBilayerRims, Lines.Utils, { modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }) })
|
||||
};
|
||||
|
||||
export const MembraneOrientationParams = {
|
||||
...BilayerSpheresParams,
|
||||
...BilayerPlanesParams,
|
||||
...BilayerRimsParams,
|
||||
visuals: PD.MultiSelect(['bilayer-planes', 'bilayer-rims'], PD.objectToOptions(MembraneOrientationVisuals)),
|
||||
};
|
||||
export type MembraneOrientationParams = typeof MembraneOrientationParams
|
||||
export type MembraneOrientationProps = PD.Values<MembraneOrientationParams>
|
||||
|
||||
export function getMembraneOrientationParams(ctx: ThemeRegistryContext, structure: Structure) {
|
||||
return PD.clone(MembraneOrientationParams);
|
||||
}
|
||||
|
||||
export type MembraneOrientationRepresentation = StructureRepresentation<MembraneOrientationParams>
|
||||
export function MembraneOrientationRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MembraneOrientationParams>): MembraneOrientationRepresentation {
|
||||
return Representation.createMulti('Membrane Orientation', ctx, getParams, StructureRepresentationStateBuilder, MembraneOrientationVisuals as unknown as Representation.Def<Structure, MembraneOrientationParams>);
|
||||
}
|
||||
|
||||
export const MembraneOrientationRepresentationProvider = StructureRepresentationProvider({
|
||||
name: 'membrane-orientation',
|
||||
label: 'Membrane Orientation',
|
||||
description: 'Displays a grid of points representing membrane layers.',
|
||||
factory: MembraneOrientationRepresentation,
|
||||
getParams: getMembraneOrientationParams,
|
||||
defaultValues: PD.getDefaultValues(MembraneOrientationParams),
|
||||
defaultColorTheme: { name: 'shape-group' },
|
||||
defaultSizeTheme: { name: 'shape-group' },
|
||||
isApplicable: (structure: Structure) => structure.elementCount > 0,
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, structure: Structure) => MembraneOrientationProvider.attach(ctx, structure, void 0, true),
|
||||
detach: (data) => MembraneOrientationProvider.ref(data, false)
|
||||
}
|
||||
});
|
||||
|
||||
function membraneLabel(data: Structure) {
|
||||
return `${lociLabel(Structure.Loci(data))} | Membrane Orientation`;
|
||||
}
|
||||
|
||||
function getBilayerRims(ctx: RuntimeContext, data: Structure, props: BilayerRimsProps, shape?: Shape<Lines>): Shape<Lines> {
|
||||
const { planePoint1: p1, planePoint2: p2, centroid, normalVector: normal, radius } = MembraneOrientationProvider.get(data).value!;
|
||||
const scaledRadius = props.radiusFactor * radius;
|
||||
const builder = LinesBuilder.create(128, 64, shape?.geometry);
|
||||
getLayerCircle(builder, p1, centroid, normal, scaledRadius, props);
|
||||
getLayerCircle(builder, p2, centroid, normal, scaledRadius, props);
|
||||
return Shape.create('Bilayer rims', data, builder.getLines(), () => props.color, () => props.linesSize, () => membraneLabel(data));
|
||||
}
|
||||
|
||||
function getLayerCircle(builder: LinesBuilder, p: Vec3, centroid: Vec3, normal: Vec3, radius: number, props: BilayerRimsProps, shape?: Shape<Lines>) {
|
||||
const circle = getCircle(p, centroid, normal, radius);
|
||||
const { indices, vertices } = circle;
|
||||
for (let j = 0, jl = indices.length; j < jl; j += 3) {
|
||||
if (props.dashedLines && j % 2 === 1) continue; // draw every other segment to get dashes
|
||||
const start = indices[j] * 3;
|
||||
const end = indices[j + 1] * 3;
|
||||
const startX = vertices[start];
|
||||
const startY = vertices[start + 1];
|
||||
const startZ = vertices[start + 2];
|
||||
const endX = vertices[end];
|
||||
const endY = vertices[end + 1];
|
||||
const endZ = vertices[end + 2];
|
||||
builder.add(startX, startY, startZ, endX, endY, endZ, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const tmpMat = Mat4();
|
||||
function getCircle(p: Vec3, centroid: Vec3, normal: Vec3, radius: number) {
|
||||
Mat4.targetTo(tmpMat, p, centroid, normal);
|
||||
Mat4.setTranslation(tmpMat, p);
|
||||
Mat4.mul(tmpMat, tmpMat, Mat4.rotX90);
|
||||
|
||||
const circle = Circle({ radius, segments: 64 });
|
||||
return transformPrimitive(circle, tmpMat);
|
||||
}
|
||||
|
||||
function getBilayerPlanes(ctx: RuntimeContext, data: Structure, props: BilayerPlanesProps, shape?: Shape<Mesh>): Shape<Mesh> {
|
||||
const { planePoint1: p1, planePoint2: p2, centroid, normalVector: normal, radius } = MembraneOrientationProvider.get(data).value!;
|
||||
const state = MeshBuilder.createState(128, 64, shape && shape.geometry);
|
||||
const scaledRadius = props.radiusFactor * radius;
|
||||
getLayerPlane(state, p1, centroid, normal, scaledRadius);
|
||||
getLayerPlane(state, p2, centroid, normal, scaledRadius);
|
||||
return Shape.create('Bilayer planes', data, MeshBuilder.getMesh(state), () => props.color, () => 1, () => membraneLabel(data));
|
||||
}
|
||||
|
||||
function getLayerPlane(state: MeshBuilder.State, p: Vec3, centroid: Vec3, normal: Vec3, radius: number) {
|
||||
const circle = getCircle(p, centroid, normal, radius);
|
||||
state.currentGroup = 0;
|
||||
MeshBuilder.addPrimitive(state, Mat4.id, circle);
|
||||
MeshBuilder.addPrimitiveFlipped(state, Mat4.id, circle);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -46,10 +46,9 @@ export function CellPackGenerateColorTheme(ctx: ThemeDataContext, props: PD.Valu
|
||||
name: 'generate',
|
||||
params: {
|
||||
hue, chroma: [30, 80], luminance: [15, 85],
|
||||
clusteringStepCount: 50, minSampleCount: 800, maxCount: 75,
|
||||
minLabel: 'Min', maxLabel: 'Max', valueLabel: (i: number) => `${i + 1}`,
|
||||
clusteringStepCount: 50, minSampleCount: 800, maxCount: 75
|
||||
}
|
||||
}});
|
||||
}}, { minLabel: 'Min', maxLabel: 'Max' });
|
||||
legend = palette.legend;
|
||||
const modelColor = new Map<number, Color>();
|
||||
for (let i = 0, il = models.length; i < il; ++i) {
|
||||
@@ -89,7 +88,6 @@ export const CellPackGenerateColorThemeProvider: ColorTheme.Provider<CellPackGen
|
||||
isApplicable: (ctx: ThemeDataContext) => {
|
||||
return (
|
||||
!!ctx.structure && ctx.structure.elementCount > 0 &&
|
||||
Model.TrajectoryInfo.get(ctx.structure.models[0]).size > 1 &&
|
||||
!!CellPackInfoProvider.get(ctx.structure).value
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ScaleLegend, TableLegend } from '../../../mol-util/legend';
|
||||
import { StructureElement, Model } from '../../../mol-model/structure';
|
||||
import { StructureElement, Model, Bond } from '../../../mol-model/structure';
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { CellPackInfoProvider } from '../property';
|
||||
|
||||
@@ -37,9 +37,12 @@ export function CellPackProvidedColorTheme(ctx: ThemeDataContext, props: PD.Valu
|
||||
}
|
||||
|
||||
color = (location: Location): Color => {
|
||||
return StructureElement.Location.is(location)
|
||||
? modelColor.get(Model.TrajectoryInfo.get(location.unit.model).index)!
|
||||
: DefaultColor;
|
||||
if (StructureElement.Location.is(location)) {
|
||||
return modelColor.get(Model.TrajectoryInfo.get(location.unit.model).index)!;
|
||||
} else if (Bond.isLocation(location)) {
|
||||
return modelColor.get(Model.TrajectoryInfo.get(location.aUnit.model).index)!;
|
||||
}
|
||||
return DefaultColor;
|
||||
};
|
||||
} else {
|
||||
color = () => DefaultColor;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -10,7 +10,7 @@ import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Ingredient, IngredientSource, CellPacking } from './data';
|
||||
import { getFromPdb, getFromCellPackDB, IngredientFiles, parseCif, parsePDBfile, getStructureMean, getFromOPM } from './util';
|
||||
import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit } from '../../mol-model/structure';
|
||||
import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure';
|
||||
import { trajectoryFromMmCIF, MmcifFormat } from '../../mol-model-formats/structure/mmcif';
|
||||
import { trajectoryFromPDB } from '../../mol-model-formats/structure/pdb';
|
||||
import { Mat4, Vec3, Quat } from '../../mol-math/linear-algebra';
|
||||
@@ -36,8 +36,8 @@ function getCellPackModelUrl(fileName: string, baseUrl: string) {
|
||||
}
|
||||
|
||||
class TrajectoryCache {
|
||||
private map = new Map<string, Model.Trajectory>();
|
||||
set(id: string, trajectory: Model.Trajectory) { this.map.set(id, trajectory); }
|
||||
private map = new Map<string, Trajectory>();
|
||||
set(id: string, trajectory: Trajectory) { this.map.set(id, trajectory); }
|
||||
get(id: string) { return this.map.get(id); }
|
||||
}
|
||||
|
||||
@@ -94,9 +94,9 @@ async function getModel(plugin: PluginContext, id: string, ingredient: Ingredien
|
||||
trajectory = await plugin.runTask(trajectoryFromMmCIF(data.mmcif));
|
||||
}
|
||||
}
|
||||
trajCache.set(id, trajectory);
|
||||
trajCache.set(id, trajectory!);
|
||||
}
|
||||
const model = trajectory[modelIndex];
|
||||
const model = await plugin.resolveTask(trajectory?.getFrameAtIndex(modelIndex)!);
|
||||
return { model, assets };
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ async function getCurve(plugin: PluginContext, name: string, ingredient: Ingredi
|
||||
const curveModelTask = Task.create('Curve Model', async ctx => {
|
||||
const format = MmcifFormat.fromFrame(cif);
|
||||
const models = await createModels(format.data.db, format, ctx);
|
||||
return models[0];
|
||||
return models.representative;
|
||||
});
|
||||
|
||||
const curveModel = await plugin.runTask(curveModelTask);
|
||||
@@ -396,21 +396,25 @@ export function createStructureFromCellPack(plugin: PluginContext, packing: Cell
|
||||
}
|
||||
|
||||
if (ctx.shouldUpdate) await ctx.update(`${name} - units`);
|
||||
const builder = Structure.Builder({ label: name });
|
||||
const units: Unit[] = [];
|
||||
let offsetInvariantId = 0;
|
||||
let offsetChainGroupId = 0;
|
||||
for (const s of structures) {
|
||||
if (ctx.shouldUpdate) await ctx.update(`${s.label}`);
|
||||
let maxInvariantId = 0;
|
||||
let maxChainGroupId = 0;
|
||||
for (const u of s.units) {
|
||||
const invariantId = u.invariantId + offsetInvariantId;
|
||||
const chainGroupId = u.chainGroupId + offsetChainGroupId;
|
||||
if (u.invariantId > maxInvariantId) maxInvariantId = u.invariantId;
|
||||
builder.addUnit(u.kind, u.model, u.conformation.operator, u.elements, Unit.Trait.None, invariantId);
|
||||
units.push(Unit.create(units.length, invariantId, chainGroupId, u.traits, u.kind, u.model, u.conformation.operator, u.elements, u.props));
|
||||
}
|
||||
offsetInvariantId += maxInvariantId + 1;
|
||||
offsetChainGroupId += maxChainGroupId + 1;
|
||||
}
|
||||
|
||||
if (ctx.shouldUpdate) await ctx.update(`${name} - structure`);
|
||||
const structure = builder.getStructure();
|
||||
const structure = new Structure(units);
|
||||
for( let i = 0, il = structure.models.length; i < il; ++i) {
|
||||
Model.TrajectoryInfo.set(structure.models[i], { size: il, index: i });
|
||||
}
|
||||
@@ -442,9 +446,9 @@ async function handleHivRna(plugin: PluginContext, packings: CellPacking[], base
|
||||
|
||||
async function loadMembrane(plugin: PluginContext, name: string, state: State, params: LoadCellPackModelParams) {
|
||||
let file: Asset.File | undefined = undefined;
|
||||
if (params.ingredients.files !== null) {
|
||||
if (params.ingredients !== null) {
|
||||
const fileName = `${name}.bcif`;
|
||||
for (const f of params.ingredients.files) {
|
||||
for (const f of params.ingredients) {
|
||||
if (fileName === f.name) {
|
||||
file = f;
|
||||
break;
|
||||
@@ -453,7 +457,7 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
|
||||
if (!file){
|
||||
// check for cif directly
|
||||
const cifileName = `${name}.cif`;
|
||||
for (const f of params.ingredients.files) {
|
||||
for (const f of params.ingredients) {
|
||||
if (cifileName === f.name) {
|
||||
file = f;
|
||||
break;
|
||||
@@ -488,7 +492,7 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
|
||||
}
|
||||
|
||||
async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, state: State, params: LoadCellPackModelParams) {
|
||||
const ingredientFiles = params.ingredients.files || [];
|
||||
const ingredientFiles = params.ingredients || [];
|
||||
|
||||
let cellPackJson: StateBuilder.To<PSO.Format.Json, StateTransformer<PSO.Data.String, PSO.Format.Json>>;
|
||||
if (params.source.name === 'id') {
|
||||
@@ -563,9 +567,7 @@ const LoadCellPackModelParams = {
|
||||
}, { options: [['id', 'Id'], ['file', 'File']] }),
|
||||
baseUrl: PD.Text(DefaultCellPackBaseUrl),
|
||||
membrane: PD.Boolean(true),
|
||||
ingredients : PD.Group({
|
||||
files: PD.FileList({ accept: '.cif,.bcif,.pdb' })
|
||||
}, { isExpanded: true }),
|
||||
ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredients' }),
|
||||
preset: PD.Group({
|
||||
traceOnly: PD.Boolean(false),
|
||||
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation']))
|
||||
|
||||
@@ -125,7 +125,7 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({
|
||||
const s = await StructureSymmetry.buildAssembly(initial_structure, a.id).runInContext(ctx);
|
||||
structures.push(s);
|
||||
}
|
||||
const builder = Structure.Builder({ label: name });
|
||||
const builder = Structure.Builder();
|
||||
let offsetInvariantId = 0;
|
||||
for (const s of structures) {
|
||||
let maxInvariantId = 0;
|
||||
|
||||
10
src/extensions/dnatco/README.md
Normal file
10
src/extensions/dnatco/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## DNATCO Extensions
|
||||
|
||||
### Confal Pyramids
|
||||
|
||||
The Confal Pyramids extensions displays tetrahedron-like pyramids. These pyramids are a simple visual representation of nucleotide conformer classes that can be assigned to individual steps in nucleic acid structures.
|
||||
|
||||
For more information, see:
|
||||
* [Černý et al., Nucleic Acids Research, 44, W284 (2016)](http://dx.doi.org/10.1093/nar/gkw381)
|
||||
* [Schneider et al., Acta Cryst D, 74, 52-64 (2018)](http://dx.doi.org/10.1107/S2059798318000050)
|
||||
* [Schneider et al., Genes, 8(10), 278, (2017)](http://dx.doi.org/10.3390/genes8100278)
|
||||
103
src/extensions/dnatco/confal-pyramids/behavior.ts
Normal file
103
src/extensions/dnatco/confal-pyramids/behavior.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Michal Malý <michal.maly@ibt.cas.cz>
|
||||
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
|
||||
*/
|
||||
|
||||
import { ConfalPyramidsColorThemeProvider } from './color';
|
||||
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
|
||||
import { ConfalPyramidsRepresentationProvider } from './representation';
|
||||
import { Loci } from '../../../mol-model/loci';
|
||||
import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
|
||||
import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { StateObjectRef } from '../../../mol-state';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure-representation-confal-pyramids',
|
||||
display: {
|
||||
name: 'Confal Pyramids', group: 'Annotation',
|
||||
description: 'Schematic depiction of conformer class and confal value.',
|
||||
},
|
||||
isApplicable(a) {
|
||||
return a.data.models.length >= 1 && a.data.models.some(m => ConfalPyramids.isApplicable(m));
|
||||
},
|
||||
params: () => StructureRepresentationPresetProvider.CommonParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const model = structureCell?.obj?.data.model;
|
||||
if (!structureCell || !model) return {};
|
||||
|
||||
await plugin.runTask(Task.create('Confal Pyramids', async runtime => {
|
||||
await ConfalPyramidsProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
|
||||
}));
|
||||
|
||||
const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
|
||||
|
||||
const pyramids = await plugin.builders.structure.tryCreateComponentStatic(structureCell, 'nucleic', { label: 'Confal Pyramids' });
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
|
||||
let pyramidsRepr;
|
||||
if (representations)
|
||||
pyramidsRepr = builder.buildRepresentation(update, pyramids, { type: ConfalPyramidsRepresentationProvider, typeParams, color: ConfalPyramidsColorThemeProvider }, { tag: 'confal-pyramdis' } );
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
return { components: { ...components, pyramids }, representations: { ...representations, pyramidsRepr } };
|
||||
}
|
||||
});
|
||||
|
||||
export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({
|
||||
name: 'dnatco-confal-pyramids-prop',
|
||||
category: 'custom-props',
|
||||
display: {
|
||||
name: 'Confal Pyramids',
|
||||
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);
|
||||
|
||||
this.ctx.builders.structure.representation.registerPreset(DnatcoConfalPyramidsPreset);
|
||||
}
|
||||
|
||||
update(p: { autoAttach: boolean, showToolTip: boolean }) {
|
||||
const updated = this.params.autoAttach !== p.autoAttach;
|
||||
this.params.autoAttach = p.autoAttach;
|
||||
this.params.showToolTip = p.showToolTip;
|
||||
this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
|
||||
return updated;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
this.ctx.builders.structure.representation.unregisterPreset(DnatcoConfalPyramidsPreset);
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(true),
|
||||
showToolTip: PD.Boolean(true)
|
||||
})
|
||||
});
|
||||
186
src/extensions/dnatco/confal-pyramids/color.ts
Normal file
186
src/extensions/dnatco/confal-pyramids/color.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Michal Malý <michal.maly@ibt.cas.cz>
|
||||
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
|
||||
*/
|
||||
|
||||
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
|
||||
import { ConfalPyramidsTypes as CPT } from './types';
|
||||
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 { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TableLegend } from '../../../mol-util/legend';
|
||||
import { iterableToArray } from '../../../mol-data/util';
|
||||
|
||||
const DefaultColor = Color(0xCCCCCC);
|
||||
const Description = 'Assigns colors to confal pyramids';
|
||||
const ErrorColor = Color(0xFFA10A);
|
||||
|
||||
type ConformerClasses = 'A' | 'B' | 'BII' | 'miB' | 'Z' | 'IC' | 'OPN' | 'SYN' | 'N';
|
||||
|
||||
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)],
|
||||
]);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values<ConfalPyramidsColorThemeParams>): ColorTheme<ConfalPyramidsColorThemeParams> {
|
||||
function color(location: Location, isSecondary: boolean): Color {
|
||||
if (CPT.isLocation(location)) {
|
||||
const { pyramid, isLower } = location.data;
|
||||
return getConformerColor(pyramid.NtC, isLower);
|
||||
}
|
||||
|
||||
return DefaultColor;
|
||||
}
|
||||
|
||||
return {
|
||||
factory: ConfalPyramidsColorTheme,
|
||||
granularity: 'group',
|
||||
color,
|
||||
props,
|
||||
description: Description,
|
||||
legend: TableLegend(iterableToArray(ColorMapping.entries()).map(([conformer, color]) => {
|
||||
return [conformer, color] as [string, Color];
|
||||
}).concat([
|
||||
[ 'Error', ErrorColor ],
|
||||
[ 'Unknown', DefaultColor ]
|
||||
]))
|
||||
};
|
||||
}
|
||||
|
||||
export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramidsColorThemeParams, 'confal-pyramids'> = {
|
||||
name: 'confal-pyramids',
|
||||
label: 'Confal Pyramids',
|
||||
category: ColorTheme.Category.Residue,
|
||||
factory: ConfalPyramidsColorTheme,
|
||||
getParams: getConfalPyramidsColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(ConfalPyramidsColorThemeParams),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => ConfalPyramids.isApplicable(m)),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ConfalPyramidsProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
|
||||
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(ConfalPyramidsProvider.descriptor, false)
|
||||
}
|
||||
};
|
||||
172
src/extensions/dnatco/confal-pyramids/property.ts
Normal file
172
src/extensions/dnatco/confal-pyramids/property.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Michal Malý <michal.maly@ibt.cas.cz>
|
||||
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
|
||||
*/
|
||||
|
||||
import { ConfalPyramidsTypes as CPT } from './types';
|
||||
import { Column, Table } from '../../../mol-data/db';
|
||||
import { toTable } from '../../../mol-io/reader/cif/schema';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { Model } from '../../../mol-model/structure';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
|
||||
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 namespace ConfalPyramids {
|
||||
export const Schema = {
|
||||
ndb_struct_ntc_step: {
|
||||
id: Column.Schema.int,
|
||||
name: Column.Schema.str,
|
||||
PDB_model_number: Column.Schema.int,
|
||||
label_entity_id_1: Column.Schema.int,
|
||||
label_asym_id_1: Column.Schema.str,
|
||||
label_seq_id_1: Column.Schema.int,
|
||||
label_comp_id_1: Column.Schema.str,
|
||||
label_alt_id_1: Column.Schema.str,
|
||||
label_entity_id_2: Column.Schema.int,
|
||||
label_asym_id_2: Column.Schema.str,
|
||||
label_seq_id_2: Column.Schema.int,
|
||||
label_comp_id_2: Column.Schema.str,
|
||||
label_alt_id_2: Column.Schema.str,
|
||||
auth_asym_id_1: Column.Schema.str,
|
||||
auth_seq_id_1: Column.Schema.int,
|
||||
auth_asym_id_2: Column.Schema.str,
|
||||
auth_seq_id_2: Column.Schema.int,
|
||||
PDB_ins_code_1: Column.Schema.str,
|
||||
PDB_ins_code_2: Column.Schema.str,
|
||||
},
|
||||
ndb_struct_ntc_step_summary: {
|
||||
step_id: Column.Schema.int,
|
||||
assigned_CANA: Column.Schema.str,
|
||||
assigned_NtC: Column.Schema.str,
|
||||
confal_score: Column.Schema.int,
|
||||
euclidean_distance_NtC_ideal: Column.Schema.float,
|
||||
cartesian_rmsd_closest_NtC_representative: Column.Schema.float,
|
||||
closest_CANA: Column.Schema.str,
|
||||
closest_NtC: Column.Schema.str,
|
||||
closest_step_golden: Column.Schema.str
|
||||
}
|
||||
};
|
||||
export type Schema = typeof Schema;
|
||||
|
||||
export async function fromCif(ctx: CustomProperty.Context, model: Model, props: ConfalPyramidsProps): Promise<CustomProperty.Data<ConfalPyramids>> {
|
||||
const info = PropertyWrapper.createInfo();
|
||||
const data = getCifData(model);
|
||||
if (data === undefined) return { value: { info, data: undefined } };
|
||||
|
||||
const fromCif = createPyramidsFromCif(model, data.steps, data.stepsSummary);
|
||||
return { value: { info, data: fromCif } };
|
||||
}
|
||||
|
||||
function getCifData(model: Model) {
|
||||
if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF');
|
||||
if (!hasNdbStructNtcCategories(model)) return undefined;
|
||||
return {
|
||||
steps: toTable(Schema.ndb_struct_ntc_step, model.sourceData.data.frame.categories.ndb_struct_ntc_step),
|
||||
stepsSummary: toTable(Schema.ndb_struct_ntc_step_summary, model.sourceData.data.frame.categories.ndb_struct_ntc_step_summary)
|
||||
};
|
||||
}
|
||||
|
||||
function hasNdbStructNtcCategories(model: Model): boolean {
|
||||
if (!MmcifFormat.is(model.sourceData)) return false;
|
||||
const names = (model.sourceData).data.frame.categoryNames;
|
||||
return names.includes('ndb_struct_ntc_step') && names.includes('ndb_struct_ntc_step_summary');
|
||||
}
|
||||
|
||||
export function isApplicable(model?: Model): boolean {
|
||||
return !!model && hasNdbStructNtcCategories(model);
|
||||
}
|
||||
}
|
||||
|
||||
export const ConfalPyramidsParams = {};
|
||||
export type ConfalPyramidsParams = typeof ConfalPyramidsParams;
|
||||
export type ConfalPyramidsProps = PD.Values<ConfalPyramidsParams>;
|
||||
|
||||
export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, ConfalPyramids> = CustomModelProperty.createProvider({
|
||||
label: 'Confal Pyramids',
|
||||
descriptor: CustomPropertyDescriptor({
|
||||
name: 'confal_pyramids',
|
||||
}),
|
||||
type: 'static',
|
||||
defaultParams: ConfalPyramidsParams,
|
||||
getParams: (data: Model) => ConfalPyramidsParams,
|
||||
isApplicable: (data: Model) => ConfalPyramids.isApplicable(data),
|
||||
obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<ConfalPyramidsProps>) => {
|
||||
const p = { ...PD.getDefaultValues(ConfalPyramidsParams), ...props };
|
||||
return ConfalPyramids.fromCif(ctx, data, p);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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 } = getNtCAndConfalScore(id.value(i), i, stepsSummary);
|
||||
|
||||
const pyramid = {
|
||||
PDB_model_number: model_num,
|
||||
name: name.value(i),
|
||||
auth_asym_id_1: auth_asym_id_1.value(i),
|
||||
auth_seq_id_1: auth_seq_id_1.value(i),
|
||||
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),
|
||||
auth_asym_id_2: auth_asym_id_2.value(i),
|
||||
auth_seq_id_2: auth_seq_id_2.value(i),
|
||||
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
|
||||
};
|
||||
|
||||
pyramids.push(pyramid);
|
||||
names.set(pyramid.name, pyramids.length - 1);
|
||||
|
||||
locations.push(CPT.Location(pyramid, false));
|
||||
locations.push(CPT.Location(pyramid, true));
|
||||
}
|
||||
|
||||
return { pyramids, names, locations, hasMultipleModels };
|
||||
}
|
||||
|
||||
function getNtCAndConfalScore(id: number, i: number, stepsSummary: StepsSummaryTable) {
|
||||
const { step_id, confal_score, assigned_NtC } = 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) };
|
||||
}
|
||||
// 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) };
|
||||
}
|
||||
throw new Error('Inconsistent mmCIF data');
|
||||
}
|
||||
186
src/extensions/dnatco/confal-pyramids/representation.ts
Normal file
186
src/extensions/dnatco/confal-pyramids/representation.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Michal Malý <michal.maly@ibt.cas.cz>
|
||||
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
|
||||
*/
|
||||
|
||||
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
|
||||
import { ConfalPyramidsUtil } from './util';
|
||||
import { ConfalPyramidsTypes as CPT } from './types';
|
||||
import { Interval } from '../../../mol-data/int';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { PickingId } from '../../../mol-geo/geometry/picking';
|
||||
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 { 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 { StructureGroup, UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { VisualContext } from '../../../mol-repr/visual';
|
||||
import { getAltResidueLociFromId } 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';
|
||||
|
||||
const t = Mat4.identity();
|
||||
const w = Vec3.zero();
|
||||
const mp = Vec3.zero();
|
||||
|
||||
function calcMidpoint(mp: Vec3, v: Vec3, w: Vec3) {
|
||||
Vec3.sub(mp, v, w);
|
||||
Vec3.scale(mp, mp, 0.5);
|
||||
Vec3.add(mp, mp, w);
|
||||
}
|
||||
|
||||
function shiftVertex(vec: Vec3, ref: Vec3, scale: number) {
|
||||
Vec3.sub(w, vec, ref);
|
||||
Vec3.scale(w, w, scale);
|
||||
Vec3.add(vec, vec, w);
|
||||
}
|
||||
|
||||
const ConfalPyramidsMeshParams = {
|
||||
...UnitsMeshParams
|
||||
};
|
||||
type ConfalPyramidsMeshParams = typeof ConfalPyramidsMeshParams;
|
||||
|
||||
function createConfalPyramidsIterator(structureGroup: StructureGroup): LocationIterator {
|
||||
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 { locations } = prop.data;
|
||||
|
||||
const getLocation = (groupIndex: number, instanceIndex: number) => {
|
||||
if (locations.length <= groupIndex) return NullLocation;
|
||||
return locations[groupIndex];
|
||||
};
|
||||
return LocationIterator(locations.length, 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 { pyramids } = prop.data;
|
||||
if (pyramids.length === 0) return Mesh.createEmpty(mesh);
|
||||
|
||||
const mb = MeshBuilder.createState(512, 512, 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 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;
|
||||
|
||||
shiftVertex(O3, P, scale);
|
||||
shiftVertex(OP1, P, scale);
|
||||
shiftVertex(OP2, P, scale);
|
||||
shiftVertex(O5, P, scale);
|
||||
calcMidpoint(mp, O3, O5);
|
||||
|
||||
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());
|
||||
|
||||
/* 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());
|
||||
};
|
||||
|
||||
const walker = new ConfalPyramidsUtil.UnitWalker(structure, unit, handler);
|
||||
walker.walk();
|
||||
|
||||
return MeshBuilder.getMesh(mb);
|
||||
}
|
||||
|
||||
function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
|
||||
const { groupId, objectId, instanceId } = pickingId;
|
||||
if (objectId !== id) return EmptyLoci;
|
||||
|
||||
const { structure } = structureGroup;
|
||||
|
||||
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 { locations } = prop.data;
|
||||
|
||||
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];
|
||||
|
||||
return getAltResidueLociFromId(structure, unit, rI, altId);
|
||||
}
|
||||
|
||||
function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
|
||||
return false; // TODO: Implement me
|
||||
}
|
||||
|
||||
function ConfalPyramidsVisual(materialId: number): UnitsVisual<ConfalPyramidsMeshParams> {
|
||||
return UnitsMeshVisual<ConfalPyramidsMeshParams>({
|
||||
defaultProps: PD.getDefaultValues(ConfalPyramidsMeshParams),
|
||||
createGeometry: createConfalPyramidsMesh,
|
||||
createLocationIterator: createConfalPyramidsIterator,
|
||||
getLoci: getConfalPyramidLoci,
|
||||
eachLocation: eachConfalPyramid,
|
||||
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ConfalPyramidsMeshParams>, currentProps: PD.Values<ConfalPyramidsMeshParams>) => {
|
||||
}
|
||||
}, materialId);
|
||||
}
|
||||
const ConfalPyramidsVisuals = {
|
||||
'confal-pyramids-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, UnitsMeshParams>) => UnitsRepresentation('Confal Pyramids Symbol Mesh', ctx, getParams, ConfalPyramidsVisual),
|
||||
};
|
||||
|
||||
export const ConfalPyramidsParams = {
|
||||
...UnitsMeshParams
|
||||
};
|
||||
export type ConfalPyramidsParams = typeof ConfalPyramidsParams;
|
||||
export function getConfalPyramidsParams(ctx: ThemeRegistryContext, structure: Structure) {
|
||||
return PD.clone(ConfalPyramidsParams);
|
||||
}
|
||||
|
||||
export type ConfalPyramidsRepresentation = StructureRepresentation<ConfalPyramidsParams>;
|
||||
export function ConfalPyramidsRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ConfalPyramidsParams>): ConfalPyramidsRepresentation {
|
||||
const repr = Representation.createMulti('Confal Pyramids', ctx, getParams, StructureRepresentationStateBuilder, ConfalPyramidsVisuals as unknown as Representation.Def<Structure, ConfalPyramidsParams>);
|
||||
return repr;
|
||||
}
|
||||
|
||||
export const ConfalPyramidsRepresentationProvider = StructureRepresentationProvider({
|
||||
name: 'confal-pyramids',
|
||||
label: 'Confal Pyramids',
|
||||
description: 'Displays schematic depiction of conformer classes and confal values',
|
||||
factory: ConfalPyramidsRepresentation,
|
||||
getParams: getConfalPyramidsParams,
|
||||
defaultValues: PD.getDefaultValues(ConfalPyramidsParams),
|
||||
defaultColorTheme: { name: 'confal-pyramids' },
|
||||
defaultSizeTheme: { name: 'uniform' },
|
||||
isApplicable: (structure: Structure) => structure.models.some(m => ConfalPyramids.isApplicable(m)),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, structure: Structure) => ConfalPyramidsProvider.attach(ctx, structure.model, void 0, true),
|
||||
detach: (data) => ConfalPyramidsProvider.ref(data.model, false),
|
||||
}
|
||||
});
|
||||
60
src/extensions/dnatco/confal-pyramids/types.ts
Normal file
60
src/extensions/dnatco/confal-pyramids/types.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Michal Malý <michal.maly@ibt.cas.cz>
|
||||
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
|
||||
*/
|
||||
|
||||
import { DataLocation } from '../../../mol-model/location';
|
||||
import { ElementIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
|
||||
|
||||
export namespace ConfalPyramidsTypes {
|
||||
export type Pyramid = {
|
||||
PDB_model_number: number,
|
||||
name: string,
|
||||
auth_asym_id_1: string,
|
||||
auth_seq_id_1: number,
|
||||
label_comp_id_1: string,
|
||||
label_alt_id_1: string,
|
||||
PDB_ins_code_1: string,
|
||||
auth_asym_id_2: string,
|
||||
auth_seq_id_2: number,
|
||||
label_comp_id_2: string,
|
||||
label_alt_id_2: string,
|
||||
PDB_ins_code_2: string,
|
||||
confal_score: number,
|
||||
NtC: string
|
||||
}
|
||||
|
||||
export interface PyramidsData {
|
||||
pyramids: Array<Pyramid>,
|
||||
names: Map<string, number>,
|
||||
locations: Array<Location>,
|
||||
hasMultipleModels: boolean
|
||||
}
|
||||
|
||||
export interface LocationData {
|
||||
readonly pyramid: Pyramid
|
||||
readonly isLower: boolean;
|
||||
}
|
||||
|
||||
export interface Element {
|
||||
structure: Structure;
|
||||
unit: Unit.Atomic;
|
||||
element: ElementIndex;
|
||||
}
|
||||
|
||||
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 isLocation(x: any): x is Location {
|
||||
return !!x && x.kind === 'data-location' && x.tag === 'pyramid';
|
||||
}
|
||||
|
||||
export function toElementLocation(location: Location) {
|
||||
return StructureElement.Location.create(location.element.structure, location.element.unit, location.element.element);
|
||||
}
|
||||
}
|
||||
299
src/extensions/dnatco/confal-pyramids/util.ts
Normal file
299
src/extensions/dnatco/confal-pyramids/util.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Michal Malý <michal.maly@ibt.cas.cz>
|
||||
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
|
||||
*/
|
||||
|
||||
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 { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
|
||||
|
||||
export namespace ConfalPyramidsUtil {
|
||||
type Residue = Segmentation.Segment<ResidueIndex>;
|
||||
|
||||
export type AtomInfo = {
|
||||
pos: Vec3,
|
||||
index: ElementIndex,
|
||||
fakeAltId: string,
|
||||
};
|
||||
|
||||
export type FirstResidueAtoms = {
|
||||
O3: AtomInfo,
|
||||
};
|
||||
|
||||
export type SecondResidueAtoms = {
|
||||
OP1: AtomInfo,
|
||||
OP2: AtomInfo,
|
||||
O5: AtomInfo,
|
||||
P: AtomInfo,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
export function hasMultipleModels(unit: Unit.Atomic): boolean {
|
||||
const prop = ConfalPyramidsProvider.get(unit.model).value;
|
||||
if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data');
|
||||
return prop.data.hasMultipleModels;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
}
|
||||
8
src/extensions/dnatco/index.ts
Normal file
8
src/extensions/dnatco/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Michal Malý <michal.maly@ibt.cas.cz>
|
||||
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
|
||||
*/
|
||||
|
||||
export { DnatcoConfalPyramids } from './confal-pyramids/behavior';
|
||||
66
src/extensions/g3d/data.ts
Normal file
66
src/extensions/g3d/data.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { decodeMsgPack } from '../../mol-io/common/msgpack/decode';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { inflate } from '../../mol-util/zip/zip';
|
||||
|
||||
export interface G3dHeader {
|
||||
magic: 'G3D',
|
||||
version: number,
|
||||
genome: string,
|
||||
name: string,
|
||||
offsets: { [resolution: string]: { offset: number, size: number } },
|
||||
resolutions: number[]
|
||||
}
|
||||
|
||||
export type G3dDataBlock = {
|
||||
header: G3dHeader,
|
||||
resolution: number,
|
||||
data: {
|
||||
[haplotype: string]: {
|
||||
[ch: string]: {
|
||||
start: number[]
|
||||
x: number[],
|
||||
y: number[],
|
||||
z: number[],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HEADER_SIZE = 64000;
|
||||
|
||||
export async function getG3dHeader(ctx: PluginContext, urlOrData: string | Uint8Array): Promise<G3dHeader> {
|
||||
const data: Uint8Array = await getRawData(ctx, urlOrData, { offset: 0, size: HEADER_SIZE });
|
||||
let last = data.length - 1;
|
||||
for (; last >= 0; last--) {
|
||||
if (data[last] !== 0) break;
|
||||
}
|
||||
const header = decodeMsgPack(data.slice(0, last + 1));
|
||||
return header;
|
||||
}
|
||||
|
||||
export async function getG3dDataBlock(ctx: PluginContext, header: G3dHeader, urlOrData: string | Uint8Array, resolution: number): Promise<G3dDataBlock> {
|
||||
if (!header.offsets[resolution]) throw new Error(`Resolution ${resolution} not available.`);
|
||||
const data = await getRawData(ctx, urlOrData, header.offsets[resolution]);
|
||||
const unzipped = await ctx.runTask(Task.create('Unzip', ctx => inflate(ctx, data)));
|
||||
|
||||
return {
|
||||
header,
|
||||
resolution,
|
||||
data: decodeMsgPack(unzipped)
|
||||
};
|
||||
}
|
||||
|
||||
async function getRawData(ctx: PluginContext, urlOrData: string | Uint8Array, range: { offset: number, size: number }) {
|
||||
if (typeof urlOrData === 'string') {
|
||||
return await ctx.runTask(ctx.fetch({ url: urlOrData, headers: [['Range', `bytes=${range.offset}-${range.offset + range.size - 1}`]], type: 'binary' }));
|
||||
} else {
|
||||
return urlOrData.slice(range.offset, range.offset + range.size);
|
||||
}
|
||||
}
|
||||
173
src/extensions/g3d/format.ts
Normal file
173
src/extensions/g3d/format.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) 2020 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 { Trajectory } from '../../mol-model/structure';
|
||||
import { TrajectoryFormatCategory, TrajectoryFormatProvider } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { PluginStateObject as SO, PluginStateTransform } from '../../mol-plugin-state/objects';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { DefaultQueryRuntimeTable } from '../../mol-script/runtime/query/base';
|
||||
import { StateAction, StateObjectRef } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { G3dHeader, getG3dDataBlock, getG3dHeader } from './data';
|
||||
import { g3dHaplotypeQuery, G3dLabelProvider, trajectoryFromG3D, G3dSymbols, G3dInfoDataProperty } from './model';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
|
||||
import { stringToWords } from '../../mol-util/string';
|
||||
import { objectForEach } from '../../mol-util/object';
|
||||
|
||||
export const G3dProvider: TrajectoryFormatProvider = {
|
||||
label: 'G3D',
|
||||
description: 'G3D',
|
||||
category: TrajectoryFormatCategory,
|
||||
binaryExtensions: ['g3d'],
|
||||
parse: async (plugin, data) => {
|
||||
const trajectory = await plugin.state.data.build()
|
||||
.to(data)
|
||||
.apply(G3DHeaderFromFile, {}, { state: { isGhost: true } })
|
||||
.apply(G3DTrajectory)
|
||||
.commit();
|
||||
|
||||
return { trajectory };
|
||||
},
|
||||
visuals: defaultStructure
|
||||
};
|
||||
|
||||
async function defaultStructure(plugin: PluginContext, data: { trajectory: StateObjectRef<SO.Molecule.Trajectory> }) {
|
||||
const builder = plugin.builders.structure;
|
||||
const model = await builder.createModel(data.trajectory);
|
||||
|
||||
if (!model) return;
|
||||
const structure = await builder.createStructure(model);
|
||||
|
||||
const info = G3dInfoDataProperty.get(model.data!);
|
||||
if (!info) return;
|
||||
|
||||
const components = plugin.build().to(structure);
|
||||
|
||||
const repr = createStructureRepresentationParams(plugin, void 0, {
|
||||
type: 'cartoon',
|
||||
color: 'polymer-index',
|
||||
size: 'uniform',
|
||||
sizeParams: { value: 0.25 }
|
||||
});
|
||||
|
||||
for (const h of info.haplotypes) {
|
||||
components
|
||||
.apply(StateTransforms.Model.StructureSelectionFromExpression, { expression: g3dHaplotypeQuery(h), label: stringToWords(h) })
|
||||
.apply(StateTransforms.Representation.StructureRepresentation3D, repr);
|
||||
}
|
||||
|
||||
await components.commit();
|
||||
}
|
||||
|
||||
export class G3dHeaderObject extends SO.Create<{
|
||||
header: G3dHeader,
|
||||
urlOrData: Uint8Array | string,
|
||||
cache: { [resolution: number]: Trajectory | undefined }
|
||||
}>({ name: 'G3D Header', typeClass: 'Data' }) { }
|
||||
|
||||
export type G3DHeaderFromFile = typeof G3DHeaderFromFile
|
||||
export const G3DHeaderFromFile = PluginStateTransform.BuiltIn({
|
||||
name: 'g3d-header-from-file',
|
||||
display: { name: 'G3D Header', description: 'Parse G3D Header' },
|
||||
from: SO.Data.Binary,
|
||||
to: G3dHeaderObject
|
||||
})({
|
||||
apply({ a }, plugin: PluginContext) {
|
||||
return Task.create('Parse G3D', async () => {
|
||||
const header = await getG3dHeader(plugin, a.data);
|
||||
return new G3dHeaderObject({ header, urlOrData: a.data, cache: { } }, { label: header.name, description: header.genome });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type G3DHeaderFromUrl = typeof G3DHeaderFromUrl
|
||||
export const G3DHeaderFromUrl = PluginStateTransform.BuiltIn({
|
||||
name: 'g3d-header-from-url',
|
||||
display: { name: 'G3D Header', description: 'Parse G3D Header' },
|
||||
params: { url: ParamDefinition.Text('') },
|
||||
from: SO.Root,
|
||||
to: G3dHeaderObject
|
||||
})({
|
||||
apply({ params }, plugin: PluginContext) {
|
||||
return Task.create('Parse G3D', async () => {
|
||||
const header = await getG3dHeader(plugin, params.url);
|
||||
return new G3dHeaderObject({ header, urlOrData: params.url, cache: { } }, { label: header.name, description: header.genome });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type G3DTrajectory = typeof G3DHeaderFromUrl
|
||||
export const G3DTrajectory = PluginStateTransform.BuiltIn({
|
||||
name: 'g3d-trajecotry',
|
||||
display: { name: 'G3D Trajectory', description: 'Create G3D Trajectory' },
|
||||
params: a => {
|
||||
if (!a) return { resolution: ParamDefinition.Numeric(200000) };
|
||||
const rs = a.data.header.resolutions;
|
||||
return {
|
||||
resolution: ParamDefinition.Select(rs[rs.length - 1], rs.map(r => [r, '' + r] as const))
|
||||
};
|
||||
},
|
||||
from: G3dHeaderObject,
|
||||
to: SO.Molecule.Trajectory
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('G3D Trajectory', async ctx => {
|
||||
if (a.data.cache[params.resolution]) {
|
||||
return new SO.Molecule.Trajectory(a.data.cache[params.resolution]!, { label: a.label, description: a.description });
|
||||
}
|
||||
const data = await getG3dDataBlock(plugin, a.data.header, a.data.urlOrData, params.resolution);
|
||||
const traj = await trajectoryFromG3D(data).runInContext(ctx);
|
||||
a.data.cache[params.resolution] = traj;
|
||||
return new SO.Molecule.Trajectory(traj, { label: a.label, description: a.description });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const LoadG3D = StateAction.build({
|
||||
display: { name: 'Load Genome 3D (G3D)', description: 'Load G3D file from the specified URL.' },
|
||||
from: SO.Root,
|
||||
params: { url: ParamDefinition.Text('') }
|
||||
})(({ params, state }, ctx: PluginContext) => Task.create('Genome3D', taskCtx => {
|
||||
return state.transaction(async () => {
|
||||
if (params.url.trim().length === 0) {
|
||||
throw new Error('Specify URL');
|
||||
}
|
||||
|
||||
ctx.behaviors.layout.leftPanelTabName.next('data');
|
||||
|
||||
const trajectory = await state.build().toRoot()
|
||||
.apply(G3DHeaderFromUrl, { url: params.url })
|
||||
.apply(G3DTrajectory)
|
||||
.commit();
|
||||
|
||||
await defaultStructure(ctx, { trajectory });
|
||||
}).runInContext(taskCtx);
|
||||
}));
|
||||
|
||||
export const G3DFormat = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
|
||||
name: 'g3d',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'G3D',
|
||||
description: 'G3D Format Support'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
|
||||
register() {
|
||||
this.ctx.state.data.actions.add(LoadG3D);
|
||||
objectForEach(G3dSymbols, s => DefaultQueryRuntimeTable.addSymbol(s));
|
||||
this.ctx.managers.lociLabels.addProvider(G3dLabelProvider);
|
||||
}
|
||||
unregister() {
|
||||
this.ctx.state.data.actions.remove(LoadG3D);
|
||||
objectForEach(G3dSymbols, s => DefaultQueryRuntimeTable.removeSymbol(s));
|
||||
this.ctx.managers.lociLabels.removeProvider(G3dLabelProvider);
|
||||
}
|
||||
}
|
||||
});
|
||||
245
src/extensions/g3d/model.ts
Normal file
245
src/extensions/g3d/model.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column, Table } from '../../mol-data/db';
|
||||
import { OrderedSet } from '../../mol-data/int';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { createModels } from '../../mol-model-formats/structure/basic/parser';
|
||||
import { BasicSchema, createBasic } from '../../mol-model-formats/structure/basic/schema';
|
||||
import { EntityBuilder } from '../../mol-model-formats/structure/common/entity';
|
||||
import { Loci } from '../../mol-model/loci';
|
||||
import { Trajectory, Unit } from '../../mol-model/structure';
|
||||
import { MoleculeType } from '../../mol-model/structure/model/types';
|
||||
import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
|
||||
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
|
||||
import { CustomPropSymbol } from '../../mol-script/language/symbol';
|
||||
import { Type } from '../../mol-script/language/type';
|
||||
import { QuerySymbolRuntime } from '../../mol-script/runtime/query/base';
|
||||
import { RuntimeContext, Task } from '../../mol-task';
|
||||
import { objectForEach } from '../../mol-util/object';
|
||||
import { G3dDataBlock } from './data';
|
||||
import { FormatPropertyProvider } from '../../mol-model-formats/structure/common/property';
|
||||
|
||||
interface NormalizedData {
|
||||
entity_id: string[],
|
||||
chromosome: string[],
|
||||
seq_id_begin: Int32Array,
|
||||
seq_id_end: Int32Array,
|
||||
start: Int32Array,
|
||||
x: Float32Array,
|
||||
y: Float32Array,
|
||||
z: Float32Array,
|
||||
r: Float32Array,
|
||||
haplotype: string[]
|
||||
}
|
||||
|
||||
function getColumns(block: G3dDataBlock) {
|
||||
const { data } = block;
|
||||
let size = 0;
|
||||
|
||||
objectForEach(data, h => objectForEach(h, g => size += g.start.length));
|
||||
|
||||
const normalized: NormalizedData = {
|
||||
entity_id: new Array(size),
|
||||
chromosome: new Array(size),
|
||||
seq_id_begin: new Int32Array(size),
|
||||
seq_id_end: new Int32Array(size),
|
||||
start: new Int32Array(size),
|
||||
x: new Float32Array(size),
|
||||
y: new Float32Array(size),
|
||||
z: new Float32Array(size),
|
||||
r: new Float32Array(size),
|
||||
haplotype: new Array(size)
|
||||
};
|
||||
|
||||
const p = [Vec3(), Vec3(), Vec3()];
|
||||
|
||||
let o = 0;
|
||||
objectForEach(data, (hs, h) => {
|
||||
objectForEach(hs, (chs, ch) => {
|
||||
const entity_id = `${ch}-${h}`;
|
||||
const l = chs.start.length;
|
||||
if (l === 0) return;
|
||||
|
||||
let x = chs.x[0];
|
||||
let y = chs.y[0];
|
||||
let z = chs.z[0];
|
||||
|
||||
Vec3.set(p[0], x, y, z);
|
||||
Vec3.set(p[2], x, y, z);
|
||||
|
||||
for (let i = 0; i < l; i++) {
|
||||
normalized.entity_id[o] = entity_id;
|
||||
normalized.chromosome[o] = ch;
|
||||
normalized.start[o] = chs.start[i];
|
||||
normalized.seq_id_begin[o] = o;
|
||||
normalized.seq_id_end[o] = o;
|
||||
|
||||
x = chs.x[i];
|
||||
y = chs.y[i];
|
||||
z = chs.z[i];
|
||||
|
||||
Vec3.set(p[1], x, y, z);
|
||||
if (i + 1 < l) Vec3.set(p[2], chs.x[i + 1], chs.y[i + 1], chs.z[i + 1]);
|
||||
else Vec3.set(p[2], x, y, z);
|
||||
|
||||
normalized.x[o] = x;
|
||||
normalized.y[o] = y;
|
||||
normalized.z[o] = z;
|
||||
normalized.r[o] = 2 / 3 * Math.min(Vec3.distance(p[0], p[1]), Vec3.distance(p[1], p[2]));
|
||||
normalized.haplotype[o] = h;
|
||||
|
||||
const _p = p[0];
|
||||
p[0] = p[1];
|
||||
p[1] = _p;
|
||||
o++;
|
||||
}
|
||||
|
||||
if (l === 1) {
|
||||
normalized.r[o - 1] = 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function getTraj(ctx: RuntimeContext, data: G3dDataBlock) {
|
||||
const normalized = getColumns(data);
|
||||
|
||||
const rowCount = normalized.seq_id_begin.length;
|
||||
const entityIds = new Array<string>(rowCount);
|
||||
const entityBuilder = new EntityBuilder();
|
||||
|
||||
const eName = { customName: '' };
|
||||
for (let i = 0; i < rowCount; ++i) {
|
||||
const e = normalized.entity_id[i];
|
||||
eName.customName = e;
|
||||
const entityId = entityBuilder.getEntityId(e, MoleculeType.DNA, e, eName);
|
||||
entityIds[i] = entityId;
|
||||
}
|
||||
|
||||
const ihm_sphere_obj_site = Table.ofPartialColumns(BasicSchema.ihm_sphere_obj_site, {
|
||||
id: Column.range(0, rowCount),
|
||||
entity_id: Column.ofStringArray(entityIds),
|
||||
seq_id_begin: Column.ofIntArray(normalized.seq_id_begin),
|
||||
seq_id_end: Column.ofIntArray(normalized.seq_id_end),
|
||||
asym_id: Column.ofStringArray(normalized.chromosome),
|
||||
|
||||
Cartn_x: Column.ofFloatArray(normalized.x),
|
||||
Cartn_y: Column.ofFloatArray(normalized.y),
|
||||
Cartn_z: Column.ofFloatArray(normalized.z),
|
||||
|
||||
object_radius: Column.ofFloatArray(normalized.r),
|
||||
rmsf: Column.ofConst(0, rowCount, Column.Schema.float),
|
||||
model_id: Column.ofConst(1, rowCount, Column.Schema.int),
|
||||
}, rowCount);
|
||||
|
||||
const basic = createBasic({
|
||||
entity: entityBuilder.getEntityTable(),
|
||||
ihm_model_list: Table.ofPartialColumns(BasicSchema.ihm_model_list, {
|
||||
model_id: Column.ofIntArray([1]),
|
||||
model_name: Column.ofStringArray(['G3D Model']),
|
||||
}, 1),
|
||||
ihm_sphere_obj_site
|
||||
});
|
||||
|
||||
const models = await createModels(basic, { kind: 'g3d', name: 'G3D', data }, ctx);
|
||||
|
||||
G3dInfoDataProperty.set(models.representative, {
|
||||
haplotypes: Object.keys(data.data),
|
||||
haplotype: normalized.haplotype,
|
||||
resolution: data.resolution,
|
||||
start: normalized.start,
|
||||
chroms: normalized.chromosome,
|
||||
});
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
export function trajectoryFromG3D(data: G3dDataBlock): Task<Trajectory> {
|
||||
return Task.create('Parse G3D', async ctx => {
|
||||
return getTraj(ctx, data);
|
||||
});
|
||||
}
|
||||
|
||||
export const G3dSymbols = {
|
||||
haplotype: QuerySymbolRuntime.Dynamic(CustomPropSymbol('g3d', 'haplotype', Type.Str),
|
||||
ctx => {
|
||||
if (Unit.isAtomic(ctx.element.unit)) return '';
|
||||
const info = (G3dInfoDataProperty as any).get(ctx.element.unit.model);
|
||||
if (!info) return '';
|
||||
const seqId = ctx.element.unit.model.coarseHierarchy.spheres.seq_id_begin.value(ctx.element.element);
|
||||
return info.haplotype[seqId] || '';
|
||||
}
|
||||
),
|
||||
chromosome: QuerySymbolRuntime.Dynamic(CustomPropSymbol('g3d', 'chromosome', Type.Str),
|
||||
ctx => {
|
||||
if (Unit.isAtomic(ctx.element.unit)) return '';
|
||||
const { asym_id } = ctx.element.unit.model.coarseHierarchy.spheres;
|
||||
return asym_id.value(ctx.element.element) || '';
|
||||
}
|
||||
),
|
||||
region: QuerySymbolRuntime.Dynamic(CustomPropSymbol('g3d', 'region', Type.Num),
|
||||
ctx => {
|
||||
if (Unit.isAtomic(ctx.element.unit)) return '';
|
||||
const info = (G3dInfoDataProperty as any).get(ctx.element.unit.model);
|
||||
if (!info) return 0;
|
||||
const seqId = ctx.element.unit.model.coarseHierarchy.spheres.seq_id_begin.value(ctx.element.element);
|
||||
return info.start[seqId] || 0;
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
export const G3dInfoDataProperty = FormatPropertyProvider.create<G3dInfoData>({ name: 'g3d_info' });
|
||||
|
||||
export function g3dHaplotypeQuery(haplotype: string) {
|
||||
return MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.rel.eq([G3dSymbols.haplotype.symbol(), haplotype]),
|
||||
});
|
||||
}
|
||||
|
||||
export function g3dChromosomeQuery(chr: string) {
|
||||
return MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.logic.and([
|
||||
MS.core.rel.eq([MS.ammp('objectPrimitive'), 'sphere']),
|
||||
MS.core.rel.eq([G3dSymbols.chromosome.symbol(), chr])
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
export function g3dRegionQuery(chr: string, start: number, end: number) {
|
||||
return MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.logic.and([
|
||||
MS.core.rel.eq([MS.ammp('objectPrimitive'), 'sphere']),
|
||||
MS.core.rel.eq([G3dSymbols.chromosome.symbol(), chr])
|
||||
]),
|
||||
'residue-test': MS.core.rel.inRange([G3dSymbols.region.symbol(), start, end])
|
||||
});
|
||||
}
|
||||
|
||||
export interface G3dInfoData {
|
||||
haplotypes: string[],
|
||||
haplotype: string[],
|
||||
start: Int32Array,
|
||||
resolution: number,
|
||||
chroms: string[]
|
||||
};
|
||||
|
||||
export const G3dLabelProvider: LociLabelProvider = {
|
||||
label: (e: Loci): string | undefined => {
|
||||
if (e.kind !== 'element-loci' || Loci.isEmpty(e)) return;
|
||||
|
||||
const first = e.elements[0];
|
||||
if (e.elements.length !== 1 || Unit.isAtomic(first.unit)) return;
|
||||
const info = G3dInfoDataProperty.get(first.unit.model);
|
||||
if (!info) return;
|
||||
|
||||
const eI = first.unit.elements[OrderedSet.getAt(first.indices, 0)];
|
||||
const seqId = first.unit.model.coarseHierarchy.spheres.seq_id_begin.value(eI);
|
||||
return `<b>Start:</b> ${info.start[seqId]} <small>| resolution ${info.resolution}<small>`;
|
||||
}
|
||||
};
|
||||
144
src/extensions/mp4-export/controls.ts
Normal file
144
src/extensions/mp4-export/controls.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { PluginStateAnimation } from '../../mol-plugin-state/animation/model';
|
||||
import { PluginComponent } from '../../mol-plugin-state/component';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { encodeMp4Animation } from './encoder';
|
||||
|
||||
export interface Mp4AnimationInfo {
|
||||
width: number,
|
||||
height: number
|
||||
}
|
||||
|
||||
export const Mp4AnimationParams = {
|
||||
quantization: PD.Numeric(18, { min: 10, max: 51 }, { description: 'Lower is better, but slower.' })
|
||||
};
|
||||
|
||||
export class Mp4Controls extends PluginComponent {
|
||||
private currentNames = new Set<string>();
|
||||
private animations: PluginStateAnimation[] = [];
|
||||
|
||||
readonly behaviors = {
|
||||
animations: this.ev.behavior<PD.Params>({ }),
|
||||
current: this.ev.behavior<{ anim: PluginStateAnimation, params: PD.Params, values: any } | undefined>(void 0),
|
||||
canApply: this.ev.behavior<PluginStateAnimation.CanApply>({ canApply: false }),
|
||||
info: this.ev.behavior<Mp4AnimationInfo>({ width: 0, height: 0 }),
|
||||
params: this.ev.behavior<PD.Values<typeof Mp4AnimationParams>>(PD.getDefaultValues(Mp4AnimationParams))
|
||||
}
|
||||
|
||||
setCurrent(name?: string) {
|
||||
const anim = this.animations.find(a => a.name === name);
|
||||
if (!anim) {
|
||||
this.behaviors.current.next(anim);
|
||||
return;
|
||||
}
|
||||
|
||||
const params = anim.params(this.plugin) as PD.Params;
|
||||
const values = PD.getDefaultValues(params);
|
||||
|
||||
this.behaviors.current.next({ anim, params, values });
|
||||
this.behaviors.canApply.next(anim.canApply?.(this.plugin) ?? { canApply: true });
|
||||
}
|
||||
|
||||
setCurrentParams(values: any) {
|
||||
this.behaviors.current.next({ ...this.behaviors.current.value!, values });
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.behaviors.current.value;
|
||||
}
|
||||
|
||||
render() {
|
||||
const task = Task.create('Export Animation', async ctx => {
|
||||
try {
|
||||
const resolution = this.plugin.helpers.viewportScreenshot?.getSizeAndViewport()!;
|
||||
const anim = this.current!;
|
||||
const movie = await encodeMp4Animation(this.plugin, ctx, {
|
||||
animation: {
|
||||
definition: anim.anim,
|
||||
params: anim.values,
|
||||
},
|
||||
...resolution,
|
||||
quantizationParameter: this.behaviors.params.value.quantization,
|
||||
pass: this.plugin.helpers.viewportScreenshot?.imagePass!,
|
||||
});
|
||||
|
||||
const filename = anim.anim.display.name.toLowerCase().replace(/\s/g, '-').replace(/[^a-z0-9_\-]/g, '');
|
||||
return { movie, filename: `${this.plugin.helpers.viewportScreenshot?.getFilename('')}_${filename}.mp4` };
|
||||
} catch (e) {
|
||||
this.plugin.log.error('' + e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
return this.plugin.runTask(task, { useOverlay: true });
|
||||
}
|
||||
|
||||
private get manager() {
|
||||
return this.plugin.managers.animation;
|
||||
}
|
||||
|
||||
private syncInfo() {
|
||||
const helper = this.plugin.helpers.viewportScreenshot;
|
||||
const size = helper?.getSizeAndViewport();
|
||||
if (!size) return;
|
||||
|
||||
this.behaviors.info.next({ width: size.viewport.width, height: size.viewport.height });
|
||||
}
|
||||
|
||||
private sync() {
|
||||
const animations = this.manager.animations.filter(a => a.isExportable);
|
||||
|
||||
const hasAll = animations.every(a => this.currentNames.has(a.name));
|
||||
if (hasAll && this.currentNames.size === animations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
current: PD.Select(animations[0]?.name,
|
||||
animations.map(a => [a.name, a.display.name] as [string, string]),
|
||||
{ label: 'Animation' })
|
||||
};
|
||||
|
||||
const current = this.behaviors.current.value;
|
||||
const hasCurrent = !!animations.find(a => a.name === current?.anim.name);
|
||||
|
||||
this.animations = animations;
|
||||
if (!hasCurrent) {
|
||||
this.setCurrent(animations[0]?.name);
|
||||
}
|
||||
this.behaviors.animations.next(params);
|
||||
}
|
||||
|
||||
private init() {
|
||||
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.helpers.viewportScreenshot?.events.previewed!, () => this.syncInfo());
|
||||
|
||||
this.subscribe(this.plugin.behaviors.state.isBusy, b => {
|
||||
const anim = this.current;
|
||||
if (!b && anim) {
|
||||
this.behaviors.canApply.next(anim.anim.canApply?.(this.plugin) ?? { canApply: true });
|
||||
}
|
||||
});
|
||||
|
||||
this.sync();
|
||||
this.syncInfo();
|
||||
}
|
||||
|
||||
constructor(private plugin: PluginContext) {
|
||||
super();
|
||||
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
105
src/extensions/mp4-export/encoder.ts
Normal file
105
src/extensions/mp4-export/encoder.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as HME from 'h264-mp4-encoder';
|
||||
import { Viewport } from '../../mol-canvas3d/camera/util';
|
||||
import { ImagePass } from '../../mol-canvas3d/passes/image';
|
||||
import { PluginStateAnimation } from '../../mol-plugin-state/animation/model';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { Color } from '../../mol-util/color';
|
||||
|
||||
export interface Mp4EncoderParams<A extends PluginStateAnimation = PluginStateAnimation> {
|
||||
pass: ImagePass,
|
||||
customBackground?: Color,
|
||||
animation: PluginStateAnimation.Instance<A>,
|
||||
width: number,
|
||||
height: number,
|
||||
viewport: Viewport,
|
||||
/** default is 30 */
|
||||
fps?: number,
|
||||
/** Number from 10 (best quality, slowest) to 51 (worst, fastest) */
|
||||
quantizationParameter?: number
|
||||
}
|
||||
|
||||
export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: PluginContext, ctx: RuntimeContext, params: Mp4EncoderParams<A>) {
|
||||
await ctx.update({ message: 'Initializing...', isIndeterminate: true });
|
||||
|
||||
validateViewport(params);
|
||||
const durationMs = PluginStateAnimation.getDuration(plugin, params.animation);
|
||||
if (durationMs === void 0) {
|
||||
throw new Error('The animation does not have the duration specified.');
|
||||
}
|
||||
|
||||
const encoder = await HME.createH264MP4Encoder();
|
||||
|
||||
const { width, height } = params;
|
||||
let vw = params.viewport.width, vh = params.viewport.height;
|
||||
|
||||
// dimensions must be a multiple of 2
|
||||
if (vw % 2 !== 0) vw -= 1;
|
||||
if (vh % 2 !== 0) vh -= 1;
|
||||
|
||||
const normalizedViewport: Viewport = { ...params.viewport, width: vw, height: vh };
|
||||
|
||||
encoder.width = vw;
|
||||
encoder.height = vh;
|
||||
if (params.quantizationParameter) encoder.quantizationParameter = params.quantizationParameter;
|
||||
if (params.fps) encoder.frameRate = params.fps;
|
||||
encoder.initialize();
|
||||
|
||||
const loop = plugin.animationLoop;
|
||||
const canvasProps = plugin.canvas3d?.props;
|
||||
const wasAnimating = loop.isAnimating;
|
||||
let stoppedAnimation = true, finalized = false;
|
||||
|
||||
try {
|
||||
loop.stop();
|
||||
loop.resetTime(0);
|
||||
|
||||
if (params.customBackground !== void 0) {
|
||||
plugin.canvas3d?.setProps({ renderer: { backgroundColor: params.customBackground }, transparentBackground: false }, true);
|
||||
}
|
||||
|
||||
const fps = encoder.frameRate;
|
||||
const N = Math.ceil(durationMs / 1000 * fps);
|
||||
const dt = durationMs / N;
|
||||
|
||||
await ctx.update({ message: 'Rendering...', isIndeterminate: false, current: 0, max: N + 1 });
|
||||
|
||||
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 });
|
||||
|
||||
const image = params.pass.getImageData(width, height, normalizedViewport);
|
||||
encoder.addFrameRgba(image.data);
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ current: i + 1 });
|
||||
}
|
||||
}
|
||||
await ctx.update({ message: 'Applying finishing touches...', isIndeterminate: true });
|
||||
await plugin.managers.animation.stop();
|
||||
stoppedAnimation = true;
|
||||
encoder.finalize();
|
||||
finalized = true;
|
||||
return encoder.FS.readFile(encoder.outputFilename);
|
||||
} finally {
|
||||
if (finalized) encoder.delete();
|
||||
if (params.customBackground !== void 0) {
|
||||
plugin.canvas3d?.setProps({ renderer: { backgroundColor: canvasProps?.renderer!.backgroundColor }, transparentBackground: canvasProps?.transparentBackground });
|
||||
}
|
||||
if (!stoppedAnimation) await plugin.managers.animation.stop();
|
||||
if (wasAnimating) loop.start();
|
||||
}
|
||||
}
|
||||
|
||||
function validateViewport(params: Mp4EncoderParams) {
|
||||
if (params.viewport.x + params.viewport.width > params.width || params.viewport.y + params.viewport.height > params.height) {
|
||||
throw new Error('Viewport exceeds the canvas dimensions.');
|
||||
}
|
||||
}
|
||||
30
src/extensions/mp4-export/index.ts
Normal file
30
src/extensions/mp4-export/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2020 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 { Mp4EncoderUI } from './ui';
|
||||
|
||||
export const Mp4Export = PluginBehavior.create<{ }>({
|
||||
name: 'extension-mp4-export',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'MP4 Animation Export'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ }> {
|
||||
register(): void {
|
||||
this.ctx.customStructureControls.set('mp4-export', Mp4EncoderUI as any);
|
||||
}
|
||||
|
||||
update() {
|
||||
return false;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
this.ctx.customStructureControls.delete('mp4-export');
|
||||
}
|
||||
},
|
||||
params: () => ({ })
|
||||
});
|
||||
123
src/extensions/mp4-export/ui.tsx
Normal file
123
src/extensions/mp4-export/ui.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { merge } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
|
||||
import { Button } from '../../mol-plugin-ui/controls/common';
|
||||
import { CameraOutlinedSvg, GetAppSvg, Icon, SubscriptionsOutlinedSvg } from '../../mol-plugin-ui/controls/icons';
|
||||
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
|
||||
import { download } from '../../mol-util/download';
|
||||
import { Mp4AnimationParams, Mp4Controls } from './controls';
|
||||
|
||||
interface State {
|
||||
busy?: boolean,
|
||||
data?: { movie: Uint8Array, filename: string };
|
||||
}
|
||||
|
||||
export class Mp4EncoderUI extends CollapsableControls<{}, State> {
|
||||
private _controls: Mp4Controls | undefined;
|
||||
|
||||
get controls() {
|
||||
return this._controls || (this._controls = new Mp4Controls(this.plugin));
|
||||
}
|
||||
|
||||
protected defaultState(): State & CollapsableState {
|
||||
return {
|
||||
header: 'Export Animation',
|
||||
isCollapsed: true,
|
||||
brand: { accent: 'cyan', svg: SubscriptionsOutlinedSvg }
|
||||
};
|
||||
}
|
||||
|
||||
private downloadControls() {
|
||||
return <>
|
||||
<div className='msp-control-offset msp-help-text'>
|
||||
<div className='msp-help-description' style={{ textAlign: 'center' }}>
|
||||
Rendering successful!
|
||||
</div>
|
||||
</div>
|
||||
<Button icon={GetAppSvg} onClick={this.save} style={{ marginTop: 1 }}>Save Animation</Button>
|
||||
<Button onClick={() => this.setState({ data: void 0 })} style={{ marginTop: 6 }}>Clear</Button>
|
||||
</>;
|
||||
}
|
||||
|
||||
protected renderControls(): JSX.Element | null {
|
||||
if (this.state.data) {
|
||||
return this.downloadControls();
|
||||
}
|
||||
|
||||
const ctrl = this.controls;
|
||||
const current = ctrl.behaviors.current.value;
|
||||
const info = ctrl.behaviors.info.value;
|
||||
const canApply = ctrl.behaviors.canApply.value;
|
||||
return <>
|
||||
<ParameterControls
|
||||
params={ctrl.behaviors.animations.value}
|
||||
values={{ current: current?.anim.name }}
|
||||
onChangeValues={xs => ctrl.setCurrent(xs.current)}
|
||||
isDisabled={this.state.busy}
|
||||
/>
|
||||
{current && <ParameterControls
|
||||
params={current.params}
|
||||
values={current.values}
|
||||
onChangeValues={xs => ctrl.setCurrentParams(xs)}
|
||||
isDisabled={this.state.busy}
|
||||
/>}
|
||||
<div className='msp-control-offset msp-help-text'>
|
||||
<div className='msp-help-description' style={{ textAlign: 'center' }}>
|
||||
Resolution: {info.width}x{info.height}<br />
|
||||
Adjust in viewport using <Icon svg={CameraOutlinedSvg} inline />
|
||||
</div>
|
||||
</div>
|
||||
<ParameterControls
|
||||
params={Mp4AnimationParams}
|
||||
values={ctrl.behaviors.params.value}
|
||||
onChangeValues={xs => ctrl.behaviors.params.next(xs)}
|
||||
isDisabled={this.state.busy}
|
||||
/>
|
||||
<Button onClick={this.generate} style={{ marginTop: 1 }}
|
||||
disabled={this.state.busy || !canApply.canApply}
|
||||
commit={canApply.canApply ? 'on' : 'off'}>
|
||||
{canApply.canApply ? 'Render' : canApply.reason ?? 'Invalid params/state'}
|
||||
</Button>
|
||||
</>;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const merged = merge(
|
||||
this.controls.behaviors.animations,
|
||||
this.controls.behaviors.current,
|
||||
this.controls.behaviors.canApply,
|
||||
this.controls.behaviors.info,
|
||||
this.controls.behaviors.params
|
||||
);
|
||||
|
||||
this.subscribe(merged.pipe(debounceTime(10)), () => {
|
||||
if (!this.state.isCollapsed) this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._controls?.dispose();
|
||||
this._controls = void 0;
|
||||
}
|
||||
|
||||
save = () => {
|
||||
download(new Blob([this.state.data!.movie]), this.state.data!.filename);
|
||||
}
|
||||
|
||||
generate = async () => {
|
||||
try {
|
||||
this.setState({ busy: true });
|
||||
const data = await this.controls.render();
|
||||
this.setState({ busy: false, data });
|
||||
} catch {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { StructureQualityReport, StructureQualityReportProvider } from './prop';
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { StructureElement } from '../../../mol-model/structure';
|
||||
import { Bond, StructureElement } from '../../../mol-model/structure';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
@@ -46,11 +46,16 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
|
||||
|
||||
if (ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(StructureQualityReportProvider.descriptor)) {
|
||||
const getIssues = StructureQualityReport.getIssues;
|
||||
const l = StructureElement.Location.create(ctx.structure);
|
||||
|
||||
if (props.type.name === 'issue-count') {
|
||||
color = (location: Location) => {
|
||||
if (StructureElement.Location.is(location)) {
|
||||
return ValidationColors[Math.min(3, getIssues(location).length) + 1];
|
||||
} else if (Bond.isLocation(location)) {
|
||||
l.unit = location.aUnit;
|
||||
l.element = location.aUnit.elements[location.aIndex];
|
||||
return ValidationColors[Math.min(3, getIssues(l).length) + 1];
|
||||
}
|
||||
return ValidationColors[0];
|
||||
};
|
||||
@@ -59,6 +64,10 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
|
||||
color = (location: Location) => {
|
||||
if (StructureElement.Location.is(location) && getIssues(location).indexOf(issue) >= 0) {
|
||||
return ValidationColors[4];
|
||||
} else if (Bond.isLocation(location)) {
|
||||
l.unit = location.aUnit;
|
||||
l.element = location.aUnit.elements[location.aIndex];
|
||||
return ValidationColors[Math.min(3, getIssues(l).length) + 1];
|
||||
}
|
||||
return ValidationColors[0];
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Model, ResidueIndex, Unit, IndexedCustomProperty } from '../../../mol-m
|
||||
import { residueIdFields } from '../../../mol-model/structure/export/categories/atom_site';
|
||||
import { StructureElement, CifExportContext, Structure } from '../../../mol-model/structure/structure';
|
||||
import { CustomPropSymbol } from '../../../mol-script/language/symbol';
|
||||
import Type from '../../../mol-script/language/type';
|
||||
import { Type } from '../../../mol-script/language/type';
|
||||
import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { arraySetAdd } from '../../../mol-util/array';
|
||||
|
||||
@@ -181,8 +181,8 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
|
||||
}
|
||||
|
||||
const assemblySymmetry = await tryCreateAssemblySymmetry(plugin, structureCell);
|
||||
const globalThemeName = assemblySymmetry.isOk ? Tag.Cluster as any : undefined;
|
||||
const preset = await PresetStructureRepresentations.auto.apply(ref, { ...params, globalThemeName }, plugin);
|
||||
const colorTheme = assemblySymmetry.isOk ? Tag.Cluster as any : undefined;
|
||||
const preset = await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin);
|
||||
|
||||
return { components: preset.components, representations: { ...preset.representations, assemblySymmetry } };
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { AssemblySymmetryProvider, AssemblySymmetry } from './prop';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { Unit, StructureElement, StructureProperties } from '../../../mol-model/structure';
|
||||
import { Unit, StructureElement, StructureProperties, Bond } from '../../../mol-model/structure';
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { ScaleLegend, TableLegend } from '../../../mol-util/legend';
|
||||
import { getPalette, getPaletteParams } from '../../../mol-util/color/palette';
|
||||
@@ -50,6 +50,8 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
|
||||
const clusters = assemblySymmetry?.value?.clusters;
|
||||
|
||||
if (clusters?.length && ctx.structure) {
|
||||
const l = StructureElement.Location.create(ctx.structure);
|
||||
|
||||
const clusterByMember = new Map<string, number>();
|
||||
for (let i = 0, il = clusters.length; i < il; ++i) {
|
||||
const { members } = clusters[i]!;
|
||||
@@ -67,12 +69,20 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
|
||||
legend = palette.legend;
|
||||
|
||||
const _emptyList: any[] = [];
|
||||
const getColor = (location: StructureElement.Location) => {
|
||||
const { assembly } = location.unit.conformation.operator;
|
||||
const asymId = getAsymId(location.unit)(location);
|
||||
const cluster = clusterByMember.get(clusterMemberKey(asymId, assembly?.operList || _emptyList));
|
||||
return cluster !== undefined ? palette.color(cluster) : DefaultColor;
|
||||
};
|
||||
|
||||
color = (location: Location): Color => {
|
||||
if (StructureElement.Location.is(location)) {
|
||||
const { assembly } = location.unit.conformation.operator;
|
||||
const asymId = getAsymId(location.unit)(location);
|
||||
const cluster = clusterByMember.get(clusterMemberKey(asymId, assembly?.operList || _emptyList));
|
||||
return cluster !== undefined ? palette.color(cluster) : DefaultColor;
|
||||
return getColor(location);
|
||||
} else if (Bond.isLocation(location)) {
|
||||
l.unit = location.aUnit;
|
||||
l.element = location.aUnit.elements[location.aIndex];
|
||||
return getColor(l);
|
||||
}
|
||||
return DefaultColor;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { AssemblySymmetryQuery, AssemblySymmetryQueryVariables } from '../graphql/types';
|
||||
import query from '../graphql/symmetry.gql';
|
||||
import { symmetry_gql } from '../graphql/symmetry.gql';
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Structure, Model, StructureSelection, QueryContext } from '../../../mol-model/structure';
|
||||
@@ -66,7 +66,7 @@ export namespace AssemblySymmetry {
|
||||
assembly_id: structure.units[0].conformation.operator.assembly?.id || '',
|
||||
entry_id: structure.units[0].model.entryId
|
||||
};
|
||||
const result = await client.request(ctx.runtime, query, variables);
|
||||
const result = await client.request(ctx.runtime, symmetry_gql, variables);
|
||||
let value: AssemblySymmetryDataValue = [];
|
||||
|
||||
if (!result.data.assembly?.rcsb_struct_symmetry) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default /* GraphQL */ `
|
||||
export const symmetry_gql = /* GraphQL */ `
|
||||
query AssemblySymmetry($assembly_id: String!, $entry_id: String!) {
|
||||
assembly(assembly_id: $assembly_id, entry_id: $entry_id) {
|
||||
rcsb_struct_symmetry {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -309,15 +309,15 @@ export const ValidationReportGeometryQualityPreset = StructureRepresentationPres
|
||||
params: () => StructureRepresentationPresetProvider.CommonParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const model = structureCell?.obj?.data.model;
|
||||
if (!structureCell || !model) return {};
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
await plugin.runTask(Task.create('Validation Report', async runtime => {
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure.models[0]);
|
||||
}));
|
||||
|
||||
const colorTheme = GeometryQualityColorThemeProvider.name as any;
|
||||
const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params, globalThemeName: colorTheme }, plugin);
|
||||
const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin);
|
||||
|
||||
const clashes = await plugin.builders.structure.tryCreateComponentFromExpression(structureCell, hasClash.expression, 'clashes', { label: 'Clashes' });
|
||||
|
||||
@@ -329,6 +329,7 @@ export const ValidationReportGeometryQualityPreset = StructureRepresentationPres
|
||||
}
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
|
||||
return { components: { ...components, clashes }, representations: { ...representations, clashesBallAndStick, clashesRepr } };
|
||||
}
|
||||
});
|
||||
@@ -345,15 +346,15 @@ export const ValidationReportDensityFitPreset = StructureRepresentationPresetPro
|
||||
params: () => StructureRepresentationPresetProvider.CommonParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const model = structureCell?.obj?.data.model;
|
||||
if (!structureCell || !model) return {};
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
await plugin.runTask(Task.create('Validation Report', async runtime => {
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure.models[0]);
|
||||
}));
|
||||
|
||||
const colorTheme = DensityFitColorThemeProvider.name as any;
|
||||
return await PresetStructureRepresentations.auto.apply(ref, { ...params, globalThemeName: colorTheme }, plugin);
|
||||
return await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -369,14 +370,14 @@ export const ValidationReportRandomCoilIndexPreset = StructureRepresentationPres
|
||||
params: () => StructureRepresentationPresetProvider.CommonParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const model = structureCell?.obj?.data.model;
|
||||
if (!structureCell || !model) return {};
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
await plugin.runTask(Task.create('Validation Report', async runtime => {
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure.models[0]);
|
||||
}));
|
||||
|
||||
const colorTheme = RandomCoilIndexColorThemeProvider.name as any;
|
||||
return await PresetStructureRepresentations.auto.apply(ref, { ...params, globalThemeName: colorTheme }, plugin);
|
||||
return await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin);
|
||||
}
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import { ThemeDataContext } from '../../../../mol-theme/theme';
|
||||
import { ColorTheme, LocationColor } from '../../../../mol-theme/color';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { Color, ColorScale } from '../../../../mol-util/color';
|
||||
import { StructureElement, Model } from '../../../../mol-model/structure';
|
||||
import { StructureElement, Model, ElementIndex, Bond } from '../../../../mol-model/structure';
|
||||
import { Location } from '../../../../mol-model/location';
|
||||
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
|
||||
import { ValidationReportProvider, ValidationReport } from '../prop';
|
||||
@@ -37,13 +37,19 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe
|
||||
if (validationReport?.value && model) {
|
||||
const { rsrz, rscc } = validationReport.value;
|
||||
const residueIndex = model.atomicHierarchy.residueAtomSegments.index;
|
||||
const getColor = (element: ElementIndex) => {
|
||||
const rsrzValue = rsrz.get(residueIndex[element]);
|
||||
if (rsrzValue !== undefined) return scaleRsrz.color(rsrzValue);
|
||||
const rsccValue = rscc.get(residueIndex[element]);
|
||||
if (rsccValue !== undefined) return scaleRscc.color(rsccValue);
|
||||
return DefaultColor;
|
||||
};
|
||||
|
||||
color = (location: Location): Color => {
|
||||
if (StructureElement.Location.is(location) && location.unit.model === model) {
|
||||
const rsrzValue = rsrz.get(residueIndex[location.element]);
|
||||
if (rsrzValue !== undefined) return scaleRsrz.color(rsrzValue);
|
||||
const rsccValue = rscc.get(residueIndex[location.element]);
|
||||
if (rsccValue !== undefined) return scaleRscc.color(rsccValue);
|
||||
return DefaultColor;
|
||||
return getColor(location.element);
|
||||
} else if (Bond.isLocation(location) && location.aUnit.model === model) {
|
||||
return getColor(location.aUnit.elements[location.aIndex]);
|
||||
}
|
||||
return DefaultColor;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ThemeDataContext } from '../../../../mol-theme/theme';
|
||||
import { ColorTheme, LocationColor } from '../../../../mol-theme/color';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
import { StructureElement } from '../../../../mol-model/structure';
|
||||
import { Bond, ElementIndex, StructureElement } from '../../../../mol-model/structure';
|
||||
import { Location } from '../../../../mol-model/location';
|
||||
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
|
||||
import { ValidationReportProvider, ValidationReport } from '../prop';
|
||||
@@ -32,7 +32,7 @@ const ColorLegend = TableLegend([
|
||||
]);
|
||||
|
||||
export function getGeometricQualityColorThemeParams(ctx: ThemeDataContext) {
|
||||
const validationReport = ctx.structure && ValidationReportProvider.get(ctx.structure.models[0]).value;
|
||||
const validationReport = !!ctx.structure && ctx.structure.models.length > 0 && ValidationReportProvider.get(ctx.structure.models[0]).value;
|
||||
const options: [string, string][] = [];
|
||||
if (validationReport) {
|
||||
const kinds = new Set<string>();
|
||||
@@ -48,7 +48,7 @@ export type GeometricQualityColorThemeParams = ReturnType<typeof getGeometricQua
|
||||
export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Values<GeometricQualityColorThemeParams>): ColorTheme<GeometricQualityColorThemeParams> {
|
||||
let color: LocationColor = () => DefaultColor;
|
||||
|
||||
const validationReport = ctx.structure && ValidationReportProvider.get(ctx.structure.models[0]);
|
||||
const validationReport = !!ctx.structure && ctx.structure.models.length > 0 ? ValidationReportProvider.get(ctx.structure.models[0]) : void 0;
|
||||
const contextHash = validationReport?.version;
|
||||
|
||||
const value = validationReport?.value;
|
||||
@@ -59,31 +59,35 @@ export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Value
|
||||
const residueIndex = model.atomicHierarchy.residueAtomSegments.index;
|
||||
const { polymerType } = model.atomicHierarchy.derived.residue;
|
||||
const ignore = new Set(props.ignore);
|
||||
const getColor = (element: ElementIndex) => {
|
||||
const rI = residueIndex[element];
|
||||
|
||||
const value = geometryIssues.get(rI);
|
||||
if (value === undefined) return DefaultColor;
|
||||
|
||||
let count = SetUtils.differenceSize(value, ignore);
|
||||
|
||||
if (count > 0 && polymerType[rI] === PolymerType.NA) {
|
||||
count = 0;
|
||||
if (!ignore.has('clash') && clashes.getVertexEdgeCount(element) > 0) count += 1;
|
||||
if (!ignore.has('mog-bond-outlier') && bondOutliers.index.has(element)) count += 1;
|
||||
if (!ignore.has('mog-angle-outlier') && angleOutliers.index.has(element)) count += 1;
|
||||
}
|
||||
|
||||
switch (count) {
|
||||
case undefined: return DefaultColor;
|
||||
case 0: return NoIssuesColor;
|
||||
case 1: return OneIssueColor;
|
||||
case 2: return TwoIssuesColor;
|
||||
default: return ThreeOrMoreIssuesColor;
|
||||
}
|
||||
};
|
||||
|
||||
color = (location: Location): Color => {
|
||||
if (StructureElement.Location.is(location) && location.unit.model === model) {
|
||||
const { element } = location;
|
||||
const rI = residueIndex[element];
|
||||
|
||||
const value = geometryIssues.get(rI);
|
||||
if (value === undefined) return DefaultColor;
|
||||
|
||||
let count = SetUtils.differenceSize(value, ignore);
|
||||
|
||||
if (count > 0 && polymerType[rI] === PolymerType.NA) {
|
||||
count = 0;
|
||||
if (!ignore.has('clash') && clashes.getVertexEdgeCount(element) > 0) count += 1;
|
||||
if (!ignore.has('mog-bond-outlier') && bondOutliers.index.has(element)) count += 1;
|
||||
if (!ignore.has('mog-angle-outlier') && angleOutliers.index.has(element)) count += 1;
|
||||
}
|
||||
|
||||
switch (count) {
|
||||
case undefined: return DefaultColor;
|
||||
case 0: return NoIssuesColor;
|
||||
case 1: return OneIssueColor;
|
||||
case 2: return TwoIssuesColor;
|
||||
default: return ThreeOrMoreIssuesColor;
|
||||
}
|
||||
return getColor(location.element);
|
||||
} else if (Bond.isLocation(location) && location.aUnit.model === model) {
|
||||
return getColor(location.aUnit.elements[location.aIndex]);
|
||||
}
|
||||
return DefaultColor;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ThemeDataContext } from '../../../../mol-theme/theme';
|
||||
import { ColorTheme, LocationColor } from '../../../../mol-theme/color';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { Color, ColorScale } from '../../../../mol-util/color';
|
||||
import { StructureElement, Model } from '../../../../mol-model/structure';
|
||||
import { StructureElement, Model, ElementIndex, Bond } from '../../../../mol-model/structure';
|
||||
import { Location } from '../../../../mol-model/location';
|
||||
import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
|
||||
import { ValidationReportProvider, ValidationReport } from '../prop';
|
||||
@@ -31,10 +31,16 @@ export function RandomCoilIndexColorTheme(ctx: ThemeDataContext, props: {}): Col
|
||||
|
||||
if (rci && model) {
|
||||
const residueIndex = model.atomicHierarchy.residueAtomSegments.index;
|
||||
const getColor = (element: ElementIndex) => {
|
||||
const value = rci.get(residueIndex[element]);
|
||||
return value === undefined ? DefaultColor : scale.color(value);
|
||||
};
|
||||
|
||||
color = (location: Location): Color => {
|
||||
if (StructureElement.Location.is(location) && location.unit.model === model) {
|
||||
const value = rci.get(residueIndex[location.element]);
|
||||
return value === undefined ? DefaultColor : scale.color(value);
|
||||
return getColor(location.element);
|
||||
} else if (Bond.isLocation(location) && location.aUnit.model === model) {
|
||||
return getColor(location.aUnit.elements[location.aIndex]);
|
||||
}
|
||||
return DefaultColor;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ import { equalEps } from '../../../mol-math/linear-algebra/3d/common';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
|
||||
import { CustomPropSymbol } from '../../../mol-script/language/symbol';
|
||||
import Type from '../../../mol-script/language/type';
|
||||
import { Type } from '../../../mol-script/language/type';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
|
||||
@@ -187,7 +187,7 @@ type InterUnitClashesProps = {
|
||||
}
|
||||
|
||||
export type IntraUnitClashes = IntAdjacencyGraph<UnitIndex, IntraUnitClashesProps>
|
||||
export type InterUnitClashes = InterUnitGraph<Unit.Atomic, UnitIndex, InterUnitClashesProps>
|
||||
export type InterUnitClashes = InterUnitGraph<number, UnitIndex, InterUnitClashesProps>
|
||||
|
||||
export interface Clashes {
|
||||
readonly interUnit: InterUnitClashes
|
||||
@@ -195,7 +195,7 @@ export interface Clashes {
|
||||
}
|
||||
|
||||
function createInterUnitClashes(structure: Structure, clashes: ValidationReport['clashes']) {
|
||||
const builder = new InterUnitGraph.Builder<Unit.Atomic, UnitIndex, InterUnitClashesProps>();
|
||||
const builder = new InterUnitGraph.Builder<number, UnitIndex, InterUnitClashesProps>();
|
||||
const { a, b, edgeProps: { id, magnitude, distance } } = clashes;
|
||||
|
||||
const pA = Vec3(), pB = Vec3();
|
||||
@@ -204,7 +204,7 @@ function createInterUnitClashes(structure: Structure, clashes: ValidationReport[
|
||||
const elementsA = unitA.elements;
|
||||
const elementsB = unitB.elements;
|
||||
|
||||
builder.startUnitPair(unitA as Unit.Atomic, unitB as Unit.Atomic);
|
||||
builder.startUnitPair(unitA.id, unitB.id);
|
||||
|
||||
for (let i = 0, il = clashes.edgeCount * 2; i < il; ++i) {
|
||||
// TODO create lookup
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Interval } from '../../../mol-data/int';
|
||||
import { RepresentationContext, RepresentationParamsGetter, Representation } from '../../../mol-repr/representation';
|
||||
import { UnitsRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider, ComplexRepresentation } from '../../../mol-repr/structure/representation';
|
||||
import { VisualContext } from '../../../mol-repr/visual';
|
||||
import { createLinkCylinderMesh, LinkCylinderParams, LinkCylinderStyle } from '../../../mol-repr/structure/visual/util/link';
|
||||
import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
|
||||
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../../../mol-repr/structure/units-visual';
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
@@ -50,7 +50,7 @@ function createIntraUnitClashCylinderMesh(ctx: VisualContext, unit: Unit, struct
|
||||
pos(elements[a[edgeIndex]], posA);
|
||||
pos(elements[b[edgeIndex]], posB);
|
||||
},
|
||||
style: (edgeIndex: number) => LinkCylinderStyle.Disk,
|
||||
style: (edgeIndex: number) => LinkStyle.Disk,
|
||||
radius: (edgeIndex: number) => magnitude[edgeIndex] * sizeFactor,
|
||||
};
|
||||
|
||||
@@ -143,7 +143,7 @@ function createIntraClashIterator(structureGroup: StructureGroup): LocationItera
|
||||
location.element = unit.elements[a[groupIndex]];
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, getLocation);
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -159,11 +159,12 @@ function createInterUnitClashCylinderMesh(ctx: VisualContext, structure: Structu
|
||||
linkCount: edgeCount,
|
||||
position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
|
||||
const b = edges[edgeIndex];
|
||||
const uA = b.unitA, uB = b.unitB;
|
||||
const uA = structure.unitMap.get(b.unitA);
|
||||
const uB = structure.unitMap.get(b.unitB);
|
||||
uA.conformation.position(uA.elements[b.indexA], posA);
|
||||
uB.conformation.position(uB.elements[b.indexB], posB);
|
||||
},
|
||||
style: (edgeIndex: number) => LinkCylinderStyle.Disk,
|
||||
style: (edgeIndex: number) => LinkStyle.Disk,
|
||||
radius: (edgeIndex: number) => edges[edgeIndex].props.magnitude * sizeFactor
|
||||
};
|
||||
|
||||
@@ -197,11 +198,13 @@ export function InterUnitClashVisual(materialId: number): ComplexVisual<InterUni
|
||||
}, materialId);
|
||||
}
|
||||
|
||||
function getInterClashBoundingSphere(clashes: InterUnitClashes, elements: number[], boundingSphere: Sphere3D) {
|
||||
function getInterClashBoundingSphere(structure: Structure, clashes: InterUnitClashes, elements: number[], boundingSphere: Sphere3D) {
|
||||
return CentroidHelper.fromPairProvider(elements.length, (i, pA, pB) => {
|
||||
const c = clashes.edges[elements[i]];
|
||||
c.unitA.conformation.position(c.unitA.elements[c.indexA], pA);
|
||||
c.unitB.conformation.position(c.unitB.elements[c.indexB], pB);
|
||||
const uA = structure.unitMap.get(c.unitA);
|
||||
const uB = structure.unitMap.get(c.unitB);
|
||||
uA.conformation.position(uA.elements[c.indexA], pA);
|
||||
uB.conformation.position(uB.elements[c.indexB], pB);
|
||||
}, boundingSphere);
|
||||
}
|
||||
|
||||
@@ -209,18 +212,20 @@ function getInterClashLabel(structure: Structure, clashes: InterUnitClashes, ele
|
||||
const idx = elements[0];
|
||||
if (idx === undefined) return '';
|
||||
const c = clashes.edges[idx];
|
||||
const uA = structure.unitMap.get(c.unitA);
|
||||
const uB = structure.unitMap.get(c.unitB);
|
||||
const mag = c.props.magnitude.toFixed(2);
|
||||
const dist = c.props.distance.toFixed(2);
|
||||
|
||||
return [
|
||||
`Clash id: ${c.props.id} | Magnitude: ${mag} \u212B | Distance: ${dist} \u212B`,
|
||||
bondLabel(Bond.Location(structure, c.unitA, c.indexA, structure, c.unitB, c.indexB))
|
||||
bondLabel(Bond.Location(structure, uA, c.indexA, structure, uB, c.indexB))
|
||||
].join('</br>');
|
||||
}
|
||||
|
||||
function InterClashLoci(structure: Structure, clashes: InterUnitClashes, elements: number[]) {
|
||||
return DataLoci('inter-clashes', clashes, elements,
|
||||
(boundingSphere: Sphere3D) => getInterClashBoundingSphere(clashes, elements, boundingSphere),
|
||||
(boundingSphere: Sphere3D) => getInterClashBoundingSphere(structure, clashes, elements, boundingSphere),
|
||||
() => getInterClashLabel(structure, clashes, elements));
|
||||
}
|
||||
|
||||
@@ -246,11 +251,11 @@ function createInterClashIterator(structure: Structure): LocationIterator {
|
||||
const location = StructureElement.Location.create(structure);
|
||||
const getLocation = (groupIndex: number) => {
|
||||
const clash = clashes.edges[groupIndex];
|
||||
location.unit = clash.unitA;
|
||||
location.element = clash.unitA.elements[clash.indexA];
|
||||
location.unit = structure.unitMap.get(clash.unitA);
|
||||
location.element = location.unit.elements[clash.indexA];
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, getLocation, true);
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation, true);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -10,22 +10,37 @@ import { Viewport, cameraProject, cameraUnproject } from './camera/util';
|
||||
import { CameraTransitionManager } from './camera/transition';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export { Camera };
|
||||
export { ICamera, Camera };
|
||||
|
||||
class Camera {
|
||||
interface ICamera {
|
||||
readonly viewport: Viewport,
|
||||
readonly view: Mat4,
|
||||
readonly projection: Mat4,
|
||||
readonly projectionView: Mat4,
|
||||
readonly inverseProjectionView: Mat4,
|
||||
readonly state: Readonly<Camera.Snapshot>,
|
||||
readonly viewOffset: Camera.ViewOffset,
|
||||
readonly far: number,
|
||||
readonly near: number,
|
||||
readonly fogFar: number,
|
||||
readonly fogNear: number,
|
||||
}
|
||||
|
||||
class Camera implements ICamera {
|
||||
readonly view: Mat4 = Mat4.identity();
|
||||
readonly projection: Mat4 = Mat4.identity();
|
||||
readonly projectionView: Mat4 = Mat4.identity();
|
||||
readonly inverseProjectionView: Mat4 = Mat4.identity();
|
||||
|
||||
private pixelScale: number
|
||||
get pixelRatio () {
|
||||
const dpr = (typeof window !== 'undefined') ? window.devicePixelRatio : 1;
|
||||
return dpr * this.pixelScale;
|
||||
}
|
||||
|
||||
readonly viewport: Viewport;
|
||||
readonly state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
|
||||
readonly viewOffset: Camera.ViewOffset = {
|
||||
enabled: false,
|
||||
fullWidth: 1, fullHeight: 1,
|
||||
offsetX: 0, offsetY: 0,
|
||||
width: 1, height: 1
|
||||
}
|
||||
readonly viewOffset = Camera.ViewOffset();
|
||||
|
||||
near = 1
|
||||
far = 10000
|
||||
@@ -52,6 +67,10 @@ class Camera {
|
||||
|
||||
update() {
|
||||
const snapshot = this.state as Camera.Snapshot;
|
||||
if (snapshot.radiusMax === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const height = 2 * Math.tan(snapshot.fov / 2) * Vec3.distance(snapshot.position, snapshot.target);
|
||||
this.zoom = this.viewport.height / height;
|
||||
|
||||
@@ -86,12 +105,7 @@ class Camera {
|
||||
}
|
||||
|
||||
getTargetDistance(radius: number) {
|
||||
const r = Math.max(radius, 0.01);
|
||||
const { fov } = this.state;
|
||||
const { width, height } = this.viewport;
|
||||
const aspect = width / height;
|
||||
const aspectFactor = (height < width ? 1 : aspect);
|
||||
return Math.abs((r / aspectFactor) / Math.sin(fov / 2));
|
||||
return Camera.targetDistance(radius, this.state.fov, this.viewport.width, this.viewport.height);
|
||||
}
|
||||
|
||||
getFocus(target: Vec3, radius: number, up?: Vec3, dir?: Vec3): Partial<Camera.Snapshot> {
|
||||
@@ -126,8 +140,9 @@ class Camera {
|
||||
return cameraUnproject(out, point, this.viewport, this.inverseProjectionView);
|
||||
}
|
||||
|
||||
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(-1, -1, 1, 1)) {
|
||||
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128), props: Partial<{ pixelScale: number }> = {}) {
|
||||
this.viewport = viewport;
|
||||
this.pixelScale = props.pixelScale || 1;
|
||||
Camera.copySnapshot(this.state, state);
|
||||
}
|
||||
}
|
||||
@@ -150,6 +165,15 @@ namespace Camera {
|
||||
height: number
|
||||
}
|
||||
|
||||
export function ViewOffset(): ViewOffset {
|
||||
return {
|
||||
enabled: false,
|
||||
fullWidth: 1, fullHeight: 1,
|
||||
offsetX: 0, offsetY: 0,
|
||||
width: 1, height: 1
|
||||
};
|
||||
}
|
||||
|
||||
export function setViewOffset(out: ViewOffset, fullWidth: number, fullHeight: number, offsetX: number, offsetY: number, width: number, height: number) {
|
||||
out.fullWidth = fullWidth;
|
||||
out.fullHeight = fullHeight;
|
||||
@@ -159,6 +183,23 @@ namespace Camera {
|
||||
out.height = height;
|
||||
}
|
||||
|
||||
export function copyViewOffset(out: ViewOffset, view: ViewOffset) {
|
||||
out.enabled = view.enabled;
|
||||
out.fullWidth = view.fullWidth;
|
||||
out.fullHeight = view.fullHeight;
|
||||
out.offsetX = view.offsetX;
|
||||
out.offsetY = view.offsetY;
|
||||
out.width = view.width;
|
||||
out.height = view.height;
|
||||
}
|
||||
|
||||
export function targetDistance(radius: number, fov: number, width: number, height: number) {
|
||||
const r = Math.max(radius, 0.01);
|
||||
const aspect = width / height;
|
||||
const aspectFactor = (height < width ? 1 : aspect);
|
||||
return Math.abs((r / aspectFactor) / Math.sin(fov / 2));
|
||||
}
|
||||
|
||||
export function createDefaultSnapshot(): Snapshot {
|
||||
return {
|
||||
mode: 'perspective',
|
||||
@@ -168,8 +209,8 @@ namespace Camera {
|
||||
up: Vec3.create(0, 1, 0),
|
||||
target: Vec3.create(0, 0, 0),
|
||||
|
||||
radius: 10,
|
||||
radiusMax: 10,
|
||||
radius: 0,
|
||||
radiusMax: 0,
|
||||
fog: 50,
|
||||
clipFar: true
|
||||
};
|
||||
@@ -211,10 +252,10 @@ namespace Camera {
|
||||
function updateOrtho(camera: Camera) {
|
||||
const { viewport, zoom, near, far, viewOffset } = camera;
|
||||
|
||||
const fullLeft = -(viewport.width - viewport.x) / 2;
|
||||
const fullRight = (viewport.width - viewport.x) / 2;
|
||||
const fullTop = (viewport.height - viewport.y) / 2;
|
||||
const fullBottom = -(viewport.height - viewport.y) / 2;
|
||||
const fullLeft = -viewport.width / 2;
|
||||
const fullRight = viewport.width / 2;
|
||||
const fullTop = viewport.height / 2;
|
||||
const fullBottom = -viewport.height / 2;
|
||||
|
||||
const dx = (fullRight - fullLeft) / (2 * zoom);
|
||||
const dy = (fullTop - fullBottom) / (2 * zoom);
|
||||
@@ -278,12 +319,12 @@ function updateClip(camera: Camera) {
|
||||
let far = cameraDistance + normalizedFar;
|
||||
|
||||
const fogNearFactor = -(50 - fog) / 50;
|
||||
let fogNear = cameraDistance - (normalizedFar * fogNearFactor);
|
||||
let fogFar = far;
|
||||
const fogNear = cameraDistance - (normalizedFar * fogNearFactor);
|
||||
const fogFar = far;
|
||||
|
||||
if (mode === 'perspective') {
|
||||
// set at least to 5 to avoid slow sphere impostor rendering
|
||||
near = Math.max(5, near);
|
||||
near = Math.max(Math.min(radiusMax, 5), near);
|
||||
far = Math.max(5, far);
|
||||
} else {
|
||||
near = Math.max(0, near);
|
||||
@@ -296,7 +337,7 @@ function updateClip(camera: Camera) {
|
||||
}
|
||||
|
||||
camera.near = near;
|
||||
camera.far = far;
|
||||
camera.far = 2 * far; // avoid precision issues distingushing far objects from background
|
||||
camera.fogNear = fogNear;
|
||||
camera.fogFar = fogFar;
|
||||
}
|
||||
141
src/mol-canvas3d/camera/stereo.ts
Normal file
141
src/mol-canvas3d/camera/stereo.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (c) 2020 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>
|
||||
*
|
||||
* Adapted from three.js, The MIT License, Copyright © 2010-2020 three.js authors
|
||||
*/
|
||||
|
||||
import { Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
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 }),
|
||||
};
|
||||
export const DefaultStereoCameraProps = PD.getDefaultValues(StereoCameraParams);
|
||||
export type StereoCameraProps = PD.Values<typeof StereoCameraParams>
|
||||
|
||||
export { StereoCamera };
|
||||
|
||||
class StereoCamera {
|
||||
readonly left: ICamera = new EyeCamera();
|
||||
readonly right: ICamera = new EyeCamera();
|
||||
|
||||
get viewport() {
|
||||
return this.parent.viewport;
|
||||
}
|
||||
|
||||
get viewOffset() {
|
||||
return this.parent.viewOffset;
|
||||
}
|
||||
|
||||
private props: StereoCameraProps
|
||||
|
||||
constructor(private parent: Camera, props: Partial<StereoCameraProps> = {}) {
|
||||
this.props = { ...DefaultStereoCameraProps, ...props };
|
||||
}
|
||||
|
||||
setProps(props: Partial<StereoCameraProps>) {
|
||||
Object.assign(this.props, props);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.parent.update();
|
||||
update(this.parent, this.props, this.left as EyeCamera, this.right as EyeCamera);
|
||||
}
|
||||
}
|
||||
|
||||
namespace StereoCamera {
|
||||
export function is(camera: Camera | StereoCamera): camera is StereoCamera {
|
||||
return 'left' in camera && 'right' in camera;
|
||||
}
|
||||
}
|
||||
|
||||
class EyeCamera implements ICamera {
|
||||
viewport = Viewport.create(0, 0, 0, 0);
|
||||
view = Mat4();
|
||||
projection = Mat4();
|
||||
projectionView = Mat4();
|
||||
inverseProjectionView = Mat4();
|
||||
state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
|
||||
viewOffset: Readonly<Camera.ViewOffset> = Camera.ViewOffset();
|
||||
far: number = 0;
|
||||
near: number = 0;
|
||||
fogFar: number = 0;
|
||||
fogNear: number = 0;
|
||||
}
|
||||
|
||||
const eyeLeft = Mat4.identity(), eyeRight = Mat4.identity();
|
||||
|
||||
function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right: EyeCamera) {
|
||||
// Copy the states
|
||||
|
||||
Viewport.copy(left.viewport, camera.viewport);
|
||||
Mat4.copy(left.view, camera.view);
|
||||
Mat4.copy(left.projection, camera.projection);
|
||||
Camera.copySnapshot(left.state, camera.state);
|
||||
Camera.copyViewOffset(left.viewOffset, camera.viewOffset);
|
||||
left.far = camera.far;
|
||||
left.near = camera.near;
|
||||
left.fogFar = camera.fogFar;
|
||||
left.fogNear = camera.fogNear;
|
||||
|
||||
Viewport.copy(right.viewport, camera.viewport);
|
||||
Mat4.copy(right.view, camera.view);
|
||||
Mat4.copy(right.projection, camera.projection);
|
||||
Camera.copySnapshot(right.state, camera.state);
|
||||
Camera.copyViewOffset(right.viewOffset, camera.viewOffset);
|
||||
right.far = camera.far;
|
||||
right.near = camera.near;
|
||||
right.fogFar = camera.fogFar;
|
||||
right.fogNear = camera.fogNear;
|
||||
|
||||
// update the view offsets
|
||||
|
||||
const w = Math.floor(camera.viewport.width / 2);
|
||||
const aspect = w / camera.viewport.height;
|
||||
|
||||
left.viewport.width = w;
|
||||
right.viewport.x += w;
|
||||
right.viewport.width -= w;
|
||||
|
||||
// update the projection and view matrices
|
||||
|
||||
const eyeSepHalf = props.eyeSeparation / 2;
|
||||
const eyeSepOnProjection = eyeSepHalf * camera.near / props.focus;
|
||||
const ymax = camera.near * Math.tan(camera.state.fov * 0.5);
|
||||
let xmin, xmax;
|
||||
|
||||
// translate xOffset
|
||||
|
||||
eyeLeft[12] = -eyeSepHalf;
|
||||
eyeRight[12] = eyeSepHalf;
|
||||
|
||||
// for left eye
|
||||
|
||||
xmin = -ymax * aspect + eyeSepOnProjection;
|
||||
xmax = ymax * aspect + eyeSepOnProjection;
|
||||
|
||||
left.projection[0] = 2 * camera.near / (xmax - xmin);
|
||||
left.projection[8] = (xmax + xmin) / (xmax - xmin);
|
||||
|
||||
Mat4.mul(left.view, left.view, eyeLeft);
|
||||
Mat4.mul(left.projectionView, left.projection, left.view);
|
||||
Mat4.invert(left.inverseProjectionView, left.projectionView);
|
||||
|
||||
// for right eye
|
||||
|
||||
xmin = -ymax * aspect - eyeSepOnProjection;
|
||||
xmax = ymax * aspect - eyeSepOnProjection;
|
||||
|
||||
right.projection[0] = 2 * camera.near / (xmax - xmin);
|
||||
right.projection[8] = (xmax + xmin) / (xmax - xmin);
|
||||
|
||||
Mat4.mul(right.view, right.view, eyeRight);
|
||||
Mat4.mul(right.projectionView, right.projection, right.view);
|
||||
Mat4.invert(right.inverseProjectionView, right.projectionView);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-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>
|
||||
@@ -7,45 +7,51 @@
|
||||
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { now } from '../mol-util/now';
|
||||
import { Vec3 } from '../mol-math/linear-algebra';
|
||||
import InputObserver, { ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer';
|
||||
import Renderer, { RendererStats, RendererParams } from '../mol-gl/renderer';
|
||||
import { Vec3, Vec2 } from '../mol-math/linear-algebra';
|
||||
import { InputObserver, ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer';
|
||||
import { Renderer, RendererStats, RendererParams } from '../mol-gl/renderer';
|
||||
import { GraphicsRenderObject } from '../mol-gl/render-object';
|
||||
import { TrackballControls, TrackballControlsParams } from './controls/trackball';
|
||||
import { Viewport } from './camera/util';
|
||||
import { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context';
|
||||
import { Representation } from '../mol-repr/representation';
|
||||
import Scene from '../mol-gl/scene';
|
||||
import { GraphicsRenderVariant } from '../mol-gl/webgl/render-item';
|
||||
import { Scene } from '../mol-gl/scene';
|
||||
import { PickingId } from '../mol-geo/geometry/picking';
|
||||
import { MarkerAction } from '../mol-util/marker-action';
|
||||
import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
|
||||
import { Camera } from './camera';
|
||||
import { ParamDefinition as PD } from '../mol-util/param-definition';
|
||||
import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper';
|
||||
import { DebugHelperParams } from './helper/bounding-sphere-helper';
|
||||
import { SetUtils } from '../mol-util/set';
|
||||
import { Canvas3dInteractionHelper } from './helper/interaction-events';
|
||||
import { PostprocessingParams, PostprocessingPass } from './passes/postprocessing';
|
||||
import { MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
|
||||
import { PixelData } from '../mol-util/image';
|
||||
import { readTexture } from '../mol-gl/compute/util';
|
||||
import { DrawPass } from './passes/draw';
|
||||
import { PickPass } from './passes/pick';
|
||||
import { PostprocessingParams } from './passes/postprocessing';
|
||||
import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
|
||||
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 { CameraHelperParams } from './helper/camera-helper';
|
||||
import { produce } from 'immer';
|
||||
import { HandleHelper, HandleHelperParams } from './helper/handle-helper';
|
||||
import { HandleHelperParams } from './helper/handle-helper';
|
||||
import { StereoCamera, StereoCameraParams } from './camera/stereo';
|
||||
import { Helper } from './helper/helper';
|
||||
import { Passes } from './passes/passes';
|
||||
import { shallowEqual } from '../mol-util';
|
||||
|
||||
export const Canvas3DParams = {
|
||||
camera: PD.Group({
|
||||
mode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']] as const, { label: 'Camera' }),
|
||||
helper: PD.Group(CameraHelperParams, { isFlat: true })
|
||||
mode: PD.Select('perspective', PD.arrayToOptions(['perspective', 'orthographic'] as const), { label: 'Camera' }),
|
||||
helper: PD.Group(CameraHelperParams, { isFlat: true }),
|
||||
stereo: PD.MappedStatic('off', {
|
||||
on: PD.Group(StereoCameraParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
|
||||
manualReset: PD.Boolean(false, { isHidden: true }),
|
||||
}, { pivot: 'mode' }),
|
||||
cameraFog: PD.MappedStatic('on', {
|
||||
on: PD.Group({
|
||||
intensity: PD.Numeric(50, { min: 1, max: 100, step: 1 }),
|
||||
intensity: PD.Numeric(15, { min: 1, max: 100, step: 1 }),
|
||||
}),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Show fog in the distance' }),
|
||||
@@ -53,6 +59,15 @@ export const Canvas3DParams = {
|
||||
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' }),
|
||||
}, { pivot: 'radius' }),
|
||||
viewport: PD.MappedStatic('canvas', {
|
||||
canvas: PD.Group({}),
|
||||
custom: PD.Group({
|
||||
x: PD.Numeric(0),
|
||||
y: PD.Numeric(0),
|
||||
width: PD.Numeric(128),
|
||||
height: PD.Numeric(128)
|
||||
})
|
||||
}),
|
||||
|
||||
cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
|
||||
transparentBackground: PD.Boolean(false),
|
||||
@@ -66,70 +81,57 @@ export const Canvas3DParams = {
|
||||
};
|
||||
export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
|
||||
export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
|
||||
|
||||
export { Canvas3D };
|
||||
|
||||
interface Canvas3D {
|
||||
readonly webgl: WebGLContext,
|
||||
|
||||
add(repr: Representation.Any): void
|
||||
remove(repr: Representation.Any): void
|
||||
/**
|
||||
* This function must be called if animate() is not set up so that add/remove actions take place.
|
||||
*/
|
||||
commit(isSynchronous?: boolean): void
|
||||
update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
|
||||
clear(): void
|
||||
syncVisibility(): void
|
||||
|
||||
requestDraw(force?: boolean): void
|
||||
animate(): void
|
||||
identify(x: number, y: number): PickingId | undefined
|
||||
mark(loci: Representation.Loci, action: MarkerAction): void
|
||||
getLoci(pickingId: PickingId): Representation.Loci
|
||||
|
||||
readonly didDraw: BehaviorSubject<now.Timestamp>
|
||||
readonly reprCount: BehaviorSubject<number>
|
||||
|
||||
handleResize(): void
|
||||
/** Focuses camera on scene's bounding sphere, centered and zoomed. */
|
||||
requestCameraReset(options?: { durationMs?: number, snapshot?: Partial<Camera.Snapshot> }): void
|
||||
readonly camera: Camera
|
||||
readonly boundingSphere: Readonly<Sphere3D>
|
||||
getPixelData(variant: GraphicsRenderVariant): PixelData
|
||||
setProps(props: Partial<Canvas3DProps> | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void)): void
|
||||
getImagePass(props: Partial<ImageProps>): ImagePass
|
||||
|
||||
/** Returns a copy of the current Canvas3D instance props */
|
||||
readonly props: Readonly<Canvas3DProps>
|
||||
readonly input: InputObserver
|
||||
readonly stats: RendererStats
|
||||
readonly interaction: Canvas3dInteractionHelper['events']
|
||||
|
||||
dispose(): void
|
||||
export type PartialCanvas3DProps = {
|
||||
[K in keyof Canvas3DProps]?: Canvas3DProps[K] extends { name: string, params: any } ? Canvas3DProps[K] : Partial<Canvas3DProps[K]>
|
||||
}
|
||||
|
||||
const requestAnimationFrame = typeof window !== 'undefined' ? window.requestAnimationFrame : (f: (time: number) => void) => setImmediate(()=>f(Date.now()));
|
||||
export { Canvas3DContext };
|
||||
|
||||
namespace Canvas3D {
|
||||
export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
|
||||
export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
|
||||
/** Can be used to create multiple Canvas3D objects */
|
||||
interface Canvas3DContext {
|
||||
readonly canvas: HTMLCanvasElement
|
||||
readonly webgl: WebGLContext
|
||||
readonly input: InputObserver
|
||||
readonly passes: Passes
|
||||
readonly attribs: Readonly<Canvas3DContext.Attribs>
|
||||
readonly contextLost: BehaviorSubject<now.Timestamp>
|
||||
readonly contextRestored: BehaviorSubject<now.Timestamp>
|
||||
dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
|
||||
}
|
||||
|
||||
export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}) {
|
||||
namespace Canvas3DContext {
|
||||
const DefaultAttribs = {
|
||||
/** true by default to avoid issues with Safari (Jan 2021) */
|
||||
antialias: true,
|
||||
/** true to support multiple Canvas3D objects with a single context */
|
||||
preserveDrawingBuffer: true,
|
||||
pixelScale: 1,
|
||||
pickScale: 0.25,
|
||||
enableWboit: true
|
||||
};
|
||||
export type Attribs = typeof DefaultAttribs
|
||||
|
||||
export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext {
|
||||
const a = { ...DefaultAttribs, ...attribs };
|
||||
const { antialias, preserveDrawingBuffer, pixelScale } = a;
|
||||
const gl = getGLContext(canvas, {
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
depth: true,
|
||||
preserveDrawingBuffer: true,
|
||||
premultipliedAlpha: false,
|
||||
antialias,
|
||||
preserveDrawingBuffer,
|
||||
alpha: true, // the renderer requires an alpha channel
|
||||
depth: true, // the renderer requires a depth buffer
|
||||
premultipliedAlpha: true, // the renderer outputs PMA
|
||||
});
|
||||
if (gl === null) throw new Error('Could not create a WebGL rendering context');
|
||||
const input = InputObserver.fromElement(canvas);
|
||||
const webgl = createContext(gl);
|
||||
|
||||
const input = InputObserver.fromElement(canvas, { pixelScale });
|
||||
const webgl = createContext(gl, { pixelScale });
|
||||
const passes = new Passes(webgl, attribs);
|
||||
|
||||
if (isDebugMode) {
|
||||
const loseContextExt = gl.getExtension('WEBGL_lose_context');
|
||||
if (loseContextExt) {
|
||||
// Hold down shift+ctrl+alt and press any mouse button to call `loseContext`.
|
||||
// After 1 second `restoreContext` will be called.
|
||||
canvas.addEventListener('mousedown', e => {
|
||||
if (webgl.isContextLost) return;
|
||||
if (!e.shiftKey || !e.ctrlKey || !e.altKey) return;
|
||||
@@ -148,35 +150,130 @@ namespace Canvas3D {
|
||||
|
||||
// https://www.khronos.org/webgl/wiki/HandlingContextLost
|
||||
|
||||
canvas.addEventListener('webglcontextlost', e => {
|
||||
const contextLost = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
|
||||
|
||||
const handleWebglContextLost = (e: Event) => {
|
||||
webgl.setContextLost();
|
||||
e.preventDefault();
|
||||
if (isDebugMode) console.log('context lost');
|
||||
}, false);
|
||||
contextLost.next(now());
|
||||
};
|
||||
|
||||
canvas.addEventListener('webglcontextrestored', () => {
|
||||
const handlewWebglContextRestored = () => {
|
||||
if (!webgl.isContextLost) return;
|
||||
webgl.handleContextRestored();
|
||||
webgl.handleContextRestored(() => {
|
||||
passes.draw.reset();
|
||||
});
|
||||
if (isDebugMode) console.log('context restored');
|
||||
}, false);
|
||||
};
|
||||
|
||||
return Canvas3D.create(webgl, input, props);
|
||||
canvas.addEventListener('webglcontextlost', handleWebglContextLost, false);
|
||||
canvas.addEventListener('webglcontextrestored', handlewWebglContextRestored, false);
|
||||
|
||||
return {
|
||||
canvas,
|
||||
webgl,
|
||||
input,
|
||||
passes,
|
||||
attribs: a,
|
||||
contextLost,
|
||||
contextRestored: webgl.contextRestored,
|
||||
dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => {
|
||||
input.dispose();
|
||||
|
||||
canvas.removeEventListener('webglcontextlost', handleWebglContextLost, false);
|
||||
canvas.removeEventListener('webglcontextrestored', handlewWebglContextRestored, false);
|
||||
webgl.destroy(options);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function create(webgl: WebGLContext, input: InputObserver, props: Partial<Canvas3DProps> = {}): Canvas3D {
|
||||
const p = { ...DefaultCanvas3DParams, ...props };
|
||||
export { Canvas3D };
|
||||
|
||||
interface Canvas3D {
|
||||
readonly webgl: WebGLContext,
|
||||
|
||||
add(repr: Representation.Any): void
|
||||
remove(repr: Representation.Any): void
|
||||
/**
|
||||
* This function must be called if animate() is not set up so that add/remove actions take place.
|
||||
*/
|
||||
commit(isSynchronous?: boolean): void
|
||||
/**
|
||||
* Funcion for external "animation" control
|
||||
* Calls commit.
|
||||
*/
|
||||
tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }): void
|
||||
update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
|
||||
clear(): void
|
||||
syncVisibility(): void
|
||||
|
||||
requestDraw(force?: boolean): void
|
||||
|
||||
/** Reset the timers, used by "animate" */
|
||||
resetTime(t: number): void
|
||||
animate(): void
|
||||
pause(): void
|
||||
identify(x: number, y: number): PickData | undefined
|
||||
mark(loci: Representation.Loci, action: MarkerAction): void
|
||||
getLoci(pickingId: PickingId | undefined): Representation.Loci
|
||||
|
||||
notifyDidDraw: boolean,
|
||||
readonly didDraw: BehaviorSubject<now.Timestamp>
|
||||
readonly commited: BehaviorSubject<now.Timestamp>
|
||||
readonly reprCount: BehaviorSubject<number>
|
||||
readonly resized: BehaviorSubject<any>
|
||||
|
||||
handleResize(): void
|
||||
/** performs handleResize on the next animation frame */
|
||||
requestResize(): void
|
||||
/** Focuses camera on scene's bounding sphere, centered and zoomed. */
|
||||
requestCameraReset(options?: { durationMs?: number, snapshot?: Partial<Camera.Snapshot> }): void
|
||||
readonly camera: Camera
|
||||
readonly boundingSphere: Readonly<Sphere3D>
|
||||
setProps(props: PartialCanvas3DProps | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void), doNotRequestDraw?: boolean /* = false */): void
|
||||
getImagePass(props: Partial<ImageProps>): ImagePass
|
||||
|
||||
/** Returns a copy of the current Canvas3D instance props */
|
||||
readonly props: Readonly<Canvas3DProps>
|
||||
readonly input: InputObserver
|
||||
readonly stats: RendererStats
|
||||
readonly interaction: Canvas3dInteractionHelper['events']
|
||||
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
const requestAnimationFrame = typeof window !== 'undefined'
|
||||
? window.requestAnimationFrame
|
||||
: (f: (time: number) => void) => setImmediate(() => f(Date.now())) as unknown as number;
|
||||
const cancelAnimationFrame = typeof window !== 'undefined'
|
||||
? window.cancelAnimationFrame
|
||||
: (handle: number) => clearImmediate(handle as unknown as NodeJS.Immediate);
|
||||
|
||||
namespace Canvas3D {
|
||||
export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
|
||||
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 };
|
||||
|
||||
const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
|
||||
const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
|
||||
const reprCount = new BehaviorSubject(0);
|
||||
|
||||
const startTime = now();
|
||||
let startTime = now();
|
||||
const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
|
||||
const commited = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
|
||||
|
||||
const { gl, contextRestored } = webgl;
|
||||
|
||||
let width = gl.drawingBufferWidth;
|
||||
let height = gl.drawingBufferHeight;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let width = 128;
|
||||
let height = 128;
|
||||
updateViewport();
|
||||
|
||||
const scene = Scene.create(webgl);
|
||||
|
||||
@@ -185,40 +282,41 @@ namespace Canvas3D {
|
||||
mode: p.camera.mode,
|
||||
fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
|
||||
clipFar: p.cameraClipping.far
|
||||
});
|
||||
}, { x, y, width, height }, { pixelScale: attribs.pixelScale });
|
||||
const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
|
||||
|
||||
const controls = TrackballControls.create(input, camera, p.trackball);
|
||||
const renderer = Renderer.create(webgl, p.renderer);
|
||||
const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
|
||||
const handleHelper = new HandleHelper(webgl, p.handle);
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
|
||||
const helper = new Helper(webgl, scene, p);
|
||||
|
||||
const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper, handleHelper, {
|
||||
cameraHelper: p.camera.helper
|
||||
});
|
||||
const pickPass = new PickPass(webgl, renderer, scene, camera, handleHelper, 0.5);
|
||||
const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing);
|
||||
const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample);
|
||||
const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height });
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera);
|
||||
const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
|
||||
|
||||
let drawPending = false;
|
||||
let cameraResetRequested = false;
|
||||
let nextCameraResetDuration: number | undefined = void 0;
|
||||
let nextCameraResetSnapshot: Partial<Camera.Snapshot> | undefined = void 0;
|
||||
let resizeRequested = false;
|
||||
|
||||
function getLoci(pickingId: PickingId) {
|
||||
let notifyDidDraw = true;
|
||||
|
||||
function getLoci(pickingId: PickingId | undefined) {
|
||||
let loci: Loci = EmptyLoci;
|
||||
let repr: Representation.Any = Representation.Empty;
|
||||
loci = handleHelper.getLoci(pickingId);
|
||||
reprRenderObjects.forEach((_, _repr) => {
|
||||
const _loci = _repr.getLoci(pickingId);
|
||||
if (!isEmptyLoci(_loci)) {
|
||||
if (!isEmptyLoci(loci)) {
|
||||
console.warn('found another loci, this should not happen');
|
||||
if (pickingId) {
|
||||
loci = helper.handle.getLoci(pickingId);
|
||||
reprRenderObjects.forEach((_, _repr) => {
|
||||
const _loci = _repr.getLoci(pickingId);
|
||||
if (!isEmptyLoci(_loci)) {
|
||||
if (!isEmptyLoci(loci)) {
|
||||
console.warn('found another loci, this should not happen');
|
||||
}
|
||||
loci = _loci;
|
||||
repr = _repr;
|
||||
}
|
||||
loci = _loci;
|
||||
repr = _repr;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return { loci, repr };
|
||||
}
|
||||
|
||||
@@ -228,36 +326,50 @@ namespace Canvas3D {
|
||||
if (repr) {
|
||||
changed = repr.mark(loci, action);
|
||||
} else {
|
||||
changed = handleHelper.mark(loci, action);
|
||||
changed = helper.handle.mark(loci, action);
|
||||
reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
|
||||
}
|
||||
if (changed) {
|
||||
scene.update(void 0, true);
|
||||
handleHelper.scene.update(void 0, true);
|
||||
const prevPickDirty = pickPass.pickDirty;
|
||||
helper.handle.scene.update(void 0, true);
|
||||
const prevPickDirty = pickHelper.dirty;
|
||||
draw(true);
|
||||
pickPass.pickDirty = prevPickDirty; // marking does not change picking buffers
|
||||
pickHelper.dirty = prevPickDirty; // marking does not change picking buffers
|
||||
}
|
||||
}
|
||||
|
||||
function render(force: boolean) {
|
||||
if (webgl.isContextLost) return false;
|
||||
|
||||
let resized = false;
|
||||
if (resizeRequested) {
|
||||
handleResize(false);
|
||||
resizeRequested = false;
|
||||
resized = true;
|
||||
}
|
||||
|
||||
if (x > gl.drawingBufferWidth || x + width < 0 ||
|
||||
y > gl.drawingBufferHeight || y + height < 0
|
||||
) return false;
|
||||
|
||||
let didRender = false;
|
||||
controls.update(currentTime);
|
||||
Viewport.set(camera.viewport, 0, 0, width, height);
|
||||
const cameraChanged = camera.update();
|
||||
multiSample.update(force || cameraChanged, currentTime);
|
||||
const multiSampleChanged = multiSampleHelper.update(force || cameraChanged, p.multiSample);
|
||||
|
||||
if (force || cameraChanged || multiSample.enabled) {
|
||||
renderer.setViewport(0, 0, width, height);
|
||||
if (multiSample.enabled) {
|
||||
multiSample.render(true, p.transparentBackground);
|
||||
} else {
|
||||
drawPass.render(!postprocessing.enabled, p.transparentBackground);
|
||||
if (postprocessing.enabled) postprocessing.render(true);
|
||||
if (resized || force || cameraChanged || multiSampleChanged) {
|
||||
let cam: Camera | StereoCamera = camera;
|
||||
if (p.camera.stereo.name === 'on') {
|
||||
stereoCamera.update();
|
||||
cam = stereoCamera;
|
||||
}
|
||||
pickPass.pickDirty = true;
|
||||
|
||||
if (MultiSamplePass.isEnabled(p.multiSample)) {
|
||||
multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
|
||||
} else {
|
||||
passes.draw.render(renderer, cam, scene, helper, true, p.transparentBackground, p.postprocessing);
|
||||
}
|
||||
pickHelper.dirty = true;
|
||||
didRender = true;
|
||||
}
|
||||
|
||||
@@ -269,7 +381,7 @@ namespace Canvas3D {
|
||||
let currentTime = 0;
|
||||
|
||||
function draw(force?: boolean) {
|
||||
if (render(!!force || forceNextDraw)) {
|
||||
if (render(!!force || forceNextDraw) && notifyDidDraw) {
|
||||
didDraw.next(now() - startTime as now.Timestamp);
|
||||
}
|
||||
forceNextDraw = false;
|
||||
@@ -282,20 +394,46 @@ namespace Canvas3D {
|
||||
forceNextDraw = !!force;
|
||||
}
|
||||
|
||||
function animate() {
|
||||
currentTime = now();
|
||||
commit();
|
||||
let animationFrameHandle = 0;
|
||||
|
||||
function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }) {
|
||||
currentTime = t;
|
||||
commit(options?.isSynchronous);
|
||||
camera.transition.tick(currentTime);
|
||||
|
||||
if (options?.manualDraw) {
|
||||
return;
|
||||
}
|
||||
|
||||
draw(false);
|
||||
if (!camera.transition.inTransition && !webgl.isContextLost) {
|
||||
interactionHelper.tick(currentTime);
|
||||
}
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function identify(x: number, y: number): PickingId | undefined {
|
||||
return webgl.isContextLost ? undefined : pickPass.identify(x, y);
|
||||
function _animate() {
|
||||
tick(now());
|
||||
animationFrameHandle = requestAnimationFrame(_animate);
|
||||
}
|
||||
|
||||
function resetTime(t: now.Timestamp) {
|
||||
startTime = t;
|
||||
controls.start(t);
|
||||
}
|
||||
|
||||
function animate() {
|
||||
controls.start(now());
|
||||
if (animationFrameHandle === 0) _animate();
|
||||
}
|
||||
|
||||
function pause() {
|
||||
cancelAnimationFrame(animationFrameHandle);
|
||||
animationFrameHandle = 0;
|
||||
}
|
||||
|
||||
function identify(x: number, y: number): PickData | undefined {
|
||||
const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
|
||||
return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
|
||||
}
|
||||
|
||||
function commit(isSynchronous: boolean = false) {
|
||||
@@ -304,9 +442,11 @@ namespace Canvas3D {
|
||||
if (allCommited) {
|
||||
resolveCameraReset();
|
||||
if (forceDrawAfterAllCommited) {
|
||||
if (helper.debug.isEnabled) helper.debug.update();
|
||||
draw(true);
|
||||
forceDrawAfterAllCommited = false;
|
||||
}
|
||||
commited.next(now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +458,7 @@ namespace Canvas3D {
|
||||
const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration;
|
||||
const focus = camera.getFocus(center, radius);
|
||||
const snapshot = nextCameraResetSnapshot ? { ...focus, ...nextCameraResetSnapshot } : focus;
|
||||
camera.setState(snapshot, duration);
|
||||
camera.setState({ ...snapshot, radiusMax: scene.boundingSphere.radius }, duration);
|
||||
}
|
||||
|
||||
nextCameraResetDuration = void 0;
|
||||
@@ -345,7 +485,8 @@ namespace Canvas3D {
|
||||
const b = r.values.boundingSphere.ref.value;
|
||||
if (!b.radius) continue;
|
||||
|
||||
if (!Sphere3D.includes(oldBoundingSphereVisible, b)) return true;
|
||||
const cameraDist = Vec3.distance(cameraSphere.center, b.center);
|
||||
if ((cameraDist > cameraSphere.radius || cameraDist > b.radius || b.radius > camera.state.radiusMax) && !Sphere3D.includes(oldBoundingSphereVisible, b)) return true;
|
||||
if (Sphere3D.overlaps(cameraSphere, b)) cameraSphereOverlapsNone = false;
|
||||
}
|
||||
|
||||
@@ -361,8 +502,8 @@ namespace Canvas3D {
|
||||
|
||||
if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false;
|
||||
|
||||
if (debugHelper.isEnabled) debugHelper.update();
|
||||
if (reprCount.value === 0 || shouldResetCamera()) {
|
||||
if (helper.debug.isEnabled) helper.debug.update();
|
||||
if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) {
|
||||
cameraResetRequested = true;
|
||||
}
|
||||
if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
|
||||
@@ -379,7 +520,16 @@ namespace Canvas3D {
|
||||
drawCount: r.values.drawCount.ref.value,
|
||||
instanceCount: r.values.instanceCount.ref.value,
|
||||
materialId: r.materialId,
|
||||
renderItemId: r.id,
|
||||
})));
|
||||
console.log(webgl.stats);
|
||||
|
||||
const { texture, attribute, elements } = webgl.resources.getByteCounts();
|
||||
console.log({
|
||||
texture: `${(texture / 1024 / 1024).toFixed(3)} MiB`,
|
||||
attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`,
|
||||
elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`,
|
||||
});
|
||||
}
|
||||
|
||||
function add(repr: Representation.Any) {
|
||||
@@ -411,7 +561,6 @@ namespace Canvas3D {
|
||||
if (renderObjects) {
|
||||
renderObjects.forEach(o => scene.remove(o));
|
||||
reprRenderObjects.delete(repr);
|
||||
scene.update(repr.renderObjects, false, true);
|
||||
forceDrawAfterAllCommited = true;
|
||||
if (isDebugMode) consoleStats();
|
||||
}
|
||||
@@ -441,7 +590,9 @@ namespace Canvas3D {
|
||||
return {
|
||||
camera: {
|
||||
mode: camera.state.mode,
|
||||
helper: { ...drawPass.props.cameraHelper }
|
||||
helper: { ...helper.camera.props },
|
||||
stereo: { ...p.camera.stereo },
|
||||
manualReset: !!p.camera.manualReset
|
||||
},
|
||||
cameraFog: camera.state.fog > 0
|
||||
? { name: 'on' as const, params: { intensity: camera.state.fog } }
|
||||
@@ -449,23 +600,36 @@ namespace Canvas3D {
|
||||
cameraClipping: { far: camera.state.clipFar, radius },
|
||||
cameraResetDurationMs: p.cameraResetDurationMs,
|
||||
transparentBackground: p.transparentBackground,
|
||||
viewport: p.viewport,
|
||||
|
||||
postprocessing: { ...postprocessing.props },
|
||||
multiSample: { ...multiSample.props },
|
||||
postprocessing: { ...p.postprocessing },
|
||||
multiSample: { ...p.multiSample },
|
||||
renderer: { ...renderer.props },
|
||||
trackball: { ...controls.props },
|
||||
debug: { ...debugHelper.props },
|
||||
handle: { ...handleHelper.props },
|
||||
debug: { ...helper.debug.props },
|
||||
handle: { ...helper.handle.props },
|
||||
};
|
||||
}
|
||||
|
||||
handleResize();
|
||||
|
||||
const contextRestoredSub = contextRestored.subscribe(() => {
|
||||
pickPass.pickDirty = true;
|
||||
pickHelper.dirty = true;
|
||||
draw(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);
|
||||
});
|
||||
|
||||
const resized = new BehaviorSubject<any>(0);
|
||||
|
||||
function handleResize(draw = true) {
|
||||
passes.updateSize();
|
||||
updateViewport();
|
||||
syncViewport();
|
||||
if (draw) requestDraw(true);
|
||||
resized.next(+new Date());
|
||||
}
|
||||
|
||||
return {
|
||||
webgl,
|
||||
|
||||
@@ -486,7 +650,7 @@ namespace Canvas3D {
|
||||
reprUpdatedSubscriptions.clear();
|
||||
reprRenderObjects.clear();
|
||||
scene.clear();
|
||||
debugHelper.clear();
|
||||
helper.debug.clear();
|
||||
requestDraw(true);
|
||||
reprCount.next(reprRenderObjects.size);
|
||||
},
|
||||
@@ -497,19 +661,24 @@ namespace Canvas3D {
|
||||
}
|
||||
|
||||
if (scene.syncVisibility()) {
|
||||
if (debugHelper.isEnabled) debugHelper.update();
|
||||
if (helper.debug.isEnabled) helper.debug.update();
|
||||
}
|
||||
requestDraw(true);
|
||||
},
|
||||
|
||||
// draw,
|
||||
requestDraw,
|
||||
tick,
|
||||
animate,
|
||||
resetTime,
|
||||
pause,
|
||||
identify,
|
||||
mark,
|
||||
getLoci,
|
||||
|
||||
handleResize,
|
||||
requestResize: () => {
|
||||
resizeRequested = true;
|
||||
},
|
||||
requestCameraReset: options => {
|
||||
nextCameraResetDuration = options?.durationMs;
|
||||
nextCameraResetSnapshot = options?.snapshot;
|
||||
@@ -517,19 +686,14 @@ namespace Canvas3D {
|
||||
},
|
||||
camera,
|
||||
boundingSphere: scene.boundingSphere,
|
||||
getPixelData: (variant: GraphicsRenderVariant) => {
|
||||
switch (variant) {
|
||||
case 'color': return webgl.getDrawingBufferPixelData();
|
||||
case 'pickObject': return pickPass.objectPickTarget.getPixelData();
|
||||
case 'pickInstance': return pickPass.instancePickTarget.getPixelData();
|
||||
case 'pickGroup': return pickPass.groupPickTarget.getPixelData();
|
||||
case 'depth': return readTexture(webgl, drawPass.depthTexture) as PixelData;
|
||||
}
|
||||
},
|
||||
get notifyDidDraw() { return notifyDidDraw; },
|
||||
set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
|
||||
didDraw,
|
||||
commited,
|
||||
reprCount,
|
||||
setProps: (properties) => {
|
||||
const props: Partial<Canvas3DProps> = typeof properties === 'function'
|
||||
resized,
|
||||
setProps: (properties, doNotRequestDraw = false) => {
|
||||
const props: PartialCanvas3DProps = typeof properties === 'function'
|
||||
? produce(getProps(), properties)
|
||||
: properties;
|
||||
|
||||
@@ -537,7 +701,7 @@ namespace Canvas3D {
|
||||
if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
|
||||
cameraState.mode = props.camera.mode;
|
||||
}
|
||||
if (props.cameraFog !== undefined) {
|
||||
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;
|
||||
}
|
||||
@@ -555,47 +719,43 @@ namespace Canvas3D {
|
||||
}
|
||||
if (Object.keys(cameraState).length > 0) camera.setState(cameraState);
|
||||
|
||||
if (props.camera?.helper) drawPass.setProps({ cameraHelper: props.camera.helper });
|
||||
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.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
|
||||
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
|
||||
if (props.viewport !== undefined) {
|
||||
const doNotUpdate = p.viewport === props.viewport ||
|
||||
(p.viewport.name === props.viewport.name && shallowEqual(p.viewport.params, props.viewport.params));
|
||||
|
||||
if (props.postprocessing) postprocessing.setProps(props.postprocessing);
|
||||
if (props.multiSample) multiSample.setProps(props.multiSample);
|
||||
if (!doNotUpdate) {
|
||||
p.viewport = props.viewport;
|
||||
updateViewport();
|
||||
syncViewport();
|
||||
}
|
||||
}
|
||||
|
||||
if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
|
||||
if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
|
||||
if (props.renderer) renderer.setProps(props.renderer);
|
||||
if (props.trackball) controls.setProps(props.trackball);
|
||||
if (props.debug) debugHelper.setProps(props.debug);
|
||||
if (props.handle) handleHelper.setProps(props.handle);
|
||||
if (props.debug) helper.debug.setProps(props.debug);
|
||||
if (props.handle) helper.handle.setProps(props.handle);
|
||||
|
||||
requestDraw(true);
|
||||
if (cameraState.mode === 'orthographic') {
|
||||
p.camera.stereo.name = 'off';
|
||||
}
|
||||
|
||||
if (!doNotRequestDraw) {
|
||||
requestDraw(true);
|
||||
}
|
||||
},
|
||||
getImagePass: (props: Partial<ImageProps> = {}) => {
|
||||
return new ImagePass(webgl, renderer, scene, camera, debugHelper, handleHelper, props);
|
||||
return new ImagePass(webgl, renderer, scene, camera, helper, passes.draw.wboitEnabled, props);
|
||||
},
|
||||
|
||||
get props() {
|
||||
const radius = scene.boundingSphere.radius > 0
|
||||
? 100 - Math.round((camera.transition.target.radius / scene.boundingSphere.radius) * 100)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
camera: {
|
||||
mode: camera.state.mode,
|
||||
helper: { ...drawPass.props.cameraHelper }
|
||||
},
|
||||
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 },
|
||||
cameraResetDurationMs: p.cameraResetDurationMs,
|
||||
transparentBackground: p.transparentBackground,
|
||||
|
||||
postprocessing: { ...postprocessing.props },
|
||||
multiSample: { ...multiSample.props },
|
||||
renderer: { ...renderer.props },
|
||||
trackball: { ...controls.props },
|
||||
debug: { ...debugHelper.props },
|
||||
handle: { ...handleHelper.props },
|
||||
};
|
||||
return getProps();
|
||||
},
|
||||
get input() {
|
||||
return input;
|
||||
@@ -610,28 +770,32 @@ namespace Canvas3D {
|
||||
contextRestoredSub.unsubscribe();
|
||||
|
||||
scene.clear();
|
||||
debugHelper.clear();
|
||||
input.dispose();
|
||||
helper.debug.clear();
|
||||
controls.dispose();
|
||||
renderer.dispose();
|
||||
interactionHelper.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
function handleResize() {
|
||||
width = gl.drawingBufferWidth;
|
||||
height = gl.drawingBufferHeight;
|
||||
function updateViewport() {
|
||||
if (p.viewport.name === 'canvas') {
|
||||
x = 0;
|
||||
y = 0;
|
||||
width = gl.drawingBufferWidth;
|
||||
height = gl.drawingBufferHeight;
|
||||
} else {
|
||||
x = p.viewport.params.x * webgl.pixelRatio;
|
||||
y = p.viewport.params.y * webgl.pixelRatio;
|
||||
width = p.viewport.params.width * webgl.pixelRatio;
|
||||
height = p.viewport.params.height * webgl.pixelRatio;
|
||||
}
|
||||
}
|
||||
|
||||
renderer.setViewport(0, 0, width, height);
|
||||
Viewport.set(camera.viewport, 0, 0, width, height);
|
||||
Viewport.set(controls.viewport, 0, 0, width, height);
|
||||
|
||||
drawPass.setSize(width, height);
|
||||
pickPass.setSize(width, height);
|
||||
postprocessing.setSize(width, height);
|
||||
multiSample.setSize(width, height);
|
||||
|
||||
requestDraw(true);
|
||||
function syncViewport() {
|
||||
pickHelper.setViewport(x, y, width, height);
|
||||
renderer.setViewport(x, y, width, height);
|
||||
Viewport.set(camera.viewport, x, y, width, height);
|
||||
Viewport.set(controls.viewport, x, y, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/mol-canvas3d/controls/object.ts
Normal file
58
src/mol-canvas3d/controls/object.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Camera } from '../camera';
|
||||
import { Viewport } from '../camera/util';
|
||||
|
||||
export namespace ObjectControls {
|
||||
function mouseOnScreen(out: Vec2, page: Vec2, viewport: Viewport) {
|
||||
return Vec2.set(
|
||||
out,
|
||||
(page[0] - viewport.x) / viewport.width,
|
||||
(page[1] - viewport.y) / viewport.height
|
||||
);
|
||||
}
|
||||
|
||||
const panMouseChange = Vec2();
|
||||
const panObjUp = Vec3();
|
||||
const panOffset = Vec3();
|
||||
const eye = Vec3();
|
||||
const panStart = Vec2();
|
||||
const panEnd = Vec2();
|
||||
|
||||
const target = Vec3();
|
||||
|
||||
/**
|
||||
* Get vector for movement in camera projection plane:
|
||||
* `pageStart` and `pageEnd` are 2d window coordinates;
|
||||
* `ref` defines the plane depth, if not given `camera.target` is used
|
||||
*/
|
||||
export function panDirection(out: Vec3, pageStart: Vec2, pageEnd: Vec2, camera: Camera, ref?: Vec3) {
|
||||
mouseOnScreen(panStart, pageStart, camera.viewport);
|
||||
mouseOnScreen(panEnd, pageEnd, camera.viewport);
|
||||
Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, panEnd), panStart);
|
||||
Vec3.sub(eye, camera.position, camera.target);
|
||||
|
||||
if (!ref || camera.state.mode === 'orthographic') Vec3.copy(target, camera.target);
|
||||
else Vec3.projectPointOnVector(target, ref, eye, camera.position);
|
||||
|
||||
const dist = Vec3.distance(camera.position, target);
|
||||
const height = 2 * Math.tan(camera.state.fov / 2) * dist;
|
||||
const zoom = camera.viewport.height / height;
|
||||
|
||||
panMouseChange[0] *= (1 / zoom) * camera.viewport.width * camera.pixelRatio;
|
||||
panMouseChange[1] *= (1 / zoom) * camera.viewport.height * camera.pixelRatio;
|
||||
|
||||
Vec3.cross(panOffset, Vec3.copy(panOffset, eye), camera.up);
|
||||
Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0]);
|
||||
|
||||
Vec3.setMagnitude(panObjUp, camera.up, panMouseChange[1]);
|
||||
Vec3.add(panOffset, panOffset, panObjUp);
|
||||
|
||||
return Vec3.negate(out, panOffset);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
|
||||
import { Viewport } from '../camera/util';
|
||||
import InputObserver, { DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys } from '../../mol-util/input/input-observer';
|
||||
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys } 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';
|
||||
@@ -36,9 +36,9 @@ export const DefaultTrackballBindings = {
|
||||
export const TrackballControlsParams = {
|
||||
noScroll: PD.Boolean(true, { isHidden: true }),
|
||||
|
||||
rotateSpeed: PD.Numeric(3.0, { min: 0.1, max: 10, step: 0.1 }),
|
||||
zoomSpeed: PD.Numeric(6.0, { min: 0.1, max: 10, step: 0.1 }),
|
||||
panSpeed: PD.Numeric(0.8, { min: 0.1, max: 5, step: 0.1 }),
|
||||
rotateSpeed: PD.Numeric(5.0, { min: 1, max: 10, step: 1 }),
|
||||
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 }),
|
||||
@@ -60,6 +60,7 @@ interface TrackballControls {
|
||||
readonly props: Readonly<TrackballControlsProps>
|
||||
setProps: (props: Partial<TrackballControlsProps>) => void
|
||||
|
||||
start: (t: number) => void
|
||||
update: (t: number) => void
|
||||
reset: () => void
|
||||
dispose: () => void
|
||||
@@ -68,7 +69,7 @@ namespace TrackballControls {
|
||||
export function create(input: InputObserver, camera: Camera, props: Partial<TrackballControlsProps> = {}): TrackballControls {
|
||||
const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props };
|
||||
|
||||
const viewport = Viewport();
|
||||
const viewport = Viewport.clone(camera.viewport);
|
||||
|
||||
let disposed = false;
|
||||
|
||||
@@ -137,7 +138,8 @@ namespace TrackballControls {
|
||||
const dy = _rotCurr[1] - _rotPrev[1];
|
||||
Vec3.set(rotMoveDir, dx, dy, 0);
|
||||
|
||||
const angle = Vec3.magnitude(rotMoveDir) * p.rotateSpeed;
|
||||
const aspectRatio = input.width / input.height;
|
||||
const angle = Vec3.magnitude(rotMoveDir) * p.rotateSpeed * input.pixelRatio * aspectRatio;
|
||||
|
||||
if (angle) {
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
@@ -227,7 +229,9 @@ namespace TrackballControls {
|
||||
Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, _panEnd), _panStart);
|
||||
|
||||
if (Vec2.squaredMagnitude(panMouseChange)) {
|
||||
Vec2.scale(panMouseChange, panMouseChange, Vec3.magnitude(_eye) * p.panSpeed);
|
||||
const factor = input.pixelRatio * p.panSpeed;
|
||||
panMouseChange[0] *= (1 / camera.zoom) * camera.viewport.width * factor;
|
||||
panMouseChange[1] *= (1 / camera.zoom) * camera.viewport.height * factor;
|
||||
|
||||
Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), camera.up);
|
||||
Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0]);
|
||||
@@ -269,11 +273,22 @@ namespace TrackballControls {
|
||||
}
|
||||
}
|
||||
|
||||
function outsideViewport(x: number, y: number) {
|
||||
x *= input.pixelRatio;
|
||||
y *= input.pixelRatio;
|
||||
return (
|
||||
x > viewport.x + viewport.width ||
|
||||
input.height - y > viewport.y + viewport.height ||
|
||||
x < viewport.x ||
|
||||
input.height - y < viewport.y
|
||||
);
|
||||
}
|
||||
|
||||
let lastUpdated = -1;
|
||||
/** Update the object's position, direction and up vectors */
|
||||
function update(t: number) {
|
||||
if (lastUpdated === t) return;
|
||||
if (p.spin) spin(t - lastUpdated);
|
||||
if (p.spin && lastUpdated > 0) spin(t - lastUpdated);
|
||||
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
|
||||
@@ -305,7 +320,12 @@ namespace TrackballControls {
|
||||
|
||||
// listeners
|
||||
|
||||
function onDrag({ pageX, pageY, buttons, modifiers, isStart }: DragInput) {
|
||||
function onDrag({ x, y, pageX, pageY, buttons, modifiers, isStart }: DragInput) {
|
||||
const isOutside = outsideViewport(x, y);
|
||||
|
||||
if (isStart && isOutside) return;
|
||||
if (!isStart && !_isInteracting) return;
|
||||
|
||||
_isInteracting = true;
|
||||
|
||||
const dragRotate = Binding.match(p.bindings.dragRotate, buttons, modifiers);
|
||||
@@ -356,7 +376,9 @@ namespace TrackballControls {
|
||||
_isInteracting = false;
|
||||
}
|
||||
|
||||
function onWheel({ dx, dy, dz, buttons, modifiers }: WheelInput) {
|
||||
function onWheel({ x, y, dx, dy, dz, buttons, modifiers }: WheelInput) {
|
||||
if (outsideViewport(x, y)) return;
|
||||
|
||||
const delta = absMax(dx, dy, dz);
|
||||
if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
|
||||
_zoomEnd[1] += delta * 0.0001;
|
||||
@@ -385,13 +407,17 @@ namespace TrackballControls {
|
||||
|
||||
const _spinSpeed = Vec2.create(0.005, 0);
|
||||
function spin(deltaT: number) {
|
||||
if (p.spinSpeed === 0) return;
|
||||
|
||||
const frameSpeed = (p.spinSpeed || 0) / 1000;
|
||||
_spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
|
||||
if (!_isInteracting) Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
|
||||
}
|
||||
|
||||
// force an update at start
|
||||
update(0);
|
||||
function start(t: number) {
|
||||
lastUpdated = -1;
|
||||
update(t);
|
||||
}
|
||||
|
||||
return {
|
||||
viewport,
|
||||
@@ -401,6 +427,7 @@ namespace TrackballControls {
|
||||
Object.assign(p, props);
|
||||
},
|
||||
|
||||
start,
|
||||
update,
|
||||
reset,
|
||||
dispose
|
||||
|
||||
@@ -9,7 +9,7 @@ import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { Color } from '../../mol-util/color';
|
||||
@@ -67,6 +67,7 @@ export class BoundingSphereHelper {
|
||||
uInstanceCount: ro.values.uInstanceCount,
|
||||
instanceCount: ro.values.instanceCount,
|
||||
aInstance: ro.values.aInstance,
|
||||
hasReflection: ro.values.hasReflection,
|
||||
});
|
||||
if (newInstanceData) this.instancesData.set(ro, newInstanceData);
|
||||
});
|
||||
@@ -131,7 +132,7 @@ function updateBoundingSphereData(scene: Scene, boundingSphere: Sphere3D, data:
|
||||
const mesh = createBoundingSphereMesh(boundingSphere, data && data.mesh);
|
||||
const renderObject = data ? data.renderObject : createBoundingSphereRenderObject(mesh, color, materialId, transform);
|
||||
if (data) {
|
||||
ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(mesh));
|
||||
ValueCell.updateIfChanged(renderObject.values.drawCount, Geometry.getDrawCount(mesh));
|
||||
} else {
|
||||
scene.add(renderObject);
|
||||
}
|
||||
@@ -159,5 +160,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, { visible: true, alphaFactor: 1, pickable: false, opaque: false, writeDepth: false }, materialId);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false, noClip: false }, materialId);
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
*/
|
||||
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { Camera } from '../camera';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { Camera, ICamera } from '../camera';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
|
||||
@@ -15,7 +15,6 @@ import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import produce from 'immer';
|
||||
@@ -71,6 +70,7 @@ 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();
|
||||
|
||||
@@ -87,43 +87,47 @@ export class CameraHelper {
|
||||
return this.props.axes.name === 'on';
|
||||
}
|
||||
|
||||
update(camera: Camera) {
|
||||
update(camera: ICamera) {
|
||||
if (!this.renderObject) return;
|
||||
|
||||
updateCamera(this.camera, camera.viewport);
|
||||
|
||||
const m = this.renderObject.values.aTransform.ref.value as unknown as Mat4;
|
||||
Mat4.extractRotation(m, camera.view);
|
||||
updateCamera(this.camera, camera.viewport, camera.viewOffset);
|
||||
Mat4.extractRotation(this.scene.view, camera.view);
|
||||
|
||||
const r = this.renderObject.values.boundingSphere.ref.value.radius;
|
||||
Mat4.setTranslation(m, Vec3.create(
|
||||
Mat4.setTranslation(this.scene.view, Vec3.create(
|
||||
-camera.viewport.width / 2 + r,
|
||||
-camera.viewport.height / 2 + r,
|
||||
0
|
||||
));
|
||||
|
||||
ValueCell.update(this.renderObject.values.aTransform, this.renderObject.values.aTransform.ref.value);
|
||||
this.scene.update([this.renderObject], true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCamera(camera: Camera, viewport: Viewport) {
|
||||
function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.ViewOffset) {
|
||||
const { near, far } = camera;
|
||||
|
||||
const fullLeft = -(viewport.width - viewport.x) / 2;
|
||||
const fullRight = (viewport.width - viewport.x) / 2;
|
||||
const fullTop = (viewport.height - viewport.y) / 2;
|
||||
const fullBottom = -(viewport.height - viewport.y) / 2;
|
||||
const fullLeft = -viewport.width / 2;
|
||||
const fullRight = viewport.width / 2;
|
||||
const fullTop = viewport.height / 2;
|
||||
const fullBottom = -viewport.height / 2;
|
||||
|
||||
const dx = (fullRight - fullLeft) / 2;
|
||||
const dy = (fullTop - fullBottom) / 2;
|
||||
const cx = (fullRight + fullLeft) / 2;
|
||||
const cy = (fullTop + fullBottom) / 2;
|
||||
|
||||
const left = cx - dx;
|
||||
const right = cx + dx;
|
||||
const top = cy + dy;
|
||||
const bottom = cy - dy;
|
||||
let left = cx - dx;
|
||||
let right = cx + dx;
|
||||
let top = cy + dy;
|
||||
let bottom = cy - dy;
|
||||
|
||||
if (viewOffset.enabled) {
|
||||
const scaleW = (fullRight - fullLeft) / viewOffset.width;
|
||||
const scaleH = (fullTop - fullBottom) / viewOffset.height;
|
||||
left += scaleW * viewOffset.offsetX;
|
||||
right = left + scaleW * viewOffset.width;
|
||||
top -= scaleH * viewOffset.offsetY;
|
||||
bottom = top - scaleH * viewOffset.height;
|
||||
}
|
||||
|
||||
Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user